class Puppet::SSL::SSLProvider

SSL Provider creates `SSLContext` objects that can be used to create secure connections.

@example To load an SSLContext from an existing private key and related certs/crls:

ssl_context = provider.load_context

@example To load an SSLContext from an existing password-protected private key and related certs/crls:

ssl_context = provider.load_context(password: 'opensesame')

@example To create an SSLContext from in-memory certs and keys:

cacerts = [<OpenSSL::X509::Certificate>]
crls = [<OpenSSL::X509::CRL>]
key = <OpenSSL::X509::PKey>
cert = <OpenSSL::X509::Certificate>
ssl_context = provider.create_context(cacerts: cacerts, crls: crls, private_key: key, client_cert: cert)

@example To create an SSLContext to connect to non-puppet HTTPS servers:

cacerts = [<OpenSSL::X509::Certificate>]
ssl_context = provider.create_root_context(cacerts: cacerts)

@api private

Public Instance Methods

create_context(cacerts:, crls:, private_key:, client_cert:, revocation: Puppet[:certificate_revocation], include_system_store: false) click to toggle source

Create an `SSLContext` using the trusted `cacerts`, `crls`, `private_key`, `client_cert`, and `revocation` mode. Connections made from the returned context will be mutually authenticated.

The `crls` parameter must contain CRLs corresponding to each CA in `cacerts` depending on the `revocation` mode:

  • `:chain` - `crls` must contain a CRL for every CA in `cacerts`

  • `:leaf` - `crls` must contain (at least) the CRL for the leaf CA in `cacerts`

  • `false` - `crls` can be empty

The `private_key` and public key from the `client_cert` must match.

@param cacerts [Array<OpenSSL::X509::Certificate>] Array of trusted CA certs @param crls [Array<OpenSSL::X509::CRL>] Array of CRLs @param private_key [OpenSSL::PKey::RSA, OpenSSL::PKey::EC] client's private key @param client_cert [OpenSSL::X509::Certificate] client's cert whose public

key matches the `private_key`

@param revocation [:chain, :leaf, false] revocation mode @param include_system_store [true, false] Also trust system CA @return [Puppet::SSL::SSLContext] A context to use to create connections @raise [Puppet::SSL::CertVerifyError] There was an issue with

one of the certs or CRLs.

@raise [Puppet::SSL::SSLError] There was an issue with the

`private_key`.

@api private

    # File lib/puppet/ssl/ssl_provider.rb
147 def create_context(cacerts:, crls:, private_key:, client_cert:, revocation: Puppet[:certificate_revocation], include_system_store: false)
148   raise ArgumentError, _("CA certs are missing") unless cacerts
149   raise ArgumentError, _("CRLs are missing") unless crls
150   raise ArgumentError, _("Private key is missing") unless private_key
151   raise ArgumentError, _("Client cert is missing") unless client_cert
152 
153   store = create_x509_store(cacerts, crls, revocation, include_system_store: include_system_store)
154   client_chain = resolve_client_chain(store, client_cert, private_key)
155 
156   Puppet::SSL::SSLContext.new(
157     store: store, cacerts: cacerts, crls: crls,
158     private_key: private_key, client_cert: client_cert, client_chain: client_chain,
159     revocation: revocation
160   ).freeze
161 end
create_insecure_context() click to toggle source

Create an insecure `SSLContext`. Connections made from the returned context will not authenticate the server, i.e. `VERIFY_NONE`, and are vulnerable to MITM. Do not call this method.

@return [Puppet::SSL::SSLContext] A context to use to create connections @api private

   # File lib/puppet/ssl/ssl_provider.rb
32 def create_insecure_context
33   store = create_x509_store([], [], false)
34 
35   Puppet::SSL::SSLContext.new(store: store, verify_peer: false).freeze
36 end
create_root_context(cacerts:, crls: [], revocation: Puppet[:certificate_revocation]) click to toggle source

