puppet 6.3.0-universal-darwin → 6.4.0-universal-darwin
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puppet might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CODEOWNERS +30 -0
- data/Gemfile.lock +9 -9
- data/lib/puppet.rb +13 -0
- data/lib/puppet/application/agent.rb +8 -12
- data/lib/puppet/application/device.rb +2 -3
- data/lib/puppet/application/filebucket.rb +6 -1
- data/lib/puppet/application/ssl.rb +102 -55
- data/lib/puppet/configurer.rb +8 -7
- data/lib/puppet/defaults.rb +3 -1
- data/lib/puppet/file_system.rb +24 -4
- data/lib/puppet/file_system/file_impl.rb +25 -0
- data/lib/puppet/file_system/jruby.rb +23 -0
- data/lib/puppet/file_system/windows.rb +84 -0
- data/lib/puppet/indirector/rest.rb +4 -2
- data/lib/puppet/loaders.rb +1 -0
- data/lib/puppet/network/http.rb +1 -0
- data/lib/puppet/network/http/base_pool.rb +18 -0
- data/lib/puppet/network/http/connection.rb +49 -17
- data/lib/puppet/network/http/nocache_pool.rb +9 -4
- data/lib/puppet/network/http/pool.rb +10 -11
- data/lib/puppet/network/http/session.rb +3 -2
- data/lib/puppet/network/http_pool.rb +32 -0
- data/lib/puppet/pops/loader/generic_plan_instantiator.rb +28 -0
- data/lib/puppet/pops/loader/loader_paths.rb +46 -10
- data/lib/puppet/pops/loader/module_loaders.rb +10 -3
- data/lib/puppet/provider/file/windows.rb +49 -1
- data/lib/puppet/provider/package/windows.rb +5 -1
- data/lib/puppet/reports/http.rb +2 -1
- data/lib/puppet/rest/client.rb +7 -3
- data/lib/puppet/rest/routes.rb +9 -44
- data/lib/puppet/ssl.rb +6 -0
- data/lib/puppet/ssl/error.rb +26 -0
- data/lib/puppet/ssl/host.rb +9 -92
- data/lib/puppet/ssl/ssl_context.rb +30 -0
- data/lib/puppet/ssl/ssl_provider.rb +232 -0
- data/lib/puppet/ssl/state_machine.rb +261 -0
- data/lib/puppet/ssl/validator.rb +1 -0
- data/lib/puppet/ssl/validator/default_validator.rb +1 -0
- data/lib/puppet/ssl/validator/no_validator.rb +2 -0
- data/lib/puppet/ssl/verifier.rb +134 -0
- data/lib/puppet/ssl/verifier_adapter.rb +48 -0
- data/lib/puppet/test/test_helper.rb +2 -1
- data/lib/puppet/type/exec.rb +30 -6
- data/lib/puppet/type/file/mode.rb +6 -1
- data/lib/puppet/type/file/source.rb +2 -2
- data/lib/puppet/type/filebucket.rb +12 -8
- data/lib/puppet/type/user.rb +14 -1
- data/lib/puppet/util/connection.rb +10 -5
- data/lib/puppet/util/feature.rb +11 -2
- data/lib/puppet/util/http_proxy.rb +3 -2
- data/lib/puppet/util/pidlock.rb +1 -1
- data/lib/puppet/util/ssl.rb +1 -10
- data/lib/puppet/util/windows/security.rb +29 -8
- data/lib/puppet/version.rb +1 -1
- data/lib/puppet/x509.rb +7 -0
- data/lib/puppet/x509/cert_provider.rb +286 -0
- data/lib/puppet/x509/pem_store.rb +55 -0
- data/locales/ja/puppet.po +740 -590
- data/locales/puppet.pot +433 -208
- data/man/man5/puppet.conf.5 +6 -3
- data/man/man8/puppet-agent.8 +1 -1
- data/man/man8/puppet-apply.8 +1 -1
- data/man/man8/puppet-catalog.8 +1 -1
- data/man/man8/puppet-config.8 +1 -1
- data/man/man8/puppet-describe.8 +1 -1
- data/man/man8/puppet-device.8 +1 -1
- data/man/man8/puppet-doc.8 +1 -1
- data/man/man8/puppet-epp.8 +1 -1
- data/man/man8/puppet-facts.8 +1 -1
- data/man/man8/puppet-filebucket.8 +6 -2
- data/man/man8/puppet-generate.8 +1 -1
- data/man/man8/puppet-help.8 +1 -1
- data/man/man8/puppet-key.8 +1 -1
- data/man/man8/puppet-lookup.8 +1 -1
- data/man/man8/puppet-man.8 +1 -1
- data/man/man8/puppet-module.8 +1 -1
- data/man/man8/puppet-node.8 +1 -1
- data/man/man8/puppet-parser.8 +1 -1
- data/man/man8/puppet-plugin.8 +1 -1
- data/man/man8/puppet-report.8 +1 -1
- data/man/man8/puppet-resource.8 +1 -1
- data/man/man8/puppet-script.8 +1 -1
- data/man/man8/puppet-ssl.8 +5 -1
- data/man/man8/puppet-status.8 +1 -1
- data/man/man8/puppet.8 +2 -2
- data/spec/fixtures/ssl/127.0.0.1-key.pem +67 -0
- data/spec/fixtures/ssl/127.0.0.1.pem +48 -0
- data/spec/fixtures/ssl/bad-basic-constraints.pem +59 -0
- data/spec/fixtures/ssl/bad-int-basic-constraints.pem +59 -0
- data/spec/fixtures/ssl/ca.pem +59 -0
- data/spec/fixtures/ssl/crl.pem +30 -0
- data/spec/fixtures/ssl/encrypted-key.pem +70 -0
- data/spec/fixtures/ssl/intermediate-agent-crl.pem +31 -0
- data/spec/fixtures/ssl/intermediate-agent.pem +60 -0
- data/spec/fixtures/ssl/intermediate-crl.pem +36 -0
- data/spec/fixtures/ssl/intermediate.pem +60 -0
- data/spec/fixtures/ssl/netlock-arany-utf8.pem +23 -0
- data/spec/fixtures/ssl/pluto-key.pem +67 -0
- data/spec/fixtures/ssl/pluto.pem +44 -0
- data/spec/fixtures/ssl/request-key.pem +67 -0
- data/spec/fixtures/ssl/request.pem +39 -0
- data/spec/fixtures/ssl/revoked-key.pem +67 -0
- data/spec/fixtures/ssl/revoked.pem +44 -0
- data/spec/fixtures/ssl/signed-key.pem +67 -0
- data/spec/fixtures/ssl/signed.pem +44 -0
- data/spec/fixtures/ssl/tampered-cert.pem +44 -0
- data/spec/fixtures/ssl/tampered-csr.pem +39 -0
- data/spec/integration/network/http_pool_spec.rb +222 -0
- data/spec/integration/provider/file/windows_spec.rb +162 -0
- data/spec/integration/rest/client_spec.rb +73 -0
- data/spec/integration/type/file_spec.rb +0 -19
- data/spec/lib/puppet/test_ca.rb +87 -50
- data/spec/lib/puppet_spec/fixtures.rb +20 -0
- data/spec/lib/puppet_spec/https.rb +84 -0
- data/spec/unit/application/agent_spec.rb +29 -30
- data/spec/unit/application/device_spec.rb +12 -49
- data/spec/unit/application/ssl_spec.rb +24 -38
- data/spec/unit/configurer_spec.rb +11 -11
- data/spec/unit/file_system/uniquefile_spec.rb +6 -0
- data/spec/unit/file_system_spec.rb +214 -0
- data/spec/unit/indirector/rest_spec.rb +3 -3
- data/spec/unit/network/http/connection_spec.rb +30 -90
- data/spec/unit/network/http/factory_spec.rb +1 -0
- data/spec/unit/network/http/nocache_pool_spec.rb +8 -8
- data/spec/unit/network/http/pool_spec.rb +63 -33
- data/spec/unit/network/http/session_spec.rb +8 -1
- data/spec/unit/network/http_pool_spec.rb +36 -0
- data/spec/unit/pops/loaders/loader_spec.rb +26 -1
- data/spec/unit/provider/package/windows_spec.rb +12 -1
- data/spec/unit/reports/http_spec.rb +7 -7
- data/spec/unit/rest/client_spec.rb +4 -6
- data/spec/unit/ssl/host_spec.rb +39 -33
- data/spec/unit/ssl/ssl_provider_spec.rb +428 -0
- data/spec/unit/ssl/state_machine_spec.rb +502 -0
- data/spec/unit/ssl/verifier_spec.rb +123 -0
- data/spec/unit/type/exec_spec.rb +63 -0
- data/spec/unit/type/file/source_spec.rb +5 -5
- data/spec/unit/type/filebucket_spec.rb +8 -6
- data/spec/unit/util/feature_spec.rb +2 -2
- data/spec/unit/util/storage_spec.rb +19 -19
- data/spec/unit/x509/cert_provider_spec.rb +527 -0
- data/spec/unit/x509/pem_store_spec.rb +160 -0
- data/tasks/generate_cert_fixtures.rake +158 -0
- metadata +78 -4
- data/MAINTAINERS +0 -47
- data/lib/puppet/rest/ssl_context.rb +0 -13
data/lib/puppet/ssl/host.rb
CHANGED
@@ -3,9 +3,10 @@ require 'puppet/ssl/key'
|
|
3
3
|
require 'puppet/ssl/certificate'
|
4
4
|
require 'puppet/ssl/certificate_request'
|
5
5
|
require 'puppet/ssl/certificate_request_attributes'
|
6
|
+
require 'puppet/ssl/state_machine'
|
6
7
|
require 'puppet/rest/errors'
|
7
8
|
require 'puppet/rest/routes'
|
8
|
-
|
9
|
+
|
9
10
|
begin
|
10
11
|
# This may fail when being loaded from Puppet Server. However loading the
|
11
12
|
# client monkey patches the SSL Store and we need to have those monkey
|
@@ -131,11 +132,9 @@ class Puppet::SSL::Host
|
|
131
132
|
unless @certificate
|
132
133
|
generate_key unless key
|
133
134
|
|
134
|
-
# get
|
135
|
-
|
136
|
-
|
137
|
-
return nil
|
138
|
-
end
|
135
|
+
# get CA and optional CRL
|
136
|
+
sm = Puppet::SSL::StateMachine.new
|
137
|
+
sm.ensure_ca_certificates
|
139
138
|
|
140
139
|
cert = get_host_certificate
|
141
140
|
return nil unless cert
|
@@ -372,7 +371,7 @@ ERROR_STRING
|
|
372
371
|
def download_csr_from_ca
|
373
372
|
begin
|
374
373
|
body = Puppet::Rest::Routes.get_certificate_request(
|
375
|
-
name, Puppet::
|
374
|
+
name, Puppet::SSL::SSLContext.new(store: ssl_store))
|
376
375
|
begin
|
377
376
|
Puppet::SSL::CertificateRequest.from_s(body)
|
378
377
|
rescue OpenSSL::X509::RequestError => e
|
@@ -390,7 +389,7 @@ ERROR_STRING
|
|
390
389
|
# @param [Puppet::SSL::CertificateRequest] csr the request to submit
|
391
390
|
def submit_certificate_request(csr)
|
392
391
|
Puppet::Rest::Routes.put_certificate_request(
|
393
|
-
csr.render, name, Puppet::
|
392
|
+
csr.render, name, Puppet::SSL::SSLContext.new(store: ssl_store))
|
394
393
|
end
|
395
394
|
|
396
395
|
def save_certificate_request(csr)
|
@@ -423,56 +422,6 @@ ERROR_STRING
|
|
423
422
|
process_crl_string(crls_pems)
|
424
423
|
end
|
425
424
|
|
426
|
-
# Ensures that the CA certificate is available for either generating or
|
427
|
-
# validating the host's cert.
|
428
|
-
# It will first check on disk, then try to download it.
|
429
|
-
# @raise [Puppet::Error] if text form of found certificate bundle is invalid
|
430
|
-
# and cannot be loaded into cert objects
|
431
|
-
# @return [Boolean] true if the CA certificate was found, false otherwise
|
432
|
-
def ensure_ca_certificate
|
433
|
-
file_path = certificate_location(CA_NAME)
|
434
|
-
if Puppet::FileSystem.exist?(file_path)
|
435
|
-
begin
|
436
|
-
# This load ensures that the file contents is a valid cert bundle.
|
437
|
-
# If the text is malformed, load_certificate_bundle will raise.
|
438
|
-
load_certificate_bundle(Puppet::FileSystem.read(file_path))
|
439
|
-
rescue Puppet::Error => e
|
440
|
-
raise Puppet::Error, _("The CA certificate at %{file_path} is invalid: %{message}") % { file_path: file_path, message: e.message }
|
441
|
-
end
|
442
|
-
else
|
443
|
-
bundle = download_ca_certificate_bundle
|
444
|
-
if bundle
|
445
|
-
save_bundle(bundle, certificate_location(CA_NAME))
|
446
|
-
true
|
447
|
-
else
|
448
|
-
false
|
449
|
-
end
|
450
|
-
end
|
451
|
-
end
|
452
|
-
public :ensure_ca_certificate
|
453
|
-
|
454
|
-
# Creates an arry of SSL Certificate objects from a PEM-encoding string
|
455
|
-
# of one or more certs.
|
456
|
-
# @param [String] bundle_string PEM-encoded string of certs
|
457
|
-
# @return [[OpenSSL::X509::Certificate], nil] the certs loaded from the
|
458
|
-
# input string, or nil if none could be loaded
|
459
|
-
def load_certificate_bundle(bundle_string)
|
460
|
-
delimiters = /-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m
|
461
|
-
certs = bundle_string.scan(delimiters)
|
462
|
-
|
463
|
-
if certs.empty?
|
464
|
-
raise Puppet::Error, _("No valid PEM-encoded certificates.")
|
465
|
-
end
|
466
|
-
|
467
|
-
certs.map do |cert|
|
468
|
-
begin
|
469
|
-
OpenSSL::X509::Certificate.new(cert)
|
470
|
-
rescue OpenSSL::X509::CertificateError => e
|
471
|
-
raise Puppet::Error, _("Could not parse certificate: %{message}") % { message: e.message }
|
472
|
-
end
|
473
|
-
end
|
474
|
-
end
|
475
|
-
|
476
425
|
# Fetches and saves the crl bundle from the CA server without validating
|
477
426
|
# its contents. Takes an optional store to use with the http_client,
|
478
427
|
# necessary for initial download of the CRL because `build_ssl_store`
|
@@ -487,7 +436,7 @@ ERROR_STRING
|
|
487
436
|
# If no SSL store was supplied, use this host's SSL store
|
488
437
|
store ||= ssl_store
|
489
438
|
Puppet::Util.replace_file(crl_path, 0644) do |file|
|
490
|
-
result = Puppet::Rest::Routes.get_crls(CA_NAME, Puppet::
|
439
|
+
result = Puppet::Rest::Routes.get_crls(CA_NAME, Puppet::SSL::SSLContext.new(store: store))
|
491
440
|
file.write(result)
|
492
441
|
end
|
493
442
|
rescue Puppet::Rest::ResponseError => e
|
@@ -495,38 +444,6 @@ ERROR_STRING
|
|
495
444
|
end
|
496
445
|
end
|
497
446
|
|
498
|
-
# Fetches the CA certificate bundle from the CA server
|
499
|
-
# @raise [Puppet::Error] if response from the server is not a valid certificate
|
500
|
-
# bundle
|
501
|
-
# @return [[OpenSSL::X509::Certificate]] the certs loaded from the response
|
502
|
-
def download_ca_certificate_bundle
|
503
|
-
begin
|
504
|
-
cert_bundle = Puppet::Rest::Routes.get_certificate(
|
505
|
-
CA_NAME,
|
506
|
-
Puppet::Rest::SSLContext.new(OpenSSL::SSL::VERIFY_NONE)
|
507
|
-
)
|
508
|
-
# This load ensures that the response body is a valid cert bundle.
|
509
|
-
# If the text is malformed, load_certificate_bundle will raise.
|
510
|
-
begin
|
511
|
-
load_certificate_bundle(cert_bundle)
|
512
|
-
rescue Puppet::Error => e
|
513
|
-
raise Puppet::Error, _("Response from the CA did not contain a valid CA certificate: %{message}") % { message: e.message }
|
514
|
-
end
|
515
|
-
rescue Puppet::Rest::ResponseError => e
|
516
|
-
raise Puppet::Error, _('Could not download CA certificate: %{message}') % { message: e.message }
|
517
|
-
end
|
518
|
-
end
|
519
|
-
|
520
|
-
# Saves the given bundle to disk to a specified file path.
|
521
|
-
# @param bundle [[OpenSSL::X509::Certificate/CRL]] the certs to save
|
522
|
-
# @param location [String] place on disk to save bundle
|
523
|
-
def save_bundle(cert_bundle, location)
|
524
|
-
Puppet::Util.replace_file(location, 0644) do |f|
|
525
|
-
bundle_string = cert_bundle.map(&:to_pem).join("\n")
|
526
|
-
f.write(bundle_string)
|
527
|
-
end
|
528
|
-
end
|
529
|
-
|
530
447
|
# Attempts to load or fetch this host's certificate. Returns nil if
|
531
448
|
# no certificate could be found.
|
532
449
|
# @return [Puppet::SSL::Certificate, nil]
|
@@ -569,7 +486,7 @@ ERROR_STRING
|
|
569
486
|
begin
|
570
487
|
cert = Puppet::Rest::Routes.get_certificate(
|
571
488
|
cert_name,
|
572
|
-
Puppet::
|
489
|
+
Puppet::SSL::SSLContext.new(store: ssl_store)
|
573
490
|
)
|
574
491
|
begin
|
575
492
|
Puppet::SSL::Certificate.from_s(cert)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'puppet/ssl'
|
2
|
+
|
3
|
+
module Puppet::SSL
|
4
|
+
SSLContext = Struct.new(
|
5
|
+
:store,
|
6
|
+
:cacerts,
|
7
|
+
:crls,
|
8
|
+
:private_key,
|
9
|
+
:client_cert,
|
10
|
+
:client_chain,
|
11
|
+
:revocation,
|
12
|
+
:verify_peer
|
13
|
+
) do
|
14
|
+
DEFAULTS = {
|
15
|
+
cacerts: [],
|
16
|
+
crls: [],
|
17
|
+
client_chain: [],
|
18
|
+
revocation: true,
|
19
|
+
verify_peer: true
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
# This is an idiom to initialize a Struct from keyword
|
23
|
+
# arguments. Ruby 2.5 introduced `keyword_init: true` for
|
24
|
+
# that purpose, but we need to support older versions.
|
25
|
+
def initialize(**kwargs)
|
26
|
+
super({})
|
27
|
+
DEFAULTS.merge(kwargs).each { |k,v| self[k] = v }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'puppet/ssl'
|
2
|
+
|
3
|
+
# SSL Provider creates `SSLContext` objects that can be used to create
|
4
|
+
# secure connections.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class Puppet::SSL::SSLProvider
|
8
|
+
# Create an insecure `SSLContext`. Connections made from the returned context
|
9
|
+
# will not authenticate the server, i.e. `VERIFY_NONE`, and are vulnerable to
|
10
|
+
# MITM. Do not call this method.
|
11
|
+
#
|
12
|
+
# @return [Puppet::SSL::SSLContext] A context to use to create connections
|
13
|
+
# @api private
|
14
|
+
def create_insecure_context
|
15
|
+
store = create_x509_store([], [], false)
|
16
|
+
|
17
|
+
Puppet::SSL::SSLContext.new(store: store, verify_peer: false).freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create an `SSLContext` using the trusted `cacerts` and optional `crls`.
|
21
|
+
# Connections made from the returned context will authenticate the server,
|
22
|
+
# i.e. `VERIFY_PEER`, but will not use a client certificate.
|
23
|
+
#
|
24
|
+
# The `crls` parameter must contain CRLs corresponding to each CA in `cacerts`
|
25
|
+
# depending on the `revocation` mode. See {#create_context}.
|
26
|
+
#
|
27
|
+
# @param cacerts [Array<OpenSSL::X509::Certificate>] Array of trusted CA certs
|
28
|
+
# @param crls [Array<OpenSSL::X509::CRL>] Array of CRLs
|
29
|
+
# @param revocation [:chain, :leaf, false] revocation mode
|
30
|
+
# @return [Puppet::SSL::SSLContext] A context to use to create connections
|
31
|
+
# @raise (see #create_context)
|
32
|
+
# @api private
|
33
|
+
def create_root_context(cacerts:, crls: [], revocation: Puppet[:certificate_revocation])
|
34
|
+
store = create_x509_store(cacerts, crls, revocation)
|
35
|
+
|
36
|
+
Puppet::SSL::SSLContext.new(store: store, cacerts: cacerts, crls: crls, revocation: revocation).freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create an `SSLContext` using the trusted `cacerts`, `crls`, `private_key`,
|
40
|
+
# `client_cert`, and `revocation` mode. Connections made from the returned
|
41
|
+
# context will be mutually authenticated.
|
42
|
+
#
|
43
|
+
# The `crls` parameter must contain CRLs corresponding to each CA in `cacerts`
|
44
|
+
# depending on the `revocation` mode:
|
45
|
+
#
|
46
|
+
# * `:chain` - `crls` must contain a CRL for every CA in `cacerts`
|
47
|
+
# * `:leaf` - `crls` must contain (at least) the CRL for the leaf CA in `cacerts`
|
48
|
+
# * `false` - `crls` can be empty
|
49
|
+
#
|
50
|
+
# The `private_key` and public key from the `client_cert` must match.
|
51
|
+
#
|
52
|
+
# @param cacerts [Array<OpenSSL::X509::Certificate>] Array of trusted CA certs
|
53
|
+
# @param crls [Array<OpenSSL::X509::CRL>] Array of CRLs
|
54
|
+
# @param private_key [OpenSSL::PKey::RSA] client's private key
|
55
|
+
# @param client_cert [OpenSSL::X509::Certificate] client's cert whose public
|
56
|
+
# key matches the `private_key`
|
57
|
+
# @param revocation [:chain, :leaf, false] revocation mode
|
58
|
+
# @return [Puppet::SSL::SSLContext] A context to use to create connections
|
59
|
+
# @raise [Puppet::SSL::CertVerifyError] There was an issue with
|
60
|
+
# one of the certs or CRLs.
|
61
|
+
# @raise [Puppet::SSL::SSLError] There was an issue with the
|
62
|
+
# `private_key`.
|
63
|
+
# @api private
|
64
|
+
def create_context(cacerts:, crls:, private_key:, client_cert:, revocation: Puppet[:certificate_revocation])
|
65
|
+
raise ArgumentError, _("CA certs are missing") unless cacerts
|
66
|
+
raise ArgumentError, _("CRLs are missing") unless crls
|
67
|
+
raise ArgumentError, _("Private key is missing") unless private_key
|
68
|
+
raise ArgumentError, _("Client cert is missing") unless client_cert
|
69
|
+
|
70
|
+
store = create_x509_store(cacerts, crls, revocation)
|
71
|
+
client_chain = verify_cert_with_store(store, client_cert)
|
72
|
+
|
73
|
+
unless private_key.is_a?(OpenSSL::PKey::RSA)
|
74
|
+
raise Puppet::SSL::SSLError, _("Unsupported key '%{type}'") % { type: private_key.class.name }
|
75
|
+
end
|
76
|
+
|
77
|
+
unless client_cert.check_private_key(private_key)
|
78
|
+
raise Puppet::SSL::SSLError, _("The certificate for '%{name}' does not match its private key") % { name: subject(client_cert) }
|
79
|
+
end
|
80
|
+
|
81
|
+
Puppet::SSL::SSLContext.new(
|
82
|
+
store: store, cacerts: cacerts, crls: crls,
|
83
|
+
private_key: private_key, client_cert: client_cert, client_chain: client_chain,
|
84
|
+
revocation: revocation
|
85
|
+
).freeze
|
86
|
+
end
|
87
|
+
|
88
|
+
# Load an `SSLContext` using available certs and keys. An exception is raised
|
89
|
+
# if any component is missing or is invalid, such as a mismatched client cert
|
90
|
+
# and private key. Connections made from the returned context will be mutually
|
91
|
+
# authenticated.
|
92
|
+
#
|
93
|
+
# @param revocation [:chain, :leaf, false] revocation mode
|
94
|
+
# @return [Puppet::SSL::SSLContext] A context to use to create connections
|
95
|
+
# @raise [Puppet::SSL::CertVerifyError] There was an issue with
|
96
|
+
# one of the certs or CRLs.
|
97
|
+
# @raise [Puppet::Error] There was an issue with one of the required components.
|
98
|
+
# @api private
|
99
|
+
def load_context(certname: Puppet[:certname], revocation: Puppet[:certificate_revocation])
|
100
|
+
cert = Puppet::X509::CertProvider.new
|
101
|
+
cacerts = cert.load_cacerts(required: true)
|
102
|
+
crls = case revocation
|
103
|
+
when :chain, :leaf
|
104
|
+
cert.load_crls(required: true)
|
105
|
+
else
|
106
|
+
[]
|
107
|
+
end
|
108
|
+
private_key = cert.load_private_key(certname, required: true)
|
109
|
+
client_cert = cert.load_client_cert(certname, required: true)
|
110
|
+
|
111
|
+
create_context(cacerts: cacerts, crls: crls, private_key: private_key, client_cert: client_cert, revocation: revocation)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Verify the `csr` was signed with a private key corresponding to the
|
115
|
+
# `public_key`. This ensures the CSR was signed by someone in possession
|
116
|
+
# of the private key, and that it hasn't been tampered with since.
|
117
|
+
#
|
118
|
+
# @param csr [OpenSSL::X509::Request] certificate signing request
|
119
|
+
# @param public_key [OpenSSL::PKey::RSA] public key
|
120
|
+
# @raise [Puppet::SSL:SSLError] The private_key for the given `public_key` was
|
121
|
+
# not used to sign the CSR.
|
122
|
+
# @api private
|
123
|
+
def verify_request(csr, public_key)
|
124
|
+
unless csr.verify(public_key)
|
125
|
+
raise Puppet::SSL::SSLError, _("The CSR for host '%{name}' does not match the public key") % { name: subject(csr) }
|
126
|
+
end
|
127
|
+
|
128
|
+
csr
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def default_flags
|
134
|
+
# checking the signature of the self-signed cert doesn't add any security,
|
135
|
+
# but it's a sanity check to make sure the cert isn't corrupt. This option
|
136
|
+
# is only available in openssl 1.1+
|
137
|
+
if defined?(OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE)
|
138
|
+
OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE
|
139
|
+
else
|
140
|
+
0
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def create_x509_store(roots, crls, revocation)
|
145
|
+
store = OpenSSL::X509::Store.new
|
146
|
+
store.purpose = OpenSSL::X509::PURPOSE_ANY
|
147
|
+
store.flags = default_flags | revocation_mode(revocation)
|
148
|
+
|
149
|
+
roots.each { |cert| store.add_cert(cert) }
|
150
|
+
crls.each { |crl| store.add_crl(crl) }
|
151
|
+
|
152
|
+
store
|
153
|
+
end
|
154
|
+
|
155
|
+
def subject(x509)
|
156
|
+
x509.subject.to_s
|
157
|
+
end
|
158
|
+
|
159
|
+
def issuer(x509)
|
160
|
+
x509.issuer.to_s
|
161
|
+
end
|
162
|
+
|
163
|
+
def revocation_mode(mode)
|
164
|
+
case mode
|
165
|
+
when false
|
166
|
+
0
|
167
|
+
when :leaf
|
168
|
+
OpenSSL::X509::V_FLAG_CRL_CHECK
|
169
|
+
else
|
170
|
+
# :chain is the default
|
171
|
+
OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def verify_cert_with_store(store, cert)
|
176
|
+
# StoreContext#initialize accepts a chain argument, but it's set to [] because
|
177
|
+
# puppet requires any intermediate CA certs needed to complete the client's
|
178
|
+
# chain to be in the CA bundle that we downloaded from the server, and
|
179
|
+
# they've already been added to the store. See PUP-9500.
|
180
|
+
|
181
|
+
store_context = OpenSSL::X509::StoreContext.new(store, cert, [])
|
182
|
+
unless store_context.verify
|
183
|
+
current_cert = store_context.current_cert
|
184
|
+
|
185
|
+
# If the client cert's intermediate CA is not in the CA bundle, then warn,
|
186
|
+
# but don't error, because SSL allows the client to send an incomplete
|
187
|
+
# chain, and have the server resolve it.
|
188
|
+
if store_context.error == OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY
|
189
|
+
Puppet.warning _("The issuer '%{issuer}' of certificate '%{subject}' cannot be found locally") % {
|
190
|
+
issuer: issuer(current_cert), subject: subject(current_cert)
|
191
|
+
}
|
192
|
+
else
|
193
|
+
raise_cert_verify_error(store_context, current_cert)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# resolved chain from leaf to root
|
198
|
+
store_context.chain
|
199
|
+
end
|
200
|
+
|
201
|
+
def raise_cert_verify_error(store_context, current_cert)
|
202
|
+
message =
|
203
|
+
case store_context.error
|
204
|
+
when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID
|
205
|
+
_("The certificate '%{subject}' is not yet valid, verify time is synchronized") % { subject: subject(current_cert) }
|
206
|
+
when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED
|
207
|
+
_("The certificate '%{subject}' has expired, verify time is synchronized") % { subject: subject(current_cert) }
|
208
|
+
when OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID
|
209
|
+
_("The CRL issued by '%{issuer}' is not yet valid, verify time is synchronized") % { issuer: issuer(current_cert) }
|
210
|
+
when OpenSSL::X509::V_ERR_CRL_HAS_EXPIRED
|
211
|
+
_("The CRL issued by '%{issuer}' has expired, verify time is synchronized") % { issuer: issuer(current_cert) }
|
212
|
+
when OpenSSL::X509::V_ERR_CERT_SIGNATURE_FAILURE
|
213
|
+
_("Invalid signature for certificate '%{subject}'") % { subject: subject(current_cert) }
|
214
|
+
when OpenSSL::X509::V_ERR_CRL_SIGNATURE_FAILURE
|
215
|
+
_("Invalid signature for CRL issued by '%{issuer}'") % { issuer: issuer(current_cert) }
|
216
|
+
when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT
|
217
|
+
_("The issuer '%{issuer}' of certificate '%{subject}' is missing") % {
|
218
|
+
issuer: issuer(current_cert), subject: subject(current_cert) }
|
219
|
+
when OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL
|
220
|
+
_("The CRL issued by '%{issuer}' is missing") % { issuer: issuer(current_cert) }
|
221
|
+
when OpenSSL::X509::V_ERR_CERT_REVOKED
|
222
|
+
_("Certificate '%{subject}' is revoked") % { subject: subject(current_cert) }
|
223
|
+
else
|
224
|
+
# error_string is labeled ASCII-8BIT, but is encoded based on Encoding.default_external
|
225
|
+
err_utf8 = Puppet::Util::CharacterEncoding.convert_to_utf_8(store_context.error_string)
|
226
|
+
_("Certificate '%{subject}' failed verification (%{err}): %{err_utf8}") % {
|
227
|
+
subject: subject(current_cert), err: store_context.error, err_utf8: err_utf8 }
|
228
|
+
end
|
229
|
+
|
230
|
+
raise Puppet::SSL::CertVerifyError.new(message, store_context.error, current_cert)
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
require 'puppet/ssl'
|
2
|
+
|
3
|
+
# This class implements a state machine for bootstrapping a host's CA and CRL
|
4
|
+
# bundles, private key and signed client certificate. Each state has a frozen
|
5
|
+
# SSLContext that it uses to make network connections. If a state makes progress
|
6
|
+
# bootstrapping the host, then the state will generate a new frozen SSLContext
|
7
|
+
# and pass that to the next state. For example, the NeedCACerts state will load
|
8
|
+
# or download a CA bundle, and generate a new SSLContext containing those CA
|
9
|
+
# certs. This way we're sure about which SSLContext is being used during any
|
10
|
+
# phase of the bootstrapping process.
|
11
|
+
#
|
12
|
+
# @private
|
13
|
+
class Puppet::SSL::StateMachine
|
14
|
+
CA_NAME = 'ca'.freeze
|
15
|
+
|
16
|
+
class SSLState
|
17
|
+
attr_reader :ssl_context
|
18
|
+
|
19
|
+
def initialize(machine, ssl_context)
|
20
|
+
@machine = machine
|
21
|
+
@ssl_context = ssl_context
|
22
|
+
@cert_provider = Puppet::X509::CertProvider.new
|
23
|
+
@ssl_provider = Puppet::SSL::SSLProvider.new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Load existing CA certs or download them. Transition to NeedCRLs.
|
28
|
+
#
|
29
|
+
class NeedCACerts < SSLState
|
30
|
+
def initialize(machine)
|
31
|
+
super(machine, nil)
|
32
|
+
@ssl_context = @ssl_provider.create_insecure_context
|
33
|
+
end
|
34
|
+
|
35
|
+
def next_state
|
36
|
+
Puppet.debug("Loading CA certs")
|
37
|
+
|
38
|
+
cacerts = @cert_provider.load_cacerts
|
39
|
+
if cacerts
|
40
|
+
next_ctx = @ssl_provider.create_root_context(cacerts: cacerts, revocation: false)
|
41
|
+
else
|
42
|
+
pem = Puppet::Rest::Routes.get_certificate(CA_NAME, @ssl_context)
|
43
|
+
cacerts = @cert_provider.load_cacerts_from_pem(pem)
|
44
|
+
# verify cacerts before saving
|
45
|
+
next_ctx = @ssl_provider.create_root_context(cacerts: cacerts, revocation: false)
|
46
|
+
@cert_provider.save_cacerts(cacerts)
|
47
|
+
end
|
48
|
+
|
49
|
+
NeedCRLs.new(@machine, next_ctx)
|
50
|
+
rescue Puppet::Rest::ResponseError => e
|
51
|
+
if e.response.code.to_i == 404
|
52
|
+
raise Puppet::Error.new(_('CA certificate is missing from the server'))
|
53
|
+
else
|
54
|
+
raise Puppet::Error.new(_('Could not download CA certificate: %{message}') % { message: e.message }, e)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# If revocation is enabled, load CRLs or download them, using the CA bundle
|
60
|
+
# from the previous state. Transition to NeedKey. Even if Puppet[:certificate_revocation]
|
61
|
+
# is leaf or chain, disable revocation when downloading the CRL, since 1) we may
|
62
|
+
# not have one yet or 2) the connection will fail if NeedCACerts downloaded a new CA
|
63
|
+
# for which we don't have a CRL
|
64
|
+
#
|
65
|
+
class NeedCRLs < SSLState
|
66
|
+
def next_state
|
67
|
+
Puppet.debug("Loading CRLs")
|
68
|
+
|
69
|
+
case Puppet[:certificate_revocation]
|
70
|
+
when :chain, :leaf
|
71
|
+
crls = @cert_provider.load_crls
|
72
|
+
if crls
|
73
|
+
next_ctx = @ssl_provider.create_root_context(cacerts: ssl_context[:cacerts], crls: crls)
|
74
|
+
else
|
75
|
+
pem = Puppet::Rest::Routes.get_crls(CA_NAME, @ssl_context)
|
76
|
+
crls = @cert_provider.load_crls_from_pem(pem)
|
77
|
+
# verify crls before saving
|
78
|
+
next_ctx = @ssl_provider.create_root_context(cacerts: ssl_context[:cacerts], crls: crls)
|
79
|
+
@cert_provider.save_crls(crls)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
Puppet.info("Certificate revocation is disabled, skipping CRL download")
|
83
|
+
next_ctx = @ssl_provider.create_root_context(cacerts: ssl_context[:cacerts], crls: [])
|
84
|
+
end
|
85
|
+
|
86
|
+
NeedKey.new(@machine, next_ctx)
|
87
|
+
rescue Puppet::Rest::ResponseError => e
|
88
|
+
if e.response.code.to_i == 404
|
89
|
+
raise Puppet::Error.new(_('CRL is missing from the server'))
|
90
|
+
else
|
91
|
+
raise Puppet::Error.new(_('Could not download CRLs: %{message}') % { message: e.message }, e)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Load or generate a private key. If the key exists, try to load the client cert
|
97
|
+
# and transition to Done. If the cert is mismatched or otherwise fails valiation,
|
98
|
+
# raise an error. If the key doesn't exist yet, generate one, and save it. If the
|
99
|
+
# cert doesn't exist yet, transition to NeedSubmitCSR.
|
100
|
+
#
|
101
|
+
class NeedKey < SSLState
|
102
|
+
def next_state
|
103
|
+
key = @cert_provider.load_private_key(Puppet[:certname])
|
104
|
+
if key
|
105
|
+
cert = @cert_provider.load_client_cert(Puppet[:certname])
|
106
|
+
if cert
|
107
|
+
next_ctx = @ssl_provider.create_context(
|
108
|
+
cacerts: @ssl_context.cacerts, crls: @ssl_context.crls, private_key: key, client_cert: cert
|
109
|
+
)
|
110
|
+
return Done.new(@machine, next_ctx)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
Puppet.info _("Creating a new SSL key for %{name}") % { name: Puppet[:certname] }
|
114
|
+
key = OpenSSL::PKey::RSA.new(Puppet[:keylength].to_i)
|
115
|
+
@cert_provider.save_private_key(Puppet[:certname], key)
|
116
|
+
end
|
117
|
+
|
118
|
+
NeedSubmitCSR.new(@machine, @ssl_context, key)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Base class for states with a private key.
|
123
|
+
#
|
124
|
+
class KeySSLState < SSLState
|
125
|
+
attr_reader :private_key
|
126
|
+
|
127
|
+
def initialize(machine, ssl_context, private_key)
|
128
|
+
super(machine, ssl_context)
|
129
|
+
@private_key = private_key
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Generate and submit a CSR using the CA cert bundle and optional CRL bundle
|
134
|
+
# from earlier states. If the request is submitted, proceed to NeedCert,
|
135
|
+
# otherwise Wait. This could be due to the server already having a CSR
|
136
|
+
# for this host (either the same or different CSR content), having a
|
137
|
+
# signed certificate, or a revoked certificate.
|
138
|
+
#
|
139
|
+
class NeedSubmitCSR < KeySSLState
|
140
|
+
def next_state
|
141
|
+
csr = @cert_provider.create_request(Puppet[:certname], @private_key)
|
142
|
+
Puppet::Rest::Routes.put_certificate_request(csr.to_pem, Puppet[:certname], @ssl_context)
|
143
|
+
@cert_provider.save_request(Puppet[:certname], csr)
|
144
|
+
NeedCert.new(@machine, @ssl_context, @private_key)
|
145
|
+
rescue Puppet::Rest::ResponseError => e
|
146
|
+
if e.response.code.to_i != 400
|
147
|
+
raise Puppet::SSL::SSLError.new(_("Failed to submit the CSR, HTTP response was %{code}") % { code: e.response.code }, e)
|
148
|
+
end
|
149
|
+
|
150
|
+
NeedCert.new(@machine, @ssl_context, @private_key)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Attempt to load or retrieve our signed cert.
|
155
|
+
#
|
156
|
+
class NeedCert < KeySSLState
|
157
|
+
def next_state
|
158
|
+
cert = OpenSSL::X509::Certificate.new(
|
159
|
+
Puppet::Rest::Routes.get_certificate(Puppet[:certname], @ssl_context)
|
160
|
+
)
|
161
|
+
# verify client cert before saving
|
162
|
+
next_ctx = @ssl_provider.create_context(
|
163
|
+
cacerts: @ssl_context.cacerts, crls: @ssl_context.crls, private_key: @private_key, client_cert: cert
|
164
|
+
)
|
165
|
+
@cert_provider.save_client_cert(Puppet[:certname], cert)
|
166
|
+
@cert_provider.delete_request(Puppet[:certname])
|
167
|
+
Done.new(@machine, next_ctx)
|
168
|
+
rescue Puppet::SSL::SSLError => e
|
169
|
+
Puppet.log_exception(e)
|
170
|
+
Wait.new(@machine, @ssl_context)
|
171
|
+
rescue OpenSSL::X509::CertificateError => e
|
172
|
+
Puppet.log_exception(e, _("Failed to parse certificate: %{message}") % {message: e.message})
|
173
|
+
Wait.new(@machine, @ssl_context)
|
174
|
+
rescue Puppet::Rest::ResponseError => e
|
175
|
+
if e.response.code.to_i == 404
|
176
|
+
Puppet.info(_("Certificate for %{certname} has not been signed yet") % {certname: Puppet[:certname]})
|
177
|
+
else
|
178
|
+
Puppet.log_exception(e, _("Failed to retrieve certificate for %{certname}: %{message}") %
|
179
|
+
{certname: Puppet[:certname], message: e.response.message})
|
180
|
+
end
|
181
|
+
Wait.new(@machine, @ssl_context)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# We cannot make progress, so wait if allowed to do so, or error.
|
186
|
+
#
|
187
|
+
class Wait < SSLState
|
188
|
+
def next_state
|
189
|
+
time = @machine.onetime ? 0 : @machine.waitforcert
|
190
|
+
if time < 1
|
191
|
+
puts _("Exiting; no certificate found and waitforcert is disabled")
|
192
|
+
exit(1)
|
193
|
+
else
|
194
|
+
sleep(time)
|
195
|
+
|
196
|
+
# our ssl directory may have been cleaned while we were
|
197
|
+
# sleeping, start over from the top
|
198
|
+
NeedCACerts.new(@machine)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# We have a CA bundle, optional CRL bundle, a private key and matching cert
|
204
|
+
# that chains to one of the root certs in our bundle.
|
205
|
+
#
|
206
|
+
class Done < SSLState; end
|
207
|
+
|
208
|
+
attr_reader :onetime, :waitforcert
|
209
|
+
|
210
|
+
def initialize(onetime: Puppet[:onetime], waitforcert: Puppet[:waitforcert])
|
211
|
+
@onetime = onetime
|
212
|
+
@waitforcert = waitforcert
|
213
|
+
end
|
214
|
+
|
215
|
+
# Run the state machine for CA certs and CRLs
|
216
|
+
#
|
217
|
+
# @return [Puppet::SSL::SSLContext] initialized SSLContext
|
218
|
+
def ensure_ca_certificates
|
219
|
+
final_state = run_machine(NeedCACerts.new(self), NeedKey)
|
220
|
+
final_state.ssl_context
|
221
|
+
end
|
222
|
+
|
223
|
+
# Run the state machine for CA certs and CRLs
|
224
|
+
#
|
225
|
+
# @return [Puppet::SSL::SSLContext] initialized SSLContext
|
226
|
+
def ensure_client_certificate
|
227
|
+
final_state = run_machine(NeedCACerts.new(self), Done)
|
228
|
+
ssl_context = final_state.ssl_context
|
229
|
+
|
230
|
+
if Puppet::Util::Log.sendlevel?(:debug)
|
231
|
+
chain = ssl_context.client_chain
|
232
|
+
# print from root to client
|
233
|
+
chain.reverse.each_with_index do |cert, i|
|
234
|
+
digest = Puppet::SSL::Digest.new('SHA256', cert.to_der)
|
235
|
+
if i == chain.length - 1
|
236
|
+
Puppet.debug(_("Verified client certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_s, digest: digest})
|
237
|
+
else
|
238
|
+
Puppet.debug(_("Verified CA certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_s, digest: digest})
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
ssl_context
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
def run_machine(state, stop)
|
249
|
+
loop do
|
250
|
+
Puppet.debug("Current SSL state #{state_name(state)}")
|
251
|
+
|
252
|
+
state = state.next_state
|
253
|
+
|
254
|
+
return state if state.is_a?(stop)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def state_name(state)
|
259
|
+
state.class.to_s.split('::').last
|
260
|
+
end
|
261
|
+
end
|