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]) 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 @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
120 def create_context(cacerts:, crls:, private_key:, client_cert:, revocation: Puppet[:certificate_revocation])
121   raise ArgumentError, _("CA certs are missing") unless cacerts
122   raise ArgumentError, _("CRLs are missing") unless crls
123   raise ArgumentError, _("Private key is missing") unless private_key
124   raise ArgumentError, _("Client cert is missing") unless client_cert
125 
126   store = create_x509_store(cacerts, crls, revocation)
127   client_chain = verify_cert_with_store(store, client_cert)
128 
129   if !private_key.is_a?(OpenSSL::PKey::RSA) && !private_key.is_a?(OpenSSL::PKey::EC)
130     raise Puppet::SSL::SSLError, _("Unsupported key '%{type}'") % { type: private_key.class.name }
131   end
132 
133   unless client_cert.check_private_key(private_key)
134     raise Puppet::SSL::SSLError, _("The certificate for '%{name}' does not match its private key") % { name: subject(client_cert) }
135   end
136 
137   Puppet::SSL::SSLContext.new(
138     store: store, cacerts: cacerts, crls: crls,
139     private_key: private_key, client_cert: client_cert, client_chain: client_chain,
140     revocation: revocation
141   ).freeze
142 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
31 def create_insecure_context
32   store = create_x509_store([], [], false)
33 
34   Puppet::SSL::SSLContext.new(store: store, verify_peer: false).freeze
35 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
50 def create_root_context(cacerts:, crls: [], revocation: Puppet[:certificate_revocation])
51   store = create_x509_store(cacerts, crls, revocation)
52 
53   Puppet::SSL::SSLContext.new(store: store, cacerts: cacerts, crls: crls, revocation: revocation).freeze
54 end
create_system_context(cacerts:, path: Puppet[:ssl_trust_store]) 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 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. @return [Puppet::SSL::SSLContext] A context to use to create connections @raise (see create_context) @api private

   # File lib/puppet/ssl/ssl_provider.rb
70 def create_system_context(cacerts:, path: Puppet[:ssl_trust_store])
71   store = create_x509_store(cacerts, [], false)
72   store.set_default_paths
73 
74   if path
75     stat = Puppet::FileSystem.stat(path)
76     if stat
77       if stat.ftype == 'file'
78         # don't add empty files as ruby/openssl will raise
79         if stat.size > 0
80           begin
81             store.add_file(path)
82           rescue => e
83             Puppet.err(_("Failed to add '%{path}' as a trusted CA file: %{detail}" % { path: path, detail: e.message }, e))
84           end
85         end
86       else
87         Puppet.warning(_("The 'ssl_trust_store' setting does not refer to a file and will be ignored: '%{path}'" % { path: path }))
88       end
89     end
90   end
91 
92   Puppet::SSL::SSLContext.new(store: store, cacerts: cacerts, crls: [], revocation: false).freeze
93 end
load_context(certname: Puppet[:certname], revocation: Puppet[:certificate_revocation], password: nil) 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.

@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
159 def load_context(certname: Puppet[:certname], revocation: Puppet[:certificate_revocation], password: nil)
160   cert = Puppet::X509::CertProvider.new
161   cacerts = cert.load_cacerts(required: true)
162   crls = case revocation
163          when :chain, :leaf
164            cert.load_crls(required: true)
165          else
166            []
167          end
168   private_key = cert.load_private_key(certname, required: true, password: password)
169   client_cert = cert.load_client_cert(certname, required: true)
170 
171   create_context(cacerts: cacerts, crls: crls,  private_key: private_key, client_cert: client_cert, revocation: revocation)
172 rescue OpenSSL::PKey::PKeyError => e
173   raise Puppet::SSL::SSLError.new(_("Failed to load private key for host '%{name}': %{message}") % { name: certname, message: e.message }, e)
174 end
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
185 def verify_request(csr, public_key)
186   unless csr.verify(public_key)
187     raise Puppet::SSL::SSLError, _("The CSR for host '%{name}' does not match the public key") % { name: subject(csr) }
188   end
189 
190   csr
191 end

Private Instance Methods

