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.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +17 -0
- data/.github/release.yml +41 -0
- data/.github/workflows/gem_release.yaml +106 -0
- data/.github/workflows/prepare_release.yml +28 -0
- data/.github/workflows/release.yml +28 -0
- data/.github/workflows/unit_tests.yaml +45 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +15 -0
- data/CODEOWNERS +4 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +20 -0
- data/LICENSE +202 -0
- data/README.md +118 -0
- data/Rakefile +30 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/puppetserver-ca +10 -0
- data/lib/puppetserver/ca/action/clean.rb +109 -0
- data/lib/puppetserver/ca/action/delete.rb +286 -0
- data/lib/puppetserver/ca/action/enable.rb +140 -0
- data/lib/puppetserver/ca/action/generate.rb +330 -0
- data/lib/puppetserver/ca/action/import.rb +196 -0
- data/lib/puppetserver/ca/action/list.rb +253 -0
- data/lib/puppetserver/ca/action/migrate.rb +97 -0
- data/lib/puppetserver/ca/action/prune.rb +289 -0
- data/lib/puppetserver/ca/action/revoke.rb +108 -0
- data/lib/puppetserver/ca/action/setup.rb +188 -0
- data/lib/puppetserver/ca/action/sign.rb +146 -0
- data/lib/puppetserver/ca/certificate_authority.rb +418 -0
- data/lib/puppetserver/ca/cli.rb +145 -0
- data/lib/puppetserver/ca/config/puppet.rb +309 -0
- data/lib/puppetserver/ca/config/puppetserver.rb +84 -0
- data/lib/puppetserver/ca/errors.rb +40 -0
- data/lib/puppetserver/ca/host.rb +176 -0
- data/lib/puppetserver/ca/local_certificate_authority.rb +304 -0
- data/lib/puppetserver/ca/logger.rb +49 -0
- data/lib/puppetserver/ca/stub.rb +17 -0
- data/lib/puppetserver/ca/utils/cli_parsing.rb +67 -0
- data/lib/puppetserver/ca/utils/config.rb +61 -0
- data/lib/puppetserver/ca/utils/file_system.rb +109 -0
- data/lib/puppetserver/ca/utils/http_client.rb +232 -0
- data/lib/puppetserver/ca/utils/inventory.rb +84 -0
- data/lib/puppetserver/ca/utils/signing_digest.rb +27 -0
- data/lib/puppetserver/ca/version.rb +5 -0
- data/lib/puppetserver/ca/x509_loader.rb +170 -0
- data/lib/puppetserver/ca.rb +7 -0
- data/openvoxserver-ca.gemspec +31 -0
- data/tasks/spec.rake +15 -0
- data/tasks/vox.rake +19 -0
- 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
|