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,309 @@
|
|
1
|
+
require 'facter'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
require 'puppetserver/ca/utils/config'
|
5
|
+
|
6
|
+
module Puppetserver
|
7
|
+
module Ca
|
8
|
+
module Config
|
9
|
+
# Provides an interface for asking for Puppet settings w/o loading
|
10
|
+
# Puppet. Includes a simple ini parser that will ignore Puppet's
|
11
|
+
# more complicated conventions.
|
12
|
+
class Puppet
|
13
|
+
# How we convert from various units to seconds.
|
14
|
+
TTL_UNITMAP = {
|
15
|
+
# 365 days isn't technically a year, but is sufficient for most purposes
|
16
|
+
"y" => 365 * 24 * 60 * 60,
|
17
|
+
"d" => 24 * 60 * 60,
|
18
|
+
"h" => 60 * 60,
|
19
|
+
"m" => 60,
|
20
|
+
"s" => 1
|
21
|
+
}
|
22
|
+
|
23
|
+
# A regex describing valid formats with groups for capturing the value and units
|
24
|
+
TTL_FORMAT = /^(\d+)(y|d|h|m|s)?$/
|
25
|
+
|
26
|
+
def self.parse(config_path, logger)
|
27
|
+
instance = new(config_path)
|
28
|
+
instance.load(logger: logger)
|
29
|
+
|
30
|
+
return instance
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :errors, :settings
|
34
|
+
|
35
|
+
def initialize(supplied_config_path = nil)
|
36
|
+
@using_default_location = !supplied_config_path
|
37
|
+
@config_path = supplied_config_path || user_specific_puppet_config
|
38
|
+
|
39
|
+
@settings = nil
|
40
|
+
@errors = []
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return the correct confdir. We check for being root on *nix,
|
44
|
+
# else the user path. We do not include a check for running
|
45
|
+
# as Adminstrator since non-development scenarios for Puppet Server
|
46
|
+
# on Windows are unsupported.
|
47
|
+
# Note that Puppet Server runs as the [pe-]puppet user but to
|
48
|
+
# start/stop it you must be root.
|
49
|
+
def user_specific_puppet_confdir
|
50
|
+
@user_specific_puppet_confdir ||= Puppetserver::Ca::Utils::Config.puppet_confdir
|
51
|
+
end
|
52
|
+
|
53
|
+
def user_specific_puppet_config
|
54
|
+
user_specific_puppet_confdir + '/puppet.conf'
|
55
|
+
end
|
56
|
+
|
57
|
+
def load(cli_overrides: {}, logger:, ca_dir_warn: true)
|
58
|
+
if explicitly_given_config_file_or_default_config_exists?
|
59
|
+
results = parse_text(File.read(@config_path))
|
60
|
+
end
|
61
|
+
|
62
|
+
results ||= {}
|
63
|
+
results[:main] ||= {}
|
64
|
+
# The [master] config section is deprecated
|
65
|
+
# We now favor [server], but support both for backwards compatibility
|
66
|
+
results[:master] ||= {}
|
67
|
+
results[:server] ||= {}
|
68
|
+
results[:agent] ||= {}
|
69
|
+
|
70
|
+
overrides = results[:agent].merge(results[:main]).merge(results[:master]).merge(results[:server])
|
71
|
+
overrides.merge!(cli_overrides)
|
72
|
+
if overrides[:masterport]
|
73
|
+
overrides[:serverport] ||= overrides.delete(:masterport)
|
74
|
+
end
|
75
|
+
|
76
|
+
@settings = resolve_settings(overrides, logger, ca_dir_warn: ca_dir_warn).freeze
|
77
|
+
end
|
78
|
+
|
79
|
+
def default_certname
|
80
|
+
hostname = Facter.value(:hostname)
|
81
|
+
domain = Facter.value(:domain)
|
82
|
+
if domain and domain != ''
|
83
|
+
fqdn = [hostname, domain].join('.')
|
84
|
+
else
|
85
|
+
fqdn = hostname
|
86
|
+
end
|
87
|
+
fqdn.chomp('.')
|
88
|
+
end
|
89
|
+
|
90
|
+
# Resolve settings from default values, with any overrides for the
|
91
|
+
# specific settings or their dependent settings (ssldir, cadir) taken into account.
|
92
|
+
def resolve_settings(overrides = {}, logger, ca_dir_warn: true)
|
93
|
+
unresolved_setting = /\$[a-z_]+/
|
94
|
+
|
95
|
+
# Returning the key for unknown keys (rather than nil) is required to
|
96
|
+
# keep unknown settings in the string for later verification.
|
97
|
+
substitutions = Hash.new {|h, k| k }
|
98
|
+
settings = {}
|
99
|
+
|
100
|
+
# Order for base settings here matters!
|
101
|
+
# These need to be evaluated before we can construct their dependent
|
102
|
+
# defaults below
|
103
|
+
base_defaults = [
|
104
|
+
[:confdir, user_specific_puppet_confdir],
|
105
|
+
[:ssldir,'$confdir/ssl'],
|
106
|
+
[:certdir, '$ssldir/certs'],
|
107
|
+
[:certname, default_certname],
|
108
|
+
[:server, 'puppet'],
|
109
|
+
[:serverport, '8140'],
|
110
|
+
[:privatekeydir, '$ssldir/private_keys'],
|
111
|
+
[:publickeydir, '$ssldir/public_keys'],
|
112
|
+
]
|
113
|
+
|
114
|
+
dependent_defaults = {
|
115
|
+
:ca_name => 'Puppet CA: $certname',
|
116
|
+
:root_ca_name => "Puppet Root CA: #{SecureRandom.hex(7)}",
|
117
|
+
:keylength => 4096,
|
118
|
+
:cacert => '$cadir/ca_crt.pem',
|
119
|
+
:cakey => '$cadir/ca_key.pem',
|
120
|
+
:capub => '$cadir/ca_pub.pem',
|
121
|
+
:csr_attributes => '$confdir/csr_attributes.yaml',
|
122
|
+
:rootkey => '$cadir/root_key.pem',
|
123
|
+
:cacrl => '$cadir/ca_crl.pem',
|
124
|
+
:serial => '$cadir/serial',
|
125
|
+
:cert_inventory => '$cadir/inventory.txt',
|
126
|
+
:ca_server => '$server',
|
127
|
+
:ca_port => '$serverport',
|
128
|
+
:localcacert => '$certdir/ca.pem',
|
129
|
+
:hostcrl => '$ssldir/crl.pem',
|
130
|
+
:hostcert => '$certdir/$certname.pem',
|
131
|
+
:hostprivkey => '$privatekeydir/$certname.pem',
|
132
|
+
:hostpubkey => '$publickeydir/$certname.pem',
|
133
|
+
:ca_ttl => '15y',
|
134
|
+
:certificate_revocation => 'true',
|
135
|
+
:signeddir => '$cadir/signed',
|
136
|
+
:server_list => '',
|
137
|
+
}
|
138
|
+
|
139
|
+
# This loops through the base defaults and gives each setting a
|
140
|
+
# default if the value isn't specified in the config file. Default
|
141
|
+
# values given may depend upon the value of a previous base setting,
|
142
|
+
# thus the creation of the substitution hash.
|
143
|
+
base_defaults.each do |setting_name, default_value|
|
144
|
+
substitution_name = '$' + setting_name.to_s
|
145
|
+
setting_value = overrides.fetch(setting_name, default_value)
|
146
|
+
subbed_value = setting_value.sub(unresolved_setting, substitutions)
|
147
|
+
settings[setting_name] = substitutions[substitution_name] = subbed_value
|
148
|
+
end
|
149
|
+
|
150
|
+
cadir = find_cadir(overrides.fetch(:cadir, false),
|
151
|
+
settings[:confdir],
|
152
|
+
settings[:ssldir],
|
153
|
+
logger,
|
154
|
+
ca_dir_warn)
|
155
|
+
settings[:cadir] = substitutions['$cadir'] = cadir
|
156
|
+
|
157
|
+
|
158
|
+
dependent_defaults.each do |setting_name, default_value|
|
159
|
+
setting_value = overrides.fetch(setting_name, default_value)
|
160
|
+
settings[setting_name] = setting_value
|
161
|
+
end
|
162
|
+
|
163
|
+
# If subject-alt-names are provided, we need to add the certname in addition
|
164
|
+
overrides[:dns_alt_names] << ',$certname' if overrides[:dns_alt_names]
|
165
|
+
|
166
|
+
# rename dns_alt_names to subject_alt_names now that we support IP alt names
|
167
|
+
settings[:subject_alt_names] = overrides.fetch(:dns_alt_names, "")
|
168
|
+
|
169
|
+
# Some special cases where we need to manipulate config settings:
|
170
|
+
settings[:ca_ttl] = munge_ttl_setting(settings[:ca_ttl])
|
171
|
+
settings[:certificate_revocation] = parse_crl_usage(settings[:certificate_revocation])
|
172
|
+
settings[:subject_alt_names] = Puppetserver::Ca::Utils::Config.munge_alt_names(settings[:subject_alt_names])
|
173
|
+
settings[:keylength] = settings[:keylength].to_i
|
174
|
+
settings[:server_list] = settings[:server_list].
|
175
|
+
split(/\s*,\s*/).
|
176
|
+
map {|entry| entry.split(":") }
|
177
|
+
|
178
|
+
update_for_server_list!(settings)
|
179
|
+
|
180
|
+
settings.each do |key, value|
|
181
|
+
next unless value.is_a? String
|
182
|
+
settings[key] = value.gsub(unresolved_setting, substitutions)
|
183
|
+
if match = settings[key].match(unresolved_setting)
|
184
|
+
@errors << "Could not parse #{match[0]} in #{value}, " +
|
185
|
+
'valid settings to be interpolated are ' +
|
186
|
+
'$ssldir, $certdir, $cadir, $certname, $server, or $masterport'
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
return settings
|
191
|
+
end
|
192
|
+
|
193
|
+
# Parse an inifile formatted String. Only captures \word character
|
194
|
+
# class keys/section names but nearly any character values (excluding
|
195
|
+
# leading whitespace) up to one of whitespace, opening curly brace, or
|
196
|
+
# hash sign (Our concern being to capture filesystem path values).
|
197
|
+
#
|
198
|
+
# ca_root and root_ca_name values may include whitespace
|
199
|
+
#
|
200
|
+
# Put values without a section into :main.
|
201
|
+
#
|
202
|
+
# Return Hash of Symbol section names with Symbol setting keys and
|
203
|
+
# String values.
|
204
|
+
def parse_text(text)
|
205
|
+
res = {}
|
206
|
+
current_section = :main
|
207
|
+
text.each_line do |line|
|
208
|
+
case line
|
209
|
+
when /^\s*\[(\w+)\].*/
|
210
|
+
current_section = $1.to_sym
|
211
|
+
when /^\s*(\w+)\s*=\s*(.+?)\s*(?=[{#]|$)/
|
212
|
+
# Using a Hash with a default key breaks RSpec expectations.
|
213
|
+
res[current_section] ||= {}
|
214
|
+
res[current_section][$1.to_sym] =
|
215
|
+
if [:ca_name, :root_ca_name].include?($1.to_sym)
|
216
|
+
$2
|
217
|
+
else
|
218
|
+
$2.split(' ')[0]
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
res
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
|
229
|
+
def find_cadir(configured_cadir, confdir, ssldir, logger, ca_dir_warn)
|
230
|
+
warning = 'The cadir is currently configured to be inside the ' +
|
231
|
+
'%{ssldir} directory. This config setting and the directory ' +
|
232
|
+
'location will not be used in a future version of puppet. ' +
|
233
|
+
'Please run the puppetserver ca tool to migrate out from the ' +
|
234
|
+
'puppet confdir to the /etc/puppetlabs/puppetserver/ca directory. ' +
|
235
|
+
'Use `puppetserver ca migrate --help` for more info.'
|
236
|
+
|
237
|
+
if configured_cadir
|
238
|
+
if ca_dir_warn && configured_cadir.start_with?(ssldir)
|
239
|
+
logger.warn(warning % {ssldir: ssldir})
|
240
|
+
end
|
241
|
+
configured_cadir
|
242
|
+
|
243
|
+
else
|
244
|
+
old_cadir = Puppetserver::Ca::Utils::Config.old_default_cadir(confdir)
|
245
|
+
new_cadir = Puppetserver::Ca::Utils::Config.new_default_cadir(confdir)
|
246
|
+
if File.exist?(old_cadir) && !File.symlink?(old_cadir)
|
247
|
+
logger.warn(warning % {ssldir: ssldir}) if ca_dir_warn
|
248
|
+
old_cadir
|
249
|
+
else
|
250
|
+
new_cadir
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def explicitly_given_config_file_or_default_config_exists?
|
256
|
+
!@using_default_location || File.exist?(@config_path)
|
257
|
+
end
|
258
|
+
|
259
|
+
def run(command)
|
260
|
+
%x( #{command} )
|
261
|
+
end
|
262
|
+
|
263
|
+
# Convert the value to Numeric, parsing numeric string with units if necessary.
|
264
|
+
def munge_ttl_setting(ca_ttl_setting)
|
265
|
+
case
|
266
|
+
when ca_ttl_setting.is_a?(Numeric)
|
267
|
+
if ca_ttl_setting < 0
|
268
|
+
@errors << "Invalid negative 'time to live' #{ca_ttl_setting.inspect} - did you mean 'unlimited'?"
|
269
|
+
end
|
270
|
+
ca_ttl_setting
|
271
|
+
|
272
|
+
when ca_ttl_setting == 'unlimited'
|
273
|
+
Float::INFINITY
|
274
|
+
|
275
|
+
when (ca_ttl_setting.is_a?(String) and ca_ttl_setting =~ TTL_FORMAT)
|
276
|
+
$1.to_i * TTL_UNITMAP[$2 || 's']
|
277
|
+
else
|
278
|
+
@errors << "Invalid 'time to live' format '#{ca_ttl_setting.inspect}' for parameter: :ca_ttl"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def parse_crl_usage(setting)
|
283
|
+
case setting.to_s
|
284
|
+
when 'true', 'chain'
|
285
|
+
:chain
|
286
|
+
when 'leaf'
|
287
|
+
:leaf
|
288
|
+
when 'false'
|
289
|
+
:ignore
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def update_for_server_list!(settings)
|
294
|
+
if settings.dig(:server_list, 0, 0) &&
|
295
|
+
settings[:ca_server] == '$server'
|
296
|
+
|
297
|
+
settings[:ca_server] = settings.dig(:server_list, 0, 0)
|
298
|
+
end
|
299
|
+
|
300
|
+
if settings.dig(:server_list, 0, 1) &&
|
301
|
+
settings[:ca_port] == '$serverport'
|
302
|
+
|
303
|
+
settings[:ca_port] = settings.dig(:server_list, 0, 1)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'hocon'
|
2
|
+
|
3
|
+
require 'puppetserver/ca/utils/config'
|
4
|
+
|
5
|
+
module Puppetserver
|
6
|
+
module Ca
|
7
|
+
module Config
|
8
|
+
# Provides an interface for querying Puppetserver settings w/o loading
|
9
|
+
# Puppetserver or any TK config service. Uses the ruby-hocon gem for parsing.
|
10
|
+
class PuppetServer
|
11
|
+
|
12
|
+
def self.parse(config_path = nil)
|
13
|
+
instance = new(config_path)
|
14
|
+
instance.load
|
15
|
+
|
16
|
+
return instance
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :errors, :settings
|
20
|
+
|
21
|
+
def initialize(supplied_config_path = nil)
|
22
|
+
@using_default_location = !supplied_config_path
|
23
|
+
@config_path = supplied_config_path || "/etc/puppetlabs/puppetserver/conf.d/ca.conf"
|
24
|
+
|
25
|
+
@settings = nil
|
26
|
+
@errors = []
|
27
|
+
end
|
28
|
+
|
29
|
+
# Populate this config object with the CA-related settings
|
30
|
+
def load
|
31
|
+
if explicitly_given_config_file_or_default_config_exists?
|
32
|
+
begin
|
33
|
+
results = Hocon.load(@config_path)
|
34
|
+
rescue Hocon::ConfigError => e
|
35
|
+
errors << e.message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
overrides = results || {}
|
40
|
+
@settings = supply_defaults(overrides).freeze
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Return the correct confdir. We check for being root on *nix,
|
46
|
+
# else the user path. We do not include a check for running
|
47
|
+
# as Adminstrator since non-development scenarios for Puppet Server
|
48
|
+
# on Windows are unsupported.
|
49
|
+
# Note that Puppet Server runs as the [pe-]puppet user but to
|
50
|
+
# start/stop it you must be root.
|
51
|
+
def user_specific_ca_dir
|
52
|
+
if Puppetserver::Ca::Utils::Config.running_as_root?
|
53
|
+
'/etc/puppetlabs/puppetserver/ca'
|
54
|
+
else
|
55
|
+
"#{ENV['HOME']}/.puppetlabs/etc/puppetserver/ca"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Supply defaults for any CA settings not present in the config file
|
60
|
+
# @param [Hash] overrides setting names and values loaded from the config file,
|
61
|
+
# for overriding the defaults
|
62
|
+
# @return [Hash] CA-related settings
|
63
|
+
def supply_defaults(overrides = {})
|
64
|
+
ca_settings = overrides['certificate-authority'] || {}
|
65
|
+
settings = {}
|
66
|
+
|
67
|
+
cadir = settings[:cadir] = ca_settings.fetch('cadir', user_specific_ca_dir)
|
68
|
+
|
69
|
+
settings[:cacert] = ca_settings.fetch('cacert', "#{cadir}/ca_crt.pem")
|
70
|
+
settings[:cakey] = ca_settings.fetch('cakey', "#{cadir}/ca_key.pem")
|
71
|
+
settings[:cacrl] = ca_settings.fetch('cacrl', "#{cadir}/ca_crl.pem")
|
72
|
+
settings[:serial] = ca_settings.fetch('serial', "#{cadir}/serial")
|
73
|
+
settings[:cert_inventory] = ca_settings.fetch('cert-inventory', "#{cadir}/inventory.txt")
|
74
|
+
|
75
|
+
return settings
|
76
|
+
end
|
77
|
+
|
78
|
+
def explicitly_given_config_file_or_default_config_exists?
|
79
|
+
!@using_default_location || File.exist?(@config_path)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Puppetserver
|
2
|
+
module Ca
|
3
|
+
class Error < StandardError
|
4
|
+
def self.create(ex, msg)
|
5
|
+
created = new(msg)
|
6
|
+
created.wrap(ex)
|
7
|
+
|
8
|
+
created
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :wrapped
|
12
|
+
|
13
|
+
def wrap(ex)
|
14
|
+
@wrapped = ex
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class FileNotFound < Error; end
|
19
|
+
class InvalidX509Object < Error; end
|
20
|
+
class ConnectionFailed < Error; end
|
21
|
+
|
22
|
+
module Errors
|
23
|
+
def self.handle_with_usage(log, errors, usage = nil)
|
24
|
+
unless errors.empty?
|
25
|
+
log.err 'Error:'
|
26
|
+
errors.each {|e| log.err e }
|
27
|
+
|
28
|
+
if usage
|
29
|
+
log.err ''
|
30
|
+
log.err usage
|
31
|
+
end
|
32
|
+
|
33
|
+
return true
|
34
|
+
else
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'openssl'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Puppetserver
|
6
|
+
module Ca
|
7
|
+
class Host
|
8
|
+
# Exclude OIDs that may conflict with how Puppet creates CSRs.
|
9
|
+
#
|
10
|
+
# We only have nominal support for Microsoft extension requests, but since we
|
11
|
+
# ultimately respect that field when looking for extension requests in a CSR
|
12
|
+
# we need to prevent that field from being written to directly.
|
13
|
+
PRIVATE_CSR_ATTRIBUTES = [
|
14
|
+
'extReq', '1.2.840.113549.1.9.14',
|
15
|
+
'msExtReq', '1.3.6.1.4.1.311.2.1.14',
|
16
|
+
]
|
17
|
+
|
18
|
+
PRIVATE_EXTENSIONS = [
|
19
|
+
'subjectAltName', '2.5.29.17',
|
20
|
+
]
|
21
|
+
|
22
|
+
# A mapping of Puppet extension short names to their OIDs. These appear in
|
23
|
+
# csr_attributes.yaml.
|
24
|
+
PUPPET_SHORT_NAMES =
|
25
|
+
{'pp_uuid' => "1.3.6.1.4.1.34380.1.1.1",
|
26
|
+
'pp_instance_id' => "1.3.6.1.4.1.34380.1.1.2",
|
27
|
+
'pp_image_name' => "1.3.6.1.4.1.34380.1.1.3",
|
28
|
+
'pp_preshared_key'=> "1.3.6.1.4.1.34380.1.1.4",
|
29
|
+
'pp_cost_center' => "1.3.6.1.4.1.34380.1.1.5",
|
30
|
+
'pp_product' => "1.3.6.1.4.1.34380.1.1.6",
|
31
|
+
'pp_project' => "1.3.6.1.4.1.34380.1.1.7",
|
32
|
+
'pp_application' => "1.3.6.1.4.1.34380.1.1.8",
|
33
|
+
'pp_service'=> "1.3.6.1.4.1.34380.1.1.9",
|
34
|
+
'pp_employee' => "1.3.6.1.4.1.34380.1.1.10",
|
35
|
+
'pp_created_by' => "1.3.6.1.4.1.34380.1.1.11",
|
36
|
+
'pp_environment' => "1.3.6.1.4.1.34380.1.1.12",
|
37
|
+
'pp_role' => "1.3.6.1.4.1.34380.1.1.13",
|
38
|
+
'pp_software_version' => "1.3.6.1.4.1.34380.1.1.14",
|
39
|
+
'pp_department' => "1.3.6.1.4.1.34380.1.1.15",
|
40
|
+
'pp_cluster' => "1.3.6.1.4.1.34380.1.1.16",
|
41
|
+
'pp_provisioner' => "1.3.6.1.4.1.34380.1.1.17",
|
42
|
+
'pp_region' => "1.3.6.1.4.1.34380.1.1.18",
|
43
|
+
'pp_datacenter' => "1.3.6.1.4.1.34380.1.1.19",
|
44
|
+
'pp_zone' => "1.3.6.1.4.1.34380.1.1.20",
|
45
|
+
'pp_network' => "1.3.6.1.4.1.34380.1.1.21",
|
46
|
+
'pp_securitypolicy' => "1.3.6.1.4.1.34380.1.1.22",
|
47
|
+
'pp_cloudplatform' => "1.3.6.1.4.1.34380.1.1.23",
|
48
|
+
'pp_apptier' => "1.3.6.1.4.1.34380.1.1.24",
|
49
|
+
'pp_hostname' => "1.3.6.1.4.1.34380.1.1.25",
|
50
|
+
'pp_owner' => "1.3.6.1.4.1.34380.1.1.26",
|
51
|
+
'pp_authorization' => "1.3.6.1.4.1.34380.1.3.1",
|
52
|
+
'pp_auth_role' => "1.3.6.1.4.1.34380.1.3.13"}
|
53
|
+
|
54
|
+
attr_reader :errors
|
55
|
+
|
56
|
+
def initialize(digest)
|
57
|
+
@digest = digest
|
58
|
+
@errors = []
|
59
|
+
end
|
60
|
+
|
61
|
+
# If both the private and public keys exist for a server then we want
|
62
|
+
# to honor them here, if only one key exists we want to surface an error,
|
63
|
+
# and if neither exist we generate a new key. This logic is necessary for
|
64
|
+
# proper bootstrapping for certain server workflows.
|
65
|
+
def create_private_key(keylength, private_path = '', public_path = '')
|
66
|
+
if File.exist?(private_path) && File.exist?(public_path)
|
67
|
+
return OpenSSL::PKey.read(File.read(private_path))
|
68
|
+
elsif !File.exist?(private_path) && !File.exist?(public_path)
|
69
|
+
return OpenSSL::PKey::RSA.new(keylength)
|
70
|
+
elsif !File.exist?(private_path) && File.exist?(public_path)
|
71
|
+
@errors << "Missing private key to match public key at #{public_path}"
|
72
|
+
return nil
|
73
|
+
elsif File.exist?(private_path) && !File.exist?(public_path)
|
74
|
+
@errors << "Missing public key to match private key at #{private_path}"
|
75
|
+
return nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def create_csr(name:, key:, cli_extensions: [], csr_attributes_path: '')
|
80
|
+
csr = OpenSSL::X509::Request.new
|
81
|
+
csr.public_key = key.public_key
|
82
|
+
csr.subject = OpenSSL::X509::Name.new([["CN", name]])
|
83
|
+
|
84
|
+
custom_attributes = get_custom_attributes(csr_attributes_path)
|
85
|
+
extension_requests = get_extension_requests(csr_attributes_path)
|
86
|
+
|
87
|
+
add_csr_attributes(csr, custom_attributes)
|
88
|
+
add_csr_extensions(csr, extension_requests, cli_extensions)
|
89
|
+
|
90
|
+
csr.sign(key, @digest) if @errors.empty?
|
91
|
+
|
92
|
+
csr
|
93
|
+
end
|
94
|
+
|
95
|
+
def extension_attribute(extensions)
|
96
|
+
seq = OpenSSL::ASN1::Sequence(extensions)
|
97
|
+
ext_req = OpenSSL::ASN1::Set([seq])
|
98
|
+
OpenSSL::X509::Attribute.new("extReq", ext_req)
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_custom_attributes(attributes_path)
|
102
|
+
if csr_attributes = load_csr_attributes(attributes_path)
|
103
|
+
csr_attributes['custom_attributes']
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_extension_requests(attributes_path)
|
108
|
+
if csr_attributes = load_csr_attributes(attributes_path)
|
109
|
+
csr_attributes['extension_requests']
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# This loads all the custom_attributes and extension requests
|
114
|
+
# from the csr_attributes.yaml
|
115
|
+
def load_csr_attributes(attributes_path)
|
116
|
+
@custom_csr_attributes ||=
|
117
|
+
if File.exist?(attributes_path)
|
118
|
+
yaml = YAML.load_file(attributes_path)
|
119
|
+
if !yaml.is_a?(Hash)
|
120
|
+
@errors << "Invalid CSR attributes, expected instance of Hash, received instance of #{yaml.class}"
|
121
|
+
return
|
122
|
+
end
|
123
|
+
yaml
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def add_csr_attributes(csr, csr_attributes)
|
128
|
+
if csr_attributes
|
129
|
+
csr_attributes.each do |oid, value|
|
130
|
+
begin
|
131
|
+
if PRIVATE_CSR_ATTRIBUTES.include? oid
|
132
|
+
@errors << "Cannot specify CSR attribute #{oid}: conflicts with internally used CSR attribute"
|
133
|
+
end
|
134
|
+
oid = PUPPET_SHORT_NAMES[oid] || oid
|
135
|
+
encoded = OpenSSL::ASN1::PrintableString.new(value.to_s)
|
136
|
+
attr_set = OpenSSL::ASN1::Set.new([encoded])
|
137
|
+
|
138
|
+
csr.add_attribute(OpenSSL::X509::Attribute.new(oid, attr_set))
|
139
|
+
rescue OpenSSL::X509::AttributeError => e
|
140
|
+
@errors << "Cannot create CSR with attribute #{oid}: #{e.message}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def add_csr_extensions(csr, extension_requests, cli_extensions)
|
147
|
+
if extension_requests || cli_extensions.any?
|
148
|
+
extensions =
|
149
|
+
if extension_requests
|
150
|
+
validated_extensions(extension_requests) + cli_extensions
|
151
|
+
else
|
152
|
+
cli_extensions
|
153
|
+
end
|
154
|
+
csr.add_attribute(extension_attribute(extensions))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def validated_extensions(extension_requests)
|
159
|
+
extensions = []
|
160
|
+
extension_requests.each do |oid, value|
|
161
|
+
begin
|
162
|
+
if PRIVATE_EXTENSIONS.include? oid
|
163
|
+
@errors << "Cannot specify CSR extension request #{oid}: conflicts with internally used extension request"
|
164
|
+
end
|
165
|
+
oid = PUPPET_SHORT_NAMES[oid] || oid
|
166
|
+
ext = OpenSSL::X509::Extension.new(oid, OpenSSL::ASN1::UTF8String.new(value.to_s).to_der, false)
|
167
|
+
extensions << ext
|
168
|
+
rescue OpenSSL::X509::ExtensionError => e
|
169
|
+
@errors << "Cannot create CSR with extension request #{oid}: #{e.message}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
extensions
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|