create_x509_store(roots, crls, revocation) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
206 def create_x509_store(roots, crls, revocation)
207   store = OpenSSL::X509::Store.new
208   store.purpose = OpenSSL::X509::PURPOSE_ANY
209   store.flags = default_flags | revocation_mode(revocation)
210 
211   roots.each { |cert| store.add_cert(cert) }
212   crls.each { |crl| store.add_crl(crl) }
213 
214   store
215 end
default_flags() click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
195 def default_flags
196   # checking the signature of the self-signed cert doesn't add any security,
197   # but it's a sanity check to make sure the cert isn't corrupt. This option
198   # is only available in openssl 1.1+
199   if defined?(OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE)
200     OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE
201   else
202     0
203   end
204 end
issuer(x509) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
221 def issuer(x509)
222   x509.issuer.to_utf8
223 end
raise_cert_verify_error(store_context, current_cert) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
263 def raise_cert_verify_error(store_context, current_cert)
264   message =
265     case store_context.error
266     when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID
267       _("The certificate '%{subject}' is not yet valid, verify time is synchronized") % { subject: subject(current_cert) }
268     when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED
269       _("The certificate '%{subject}' has expired, verify time is synchronized") %  { subject: subject(current_cert) }
270     when OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID
271       _("The CRL issued by '%{issuer}' is not yet valid, verify time is synchronized") % { issuer: issuer(current_cert) }
272     when OpenSSL::X509::V_ERR_CRL_HAS_EXPIRED
273       _("The CRL issued by '%{issuer}' has expired, verify time is synchronized") % { issuer: issuer(current_cert) }
274     when OpenSSL::X509::V_ERR_CERT_SIGNATURE_FAILURE
275       _("Invalid signature for certificate '%{subject}'") % { subject: subject(current_cert) }
276     when OpenSSL::X509::V_ERR_CRL_SIGNATURE_FAILURE
277       _("Invalid signature for CRL issued by '%{issuer}'") % { issuer: issuer(current_cert) }
278     when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT
279       _("The issuer '%{issuer}' of certificate '%{subject}' is missing") % {
280         issuer: issuer(current_cert), subject: subject(current_cert) }
281     when OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL
282       _("The CRL issued by '%{issuer}' is missing") % { issuer: issuer(current_cert) }
283     when OpenSSL::X509::V_ERR_CERT_REVOKED
284       _("Certificate '%{subject}' is revoked") % { subject: subject(current_cert) }
285     else
286       # error_string is labeled ASCII-8BIT, but is encoded based on Encoding.default_external
287       err_utf8 = Puppet::Util::CharacterEncoding.convert_to_utf_8(store_context.error_string)
288       _("Certificate '%{subject}' failed verification (%{err}): %{err_utf8}") % {
289         subject: subject(current_cert), err: store_context.error, err_utf8: err_utf8 }
290     end
291 
292   raise Puppet::SSL::CertVerifyError.new(message, store_context.error, current_cert)
293 end
revocation_mode(mode) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
225 def revocation_mode(mode)
226   case mode
227   when false
228     0
229   when :leaf
230     OpenSSL::X509::V_FLAG_CRL_CHECK
231   else
232     # :chain is the default
233     OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
234   end
235 end
subject(x509) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
217 def subject(x509)
218   x509.subject.to_utf8
219 end
verify_cert_with_store(store, cert) click to toggle source
    # File lib/puppet/ssl/ssl_provider.rb
237 def verify_cert_with_store(store, cert)
238   # StoreContext#initialize accepts a chain argument, but it's set to [] because
239   # puppet requires any intermediate CA certs needed to complete the client's
240   # chain to be in the CA bundle that we downloaded from the server, and
241   # they've already been added to the store. See PUP-9500.
242 
243   store_context = OpenSSL::X509::StoreContext.new(store, cert, [])
244   unless store_context.verify
245     current_cert = store_context.current_cert
246 
247     # If the client cert's intermediate CA is not in the CA bundle, then warn,
248     # but don't error, because SSL allows the client to send an incomplete
249     # chain, and have the server resolve it.
250     if store_context.error == OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY
251       Puppet.warning _("The issuer '%{issuer}' of certificate '%{subject}' cannot be found locally") % {
252         issuer: issuer(current_cert), subject: subject(current_cert)
253       }
254     else
255       raise_cert_verify_error(store_context, current_cert)
256     end
257   end
258 
259   # resolved chain from leaf to root
260   store_context.chain
261 end