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.

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