openvoxserver-ca 3.0.0.pre.rc1

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +17 -0
  3. data/.github/release.yml +41 -0
  4. data/.github/workflows/gem_release.yaml +106 -0
  5. data/.github/workflows/prepare_release.yml +28 -0
  6. data/.github/workflows/release.yml +28 -0
  7. data/.github/workflows/unit_tests.yaml +45 -0
  8. data/.gitignore +14 -0
  9. data/.rspec +2 -0
  10. data/.travis.yml +16 -0
  11. data/CHANGELOG.md +15 -0
  12. data/CODEOWNERS +4 -0
  13. data/CODE_OF_CONDUCT.md +74 -0
  14. data/CONTRIBUTING.md +15 -0
  15. data/Gemfile +20 -0
  16. data/LICENSE +202 -0
  17. data/README.md +118 -0
  18. data/Rakefile +30 -0
  19. data/bin/console +14 -0
  20. data/bin/setup +8 -0
  21. data/exe/puppetserver-ca +10 -0
  22. data/lib/puppetserver/ca/action/clean.rb +109 -0
  23. data/lib/puppetserver/ca/action/delete.rb +286 -0
  24. data/lib/puppetserver/ca/action/enable.rb +140 -0
  25. data/lib/puppetserver/ca/action/generate.rb +330 -0
  26. data/lib/puppetserver/ca/action/import.rb +196 -0
  27. data/lib/puppetserver/ca/action/list.rb +253 -0
  28. data/lib/puppetserver/ca/action/migrate.rb +97 -0
  29. data/lib/puppetserver/ca/action/prune.rb +289 -0
  30. data/lib/puppetserver/ca/action/revoke.rb +108 -0
  31. data/lib/puppetserver/ca/action/setup.rb +188 -0
  32. data/lib/puppetserver/ca/action/sign.rb +146 -0
  33. data/lib/puppetserver/ca/certificate_authority.rb +418 -0
  34. data/lib/puppetserver/ca/cli.rb +145 -0
  35. data/lib/puppetserver/ca/config/puppet.rb +309 -0
  36. data/lib/puppetserver/ca/config/puppetserver.rb +84 -0
  37. data/lib/puppetserver/ca/errors.rb +40 -0
  38. data/lib/puppetserver/ca/host.rb +176 -0
  39. data/lib/puppetserver/ca/local_certificate_authority.rb +304 -0
  40. data/lib/puppetserver/ca/logger.rb +49 -0
  41. data/lib/puppetserver/ca/stub.rb +17 -0
  42. data/lib/puppetserver/ca/utils/cli_parsing.rb +67 -0
  43. data/lib/puppetserver/ca/utils/config.rb +61 -0
  44. data/lib/puppetserver/ca/utils/file_system.rb +109 -0
  45. data/lib/puppetserver/ca/utils/http_client.rb +232 -0
  46. data/lib/puppetserver/ca/utils/inventory.rb +84 -0
  47. data/lib/puppetserver/ca/utils/signing_digest.rb +27 -0
  48. data/lib/puppetserver/ca/version.rb +5 -0
  49. data/lib/puppetserver/ca/x509_loader.rb +170 -0
  50. data/lib/puppetserver/ca.rb +7 -0
  51. data/openvoxserver-ca.gemspec +31 -0
  52. data/tasks/spec.rake +15 -0
  53. data/tasks/vox.rake +19 -0
  54. metadata +154 -0
