puppetserver-ca 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,227 @@
1
+ require 'optparse'
2
+ require 'openssl'
3
+ require 'puppetserver/utils/file_utilities'
4
+ require 'puppetserver/ca/host'
5
+ require 'puppetserver/utils/signing_digest'
6
+
7
+ module Puppetserver
8
+ module Ca
9
+ class GenerateAction
10
+ include Puppetserver::Utils
11
+
12
+ CA_EXTENSIONS = [
13
+ ["basicConstraints", "CA:TRUE", true],
14
+ ["keyUsage", "keyCertSign, cRLSign", true],
15
+ ["subjectKeyIdentifier", "hash", false],
16
+ ["authorityKeyIdentifier", "keyid:always", false]
17
+ ].freeze
18
+
19
+ # Make the certificate valid as of yesterday, because so many people's
20
+ # clocks are out of sync. This gives one more day of validity than people
21
+ # might expect, but is better than making every person who has a messed up
22
+ # clock fail, and better than having every cert we generate expire a day
23
+ # before the user expected it to when they asked for "one year".
24
+ CERT_VALID_FROM = (Time.now - (60*60*24)).freeze
25
+
26
+ SUMMARY = "Generate a root and intermediate signing CA for Puppet Server"
27
+ BANNER = <<-BANNER
28
+ Usage:
29
+ puppetserver ca generate [--help]
30
+ puppetserver ca generate [--config PATH]
31
+
32
+ Description:
33
+ Generate a root and intermediate signing CA for Puppet Server
34
+ and store generated CA keys, certs, and crls on disk.
35
+
36
+ To determine the target location, the default puppet.conf
37
+ is consulted for custom values. If using a custom puppet.conf
38
+ provide it with the --config flag
39
+
40
+ Options:
41
+ BANNER
42
+
43
+ def initialize(logger)
44
+ @logger = logger
45
+ end
46
+
47
+ def run(input)
48
+ # Validate config_path provided
49
+ config_path = input['config']
50
+ if config_path
51
+ errors = FileUtilities.validate_file_paths(config_path)
52
+ return 1 if Utils.handle_errors(@logger, errors)
53
+ end
54
+
55
+ # Load, resolve, and validate puppet config settings
56
+ puppet = PuppetConfig.parse(config_path)
57
+ return 1 if Utils.handle_errors(@logger, puppet.errors)
58
+
59
+ # Load most secure signing digest we can for cers/crl/csr signing.
60
+ signer = SigningDigest.new
61
+ return 1 if Utils.handle_errors(@logger, signer.errors)
62
+
63
+ # Generate root and intermediate ca and put all the certificates, crls,
64
+ # and keys where they should go.
65
+ generate_root_and_intermediate_ca(puppet.settings, signer.digest)
66
+
67
+ # Puppet's internal CA expects these file to exist.
68
+ FileUtilities.ensure_file(puppet.settings[:serial], "001", 0640)
69
+ FileUtilities.ensure_file(puppet.settings[:cert_inventory], "", 0640)
70
+
71
+ @logger.inform "Generation succeeded. Find your files in #{puppet.settings[:cadir]}"
72
+ return 0
73
+ end
74
+
75
+ def generate_root_and_intermediate_ca(settings, signing_digest)
76
+ valid_until = Time.now + settings[:ca_ttl]
77
+ host = Puppetserver::Ca::Host.new(signing_digest)
78
+
79
+ root_key = host.create_private_key(settings[:keylength])
80
+ root_cert = self_signed_ca(root_key, settings[:root_ca_name], valid_until, signing_digest)
81
+ root_crl = create_crl_for(root_cert, root_key, valid_until, signing_digest)
82
+
83
+ int_key = host.create_private_key(settings[:keylength])
84
+ int_csr = host.create_csr(settings[:ca_name], int_key)
85
+ int_cert = sign_intermediate(root_key, root_cert, int_csr, valid_until, signing_digest)
86
+ int_crl = create_crl_for(int_cert, int_key, valid_until, signing_digest)
87
+
88
+ FileUtilities.ensure_dir(settings[:cadir])
89
+
90
+ file_properties = [
91
+ [settings[:cacert], [int_cert, root_cert]],
92
+ [settings[:cakey], int_key],
93
+ [settings[:rootkey], root_key],
94
+ [settings[:cacrl], [int_crl, root_crl]]
95
+ ]
96
+
97
+ file_properties.each do |location, content|
98
+ @logger.warn "#{location} exists, overwriting" if File.exist?(location)
99
+ FileUtilities.write_file(location, content, 0640)
100
+ end
101
+ end
102
+
103
+ def self_signed_ca(key, name, valid_until, signing_digest)
104
+ cert = OpenSSL::X509::Certificate.new
105
+
106
+ cert.public_key = key.public_key
107
+ cert.subject = OpenSSL::X509::Name.new([["CN", name]])
108
+ cert.issuer = cert.subject
109
+ cert.version = 2
110
+ cert.serial = 1
111
+
112
+ cert.not_before = CERT_VALID_FROM
113
+ cert.not_after = valid_until
114
+
115
+ ef = extension_factory_for(cert, cert)
116
+ CA_EXTENSIONS.each do |ext|
117
+ extension = ef.create_extension(*ext)
118
+ cert.add_extension(extension)
119
+ end
120
+
121
+ cert.sign(key, signing_digest)
122
+
123
+ cert
124
+ end
125
+
126
+ def extension_factory_for(ca, cert = nil)
127
+ ef = OpenSSL::X509::ExtensionFactory.new
128
+ ef.issuer_certificate = ca
129
+ ef.subject_certificate = cert if cert
130
+
131
+ ef
132
+ end
133
+
134
+ def create_crl_for(ca_cert, ca_key, valid_until, signing_digest)
135
+ crl = OpenSSL::X509::CRL.new
136
+ crl.version = 1
137
+ crl.issuer = ca_cert.subject
138
+
139
+ ef = extension_factory_for(ca_cert)
140
+ crl.add_extension(
141
+ ef.create_extension(["authorityKeyIdentifier", "keyid:always", false]))
142
+ crl.add_extension(
143
+ OpenSSL::X509::Extension.new("crlNumber", OpenSSL::ASN1::Integer(0)))
144
+
145
+ crl.last_update = CERT_VALID_FROM
146
+ crl.next_update = valid_until
147
+ crl.sign(ca_key, signing_digest)
148
+
149
+ crl
150
+ end
151
+
152
+ def sign_intermediate(ca_key, ca_cert, csr, valid_until, signing_digest)
153
+ cert = OpenSSL::X509::Certificate.new
154
+
155
+ cert.public_key = csr.public_key
156
+ cert.subject = csr.subject
157
+ cert.issuer = ca_cert.subject
158
+ cert.version = 2
159
+ cert.serial = 2
160
+
161
+ cert.not_before = CERT_VALID_FROM
162
+ cert.not_after = valid_until
163
+
164
+ ef = extension_factory_for(ca_cert, cert)
165
+ CA_EXTENSIONS.each do |ext|
166
+ extension = ef.create_extension(*ext)
167
+ cert.add_extension(extension)
168
+ end
169
+ cert.sign(ca_key, signing_digest)
170
+
171
+ cert
172
+ end
173
+
174
+ def parse(cli_args)
175
+ parser, inputs, unparsed = parse_inputs(cli_args)
176
+
177
+ if !unparsed.empty?
178
+ @logger.err 'Error:'
179
+ @logger.err 'Unknown arguments or flags:'
180
+ unparsed.each do |arg|
181
+ @logger.err " #{arg}"
182
+ end
183
+
184
+ @logger.err ''
185
+ @logger.err parser.help
186
+
187
+ exit_code = 1
188
+ else
189
+ exit_code = nil
190
+ end
191
+
192
+ return inputs, exit_code
193
+ end
194
+
195
+ def parse_inputs(inputs)
196
+ parsed = {}
197
+ unparsed = []
198
+
199
+ parser = self.class.parser(parsed)
200
+
201
+ begin
202
+ parser.order!(inputs) do |nonopt|
203
+ unparsed << nonopt
204
+ end
205
+ rescue OptionParser::ParseError => e
206
+ unparsed += e.args
207
+ unparsed << inputs.shift unless inputs.first =~ /^-{1,2}/
208
+ retry
209
+ end
210
+
211
+ return parser, parsed, unparsed
212
+ end
213
+
214
+ def self.parser(parsed = {})
215
+ OptionParser.new do |opts|
216
+ opts.banner = BANNER
217
+ opts.on('--help', 'Display this generate specific help output') do |help|
218
+ parsed['help'] = true
219
+ end
220
+ opts.on('--config CONF', 'Path to puppet.conf') do |conf|
221
+ parsed['config'] = conf
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,26 @@
1
+ require 'openssl'
2
+
3
+ module Puppetserver
4
+ module Ca
5
+ class Host
6
+
7
+ def initialize(digest)
8
+ @digest = digest
9
+ end
10
+
11
+ def create_private_key(keylength)
12
+ OpenSSL::PKey::RSA.new(keylength)
13
+ end
14
+
15
+ def create_csr(name, key)
16
+ csr = OpenSSL::X509::Request.new
17
+ csr.public_key = key.public_key
18
+ csr.subject = OpenSSL::X509::Name.new([["CN", name]])
19
+ csr.version = 2
20
+ csr.sign(key, @digest)
21
+
22
+ csr
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,6 +1,5 @@
1
- require 'etc'
2
- require 'fileutils'
3
1
  require 'optparse'
