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.

Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/CODEOWNERS +30 -0
  3. data/Gemfile.lock +9 -9
  4. data/lib/puppet.rb +13 -0
  5. data/lib/puppet/application/agent.rb +8 -12
  6. data/lib/puppet/application/device.rb +2 -3
  7. data/lib/puppet/application/filebucket.rb +6 -1
  8. data/lib/puppet/application/ssl.rb +102 -55
  9. data/lib/puppet/configurer.rb +8 -7
  10. data/lib/puppet/defaults.rb +3 -1
  11. data/lib/puppet/file_system.rb +24 -4
  12. data/lib/puppet/file_system/file_impl.rb +25 -0
  13. data/lib/puppet/file_system/jruby.rb +23 -0
  14. data/lib/puppet/file_system/windows.rb +84 -0
  15. data/lib/puppet/indirector/rest.rb +4 -2
  16. data/lib/puppet/loaders.rb +1 -0
  17. data/lib/puppet/network/http.rb +1 -0
  18. data/lib/puppet/network/http/base_pool.rb +18 -0
  19. data/lib/puppet/network/http/connection.rb +49 -17
  20. data/lib/puppet/network/http/nocache_pool.rb +9 -4
  21. data/lib/puppet/network/http/pool.rb +10 -11
  22. data/lib/puppet/network/http/session.rb +3 -2
  23. data/lib/puppet/network/http_pool.rb +32 -0
  24. data/lib/puppet/pops/loader/generic_plan_instantiator.rb +28 -0
  25. data/lib/puppet/pops/loader/loader_paths.rb +46 -10
  26. data/lib/puppet/pops/loader/module_loaders.rb +10 -3
  27. data/lib/puppet/provider/file/windows.rb +49 -1
  28. data/lib/puppet/provider/package/windows.rb +5 -1
  29. data/lib/puppet/reports/http.rb +2 -1
  30. data/lib/puppet/rest/client.rb +7 -3
  31. data/lib/puppet/rest/routes.rb +9 -44
  32. data/lib/puppet/ssl.rb +6 -0
  33. data/lib/puppet/ssl/error.rb +26 -0
  34. data/lib/puppet/ssl/host.rb +9 -92
  35. data/lib/puppet/ssl/ssl_context.rb +30 -0
  36. data/lib/puppet/ssl/ssl_provider.rb +232 -0
  37. data/lib/puppet/ssl/state_machine.rb +261 -0
  38. data/lib/puppet/ssl/validator.rb +1 -0
  39. data/lib/puppet/ssl/validator/default_validator.rb +1 -0
  40. data/lib/puppet/ssl/validator/no_validator.rb +2 -0
  41. data/lib/puppet/ssl/verifier.rb +134 -0
  42. data/lib/puppet/ssl/verifier_adapter.rb +48 -0
  43. data/lib/puppet/test/test_helper.rb +2 -1
  44. data/lib/puppet/type/exec.rb +30 -6
  45. data/lib/puppet/type/file/mode.rb +6 -1
  46. data/lib/puppet/type/file/source.rb +2 -2
  47. data/lib/puppet/type/filebucket.rb +12 -8
  48. data/lib/puppet/type/user.rb +14 -1
  49. data/lib/puppet/util/connection.rb +10 -5
  50. data/lib/puppet/util/feature.rb +11 -2
  51. data/lib/puppet/util/http_proxy.rb +3 -2
  52. data/lib/puppet/util/pidlock.rb +1 -1
  53. data/lib/puppet/util/ssl.rb +1 -10
  54. data/lib/puppet/util/windows/security.rb +29 -8
  55. data/lib/puppet/version.rb +1 -1
  56. data/lib/puppet/x509.rb +7 -0
  57. data/lib/puppet/x509/cert_provider.rb +286 -0
  58. data/lib/puppet/x509/pem_store.rb +55 -0
  59. data/locales/ja/puppet.po +740 -590
  60. data/locales/puppet.pot +433 -208
  61. data/man/man5/puppet.conf.5 +6 -3
  62. data/man/man8/puppet-agent.8 +1 -1
  63. data/man/man8/puppet-apply.8 +1 -1
  64. data/man/man8/puppet-catalog.8 +1 -1
  65. data/man/man8/puppet-config.8 +1 -1
  66. data/man/man8/puppet-describe.8 +1 -1
  67. data/man/man8/puppet-device.8 +1 -1
  68. data/man/man8/puppet-doc.8 +1 -1
  69. data/man/man8/puppet-epp.8 +1 -1
  70. data/man/man8/puppet-facts.8 +1 -1
  71. data/man/man8/puppet-filebucket.8 +6 -2
  72. data/man/man8/puppet-generate.8 +1 -1
  73. data/man/man8/puppet-help.8 +1 -1
  74. data/man/man8/puppet-key.8 +1 -1
  75. data/man/man8/puppet-lookup.8 +1 -1
  76. data/man/man8/puppet-man.8 +1 -1
  77. data/man/man8/puppet-module.8 +1 -1
  78. data/man/man8/puppet-node.8 +1 -1
  79. data/man/man8/puppet-parser.8 +1 -1
  80. data/man/man8/puppet-plugin.8 +1 -1
  81. data/man/man8/puppet-report.8 +1 -1
  82. data/man/man8/puppet-resource.8 +1 -1
  83. data/man/man8/puppet-script.8 +1 -1
  84. data/man/man8/puppet-ssl.8 +5 -1
  85. data/man/man8/puppet-status.8 +1 -1
  86. data/man/man8/puppet.8 +2 -2
  87. data/spec/fixtures/ssl/127.0.0.1-key.pem +67 -0
  88. data/spec/fixtures/ssl/127.0.0.1.pem +48 -0
  89. data/spec/fixtures/ssl/bad-basic-constraints.pem +59 -0
  90. data/spec/fixtures/ssl/bad-int-basic-constraints.pem +59 -0
  91. data/spec/fixtures/ssl/ca.pem +59 -0
  92. data/spec/fixtures/ssl/crl.pem +30 -0
  93. data/spec/fixtures/ssl/encrypted-key.pem +70 -0
  94. data/spec/fixtures/ssl/intermediate-agent-crl.pem +31 -0
  95. data/spec/fixtures/ssl/intermediate-agent.pem +60 -0
  96. data/spec/fixtures/ssl/intermediate-crl.pem +36 -0
  97. data/spec/fixtures/ssl/intermediate.pem +60 -0
  98. data/spec/fixtures/ssl/netlock-arany-utf8.pem +23 -0
  99. data/spec/fixtures/ssl/pluto-key.pem +67 -0
  100. data/spec/fixtures/ssl/pluto.pem +44 -0
  101. data/spec/fixtures/ssl/request-key.pem +67 -0
  102. data/spec/fixtures/ssl/request.pem +39 -0
  103. data/spec/fixtures/ssl/revoked-key.pem +67 -0
  104. data/spec/fixtures/ssl/revoked.pem +44 -0
  105. data/spec/fixtures/ssl/signed-key.pem +67 -0
  106. data/spec/fixtures/ssl/signed.pem +44 -0
  107. data/spec/fixtures/ssl/tampered-cert.pem +44 -0
  108. data/spec/fixtures/ssl/tampered-csr.pem +39 -0
  109. data/spec/integration/network/http_pool_spec.rb +222 -0
  110. data/spec/integration/provider/file/windows_spec.rb +162 -0
  111. data/spec/integration/rest/client_spec.rb +73 -0
  112. data/spec/integration/type/file_spec.rb +0 -19
  113. data/spec/lib/puppet/test_ca.rb +87 -50
  114. data/spec/lib/puppet_spec/fixtures.rb +20 -0
  115. data/spec/lib/puppet_spec/https.rb +84 -0
  116. data/spec/unit/application/agent_spec.rb +29 -30
  117. data/spec/unit/application/device_spec.rb +12 -49
  118. data/spec/unit/application/ssl_spec.rb +24 -38
  119. data/spec/unit/configurer_spec.rb +11 -11
  120. data/spec/unit/file_system/uniquefile_spec.rb +6 -0
  121. data/spec/unit/file_system_spec.rb +214 -0
  122. data/spec/unit/indirector/rest_spec.rb +3 -3
  123. data/spec/unit/network/http/connection_spec.rb +30 -90
  124. data/spec/unit/network/http/factory_spec.rb +1 -0
  125. data/spec/unit/network/http/nocache_pool_spec.rb +8 -8
  126. data/spec/unit/network/http/pool_spec.rb +63 -33
  127. data/spec/unit/network/http/session_spec.rb +8 -1
  128. data/spec/unit/network/http_pool_spec.rb +36 -0
  129. data/spec/unit/pops/loaders/loader_spec.rb +26 -1
  130. data/spec/unit/provider/package/windows_spec.rb +12 -1
  131. data/spec/unit/reports/http_spec.rb +7 -7
  132. data/spec/unit/rest/client_spec.rb +4 -6
  133. data/spec/unit/ssl/host_spec.rb +39 -33
  134. data/spec/unit/ssl/ssl_provider_spec.rb +428 -0
  135. data/spec/unit/ssl/state_machine_spec.rb +502 -0
  136. data/spec/unit/ssl/verifier_spec.rb +123 -0
  137. data/spec/unit/type/exec_spec.rb +63 -0
  138. data/spec/unit/type/file/source_spec.rb +5 -5
  139. data/spec/unit/type/filebucket_spec.rb +8 -6
  140. data/spec/unit/util/feature_spec.rb +2 -2
  141. data/spec/unit/util/storage_spec.rb +19 -19
  142. data/spec/unit/x509/cert_provider_spec.rb +527 -0
  143. data/spec/unit/x509/pem_store_spec.rb +160 -0
  144. data/tasks/generate_cert_fixtures.rake +158 -0
  145. metadata +78 -4
  146. data/MAINTAINERS +0 -47
  147. data/lib/puppet/rest/ssl_context.rb +0 -13
@@ -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
- require 'puppet/rest/ssl_context'
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 the CA cert first, since it's required for the normal cert
135
- # to be of any use. If we can't get it, quit.
136
- if !ensure_ca_certificate
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::Rest::SSLContext.new(OpenSSL::SSL::VERIFY_PEER, ssl_store))
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::Rest::SSLContext.new(OpenSSL::SSL::VERIFY_PEER, ssl_store))
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::Rest::SSLContext.new(OpenSSL::SSL::VERIFY_PEER, store))
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::Rest::SSLContext.new(OpenSSL::SSL::VERIFY_PEER, ssl_store)
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