require 'puppet/x509' # Class for loading and saving cert related objects. # # @api private class Puppet::X509::CertProvider include Puppet::X509::PemStore # Only allow printing ascii characters, excluding / VALID_CERTNAME = /\A[ -.0-~]+\Z/ CERT_DELIMITERS = /-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m CRL_DELIMITERS = /-----BEGIN X509 CRL-----.*?-----END X509 CRL-----/m def initialize(capath: Puppet[:localcacert], crlpath: Puppet[:hostcrl], privatekeydir: Puppet[:privatekeydir], certdir: Puppet[:certdir], requestdir: Puppet[:requestdir]) @capath = capath @crlpath = crlpath @privatekeydir = privatekeydir @certdir = certdir @requestdir = requestdir end # Save `certs` to the configured `capath`. # # @param certs [Array] Array of CA certs to save # @raise [Puppet::Error] if the certs cannot be saved # @api private def save_cacerts(certs) save_pem(certs.map(&:to_pem).join, @capath, **permissions_for_setting(:localcacert)) rescue SystemCallError => e raise Puppet::Error.new(_("Failed to save CA certificates to '%{capath}'") % {capath: @capath}, e) end # Load CA certs from the configured `capath`. # # @param required [Boolean] If true, raise if they are missing # @return (see #load_cacerts_from_pem) # @raise (see #load_cacerts_from_pem) # @raise [Puppet::Error] if the certs cannot be loaded # @api private def load_cacerts(required: false) pem = load_pem(@capath) if !pem && required raise Puppet::Error, _("The CA certificates are missing from '%{path}'") % { path: @capath } end pem ? load_cacerts_from_pem(pem) : nil rescue SystemCallError => e raise Puppet::Error.new(_("Failed to load CA certificates from '%{capath}'") % {capath: @capath}, e) end # Load PEM encoded CA certificates. # # @param pem [String] PEM encoded certificate(s) # @return [Array] Array of CA certs # @raise [OpenSSL::X509::CertificateError] The `pem` text does not contain a valid cert # @api private def load_cacerts_from_pem(pem) # TRANSLATORS 'PEM' is an acronym and shouldn't be translated raise OpenSSL::X509::CertificateError, _("Failed to parse CA certificates as PEM") if pem !~ CERT_DELIMITERS pem.scan(CERT_DELIMITERS).map do |text| OpenSSL::X509::Certificate.new(text) end end # Save `crls` to the configured `crlpath`. # # @param crls [Array] Array of CRLs to save # @raise [Puppet::Error] if the CRLs cannot be saved # @api private def save_crls(crls) save_pem(crls.map(&:to_pem).join, @crlpath, **permissions_for_setting(:hostcrl)) rescue SystemCallError => e raise Puppet::Error.new(_("Failed to save CRLs to '%{crlpath}'") % {crlpath: @crlpath}, e) end # Load CRLs from the configured `crlpath` path. # # @param required [Boolean] If true, raise if they are missing # @return (see #load_crls_from_pem) # @raise (see #load_crls_from_pem) # @raise [Puppet::Error] if the CRLs cannot be loaded # @api private def load_crls(required: false) pem = load_pem(@crlpath) if !pem && required raise Puppet::Error, _("The CRL is missing from '%{path}'") % { path: @crlpath } end pem ? load_crls_from_pem(pem) : nil rescue SystemCallError => e raise Puppet::Error.new(_("Failed to load CRLs from '%{crlpath}'") % {crlpath: @crlpath}, e) end # Load PEM encoded CRL(s). # # @param pem [String] PEM encoded CRL(s) # @return [Array] Array of CRLs # @raise [OpenSSL::X509::CRLError] The `pem` text does not contain a valid CRL # @api private def load_crls_from_pem(pem) # TRANSLATORS 'PEM' is an acronym and shouldn't be translated raise OpenSSL::X509::CRLError, _("Failed to parse CRLs as PEM") if pem !~ CRL_DELIMITERS pem.scan(CRL_DELIMITERS).map do |text| OpenSSL::X509::CRL.new(text) end end # Save named private key in the configured `privatekeydir`. For # historical reasons, names are case insensitive. # # @param name [String] The private key identity # @param key [OpenSSL::PKey::RSA] private key # @raise [Puppet::Error] if the private key cannot be saved # @api private def save_private_key(name, key) path = to_path(@privatekeydir, name) save_pem(key.to_pem, path, **permissions_for_setting(:hostprivkey)) rescue SystemCallError => e raise Puppet::Error.new(_("Failed to save private key for '%{name}'") % {name: name}, e) end # Load a private key from the configured `privatekeydir`. For # historical reasons, names are case-insensitive. # # @param name [String] The private key identity # @param required [Boolean] If true, raise if it is missing # @return (see #load_private_key_from_pem) # @raise (see #load_private_key_from_pem) # @raise [Puppet::Error] if the private key cannot be loaded # @api private def load_private_key(name, required: false) path = to_path(@privatekeydir, name) pem = load_pem(path) if !pem && required raise Puppet::Error, _("The private key is missing from '%{path}'") % { path: path } end pem ? load_private_key_from_pem(pem) : nil rescue SystemCallError => e raise Puppet::Error.new(_("Failed to load private key for '%{name}'") % {name: name}, e) end # Load a PEM encoded private key. # # @param pem [String] PEM encoded private key # @return [OpenSSL::PKey::RSA] The private key # @raise [OpenSSL::PKey::RSAError] The `pem` text does not contain a valid key # @api private def load_private_key_from_pem(pem) # set a non-nil passphrase to ensure openssl doesn't prompt # but ruby 2.4.0 & 2.4.1 require at least 4 bytes, see # https://github.com/ruby/ruby/commit/f012932218fd609f75f9268812df61fb26e2d0f1#diff-40e4270ec386990ac60d7ab5ff8045a4 OpenSSL::PKey::RSA.new(pem, ' ') end # Save a named client cert to the configured `certdir`. # # @param name [String] The client cert identity # @param cert [OpenSSL::X509::Certificate] The cert to save # @raise [Puppet::Error] if the client cert cannot be saved # @api private def save_client_cert(name, cert) path = to_path(@certdir, name) save_pem(cert.to_pem, path, **permissions_for_setting(:hostcert)) rescue SystemCallError => e raise Puppet::Error.new(_("Failed to save client certificate for '%{name}'") % {name: name}, e) end # Load a named client cert from the configured `certdir`. # # @param name [String] The client cert identity # @param required [Boolean] If true, raise it is missing # @return (see #load_request_from_pem) # @raise (see #load_client_cert_from_pem) # @raise [Puppet::Error] if the client cert cannot be loaded # @api private def load_client_cert(name, required: false) path = to_path(@certdir, name) pem = load_pem(path) if !pem && required raise Puppet::Error, _("The client certificate is missing from '%{path}'") % { path: path } end pem ? load_client_cert_from_pem(pem) : nil rescue SystemCallError => e raise Puppet::Error.new(_("Failed to load client certificate for '%{name}'") % {name: name}, e) end # Load a PEM encoded certificate. # # @param pem [String] PEM encoded cert # @return [OpenSSL::X509::Certificate] the certificate # @raise [OpenSSL::X509::CertificateError] The `pem` text does not contain a valid cert # @api private def load_client_cert_from_pem(pem) OpenSSL::X509::Certificate.new(pem) end # Create a certificate signing request (CSR). # # @param name [String] the request identity # @param private_key [OpenSSL::PKey::RSA] private key # @return [Puppet::X509::Request] The request # def create_request(name, private_key) options = {} if Puppet[:dns_alt_names] && Puppet[:dns_alt_names] != '' options[:dns_alt_names] = Puppet[:dns_alt_names] end csr_attributes = Puppet::SSL::CertificateRequestAttributes.new(Puppet[:csr_attributes]) if csr_attributes.load options[:csr_attributes] = csr_attributes.custom_attributes options[:extension_requests] = csr_attributes.extension_requests end csr = Puppet::SSL::CertificateRequest.new(name) csr.generate(private_key, options) end # Save a certificate signing request (CSR) to the configured `requestdir`. # # @param name [String] the request identity # @param csr [OpenSSL::X509::Request] the request # @raise [Puppet::Error] if the cert request cannot be saved # @api private def save_request(name, csr) path = to_path(@requestdir, name) save_pem(csr.to_pem, path, **permissions_for_setting(:hostcsr)) rescue SystemCallError => e raise Puppet::Error.new(_("Failed to save certificate request for '%{name}'") % {name: name}, e) end # Load a named certificate signing request (CSR) from the configured `requestdir`. # # @param name [String] The request identity # @return (see #load_request_from_pem) # @raise (see #load_request_from_pem) # @raise [Puppet::Error] if the cert request cannot be saved # @api private def load_request(name) path = to_path(@requestdir, name) pem = load_pem(path) pem ? load_request_from_pem(pem) : nil rescue SystemCallError => e raise Puppet::Error.new(_("Failed to load certificate request for '%{name}'") % {name: name}, e) end # Delete a named certificate signing request (CSR) from the configured `requestdir`. # # @param name [String] The request identity # @return [Boolean] true if the CSR was deleted def delete_request(name) path = to_path(@requestdir, name) delete_pem(path) rescue SystemCallError => e raise Puppet::Error.new(_("Failed to delete certificate request for '%{name}'") % {name: name}, e) end # Load a PEM encoded certificate signing request (CSR). # # @param pem [String] PEM encoded request # @return [OpenSSL::X509::Request] the request # @raise [OpenSSL::X509::RequestError] The `pem` text does not contain a valid request # @api private def load_request_from_pem(pem) OpenSSL::X509::Request.new(pem) end private def to_path(base, name) raise _("Certname %{name} must not contain unprintable or non-ASCII characters") % { name: name.inspect } unless name =~ VALID_CERTNAME File.join(base, "#{name.downcase}.pem") end def permissions_for_setting(name) setting = Puppet.settings.setting(name) perm = { mode: setting.mode.to_i(8) } perm.merge!(owner: setting.owner, group: setting.group) if Puppet.features.root? && !Puppet::Util::Platform.windows? perm end end