2
+ require 'puppetserver/utils/file_utilities'
4
3
  require 'puppetserver/ca/x509_loader'
5
4
  require 'puppetserver/ca/puppet_config'
6
5
 
@@ -11,7 +10,7 @@ module Puppetserver
11
10
  SUMMARY = "Import the CA's key, certs, and crls"
12
11
  BANNER = <<-BANNER
13
12
  Usage:
14
- puppetserver ca import [--help|--version]
13
+ puppetserver ca import [--help]
15
14
  puppetserver ca import [--config PATH]
16
15
  --private-key PATH --cert-bundle PATH --crl-chain PATH
17
16
 
@@ -38,7 +37,7 @@ BANNER
38
37
 
39
38
  files = [bundle_path, key_path, chain_path, config_path].compact
40
39
 
41
- errors = validate_file_paths(files)
40
+ errors = Puppetserver::Utils::FileUtilities.validate_file_paths(files)
42
41
  return 1 if log_possible_errors(errors)
43
42
 
44
43
  loader = X509Loader.new(bundle_path, key_path, chain_path)
@@ -47,65 +46,22 @@ BANNER
47
46
  puppet = PuppetConfig.parse(config_path)
48
47
  return 1 if log_possible_errors(puppet.errors)
49
48
 
50
- user, group = find_user_and_group
49
+ Puppetserver::Utils::FileUtilities.ensure_dir(puppet.settings[:cadir])
51
50
 
