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 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 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 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 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 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
# File lib/puppet/ssl/ssl_provider.rb 213 def print(ssl_context, alg = 'SHA256') 214 if Puppet::Util::Log.sendlevel?(:debug) 215 chain = ssl_context.client_chain 216 # print from root to client 217 chain.reverse.each_with_index do |cert, i| 218 digest = Puppet::SSL::Digest.new(alg, cert.to_der) 219 if i == chain.length - 1 220 Puppet.debug(_("Verified client certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_utf8, digest: digest}) 221 else 222 Puppet.debug(_("Verified CA certificate '%{subject}' fingerprint %{digest}") % {subject: cert.subject.to_utf8, digest: digest}) 223 end 224 end 225 ssl_context.crls.each do |crl| 226 oid_values = Hash[crl.extensions.map { |ext| [ext.oid, ext.value] }] 227 crlNumber = oid_values['crlNumber'] || 'unknown' 228 authKeyId = (oid_values['authorityKeyIdentifier'] || 'unknown').chomp! 229 Puppet.debug("Using CRL '#{crl.issuer.to_utf8}' authorityKeyIdentifier '#{authKeyId}' crlNumber '#{crlNumber }'") 230 end 231 end 232 end
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
# 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
# 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
# File lib/puppet/ssl/ssl_provider.rb 264 def issuer(x509) 265 x509.issuer.to_utf8 266 end
# 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
# 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
# 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
# File lib/puppet/ssl/ssl_provider.rb 260 def subject(x509) 261 x509.subject.to_utf8 262 end
# 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