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,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