52
- if !File.exist?(puppet.settings[:cadir])
53
- FileUtils.mkdir_p(puppet.settings[:cadir], mode: 0750)
54
- FileUtils.chown(user, group, puppet.settings[:cadir])
55
- end
56
-
57
- write_file(puppet.settings[:cacert], loader.certs, user, group, 0640)
51
+ Puppetserver::Utils::FileUtilities.write_file(puppet.settings[:cacert], loader.certs, 0640)
58
52
 
59
- write_file(puppet.settings[:cakey], loader.key, user, group, 0640)
53
+ Puppetserver::Utils::FileUtilities.write_file(puppet.settings[:cakey], loader.key, 0640)
60
54
 
61
- write_file(puppet.settings[:cacrl], loader.crls, user, group, 0640)
55
+ Puppetserver::Utils::FileUtilities.write_file(puppet.settings[:cacrl], loader.crls, 0640)
62
56
 
63
- if !File.exist?(puppet.settings[:serial])
64
- write_file(puppet.settings[:serial], "001", user, group, 0640)
65
- end
66
-
67
- if !File.exist?(puppet.settings[:cert_inventory])
68
- write_file(puppet.settings[:cert_inventory],
69
- "", user, group, 0640)
70
- end
57
+ # Puppet's internal CA expects these file to exist.
58
+ Puppetserver::Utils::FileUtilities.ensure_file(puppet.settings[:serial], "001", 0640)
59
+ Puppetserver::Utils::FileUtilities.ensure_file(puppet.settings[:cert_inventory], "", 0640)
71
60
 