@@ -0,0 +1,253 @@
1
+ require 'json'
2
+ require 'optparse'
3
+
4
+ require 'puppetserver/ca/errors'
5
+ require 'puppetserver/ca/certificate_authority'
6
+ require 'puppetserver/ca/config/puppet'
7
+ require 'puppetserver/ca/utils/cli_parsing'
8
+ require 'puppetserver/ca/utils/file_system'
9
+
10
+ module Puppetserver
11
+ module Ca
12
+ module Action
13
+ class List
14
+
15
+ include Puppetserver::Ca::Utils
16
+
17
+ SUMMARY = 'List certificates and CSRs'
18
+ BANNER = <<-BANNER
19
+ Usage:
20
+ puppetserver ca list [--help]
21
+ puppetserver ca list [--config]
22
+ puppetserver ca list [--all]
23
+ puppetserver ca list --certname NAME[,NAME]
24
+
25
+ Description:
26
+ List outstanding certificate requests. If --all is specified, signed and
27
+ revoked certificates will be listed as well.
28
+
29
+ Options:
30
+ BANNER
31
+
32
+ BODY = JSON.dump({desired_state: 'signed'})
33
+ VALID_FORMAT = ['text', 'json']
34
+
35
+ def initialize(logger)
36
+ @logger = logger
37
+ end
38
+
39
+ def self.parser(parsed = {})
40
+ OptionParser.new do |opts|
41
+ opts.banner = BANNER
42
+ opts.on('--config CONF', 'Custom path to Puppet\'s config file') do |conf|
43
+ parsed['config'] = conf
44
+ end
45
+ opts.on('--help', 'Display this command-specific help output') do |help|
46
+ parsed['help'] = true
47
+ end
48
+ opts.on('--all', 'List all certificates') do |a|
49
+ parsed['all'] = true
50
+ end
51
+ opts.on('--format FORMAT', "Valid formats are: 'text' (default), 'json'") do |f|
52
+ parsed['format'] = f
53
+ end
54
+ opts.on('--certname NAME[,NAME]', Array, 'List the specified cert(s)') do |cert|
55
+ parsed['certname'] = cert
56
+ end
57
+ end
58
+ end
59
+
60
+ def run(input)
61
+ config = input['config']
62
+ certnames = input['certname'] || []
63
+ all = input['all']
64
+ output_format = input['format'] || "text"
65
+ missing = []
66
+
67
+ unless VALID_FORMAT.include?(output_format)
68
+ Errors.handle_with_usage(@logger, ["Unknown format flag '#{output_format}'. Valid formats are '#{VALID_FORMAT.join("', '")}'."])
69
+ return 1
70
+ end
71
+
72
+ if all && certnames.any?
73
+ Errors.handle_with_usage(@logger, ['Cannot combine use of --all and --certname.'])
74
+ return 1
75
+ end
76
+
77
+ if config
78
+ errors = FileSystem.validate_file_paths(config)
79
+ return 1 if Errors.handle_with_usage(@logger, errors)
80
+ end
81
+
82
+ puppet = Config::Puppet.parse(config, @logger)
83
+ return 1 if Errors.handle_with_usage(@logger, puppet.errors)
84
+
85
+ if certnames.any?
86
+ filter_names = lambda { |x| certnames.include?(x['name']) }
87
+ else
88
+ filter_names = lambda { |x| true }
89
+ end
90
+
91
+ if (all || certnames.any?)
92
+ found_certs = get_certs_or_csrs(puppet.settings)
93
+ if found_certs.nil?
94
+ # nil is different from no certs found
95
+ @logger.err('Error while getting certificates')
96
+ return 1
97
+ end
98
+ all_certs = found_certs.select { |cert| filter_names.call(cert) }
99
+ requested, signed, revoked = separate_certs(all_certs)
100
+ missing = certnames - all_certs.map { |cert| cert['name'] }
101
+ output_certs_by_state(all, output_format, requested, signed, revoked, missing)
102
+ else
103
+ all_csrs = get_certs_or_csrs(puppet.settings, "requested")
104
+ if all_csrs.nil?
105
+ # nil is different from no certs found
106
+ @logger.err('Error while getting certificate requests')
107
+ return 1
108
+ end
109
+ output_certs_by_state(all, output_format, all_csrs)
110
+ end
111
+
112
+ return missing.any? ? 1 : 0
113
+ end
114
+
115
+ def output_certs_by_state(all, output_format, requested, signed = [], revoked = [], missing = [])
116
+ if output_format == 'json'
117
+ output_certs_json_format(all, requested, signed, revoked, missing)
118
+ else
119
+ output_certs_text_format(requested, signed, revoked, missing)
120
+ end
121
+ end
122
+
123
+ def output_certs_json_format(all, requested, signed, revoked, missing)
124
+ grouped_cert = {}
125
+
126
+ if all
127
+ grouped_cert = { "requested" => requested,
128
+ "signed" => signed,
129
+ "revoked" => revoked }.to_json
130
+ @logger.inform(grouped_cert)
131
+ else
132
+ grouped_cert["requested"] = requested unless requested.empty?
133
+ grouped_cert["signed"] = signed unless signed.empty?
134
+ grouped_cert["revoked"] = revoked unless revoked.empty?
135
+ grouped_cert["missing"] = missing unless missing.empty?
136
+
137
+ # If neither the '--all' flag or the '--certname' flag was passed in
138
+ # and the requested cert array is empty, we output a JSON object
139
+ # with an empty 'requested' key. Otherwise, we display
140
+ # any of the classes that are currently in grouped_cert
141
+ if grouped_cert.empty?
142
+ @logger.inform({ "requested" => requested }.to_json)
143
+ else
144
+ @logger.inform(grouped_cert.to_json)
145
+ end
146
+ end
147
+ end
148
+
149
+ def output_certs_text_format(requested, signed, revoked, missing)
150
+ if revoked.empty? && signed.empty? && requested.empty? && missing.empty?
151
+ @logger.inform "No certificates to list"
152
+ return
153
+ end
154
+
155
+ unless requested.empty?
156
+ @logger.inform "Requested Certificates:"
157
+ output_certs(requested)
158
+ end
159
+
160
+ unless signed.empty?
161
+ @logger.inform "Signed Certificates:"
162
+ output_certs(signed)
163
+ end
164
+
165
+ unless revoked.empty?
166
+ @logger.inform "Revoked Certificates:"
167
+ output_certs(revoked)
168
+ end
169
+
170
+ unless missing.empty?
171
+ @logger.inform "Missing Certificates:"
172
+ missing.each do |name|
173
+ @logger.inform " #{name}"
174
+ end
175
+ end
176
+ end
177
+
178
+ def output_certs(certs)
179
+ cert_column_width = certs.map { |c| c['name'].size }.max
180
+
181
+ certs.each do |cert|
182
+ @logger.inform(format_cert(cert, cert_column_width))
183
+ end
184
+ end
185
+
186
+ def format_cert(cert, cert_column_width)
187
+ [
188
+ format_cert_and_sha(cert, cert_column_width),
189
+ format_alt_names(cert),
190
+ format_authorization_extensions(cert)
191
+ ].compact.join("\t")
192
+ end
193
+
194
+ def format_cert_and_sha(cert, cert_column_width)
195
+ justified_certname = cert['name'].ljust(cert_column_width + 6)
196
+ sha = cert['fingerprints']['SHA256']
197
+ " #{justified_certname} (SHA256) #{sha}"
198
+ end
199
+
200
+ def format_alt_names(cert)
201
+ # In newer versions of the CA api we return subject_alt_names
202
+ # in addition to dns_alt_names, this field includes DNS alt
203
+ # names but also IP alt names.
204
+ alt_names = cert['subject_alt_names'] || cert['dns_alt_names']
205
+ "alt names: #{alt_names}" unless alt_names.empty?
206
+ end
207
+
208
+ def format_authorization_extensions(cert)
209
+ auth_exts = cert['authorization_extensions']
210
+ return nil if auth_exts.nil? || auth_exts.empty?
211
+
212
+ values = auth_exts.map { |ext, value| "#{ext}: #{value}" }.join(', ')
213
+ "authorization extensions: [#{values}]"
214
+ end
215
+
216
+ def separate_certs(all_certs)
217
+ certs = all_certs.group_by { |v| v["state"]}
218
+ requested = certs.fetch("requested", [])
219
+ signed = certs.fetch("signed", [])
220
+ revoked = certs.fetch("revoked", [])
221
+ return requested, signed, revoked
222
+ end
223
+
224
+ def get_certs_or_csrs(settings, queried_state = nil)
225
+ query = queried_state ? { :state => queried_state } : {}
226
+ result = Puppetserver::Ca::CertificateAuthority.new(@logger, settings).get_certificate_statuses(query)
227
+
228
+ if result
229
+ return JSON.parse(result.body)
230
+ else
231
+ return nil
232
+ end
233
+ end
234
+
235
+ def parse(args)
236
+ results = {}
237
+ parser = self.class.parser(results)
238
+
239
+ errors = CliParsing.parse_with_errors(parser, args)
240
+
241
+ errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
242
+
243
+ if errors_were_handled
244
+ exit_code = 1
245
+ else
246
+ exit_code = nil
247
+ end
248
+ return results, exit_code
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,97 @@
1
+ require 'puppetserver/ca/utils/cli_parsing'
2
+ require 'puppetserver/ca/utils/file_system'
3
+ require 'puppetserver/ca/utils/http_client'
4
+ require 'puppetserver/ca/utils/config'
5
+
6
+ module Puppetserver
7
+ module Ca
8
+ module Action
9
+ class Migrate
10
+ include Puppetserver::Ca::Utils
11
+ PUPPETSERVER_CA_DIR = Puppetserver::Ca::Utils::Config.new_default_cadir
12
+
13
+ SUMMARY = "Migrate the existing CA directory to #{PUPPETSERVER_CA_DIR}"
14
+ BANNER = <<-BANNER
15
+ Usage:
16
+ puppetserver ca migrate [--help]
17
+ puppetserver ca migrate [--config PATH]
18
+
19
+ Description:
20
+ Migrate an existing CA directory to #{PUPPETSERVER_CA_DIR}. This is for
21
+ upgrading from Puppet Platform 6.x to Puppet 7. Uses the default puppet.conf
22
+ in your installation, or use a different config by supplying the `--config` flag.
23
+
24
+ Options:
25
+ BANNER
26
+
27
+ def initialize(logger)
28
+ @logger = logger
29
+ end
30
+
31
+ def run(input)
32
+ config_path = input['config']
33
+ puppet = Config::Puppet.new(config_path)
34
+ puppet.load(logger: @logger, ca_dir_warn: false)
35
+ return 1 if HttpClient.check_server_online(puppet.settings, @logger)
36
+
37
+ errors = FileSystem.check_for_existing_files(PUPPETSERVER_CA_DIR)
38
+ if !errors.empty?
39
+ instructions = <<-ERR
40
+ Migration will not overwrite the directory at #{PUPPETSERVER_CA_DIR}. Have you already
41
+ run this migration tool? Is this a puppet 7 installation? It is likely that you have
42
+ already successfully run the migration or do not need to run it.
43
+ ERR
44
+ errors << instructions
45
+ Errors.handle_with_usage(@logger, errors)
46
+ return 1
47
+ end
48
+
49
+ current_cadir = puppet.settings[:cadir]
50
+ if FileSystem.check_for_existing_files(current_cadir).empty?
51
+ error_message = <<-ERR
52
+ No CA dir found at #{current_cadir}. Please check the configured cadir setting in your
53
+ puppet.conf file and verify its contents.
54
+ ERR
55
+ Errors.handle_with_usage(@logger, [error_message])
56
+ return 1
57
+ end
58
+
59
+ migrate(current_cadir)
60
+
61
+ @logger.inform <<-SUCCESS_MESSAGE
62
+ CA dir successfully migrated to #{PUPPETSERVER_CA_DIR}. Symlink placed at #{current_cadir}
63
+ for backwards compatibility. The puppetserver can be safely restarted now.
64
+ SUCCESS_MESSAGE
65
+ return 0
66
+ end
67
+
68
+ def migrate(old_cadir, new_cadir=PUPPETSERVER_CA_DIR)
69
+ FileUtils.mv(old_cadir, new_cadir)
70
+ FileSystem.forcibly_symlink(new_cadir, old_cadir)
71
+ end
72
+
73
+ def parse(args)
74
+ results = {}
75
+ parser = self.class.parser(results)
76
+ errors = CliParsing.parse_with_errors(parser, args)
77
+ errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
78
+ exit_code = errors_were_handled ? 1 : nil
79
+ return results, exit_code
80
+ end
81
+
82
+ def self.parser(parsed = {})
83
+ OptionParser.new do |opts|
84
+ opts.banner = BANNER
85
+ opts.on('--help', 'Display this command-specific help output') do |help|
86
+ parsed['help'] = true
87
+ end
88
+ opts.on('--config CONF', 'Path to puppet.conf') do |conf|
89
+ parsed['config'] = conf
90
+ end
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,289 @@
1
+ require 'optparse'
2
+ require 'openssl'
3
+ require 'set'
4
+ require 'puppetserver/ca/errors'
5
+ require 'puppetserver/ca/utils/cli_parsing'
6
+ require 'puppetserver/ca/utils/file_system'
7
+ require 'puppetserver/ca/utils/config'
8
+ require 'puppetserver/ca/x509_loader'
9
+ require 'puppetserver/ca/config/puppet'
10
+
11
+ module Puppetserver
12
+ module Ca
13
+ module Action
14
+ class Prune
15
+ include Puppetserver::Ca::Utils
16
+
17
+ SUMMARY = "Prune the local CRL on disk to remove certificate entries"
18
+ BANNER = <<-BANNER
19
+ Usage:
20
+ puppetserver ca prune [--help]
21
+ puppetserver ca prune [--config]
22
+ puppetserver ca prune [--config] [--remove-duplicates]
23
+ puppetserver ca prune [--config] [--remove-expired]
24
+ puppetserver ca prune [--config] [--remove-entries] [--serial NUMBER[,NUMBER]] [--certname NAME[,NAME]]
25
+
26
+ Description:
27
+ Prune the list of revoked certificates. If no options are provided or
28
+ --remove-duplicates is specified, prune CRL of any duplicate entries.
29
+ If --remove-expired is specified, remove expired entries from CRL.
30
+ If --remove-entries is specified, remove matching entries provided by
31
+ --serial and/or --certname values. This command will only prune the CRL
32
+ issued by Puppet's CA cert.
33
+
34
+ Options:
35
+ BANNER
36
+
37
+ def initialize(logger)
38
+ @logger = logger
39
+ end
40
+
41
+ def run(inputs)
42
+ config_path = inputs['config']
43
+ remove_duplicates = inputs['remove-duplicates']
44
+ remove_expired = inputs['remove-expired']
45
+ remove_entries = inputs['remove-entries']
46
+ serialnumbers = inputs['serial']
47
+ certnames = inputs['certname']
48
+ exit_code = 0
49
+
50
+ # Validate the config path.
51
+ if config_path
52
+ errors = FileSystem.validate_file_paths(config_path)
53
+ return 1 if Errors.handle_with_usage(@logger, errors)
54
+ end
55
+
56
+ # Validate puppet config setting.
57
+ puppet = Config::Puppet.new(config_path)
58
+ puppet.load(logger: @logger)
59
+ return 1 if Errors.handle_with_usage(@logger, puppet.errors)
60
+
61
+ # Validate arguments
62
+ if (remove_entries && (!serialnumbers && !certnames))
63
+ return 1 if Errors.handle_with_usage(@logger,["--remove-entries option require --serial or --certname values"])
64
+ end
65
+
66
+ # Validate that we are offline
67
+ return 1 if HttpClient.check_server_online(puppet.settings, @logger)
68
+
69
+ # Getting the CRL(s)
70
+ loader = X509Loader.new(puppet.settings[:cacert], puppet.settings[:cakey], puppet.settings[:cacrl])
71
+ inventory_file = puppet.settings[:cert_inventory]
72
+ cadir = puppet.settings[:cadir]
73
+
74
+ verified_crls = loader.crls.select { |crl| crl.verify(loader.key) }
75
+ number_of_removed_duplicates = 0
76
+ number_of_removed_crl_entries = 0
77
+
78
+ if verified_crls.length == 1
79
+ puppet_crl = verified_crls.first
80
+ @logger.inform("Total number of certificates found in Puppet's CRL is: #{puppet_crl.revoked.length}.")
81
+
82
+ if remove_entries
83
+ if serialnumbers
84
+ number_of_removed_crl_entries += prune_using_serial(puppet_crl, loader.key, serialnumbers)
85
+ end
86
+ if certnames
87
+ number_of_removed_crl_entries += prune_using_certname(puppet_crl, loader.key, inventory_file, cadir, certnames)
88
+ end
89
+ end
90
+
91
+ if remove_expired
92
+ number_of_removed_crl_entries += prune_expired(puppet_crl, loader.key, inventory_file, cadir)
93
+ end
94
+
95
+ if (remove_duplicates || (!remove_entries && !remove_expired))
96
+ number_of_removed_duplicates += prune_CRL(puppet_crl)
97
+ end
98
+
99
+
100
+ if (number_of_removed_duplicates > 0 || number_of_removed_crl_entries > 0)
101
+ update_pruned_CRL(puppet_crl, loader.key)
102
+ FileSystem.write_file(puppet.settings[:cacrl], loader.crls, 0644)
103
+ @logger.inform("Removed #{number_of_removed_duplicates} duplicated certs from Puppet's CRL.") if number_of_removed_duplicates > 0
104
+ @logger.inform("Removed #{number_of_removed_crl_entries} certs from Puppet's CRL.") if number_of_removed_crl_entries > 0
105
+ else
106
+ @logger.inform("No matching revocations found in the CRL for pruning")
107
+ end
108
+ else
109
+ @logger.err("Could not identify Puppet's CRL. Aborting prune action.")
110
+ exit_code = 1
111
+ end
112
+ return exit_code
113
+ end
114
+
115
+ def prune_CRL(crl)
116
+ number_of_removed_duplicates = 0
117
+
118
+ existed_serial_number = Set.new()
119
+ revoked_list = crl.revoked
120
+ @logger.debug("Pruning duplicate entries in CRL for issuer " \
121
+ "#{crl.issuer.to_s(OpenSSL::X509::Name::RFC2253)}") if @logger.debug?
122
+
123
+ revoked_list.delete_if do |revoked|
124
+ if existed_serial_number.add?(revoked.serial)
125
+ false
126
+ else
127
+ number_of_removed_duplicates += 1
128
+ @logger.debug("Removing duplicate of #{revoked.serial}, " \
129
+ "revoked on #{revoked.time}\n") if @logger.debug?
130
+ true
131
+ end
132
+ end
133
+ crl.revoked=(revoked_list)
134
+
135
+ return number_of_removed_duplicates
136
+ end
137
+
138
+ def update_pruned_CRL(crl, pkey)
139
+ # Updating extensions in-place does not work with some ruby versions / implementation. Copy & recreate them.
140
+ extensions = crl.extensions
141
+ crl.extensions = []
142
+
143
+ ef = OpenSSL::X509::ExtensionFactory.new
144
+ ef.crl = crl
145
+
146
+ extensions.each do |ext|
147
+ if ext.oid == "crlNumber"
148
+ if RUBY_ENGINE == "jruby"
149
+ # Creating a crlNumber extension without an ExtensionFactory produce incorrect result on jruby
150
+ crl.add_extension(ef.create_extension("crlNumber", ext.value.next))
151
+ else
152
+ # Creating a crlNumber extension with an ExtensionFactory rais on exception on MRI
153
+ crl.add_extension(OpenSSL::X509::Extension.new("crlNumber", ext.value.next))
154
+ end
155
+ else
156
+ crl.add_extension(ext)
157
+ end
158
+ end
159
+
160
+ crl.sign(pkey, OpenSSL::Digest::SHA256.new)
161
+ end
162
+
163
+ def prune_using_serial(crl, key, serialnumbers)
164
+ removed_serials = []
165
+ revoked_list = crl.revoked
166
+ @logger.debug("Removing entries in CRL for issuer " \
167
+ "#{crl.issuer.to_s(OpenSSL::X509::Name::RFC2253)}") if @logger.debug?
168
+ serialnumbers.each do |serial|
169
+ if serial.match(/^(?:0[xX])?[A-Fa-f0-9]+$/)
170
+ revoked_list.delete_if do |revoked|
171
+ if revoked.serial == OpenSSL::BN.new(serial.hex)
172
+ removed_serials.push(serial)
173
+ true
174
+ end
175
+ end
176
+ end
177
+ end
178
+ crl.revoked = (revoked_list)
179
+ @logger.debug("Removed these CRL entries : #{removed_serials}") if @logger.debug?
180
+ return removed_serials.length
181
+ end
182
+
183
+ def prune_using_certname(crl, key, inventory_file, cadir, certnames)
184
+ serialnumbers = []
185
+ @logger.debug("Checking inventory file #{inventory_file} for matching cert names") if @logger.debug?
186
+ errors = FileSystem.validate_file_paths(inventory_file)
187
+ if errors.empty?
188
+ File.open(inventory_file).each_line do |line|
189
+ certnames.each do |certname|
190
+ if line.match(/\/CN=#{certname}$/) && line.split.length == 4
191
+ serialnumbers.push(line.split.first)
192
+ certnames.delete(certname)
193
+ end
194
+ end
195
+ end
196
+ else
197
+ @logger.warn "Reading inventory file at #{inventory_file} failed with error #{errors}"
198
+ end
199
+ if certnames
200
+ @logger.debug("Checking CA dir #{cadir} for matching cert names")
201
+ certnames.each do |certname|
202
+ cert_file = "#{cadir}/signed/#{certname}.pem"
203
+ if File.file?(cert_file)
204
+ raw = File.read(cert_file)
205
+ certificate = OpenSSL::X509::Certificate.new(raw)
206
+ serial = certificate.serial
207
+ serialnumbers.push(serial.to_s(16))
208
+ end
209
+ end
210
+ prune_using_serial(crl, key, serialnumbers)
211
+ end
212
+ end
213
+
214
+ def prune_expired (crl, key, inventory_file, cadir)
215
+ serialnumbers = []
216
+ signed_dir = "#{cadir}/signed"
217
+ @logger.debug("Checking inventory file #{inventory_file} for expired entries") if @logger.debug?
218
+ errors = FileSystem.validate_file_paths(inventory_file)
219
+ if errors.empty?
220
+ File.open(inventory_file).each_line do |line|
221
+ if line.match(/\/CN=.*$/) && line.split.length == 4
222
+ not_after = line.split[2]
223
+ begin
224
+ not_after = Time.parse(line.split[2])
225
+ serialnumbers.push(line.split.first) if not_after < Time.now
226
+ rescue ArgumentError
227
+ @logger.warn "Invalid not_after time found in inventory.txt file at #{line}"
228
+ next
229
+ end
230
+ end
231
+ end
232
+ else
233
+ @logger.warn "Reading inventory file at #{inventory_file} failed with error #{errors}"
234
+ end
235
+ @logger.debug("Checking CA dir #{cadir} for expired certs")
236
+ Dir.foreach(signed_dir) do |filename|
237
+ if File.extname(filename) == '.pem'
238
+ raw = File.read("#{signed_dir}/#{filename}")
239
+ certificate = OpenSSL::X509::Certificate.new(raw)
240
+ serialnumbers.push(certificate.serial.to_s(16)) if certificate.not_after < Time.now
241
+ end
242
+ end
243
+ prune_using_serial(crl, key, serialnumbers)
244
+ end
245
+
246
+ def self.parser(parsed = {})
247
+ OptionParser.new do |opts|
248
+ opts.banner = BANNER
249
+ opts.on('--help', 'Display this command-specific help output') do |help|
250
+ parsed['help'] = true
251
+ end
252
+ opts.on('--config CONF', 'Path to the puppet.conf file on disk') do |conf|
253
+ parsed['config'] = conf
254
+ end
255
+ opts.on('--remove-duplicates', 'Remove duplicate entries from CRL(default)') do |remove_duplicates|
256
+ parsed['remove-duplicates'] = true
257
+ end
258
+ opts.on('--remove-expired', 'Remove expired entries from CRL') do |remove_expired|
259
+ parsed['remove-expired'] = true
260
+ end
261
+ opts.on('--remove-entries', 'Remove entries from CRL') do |remove_entries|
262
+ parsed['remove-entries'] = true
263
+ end
264
+ opts.on('--serial NUMBER[,NUMBER]', Array, 'Serial numbers(s) in HEX to be removed from CRL') do |serialnumbers|
265
+ parsed['serial'] = serialnumbers
266
+ end
267
+ opts.on('--certname NAME[,NAME]', Array, 'Name(s) of the cert(s) to be removed from CRL') do |certnames|
268
+ parsed['certname'] = certnames
269
+ end
270
+ end
271
+ end
272
+
273
+ def parse(args)
274
+ results = {}
275
+ parser = self.class.parser(results)
276
+ errors = CliParsing.parse_with_errors(parser, args)
277
+ errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
278
+
279
+ if errors_were_handled
280
+ exit_code = 1
281
+ else
282
+ exit_code = nil
283
+ end
284
+ return results, exit_code
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end