require 'puppet/ssl/host' require 'puppet/ssl/certificate_request' require 'puppet/util/cacher' # 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. class Puppet::SSL::CertificateAuthority require 'puppet/ssl/certificate_factory' require 'puppet/ssl/inventory' require 'puppet/ssl/certificate_revocation_list' require 'puppet/ssl/certificate_authority/interface' class CertificateVerificationError < RuntimeError attr_accessor :error_code def initialize(code) @error_code = code end end class << self include Puppet::Util::Cacher cached_attr(:singleton_instance) { new } end def self.ca? return false unless Puppet[:ca] return false unless Puppet[:name] == "puppetmasterd" return true end # If this process can function as a CA, then return a singleton # instance. def self.instance return nil unless ca? singleton_instance end attr_reader :name, :host # 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. def apply(method, options) unless options[:to] raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" end applier = Interface.new(method, options) applier.apply(self) end # If autosign is configured, then autosign all CSRs that match our configuration. def autosign return unless auto = autosign? store = nil if auto != true store = autosign_store(auto) end Puppet::SSL::CertificateRequest.search("*").each do |csr| sign(csr.name) if auto == true or store.allowed?(csr.name, "127.1.1.1") end end # Do we autosign? This returns true, false, or a filename. def autosign? auto = Puppet[:autosign] return false if ['false', false].include?(auto) return true if ['true', true].include?(auto) raise ArgumentError, "The autosign configuration '%s' must be a fully qualified file" % auto unless auto =~ /^\// if FileTest.exist?(auto) return auto else return false end end # Create an AuthStore for autosigning. 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 # Retrieve (or create, if necessary) the certificate revocation list. def crl unless defined?(@crl) unless @crl = Puppet::SSL::CertificateRevocationList.find(Puppet::SSL::CA_NAME) @crl = Puppet::SSL::CertificateRevocationList.new(Puppet::SSL::CA_NAME) @crl.generate(host.certificate.content, host.key.content) @crl.save end end @crl end # Delegate this to our Host class. def destroy(name) Puppet::SSL::Host.destroy(name) end # Generate a new certificate. def generate(name) raise ArgumentError, "A Certificate already exists for %s" % name if Puppet::SSL::Certificate.find(name) host = Puppet::SSL::Host.new(name) host.generate_certificate_request sign(name) end # Generate our CA certificate. 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) request.generate(host.key) # Create a self-signed certificate. @certificate = sign(host.name, :ca, request) # And make sure we initialize our CRL. crl() end def initialize Puppet.settings.use :main, :ssl, :ca @name = Puppet[:certname] @host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name) setup() end # Retrieve (or create, if necessary) our inventory manager. def inventory unless defined?(@inventory) @inventory = Puppet::SSL::Inventory.new end @inventory end # Generate a new password for the CA. 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: %s" % detail.to_s end @password = pass return pass end # List all signed certificates. def list Puppet::SSL::Certificate.search("*").collect { |c| c.name } end # Read the next serial from the serial file, and increment the # file so this one is considered used. 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. unless FileTest.exist?(Puppet[:serial]) serial = 0x1 end Puppet.settings.readwritelock(:serial) { |f| if FileTest.exist?(Puppet[:serial]) serial ||= File.read(Puppet.settings[:serial]).chomp.hex end # We store the next valid serial, not the one we just used. f << "%04X" % (serial + 1) } return serial end # Does the password file exist? def password? FileTest.exist? Puppet[:capass] end # Print a given host's certificate as text. def print(name) if cert = Puppet::SSL::Certificate.find(name) return cert.to_text else return nil end end # Revoke a given certificate. def revoke(name) raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl if cert = Puppet::SSL::Certificate.find(name) serial = cert.content.serial elsif ! serial = inventory.serial(name) raise ArgumentError, "Could not find a serial number for %s" % name end crl.revoke(serial, host.key.content) end # 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. def setup generate_ca_certificate unless @host.certificate end # Sign a given certificate request. def sign(hostname, cert_type = :server, self_signing_csr = nil) # This is a self-signed certificate if self_signing_csr csr = self_signing_csr issuer = csr.content else unless csr = Puppet::SSL::CertificateRequest.find(hostname) raise ArgumentError, "Could not find certificate request for %s" % hostname end issuer = host.certificate.content end cert = Puppet::SSL::Certificate.new(hostname) cert.content = Puppet::SSL::CertificateFactory.new(cert_type, csr.content, issuer, next_serial).result cert.content.sign(host.key.content, OpenSSL::Digest::SHA1.new) Puppet.notice "Signed certificate request for %s" % 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. cert.save # And remove the CSR if this wasn't self signed. Puppet::SSL::CertificateRequest.destroy(csr.name) unless self_signing_csr return cert end # Verify a given host's certificate. def verify(name) unless cert = Puppet::SSL::Certificate.find(name) raise ArgumentError, "Could not find a certificate for %s" % 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 unless store.verify(cert.content) raise CertificateVerificationError.new(store.error), store.error_string end end def fingerprint(name, md = :MD5) unless cert = Puppet::SSL::Certificate.find(name) || Puppet::SSL::CertificateRequest.find(name) raise ArgumentError, "Could not find a certificate or csr for %s" % name end cert.fingerprint(md) end # List the waiting certificate requests. def waiting? Puppet::SSL::CertificateRequest.search("*").collect { |r| r.name } end end