61
+ @logger.inform "Import succeeded. Find your files in #{puppet.settings[:cadir]}"
72
62
  return 0
73
63
  end
74
64
 
75
- def find_user_and_group
76
- if !running_as_root?
77
- return Process.euid, Process.egid
78
- else
79
- if pe_puppet_exists?
80
- return 'pe-puppet', 'pe-puppet'
81
- else
82
- return 'puppet', 'puppet'
83
- end
84
- end
85
- end
86
-
87
- def running_as_root?
88
- !Gem.win_platform? && Process.euid == 0
89
- end
90
-
91
- def pe_puppet_exists?
92
- !!(Etc.getpwnam('pe-puppet') rescue nil)
93
- end
94
-
95
- def write_file(path, one_or_more_objects, user, group, mode)
96
- if File.exist?(path)
97
- @logger.warn("#{path} exists, overwriting")
98
- end
99
-
100
- File.open(path, 'w', mode) do |f|
101
- Array(one_or_more_objects).each do |object|
102
- f.puts object.to_s
103
- end
104
- end
105
-
106
- FileUtils.chown(user, group, path)
107
- end
108
-
109
65
  def log_possible_errors(maybe_errors)
110
66
  errors = Array(maybe_errors).compact
111
67
  unless errors.empty?
@@ -178,9 +134,6 @@ BANNER
178
134
  opts.on('--help', 'Display this import specific help output') do |help|
179
135
  parsed['help'] = true
180
136
  end
181
- opts.on('--version', 'Output the version') do |v|
182
- parsed['version'] = true
183
- end
184
137
  opts.on('--config CONF', 'Path to puppet.conf') do |conf|
185
138
  parsed['config'] = conf
186
139
  end
@@ -195,17 +148,6 @@ BANNER
195
148
  end
196
149
  end
197
150
  end
198
-
199
- def validate_file_paths(one_or_more_paths)
200
- errors = []
201
- Array(one_or_more_paths).each do |path|
202
- if !File.exist?(path) || !File.readable?(path)
203
- errors << "Could not read file '#{path}'"
204
- end
205
- end
206
-
207
- errors
208
- end
209
151
  end
210
152
  end
211
153
  end