Create an `SSLContext` using the trusted `cacerts` and optional `crls`. Connections made from the returned context will authenticate the server, i.e. `VERIFY_PEER`, but will not use a client certificate.

The `crls` parameter must contain CRLs corresponding to each CA in `cacerts` depending on the `revocation` mode. See {#create_context}.

@param cacerts [Array<OpenSSL::X509::Certificate>] Array of trusted CA certs @param crls [Array<OpenSSL::X509::CRL>] Array of CRLs @param revocation [:chain, :leaf, false] revocation mode @return [Puppet::SSL::SSLContext] A context to use to create connections @raise (see create_context) @api private

   # File lib/puppet/ssl/ssl_provider.rb
51 def create_root_context(cacerts:, crls: [], revocation: Puppet[:certificate_revocation])
52   store = create_x509_store(cacerts, crls, revocation)
53 
54   Puppet::SSL::SSLContext.new(store: store, cacerts: cacerts, crls: crls, revocation: revocation).freeze
55 end
create_system_context(cacerts:, path: Puppet[:ssl_trust_store], include_client_cert: false) click to toggle source

Create an `SSLContext` using the trusted `cacerts` and any certs in OpenSSL's default verify path locations. When running puppet as a gem, the location is system dependent. When running puppet from puppet-agent packages, the location refers to the cacerts bundle in the puppet-agent package.

Connections made from the returned context will authenticate the server, i.e. `VERIFY_PEER`, but will not use a client certificate (unless requested) and will not perform revocation checking.

@param cacerts [Array<OpenSSL::X509::Certificate>] Array of trusted CA certs @param path [String, nil] A file containing additional trusted CA certs. @param include_client_cert [true, false] If true, the client cert will be added to the context

allowing mutual TLS authentication. The default is false. If the client cert doesn't exist
then the option will be ignored.

@return [Puppet::SSL::SSLContext] A context to use to create connections @raise (see create_context) @api private

    # File lib/puppet/ssl/ssl_provider.rb
 74 def create_system_context(cacerts:, path: Puppet[:ssl_trust_store], include_client_cert: false)
 75   store = create_x509_store(cacerts, [], false, include_system_store: true)
 76 
 77   if path
 78     stat = Puppet::FileSystem.stat(path)
 79     if stat
 80       if stat.ftype == 'file'
 81         # don't add empty files as ruby/openssl will raise
 82         if stat.size > 0
 83           begin
 84             store.add_file(path)
 85           rescue => e
 86             Puppet.err(_("Failed to add '%{path}' as a trusted CA file: %{detail}" % { path: path, detail: e.message }, e))
 87           end
 88         end
 89       else
 90         Puppet.warning(_("The 'ssl_trust_store' setting does not refer to a file and will be ignored: '%{path}'" % { path: path }))
 91       end
 92     end
 93   end
 94 
 95   if include_client_cert
 96     cert_provider = Puppet::X509::CertProvider.new
 97     private_key = cert_provider.load_private_key(Puppet[:certname], required: false)
 98     unless private_key
 99       Puppet.warning("Private key for '#{Puppet[:certname]}' does not exist")
100     end
101 
102     client_cert = cert_provider.load_client_cert(Puppet[:certname], required: false)
103     unless client_cert
104       Puppet.warning("Client certificate for '#{Puppet[:certname]}' does not exist")
105     end
106 
107     if private_key && client_cert
108       client_chain = resolve_client_chain(store, client_cert, private_key)
109 
110       return Puppet::SSL::SSLContext.new(
111         store: store, cacerts: cacerts, crls: [],
112         private_key: private_key, client_cert: client_cert, client_chain: client_chain,
113         revocation: false
114       ).freeze
115     end
116   end
117 
118   Puppet::SSL::SSLContext.new(store: store, cacerts: cacerts, crls: [], revocation: false).freeze
119 end
load_context(certname: Puppet[:certname], revocation: Puppet[:certificate_revocation], password: nil, include_system_store: false) click to toggle source

Load an `SSLContext` using available certs and keys. An exception is raised if any component is missing or is invalid, such as a mismatched client cert and private key. Connections made from the returned context will be mutually authenticated.

@param certname [String] Which cert & key to load @param revocation [:chain, :leaf, false] revocation mode @param password [String, nil] If the private key is encrypted, decrypt

it using the password. If the key is encrypted, but a password is
not specified, then the key cannot be loaded.

@param include_system_store [true, false] Also trust system CA @return [Puppet::SSL::SSLContext] A context to use to create connections @raise [Puppet::SSL::CertVerifyError] There was an issue with

one of the certs or CRLs.

@raise [Puppet::Error] There was an issue with one of the required components. @api private

    # File lib/puppet/ssl/ssl_provider.rb
179 def load_context(certname: Puppet[:certname], revocation: Puppet[:certificate_revocation], password: nil, include_system_store: false)
180   cert = Puppet::X509::CertProvider.new
181   cacerts = cert.load_cacerts(required: true)
182   crls = case revocation
183          when :chain, :leaf
184            cert.load_crls(required: true)
185          else
186            []
187          end
188   private_key = cert.load_private_key(certname, required: true, password: password)
189   client_cert = cert.load_client_cert(certname, required: true)
190 
191   create_context(cacerts: cacerts, crls: crls,  private_key: private_key, client_cert: client_cert, revocation: revocation, include_system_store: include_system_store)
192 rescue OpenSSL::PKey::PKeyError => e
193   raise Puppet::SSL::SSLError.new(_("Failed to load private key for host '%{name}': %{message}") % { name: certname, message: e.message }, e)
194 end
print(ssl_context, alg = 'SHA256') click to toggle source
verify_request(csr, public_key) click to toggle source

Verify the `csr` was signed with a private key corresponding to the `public_key`. This ensures the CSR was signed by someone in possession of the private key, and that it hasn't been tampered with since.

@param csr [OpenSSL::X509::Request] certificate signing request @param public_key [OpenSSL::PKey::RSA, OpenSSL::PKey::EC] public key @raise [Puppet::SSL:SSLError] The private_key for the given `public_key` was

not used to sign the CSR.

@api private

    # File lib/puppet/ssl/ssl_provider.rb
205 def verify_request(csr, public_key)
206   unless csr.verify(public_key)
207     raise Puppet::SSL::SSLError, _("The CSR for host '%{name}' does not match the public key") % { name: subject(csr) }
208   end
209 
210   csr
211 end

Private Instance Methods

create_x509_store(roots, crls, revocation, include_system_store: false) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
247 def create_x509_store(roots, crls, revocation, include_system_store: false)
248   store = OpenSSL::X509::Store.new
249   store.purpose = OpenSSL::X509::PURPOSE_ANY
250   store.flags = default_flags | revocation_mode(revocation)
251 
252   roots.each { |cert| store.add_cert(cert) }
253   crls.each { |crl| store.add_crl(crl) }
254 
255   store.set_default_paths if include_system_store
256 
257   store
258 end
default_flags() click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
236 def default_flags
237   # checking the signature of the self-signed cert doesn't add any security,
238   # but it's a sanity check to make sure the cert isn't corrupt. This option
239   # is not available in JRuby's OpenSSL library.
240   if defined?(OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE)
241     OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE
242   else
243     0
244   end
245 end
issuer(x509) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
264 def issuer(x509)
265   x509.issuer.to_utf8
266 end
raise_cert_verify_error(store_context, current_cert) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
320 def raise_cert_verify_error(store_context, current_cert)
321   message =
322     case store_context.error
323     when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID
324       _("The certificate '%{subject}' is not yet valid, verify time is synchronized") % { subject: subject(current_cert) }
325     when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED
326       _("The certificate '%{subject}' has expired, verify time is synchronized") %  { subject: subject(current_cert) }
327     when OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID
328       _("The CRL issued by '%{issuer}' is not yet valid, verify time is synchronized") % { issuer: issuer(current_cert) }
329     when OpenSSL::X509::V_ERR_CRL_HAS_EXPIRED
330       _("The CRL issued by '%{issuer}' has expired, verify time is synchronized") % { issuer: issuer(current_cert) }
331     when OpenSSL::X509::V_ERR_CERT_SIGNATURE_FAILURE
332       _("Invalid signature for certificate '%{subject}'") % { subject: subject(current_cert) }
333     when OpenSSL::X509::V_ERR_CRL_SIGNATURE_FAILURE
334       _("Invalid signature for CRL issued by '%{issuer}'") % { issuer: issuer(current_cert) }
335     when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT
336       _("The issuer '%{issuer}' of certificate '%{subject}' is missing") % {
337         issuer: issuer(current_cert), subject: subject(current_cert) }
338     when OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL
339       _("The CRL issued by '%{issuer}' is missing") % { issuer: issuer(current_cert) }
340     when OpenSSL::X509::V_ERR_CERT_REVOKED
341       _("Certificate '%{subject}' is revoked") % { subject: subject(current_cert) }
342     else
343       # error_string is labeled ASCII-8BIT, but is encoded based on Encoding.default_external
344       err_utf8 = Puppet::Util::CharacterEncoding.convert_to_utf_8(store_context.error_string)
345       _("Certificate '%{subject}' failed verification (%{err}): %{err_utf8}") % {
346         subject: subject(current_cert), err: store_context.error, err_utf8: err_utf8 }
347     end
348 
349   raise Puppet::SSL::CertVerifyError.new(message, store_context.error, current_cert)
350 end
resolve_client_chain(store, client_cert, private_key) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
280 def resolve_client_chain(store, client_cert, private_key)
281   client_chain = verify_cert_with_store(store, client_cert)
282 
283   if !private_key.is_a?(OpenSSL::PKey::RSA) && !private_key.is_a?(OpenSSL::PKey::EC)
284     raise Puppet::SSL::SSLError, _("Unsupported key '%{type}'") % { type: private_key.class.name }
285   end
286 
287   unless client_cert.check_private_key(private_key)
288     raise Puppet::SSL::SSLError, _("The certificate for '%{name}' does not match its private key") % { name: subject(client_cert) }
289   end
290 
291   client_chain
292 end
revocation_mode(mode) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
268 def revocation_mode(mode)
269   case mode
270   when false
271     0
272   when :leaf
273     OpenSSL::X509::V_FLAG_CRL_CHECK
274   else
275     # :chain is the default
276     OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
277   end
278 end
subject(x509) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
260 def subject(x509)
261   x509.subject.to_utf8
262 end
verify_cert_with_store(store, cert) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
294 def verify_cert_with_store(store, cert)
295   # StoreContext#initialize accepts a chain argument, but it's set to [] because
296   # puppet requires any intermediate CA certs needed to complete the client's
297   # chain to be in the CA bundle that we downloaded from the server, and
298   # they've already been added to the store. See PUP-9500.
299 
300   store_context = OpenSSL::X509::StoreContext.new(store, cert, [])
301   unless store_context.verify
302     current_cert = store_context.current_cert
303 
304     # If the client cert's intermediate CA is not in the CA bundle, then warn,
305     # but don't error, because SSL allows the client to send an incomplete
306     # chain, and have the server resolve it.
307     if store_context.error == OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY
308       Puppet.warning _("The issuer '%{issuer}' of certificate '%{subject}' cannot be found locally") % {
309         issuer: issuer(current_cert), subject: subject(current_cert)
310       }
311     else
312       raise_cert_verify_error(store_context, current_cert)
313     end
314   end
315 
316   # resolved chain from leaf to root
317   store_context.chain
318 end