# class CertificateAuthority

The class that knows how to sign certificates. It creates a ‘special’ SSL::Host whose name is ‘ca’, thus indicating that, well, it’s the CA. There’s some magic in the indirector/ssl_file terminus base class that does that for us.

    This class mostly just signs certs for us, but

it can also be seen as a general interface into all of the SSL stuff.

### Public Class Methods

ca?() click to toggle source

    # File lib/puppet/ssl/certificate_authority.rb, line 55
    def self.ca?
      return false unless Puppet[:ca]
      return false unless Puppet.run_mode.master?
      true
    end

instance() click to toggle source

If this process can function as a CA, then return a singleton instance.

    # File lib/puppet/ssl/certificate_authority.rb, line 63
    def self.instance
      return nil unless ca?

      singleton_instance
    end

new() click to toggle source

    # File lib/puppet/ssl/certificate_authority.rb, line 164
    def initialize
      Puppet.settings.use :main, :ssl, :ca

      @name = Puppet[:certname]

      @host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name)

      setup
    end

singleton_instance() click to toggle source

    # File lib/puppet/ssl/certificate_authority.rb, line 41
    def self.singleton_instance
      synchronize do
        @singleton_instance ||= new
      end
    end

### Public Instance Methods

apply(method, options) click to toggle source

Create and run an applicator. I wanted to build an interface where you could do something like ‘ca.apply(:generate).to(:all) but I don’t think it’s really possible.

    # File lib/puppet/ssl/certificate_authority.rb, line 73
    def apply(method, options)
      raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" unless options[:to]
      applier = Interface.new(method, options)
      applier.apply(self)
    end

autosign() click to toggle source

If autosign is configured, then autosign all CSRs that match our configuration.

    # File lib/puppet/ssl/certificate_authority.rb, line 80
    def autosign
      return unless auto = autosign?

      store = nil
      store = autosign_store(auto) if auto != true

      Puppet::SSL::CertificateRequest.indirection.search("*").each do |csr|
        sign(csr.name) if auto == true or store.allowed?(csr.name, "127.1.1.1")
      end
    end

autosign?() click to toggle source

Do we autosign? This returns true, false, or a filename.

    # File lib/puppet/ssl/certificate_authority.rb, line 92
    def autosign?
      auto = Puppet[:autosign]
      return false if ['false', false].include?(auto)
      return true if ['true', true].include?(auto)

      raise ArgumentError, "The autosign configuration '#{auto}' must be a fully qualified file" unless Puppet::Util.absolute_path?(auto)
      FileTest.exist?(auto) && auto
    end

autosign_store(file) click to toggle source

Create an AuthStore for autosigning.

    # File lib/puppet/ssl/certificate_authority.rb, line 102
    def autosign_store(file)
      auth = Puppet::Network::AuthStore.new
      File.readlines(file).each do |line|
        next if line =~ /^\s*#/
        next if line =~ /^\s*$/
        auth.allow(line.chomp)
      end

      auth
    end