@@ -0,0 +1,155 @@
1
+ require 'puppetserver/ca/utils'
2
+ require 'puppetserver/utils/http_client'
3
+ require 'puppetserver/utils/file_utilities'
4
+ require 'puppetserver/ca/puppet_config'
5
+ require 'optparse'
6
+ require 'json'
7
+
8
+ module Puppetserver
9
+ module Ca
10
+ class ListAction
11
+
12
+ include Puppetserver::Utils
13
+
14
+ SUMMARY = 'List all certificate requests'
15
+ BANNER = <<-BANNER
16
+ Usage:
17
+ puppetserver ca list [--help]
18
+ puppetserver ca list [--config]
19
+ puppetserver ca list [--all]
20
+
21
+ Description:
22
+ List outstanding certificate requests. If --all is specified, signed and revoked certificates will be listed as well.
23
+
24
+ Options:
25
+ BANNER
26
+
27
+ BODY = JSON.dump({desired_state: 'signed'})
28
+
29
+ def initialize(logger)
30
+ @logger = logger
31
+ end
32
+
33
+ def self.parser(parsed = {})
34
+ OptionParser.new do |opts|
35
+ opts.banner = BANNER
36
+ opts.on('--config CONF', 'Custom path to Puppet\'s config file') do |conf|
37
+ parsed['config'] = conf
38
+ end
39
+ opts.on('--help', 'Display this command specific help output') do |help|
40
+ parsed['help'] = true
41
+ end
42
+ opts.on('--all', 'List all certificates') do |a|
43
+ parsed['all'] = true
44
+ end
45
+ end
46
+ end
47
+
48
+ def run(input)
49
+ config = input['config']
50
+
51
+ if config
52
+ errors = FileUtilities.validate_file_paths(config)
53
+ return 1 if Utils.handle_errors(@logger, errors)
54
+ end
55
+
56
+ puppet = PuppetConfig.parse(config)
57
+ return 1 if Utils.handle_errors(@logger, puppet.errors)
58
+
59
+ all_certs = get_all_certs(puppet.settings)
60
+ return 1 if all_certs.nil?
61
+
62
+ requested, signed, revoked = separate_certs(all_certs)
63
+ input['all'] ? output_certs_by_state(requested, signed, revoked) : output_certs_by_state(requested)
64
+
65
+ return 0
66
+ end
67
+
68
+ def output_certs_by_state(requested, signed = [], revoked = [])
69
+ if revoked.empty? && signed.empty? && requested.empty?
70
+ @logger.inform "No certificates to list"
71
+ return
72
+ end
73
+
74
+ unless requested.empty?
75
+ @logger.inform "Requested Certificates:"
76
+ output_certs(requested)
77
+ end
78
+
79
+ unless signed.empty?
80
+ @logger.inform "Signed Certificates:"
81
+ output_certs(signed)
82
+ end
83
+
84
+ unless revoked.empty?
85
+ @logger.inform "Revoked Certificates:"
86
+ output_certs(revoked)
87
+ end
88
+ end
89
+
90
+ def output_certs(certs)
91
+ padded = 0
92
+ certs.each do |cert|
93
+ cert_size = cert["name"].size
94
+ padded = cert_size if cert_size > padded
95
+ end
96
+
97
+ certs.each do |cert|
98
+ @logger.inform " #{cert["name"]}".ljust(padded + 6) + " (SHA256) " + " #{cert["fingerprints"]["SHA256"]}" +
99
+ (cert["dns_alt_names"].empty? ? "" : "\talt names: #{cert["dns_alt_names"]}")
100
+ end
101
+ end
102
+
103
+ def http_client(settings)
104
+ @client ||= HttpClient.new(settings[:localcacert],
105
+ settings[:certificate_revocation],
106
+ settings[:hostcrl])
107
+ end
108
+
109
+ def get_certificate_statuses(settings)
110
+ client = http_client(settings)
111
+ url = client.make_ca_url(settings[:ca_server],
112
+ settings[:ca_port],
113
+ 'certificate_statuses',
114
+ 'any_key')
115
+ client.with_connection(url) do |connection|
116
+ connection.get(url)
117
+ end
118
+ end
119
+
120
+ def separate_certs(all_certs)
121
+ certs = all_certs.group_by { |v| v["state"]}
122
+ requested = certs.fetch("requested", [])
123
+ signed = certs.fetch("signed", [])
124
+ revoked = certs.fetch("revoked", [])
125
+ return requested, signed, revoked
126
+ end
127
+
128
+ def get_all_certs(settings)
129
+ result = get_certificate_statuses(settings)
130
+
131
+ unless result.code == '200'
132
+ @logger.err 'Error:'
133
+ @logger.err " code: #{result.code}"
134
+ @logger.err " body: #{result.body}" if result.body
135
+ return nil
136
+ end
137
+
138
+ JSON.parse(result.body)
139
+ end
140
+
141
+ def parse(args)
142
+ results = {}
143
+ parser = self.class.parser(results)
144
+
145
+ errors = Utils.parse_with_errors(parser, args)
146
+
147
+ errors_were_handled = Utils.handle_errors(@logger, errors, parser.help)
148
+
149
+ exit_code = errors_were_handled ? 1 : nil
150
+
151
+ return results, exit_code
152
+ end
153
+ end
154
+ end
155
+ end