check_internal_signing_policies(hostname, csr, allow_dns_alt_names) click to toggle source

    # File lib/puppet/ssl/certificate_authority.rb, line 302
    def check_internal_signing_policies(hostname, csr, allow_dns_alt_names)
      # Reject unknown request extensions.
      unknown_req = csr.request_extensions.
        reject {|x| RequestExtensionWhitelist.include? x["oid"] }

      if unknown_req and not unknown_req.empty?
        names = unknown_req.map {|x| x["oid"] }.sort.uniq.join(", ")
        raise CertificateSigningError.new(hostname), "CSR has request extensions that are not permitted: #{names}"
      end

      # Do not sign misleading CSRs
      cn = csr.content.subject.to_a.assoc("CN")[1]
      if hostname != cn
        raise CertificateSigningError.new(hostname), "CSR subject common name #{cn.inspect} does not match expected certname #{hostname.inspect}"
      end

      if hostname !~ Puppet::SSL::Base::VALID_CERTNAME
        raise CertificateSigningError.new(hostname), "CSR #{hostname.inspect} subject contains unprintable or non-ASCII characters"
      end

      # Wildcards: we don't allow 'em at any point.
      #
      # The stringification here makes the content visible, and saves us having
      # to scrobble through the content of the CSR subject field to make sure it
      # is what we expect where we expect it.
      if csr.content.subject.to_s.include? '*'
        raise CertificateSigningError.new(hostname), "CSR subject contains a wildcard, which is not allowed: #{csr.content.subject.to_s}"
      end

      unless csr.content.verify(csr.content.public_key)
        raise CertificateSigningError.new(hostname), "CSR contains a public key that does not correspond to the signing key"
      end

      unless csr.subject_alt_names.empty?
        # If you alt names are allowed, they are required. Otherwise they are
        # disallowed. Self-signed certs are implicitly trusted, however.
        unless allow_dns_alt_names
          raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains subject alternative names (#{csr.subject_alt_names.join(', ')}), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{csr.name}` to sign this request."
        end

        # If subjectAltNames are present, validate that they are only for DNS
        # labels, not any other kind.
        unless csr.subject_alt_names.all? {|x| x =~ /^DNS:/ }
          raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains a subjectAltName outside the DNS label space: #{csr.subject_alt_names.join(', ')}.  To continue, this CSR needs to be cleaned."
        end

        # Check for wildcards in the subjectAltName fields too.
        if csr.subject_alt_names.any? {|x| x.include? '*' }
          raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' subjectAltName contains a wildcard, which is not allowed: #{csr.subject_alt_names.join(', ')}  To continue, this CSR needs to be cleaned."
        end
      end

      return true                 # good enough for us!
    end

crl() click to toggle source

Retrieve (or create, if necessary) the certificate revocation list.

    # File lib/puppet/ssl/certificate_authority.rb, line 114
    def crl
      unless defined?(@crl)
        unless @crl = Puppet::SSL::CertificateRevocationList.indirection.find(Puppet::SSL::CA_NAME)
          @crl = Puppet::SSL::CertificateRevocationList.new(Puppet::SSL::CA_NAME)
          @crl.generate(host.certificate.content, host.key.content)
          Puppet::SSL::CertificateRevocationList.indirection.save(@crl)
        end
      end
      @crl
    end

destroy(name) click to toggle source

Delegate this to our Host class.

    # File lib/puppet/ssl/certificate_authority.rb, line 126
    def destroy(name)
      Puppet::SSL::Host.destroy(name)
    end

fingerprint(name, md = :SHA256) click to toggle source

    # File lib/puppet/ssl/certificate_authority.rb, line 371
    def fingerprint(name, md = :SHA256)
      unless cert = Puppet::SSL::Certificate.indirection.find(name) || Puppet::SSL::CertificateRequest.indirection.find(name)
        raise ArgumentError, "Could not find a certificate or csr for #{name}"
      end
      cert.fingerprint(md)
    end

generate(name, options = {}) click to toggle source

Generate a new certificate.

    # File lib/puppet/ssl/certificate_authority.rb, line 131
    def generate(name, options = {})
      raise ArgumentError, "A Certificate already exists for #{name}" if Puppet::SSL::Certificate.indirection.find(name)
      host = Puppet::SSL::Host.new(name)

      # Pass on any requested subjectAltName field.
      san = options[:dns_alt_names]

      host = Puppet::SSL::Host.new(name)
      host.generate_certificate_request(:dns_alt_names => san)
      sign(name, !!san)
    end

generate_ca_certificate() click to toggle source

Generate our CA certificate.

    # File lib/puppet/ssl/certificate_authority.rb, line 144
    def generate_ca_certificate
      generate_password unless password?

      host.generate_key unless host.key

      # Create a new cert request.  We do this specially, because we don't want
      # to actually save the request anywhere.
      request = Puppet::SSL::CertificateRequest.new(host.name)

      # We deliberately do not put any subjectAltName in here: the CA
      # certificate absolutely does not need them. --daniel 2011-10-13
      request.generate(host.key)

      # Create a self-signed certificate.
      @certificate = sign(host.name, false, request)

      # And make sure we initialize our CRL.
      crl
    end

generate_password() click to toggle source

Generate a new password for the CA.

    # File lib/puppet/ssl/certificate_authority.rb, line 180
    def generate_password
      pass = ""
      20.times { pass += (rand(74) + 48).chr }

      begin
        Puppet.settings.write(:capass) { |f| f.print pass }
      rescue Errno::EACCES => detail
        raise Puppet::Error, "Could not write CA password: #{detail}"
      end

      @password = pass

      pass
    end

inventory() click to toggle source

Retrieve (or create, if necessary) our inventory manager.

    # File lib/puppet/ssl/certificate_authority.rb, line 175
    def inventory
      @inventory ||= Puppet::SSL::Inventory.new
    end

list() click to toggle source

List all signed certificates.

    # File lib/puppet/ssl/certificate_authority.rb, line 196
    def list
      Puppet::SSL::Certificate.indirection.search("*").collect { |c| c.name }
    end

next_serial() click to toggle source

Read the next serial from the serial file, and increment the file so this one is considered used.

    # File lib/puppet/ssl/certificate_authority.rb, line 202
    def next_serial
      serial = nil

      # This is slightly odd.  If the file doesn't exist, our readwritelock creates
      # it, but with a mode we can't actually read in some cases.  So, use
      # a default before the lock.
      serial = 0x1 unless FileTest.exist?(Puppet[:serial])

      Puppet.settings.readwritelock(:serial) { |f|
        serial ||= File.read(Puppet.settings[:serial]).chomp.hex if FileTest.exist?(Puppet[:serial])

        # We store the next valid serial, not the one we just used.
        f << "%04X" % (serial + 1)
      }

      serial
    end

password?() click to toggle source

Does the password file exist?

    # File lib/puppet/ssl/certificate_authority.rb, line 221
    def password?
      FileTest.exist? Puppet[:capass]
    end

print(name) click to toggle source

Print a given host’s certificate as text.

    # File lib/puppet/ssl/certificate_authority.rb, line 226
    def print(name)
      (cert = Puppet::SSL::Certificate.indirection.find(name)) ? cert.to_text : nil
    end

revoke(name) click to toggle source

Revoke a given certificate.

    # File lib/puppet/ssl/certificate_authority.rb, line 231
    def revoke(name)
      raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl

      if cert = Puppet::SSL::Certificate.indirection.find(name)
        serial = cert.content.serial
      elsif name =~ /^0x[0-9A-Fa-f]+$/
        serial = name.hex
      elsif ! serial = inventory.serial(name)
        raise ArgumentError, "Could not find a serial number for #{name}"
      end
      crl.revoke(serial, host.key.content)
    end

setup() click to toggle source

This initializes our CA so it actually works. This should be a private method, except that you can’t any-instance stub private methods, which is awesome. This method only really exists to provide a stub-point during testing.

    # File lib/puppet/ssl/certificate_authority.rb, line 248
    def setup
      generate_ca_certificate unless @host.certificate
    end

sign(hostname, allow_dns_alt_names = false, self_signing_csr = nil) click to toggle source

Sign a given certificate request.

    # File lib/puppet/ssl/certificate_authority.rb, line 253
    def sign(hostname, allow_dns_alt_names = false, self_signing_csr = nil)
      # This is a self-signed certificate
      if self_signing_csr
        # # This is a self-signed certificate, which is for the CA.  Since this
        # # forces the certificate to be self-signed, anyone who manages to trick
        # # the system into going through this path gets a certificate they could
        # # generate anyway.  There should be no security risk from that.
        csr = self_signing_csr
        cert_type = :ca
        issuer = csr.content
      else
        allow_dns_alt_names = true if hostname == Puppet[:certname].downcase
        unless csr = Puppet::SSL::CertificateRequest.indirection.find(hostname)
          raise ArgumentError, "Could not find certificate request for #{hostname}"
        end

        cert_type = :server
        issuer = host.certificate.content

        # Make sure that the CSR conforms to our internal signing policies.
        # This will raise if the CSR doesn't conform, but just in case...
        check_internal_signing_policies(hostname, csr, allow_dns_alt_names) or
          raise CertificateSigningError.new(hostname), "CSR had an unknown failure checking internal signing policies, will not sign!"
      end

      cert = Puppet::SSL::Certificate.new(hostname)
      cert.content = Puppet::SSL::CertificateFactory.
        build(cert_type, csr, issuer, next_serial)

      signer = Puppet::SSL::CertificateSigner.new
      signer.sign(cert.content, host.key.content)

      Puppet.notice "Signed certificate request for #{hostname}"

      # Add the cert to the inventory before we save it, since
      # otherwise we could end up with it being duplicated, if
      # this is the first time we build the inventory file.
      inventory.add(cert)

      # Save the now-signed cert.  This should get routed correctly depending
      # on the certificate type.
      Puppet::SSL::Certificate.indirection.save(cert)

      # And remove the CSR if this wasn't self signed.
      Puppet::SSL::CertificateRequest.indirection.destroy(csr.name) unless self_signing_csr

      cert
    end

verify(name) click to toggle source

Verify a given host’s certificate.

    # File lib/puppet/ssl/certificate_authority.rb, line 358
    def verify(name)
      unless cert = Puppet::SSL::Certificate.indirection.find(name)
        raise ArgumentError, "Could not find a certificate for #{name}"
      end
      store = OpenSSL::X509::Store.new
      store.add_file Puppet[:cacert]
      store.add_crl crl.content if self.crl
      store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
      store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK if Puppet.settings[:certificate_revocation]

      raise CertificateVerificationError.new(store.error), store.error_string unless store.verify(cert.content)
    end

waiting?() click to toggle source

List the waiting certificate requests.

    # File lib/puppet/ssl/certificate_authority.rb, line 379
    def waiting?
      Puppet::SSL::CertificateRequest.indirection.search("*").collect { |r| r.name }
    end