diff --git a/lib/puppet/ssl/certificate_authority.rb b/lib/puppet/ssl/certificate_authority.rb index 7caf44978..bbbf326d8 100644 --- a/lib/puppet/ssl/certificate_authority.rb +++ b/lib/puppet/ssl/certificate_authority.rb @@ -1,508 +1,510 @@ require 'puppet/ssl/host' require 'puppet/ssl/certificate_request' require 'puppet/ssl/certificate_signer' require 'puppet/util' # 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 # We will only sign extensions on this whitelist, ever. Any CSR with a # requested extension that we don't recognize is rejected, against the risk # that it will introduce some security issue through our ignorance of it. # # Adding an extension to this whitelist simply means we will consider it # further, not that we will always accept a certificate with an extension # requested on this list. RequestExtensionWhitelist = %w{subjectAltName} require 'puppet/ssl/certificate_factory' require 'puppet/ssl/inventory' require 'puppet/ssl/certificate_revocation_list' require 'puppet/ssl/certificate_authority/interface' require 'puppet/ssl/certificate_authority/autosign_command' require 'puppet/network/authstore' class CertificateVerificationError < RuntimeError attr_accessor :error_code def initialize(code) @error_code = code end end def self.singleton_instance @singleton_instance ||= new end class CertificateSigningError < RuntimeError attr_accessor :host def initialize(host) @host = host end end def self.ca? # running as ca? - ensure boolean answer !!(Puppet[:ca] && Puppet.run_mode.master?) end # If this process can function as a CA, then return a singleton instance. def self.instance ca? ? singleton_instance : nil end attr_reader :name, :host # If autosign is configured, autosign the csr we are passed. # @param csr [Puppet::SSL::CertificateRequest] The csr to sign. # @return [Void] # @api private def autosign(csr) if autosign?(csr) Puppet.info "Autosigning #{csr.name}" sign(csr.name) end end # Determine if a CSR can be autosigned by the autosign store or autosign command # # @param csr [Puppet::SSL::CertificateRequest] The CSR to check # @return [true, false] # @api private def autosign?(csr) auto = Puppet[:autosign] decider = case auto when false AutosignNever.new when true AutosignAlways.new else file = Puppet::FileSystem.pathname(auto) if Puppet::FileSystem.executable?(file) Puppet::SSL::CertificateAuthority::AutosignCommand.new(auto) elsif Puppet::FileSystem.exist?(file) AutosignConfig.new(file) else AutosignNever.new end end decider.allowed?(csr) end # Retrieves (or creates, if necessary) the certificate revocation list. 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 # Delegates this to our Host class. def destroy(name) Puppet::SSL::Host.destroy(name) end # Generates a new certificate. # @return Puppet::SSL::Certificate def generate(name, options = {}) raise ArgumentError, "A Certificate already exists for #{name}" if Puppet::SSL::Certificate.indirection.find(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) # CSR may have been implicitly autosigned, generating a certificate # Or sign explicitly host.certificate || sign(name, !!san) 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) # 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 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 @inventory ||= Puppet::SSL::Inventory.new end # Generate a new password for the CA. def generate_password pass = "" 20.times { pass += (rand(74) + 48).chr } begin Puppet.settings.setting(:capass).open('w') { |f| f.print pass } rescue Errno::EACCES => detail raise Puppet::Error, "Could not write CA password: #{detail}", detail.backtrace end @password = pass pass end # Lists the names of all signed certificates. # # @param name [Array] filter to cerificate names # # @return [Array] def list(name='*') list_certificates(name).collect { |c| c.name } end # Return all the certificate objects as found by the indirector # API for PE license checking. # # Created to prevent the case of reading all certs from disk, getting # just their names and verifying the cert for each name, which then # causes the cert to again be read from disk. # # @author Jeff Weiss # @api Puppet Enterprise Licensing # # @param name [Array] filter to cerificate names # # @return [Array] def list_certificates(name='*') Puppet::SSL::Certificate.indirection.search(name) end # Read the next serial from the serial file, and increment the # file so this one is considered used. def next_serial serial = 1 Puppet.settings.setting(:serial).exclusive_open('a+') do |f| f.rewind serial = f.read.chomp.hex if serial == 0 serial = 1 end f.truncate(0) f.rewind # We store the next valid serial, not the one we just used. f << "%04X" % (serial + 1) end serial end # Does the password file exist? def password? Puppet::FileSystem.exist?(Puppet[:capass]) end # Print a given host's certificate as text. def print(name) (cert = Puppet::SSL::Certificate.indirection.find(name)) ? cert.to_text : nil 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.indirection.find(name) serial = cert.content.serial elsif name =~ /^0x[0-9A-Fa-f]+$/ serial = name.hex - elsif ! serial = inventory.serial(name) + elsif serial = inventory.serials(name) and serial.empty? raise ArgumentError, "Could not find a serial number for #{name}" end - crl.revoke(serial, host.key.content) + [serial].flatten.each do |s| + crl.revoke(s, host.key.content) + end 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, 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 def check_internal_signing_policies(hostname, csr, allow_dns_alt_names) # Reject unknown request extensions. unknown_req = csr.request_extensions.reject do |x| RequestExtensionWhitelist.include? x["oid"] or Puppet::SSL::Oids.subtree_of?('ppRegCertExt', x["oid"], true) or Puppet::SSL::Oids.subtree_of?('ppPrivCertExt', x["oid"], true) end 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 # Utility method for optionally caching the X509 Store for verifying a # large number of certificates in a short amount of time--exactly the # case we have during PE license checking. # # @example Use the cached X509 store # x509store(:cache => true) # # @example Use a freshly create X509 store # x509store # x509store(:cache => false) # # @param [Hash] options the options used for retrieving the X509 Store # @option options [Boolean] :cache whether or not to use a cached version # of the X509 Store # # @return [OpenSSL::X509::Store] def x509_store(options = {}) if (options[:cache]) return @x509store unless @x509store.nil? @x509store = create_x509_store else create_x509_store end end private :x509_store # Creates a brand new OpenSSL::X509::Store with the appropriate # Certificate Revocation List and flags # # @return [OpenSSL::X509::Store] def create_x509_store 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 if Puppet.settings[:certificate_revocation] store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL | OpenSSL::X509::V_FLAG_CRL_CHECK end store end private :create_x509_store # Utility method which is API for PE license checking. # This is used rather than `verify` because # 1) We have already read the certificate from disk into memory. # To read the certificate from disk again is just wasteful. # 2) Because we're checking a large number of certificates against # a transient CertificateAuthority, we can relatively safely cache # the X509 Store that actually does the verification. # # Long running instances of CertificateAuthority will certainly # want to use `verify` because it will recreate the X509 Store with # the absolutely latest CRL. # # Additionally, this method explicitly returns a boolean whereas # `verify` will raise an error if the certificate has been revoked. # # @author Jeff Weiss # @api Puppet Enterprise Licensing # # @param cert [Puppet::SSL::Certificate] the certificate to check validity of # # @return [Boolean] true if signed, false if unsigned or revoked def certificate_is_alive?(cert) x509_store(:cache => true).verify(cert.content) end # Verify a given host's certificate. The certname is passed in, and # the indirector will be used to locate the actual contents of the # certificate with that name. # # @param name [String] certificate name to verify # # @raise [ArgumentError] if the certificate name cannot be found # (i.e. doesn't exist or is unsigned) # @raise [CertificateVerficationError] if the certificate has been revoked # # @return [Boolean] true if signed, there are no cases where false is returned def verify(name) unless cert = Puppet::SSL::Certificate.indirection.find(name) raise ArgumentError, "Could not find a certificate for #{name}" end store = x509_store raise CertificateVerificationError.new(store.error), store.error_string unless store.verify(cert.content) end 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 # List the waiting certificate requests. def waiting? Puppet::SSL::CertificateRequest.indirection.search("*").collect { |r| r.name } end # @api private class AutosignAlways def allowed?(csr) true end end # @api private class AutosignNever def allowed?(csr) false end end # @api private class AutosignConfig def initialize(config_file) @config = config_file end def allowed?(csr) autosign_store.allowed?(csr.name, '127.1.1.1') end private def autosign_store auth = Puppet::Network::AuthStore.new Puppet::FileSystem.each_line(@config) do |line| next if line =~ /^\s*#/ next if line =~ /^\s*$/ auth.allow(line.chomp) end auth end end end diff --git a/lib/puppet/ssl/inventory.rb b/lib/puppet/ssl/inventory.rb index e3ad3121f..83b53889a 100644 --- a/lib/puppet/ssl/inventory.rb +++ b/lib/puppet/ssl/inventory.rb @@ -1,50 +1,55 @@ require 'puppet/ssl' require 'puppet/ssl/certificate' # Keep track of all of our known certificates. class Puppet::SSL::Inventory attr_reader :path # Add a certificate to our inventory. def add(cert) cert = cert.content if cert.is_a?(Puppet::SSL::Certificate) Puppet.settings.setting(:cert_inventory).open("a") do |f| f.print format(cert) end end # Format our certificate for output. def format(cert) iso = '%Y-%m-%dT%H:%M:%S%Z' "0x%04x %s %s %s\n" % [cert.serial, cert.not_before.strftime(iso), cert.not_after.strftime(iso), cert.subject] end def initialize @path = Puppet[:cert_inventory] end # Rebuild the inventory from scratch. This should happen if # the file is entirely missing or if it's somehow corrupted. def rebuild Puppet.notice "Rebuilding inventory file" Puppet.settings.setting(:cert_inventory).open('w') do |f| Puppet::SSL::Certificate.indirection.search("*").each do |cert| f.print format(cert.content) end end end # Find the serial number for a given certificate. def serial(name) + Puppet.deprecation_warning 'Inventory#serial is deprecated, use Inventory#serials instead.' return nil unless Puppet::FileSystem.exist?(@path) + serials(name).first + end - File.readlines(@path).each do |line| - next unless line =~ /^(\S+).+\/CN=#{name}$/ - - return Integer($1) - end + # Find all serial numbers for a given certificate. If none can be found, returns + # an empty array. + def serials(name) + return [] unless Puppet::FileSystem.exist?(@path) - return nil + File.readlines(@path).collect do |line| + /^(\S+).+\/CN=#{name}$/.match(line) + end.compact.map { |m| Integer(m[1]) } end + end diff --git a/spec/integration/ssl/certificate_authority_spec.rb b/spec/integration/ssl/certificate_authority_spec.rb index acbc38cbc..3cf494afa 100755 --- a/spec/integration/ssl/certificate_authority_spec.rb +++ b/spec/integration/ssl/certificate_authority_spec.rb @@ -1,137 +1,163 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/ssl/certificate_authority' describe Puppet::SSL::CertificateAuthority, :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files let(:ca) { @ca } before do dir = tmpdir("ca_integration_testing") Puppet.settings[:confdir] = dir Puppet.settings[:vardir] = dir Puppet.settings[:group] = Process.gid Puppet::SSL::Host.ca_location = :local # this has the side-effect of creating the various directories that we need @ca = Puppet::SSL::CertificateAuthority.new end it "should be able to generate a new host certificate" do ca.generate("newhost") Puppet::SSL::Certificate.indirection.find("newhost").should be_instance_of(Puppet::SSL::Certificate) end it "should be able to revoke a host certificate" do ca.generate("newhost") ca.revoke("newhost") expect { ca.verify("newhost") }.to raise_error(Puppet::SSL::CertificateAuthority::CertificateVerificationError, "certificate revoked") end describe "when signing certificates" do it "should save the signed certificate" do host = certificate_request_for("luke.madstop.com") ca.sign("luke.madstop.com") Puppet::SSL::Certificate.indirection.find("luke.madstop.com").should be_instance_of(Puppet::SSL::Certificate) end it "should be able to sign multiple certificates" do host = certificate_request_for("luke.madstop.com") other = certificate_request_for("other.madstop.com") ca.sign("luke.madstop.com") ca.sign("other.madstop.com") Puppet::SSL::Certificate.indirection.find("other.madstop.com").should be_instance_of(Puppet::SSL::Certificate) Puppet::SSL::Certificate.indirection.find("luke.madstop.com").should be_instance_of(Puppet::SSL::Certificate) end it "should save the signed certificate to the :signeddir" do host = certificate_request_for("luke.madstop.com") ca.sign("luke.madstop.com") client_cert = File.join(Puppet[:signeddir], "luke.madstop.com.pem") File.read(client_cert).should == Puppet::SSL::Certificate.indirection.find("luke.madstop.com").content.to_s end it "should save valid certificates" do host = certificate_request_for("luke.madstop.com") ca.sign("luke.madstop.com") unless ssl = Puppet::Util::which('openssl') pending "No ssl available" else ca_cert = Puppet[:cacert] client_cert = File.join(Puppet[:signeddir], "luke.madstop.com.pem") output = %x{openssl verify -CAfile #{ca_cert} #{client_cert}} $CHILD_STATUS.should == 0 end end it "should verify proof of possession when signing certificates" do host = certificate_request_for("luke.madstop.com") csr = host.certificate_request wrong_key = Puppet::SSL::Key.new(host.name) wrong_key.generate csr.content.public_key = wrong_key.content.public_key # The correct key has to be removed so we can save the incorrect one Puppet::SSL::CertificateRequest.indirection.destroy(host.name) Puppet::SSL::CertificateRequest.indirection.save(csr) expect { ca.sign(host.name) }.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, "CSR contains a public key that does not correspond to the signing key" ) end end + describe "when revoking certificate" do + it "should work for one certificate" do + certificate_request_for("luke.madstop.com") + + ca.sign("luke.madstop.com") + ca.revoke("luke.madstop.com") + + expect { ca.verify("luke.madstop.com") }.to raise_error( + Puppet::SSL::CertificateAuthority::CertificateVerificationError, + "certificate revoked" + ) + end + + it "should work for several certificates" do + 3.times.each do |c| + certificate_request_for("luke.madstop.com") + ca.sign("luke.madstop.com") + ca.destroy("luke.madstop.com") + end + ca.revoke("luke.madstop.com") + + ca.crl.content.revoked.map { |r| r.serial }.should == [2,3,4] # ca has serial 1 + end + + end + it "allows autosigning certificates concurrently", :unless => Puppet::Util::Platform.windows? do Puppet[:autosign] = true hosts = (0..4).collect { |i| certificate_request_for("host#{i}") } run_in_parallel(5) do |i| ca.autosign(Puppet::SSL::CertificateRequest.indirection.find(hosts[i].name)) end certs = hosts.collect { |host| Puppet::SSL::Certificate.indirection.find(host.name).content } serial_numbers = certs.collect(&:serial) serial_numbers.sort.should == [2, 3, 4, 5, 6] # serial 1 is the ca certificate end def certificate_request_for(hostname) key = Puppet::SSL::Key.new(hostname) key.generate host = Puppet::SSL::Host.new(hostname) host.key = key host.generate_certificate_request host end def run_in_parallel(number) children = [] number.times do |i| children << Kernel.fork do yield i end end children.each { |pid| Process.wait(pid) } end end diff --git a/spec/unit/ssl/certificate_authority_spec.rb b/spec/unit/ssl/certificate_authority_spec.rb index ef5a86862..5f5d78d91 100755 --- a/spec/unit/ssl/certificate_authority_spec.rb +++ b/spec/unit/ssl/certificate_authority_spec.rb @@ -1,1107 +1,1131 @@ #! /usr/bin/env ruby # encoding: ASCII-8BIT require 'spec_helper' require 'puppet/ssl/certificate_authority' describe Puppet::SSL::CertificateAuthority do after do Puppet::SSL::CertificateAuthority.instance_variable_set(:@singleton_instance, nil) Puppet.settings.clearused end def stub_ca_host @key = mock 'key' @key.stubs(:content).returns "cakey" @cacert = mock 'certificate' @cacert.stubs(:content).returns "cacertificate" @host = stub 'ssl_host', :key => @key, :certificate => @cacert, :name => Puppet::SSL::Host.ca_name end it "should have a class method for returning a singleton instance" do Puppet::SSL::CertificateAuthority.should respond_to(:instance) end describe "when finding an existing instance" do describe "and the host is a CA host and the run_mode is master" do before do Puppet[:ca] = true Puppet.run_mode.stubs(:master?).returns true @ca = mock('ca') Puppet::SSL::CertificateAuthority.stubs(:new).returns @ca end it "should return an instance" do Puppet::SSL::CertificateAuthority.instance.should equal(@ca) end it "should always return the same instance" do Puppet::SSL::CertificateAuthority.instance.should equal(Puppet::SSL::CertificateAuthority.instance) end end describe "and the host is not a CA host" do it "should return nil" do Puppet[:ca] = false Puppet.run_mode.stubs(:master?).returns true ca = mock('ca') Puppet::SSL::CertificateAuthority.expects(:new).never Puppet::SSL::CertificateAuthority.instance.should be_nil end end describe "and the run_mode is not master" do it "should return nil" do Puppet[:ca] = true Puppet.run_mode.stubs(:master?).returns false ca = mock('ca') Puppet::SSL::CertificateAuthority.expects(:new).never Puppet::SSL::CertificateAuthority.instance.should be_nil end end end describe "when initializing" do before do Puppet.settings.stubs(:use) Puppet::SSL::CertificateAuthority.any_instance.stubs(:setup) end it "should always set its name to the value of :certname" do Puppet[:certname] = "ca_testing" Puppet::SSL::CertificateAuthority.new.name.should == "ca_testing" end it "should create an SSL::Host instance whose name is the 'ca_name'" do Puppet::SSL::Host.expects(:ca_name).returns "caname" host = stub 'host' Puppet::SSL::Host.expects(:new).with("caname").returns host Puppet::SSL::CertificateAuthority.new end it "should use the :main, :ca, and :ssl settings sections" do Puppet.settings.expects(:use).with(:main, :ssl, :ca) Puppet::SSL::CertificateAuthority.new end it "should make sure the CA is set up" do Puppet::SSL::CertificateAuthority.any_instance.expects(:setup) Puppet::SSL::CertificateAuthority.new end end describe "when setting itself up" do it "should generate the CA certificate if it does not have one" do Puppet.settings.stubs :use host = stub 'host' Puppet::SSL::Host.stubs(:new).returns host host.expects(:certificate).returns nil Puppet::SSL::CertificateAuthority.any_instance.expects(:generate_ca_certificate) Puppet::SSL::CertificateAuthority.new end end describe "when retrieving the certificate revocation list" do before do Puppet.settings.stubs(:use) Puppet[:cacrl] = "/my/crl" cert = stub("certificate", :content => "real_cert") key = stub("key", :content => "real_key") @host = stub 'host', :certificate => cert, :name => "hostname", :key => key Puppet::SSL::CertificateAuthority.any_instance.stubs(:setup) @ca = Puppet::SSL::CertificateAuthority.new @ca.stubs(:host).returns @host end it "should return any found CRL instance" do crl = mock 'crl' Puppet::SSL::CertificateRevocationList.indirection.expects(:find).returns crl @ca.crl.should equal(crl) end it "should create, generate, and save a new CRL instance of no CRL can be found" do crl = Puppet::SSL::CertificateRevocationList.new("fakename") Puppet::SSL::CertificateRevocationList.indirection.expects(:find).returns nil Puppet::SSL::CertificateRevocationList.expects(:new).returns crl crl.expects(:generate).with(@ca.host.certificate.content, @ca.host.key.content) Puppet::SSL::CertificateRevocationList.indirection.expects(:save).with(crl) @ca.crl.should equal(crl) end end describe "when generating a self-signed CA certificate" do before do Puppet.settings.stubs(:use) Puppet::SSL::CertificateAuthority.any_instance.stubs(:setup) Puppet::SSL::CertificateAuthority.any_instance.stubs(:crl) @ca = Puppet::SSL::CertificateAuthority.new @host = stub 'host', :key => mock("key"), :name => "hostname", :certificate => mock('certificate') Puppet::SSL::CertificateRequest.any_instance.stubs(:generate) @ca.stubs(:host).returns @host end it "should create and store a password at :capass" do Puppet[:capass] = File.expand_path("/path/to/pass") Puppet::FileSystem.expects(:exist?).with(Puppet[:capass]).returns false fh = StringIO.new Puppet.settings.setting(:capass).expects(:open).with('w').yields fh @ca.stubs(:sign) @ca.generate_ca_certificate expect(fh.string.length).to be > 18 end it "should generate a key if one does not exist" do @ca.stubs :generate_password @ca.stubs :sign @ca.host.expects(:key).returns nil @ca.host.expects(:generate_key) @ca.generate_ca_certificate end it "should create and sign a self-signed cert using the CA name" do request = mock 'request' Puppet::SSL::CertificateRequest.expects(:new).with(@ca.host.name).returns request request.expects(:generate).with(@ca.host.key) request.stubs(:request_extensions => []) @ca.expects(:sign).with(@host.name, false, request) @ca.stubs :generate_password @ca.generate_ca_certificate end it "should generate its CRL" do @ca.stubs :generate_password @ca.stubs :sign @ca.host.expects(:key).returns nil @ca.host.expects(:generate_key) @ca.expects(:crl) @ca.generate_ca_certificate end end describe "when signing" do before do Puppet.settings.stubs(:use) Puppet::SSL::CertificateAuthority.any_instance.stubs(:password?).returns true stub_ca_host Puppet::SSL::Host.expects(:new).with(Puppet::SSL::Host.ca_name).returns @host @ca = Puppet::SSL::CertificateAuthority.new @name = "myhost" @real_cert = stub 'realcert', :sign => nil @cert = Puppet::SSL::Certificate.new(@name) @cert.content = @real_cert Puppet::SSL::Certificate.stubs(:new).returns @cert Puppet::SSL::Certificate.indirection.stubs(:save) # Stub out the factory Puppet::SSL::CertificateFactory.stubs(:build).returns @cert.content @request_content = stub "request content stub", :subject => OpenSSL::X509::Name.new([['CN', @name]]), :public_key => stub('public_key') @request = stub 'request', :name => @name, :request_extensions => [], :subject_alt_names => [], :content => @request_content @request_content.stubs(:verify).returns(true) # And the inventory @inventory = stub 'inventory', :add => nil @ca.stubs(:inventory).returns @inventory Puppet::SSL::CertificateRequest.indirection.stubs(:destroy) end describe "its own certificate" do before do @serial = 10 @ca.stubs(:next_serial).returns @serial end it "should not look up a certificate request for the host" do Puppet::SSL::CertificateRequest.indirection.expects(:find).never @ca.sign(@name, true, @request) end it "should use a certificate type of :ca" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[0].should == :ca end.returns @cert.content @ca.sign(@name, :ca, @request) end it "should pass the provided CSR as the CSR" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[1].should == @request end.returns @cert.content @ca.sign(@name, :ca, @request) end it "should use the provided CSR's content as the issuer" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[2].subject.to_s.should == "/CN=myhost" end.returns @cert.content @ca.sign(@name, :ca, @request) end it "should pass the next serial as the serial number" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[3].should == @serial end.returns @cert.content @ca.sign(@name, :ca, @request) end it "should sign the certificate request even if it contains alt names" do @request.stubs(:subject_alt_names).returns %w[DNS:foo DNS:bar DNS:baz] expect do @ca.sign(@name, false, @request) end.not_to raise_error end it "should save the resulting certificate" do Puppet::SSL::Certificate.indirection.expects(:save).with(@cert) @ca.sign(@name, :ca, @request) end end describe "another host's certificate" do before do @serial = 10 @ca.stubs(:next_serial).returns @serial Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request Puppet::SSL::CertificateRequest.indirection.stubs :save end it "should use a certificate type of :server" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[0] == :server end.returns @cert.content @ca.sign(@name) end it "should use look up a CSR for the host in the :ca_file terminus" do Puppet::SSL::CertificateRequest.indirection.expects(:find).with(@name).returns @request @ca.sign(@name) end it "should fail if no CSR can be found for the host" do Puppet::SSL::CertificateRequest.indirection.expects(:find).with(@name).returns nil expect { @ca.sign(@name) }.to raise_error(ArgumentError) end it "should fail if an unknown request extension is present" do @request.stubs :request_extensions => [{ "oid" => "bananas", "value" => "delicious" }] expect { @ca.sign(@name) }.to raise_error(/CSR has request extensions that are not permitted/) end it "should fail if the CSR contains alt names and they are not expected" do @request.stubs(:subject_alt_names).returns %w[DNS:foo DNS:bar DNS:baz] expect do @ca.sign(@name, false) end.to raise_error(Puppet::SSL::CertificateAuthority::CertificateSigningError, /CSR '#{@name}' contains subject alternative names \(.*?\), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{@name}` to sign this request./) end it "should not fail if the CSR does not contain alt names and they are expected" do @request.stubs(:subject_alt_names).returns [] expect { @ca.sign(@name, true) }.to_not raise_error end it "should reject alt names by default" do @request.stubs(:subject_alt_names).returns %w[DNS:foo DNS:bar DNS:baz] expect do @ca.sign(@name) end.to raise_error(Puppet::SSL::CertificateAuthority::CertificateSigningError, /CSR '#{@name}' contains subject alternative names \(.*?\), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{@name}` to sign this request./) end it "should use the CA certificate as the issuer" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[2] == @cacert.content end.returns @cert.content signed = @ca.sign(@name) end it "should pass the next serial as the serial number" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[3] == @serial end.returns @cert.content @ca.sign(@name) end it "should sign the resulting certificate using its real key and a digest" do digest = mock 'digest' OpenSSL::Digest::SHA256.expects(:new).returns digest key = stub 'key', :content => "real_key" @ca.host.stubs(:key).returns key @cert.content.expects(:sign).with("real_key", digest) @ca.sign(@name) end it "should save the resulting certificate" do Puppet::SSL::Certificate.indirection.stubs(:save).with(@cert) @ca.sign(@name) end it "should remove the host's certificate request" do Puppet::SSL::CertificateRequest.indirection.expects(:destroy).with(@name) @ca.sign(@name) end it "should check the internal signing policies" do @ca.expects(:check_internal_signing_policies).returns true @ca.sign(@name) end end context "#check_internal_signing_policies" do before do @serial = 10 @ca.stubs(:next_serial).returns @serial Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request @cert.stubs :save end it "should reject CSRs whose CN doesn't match the name for which we're signing them" do # Shorten this so the test doesn't take too long Puppet[:keylength] = 1024 key = Puppet::SSL::Key.new('the_certname') key.generate csr = Puppet::SSL::CertificateRequest.new('the_certname') csr.generate(key) expect do @ca.check_internal_signing_policies('not_the_certname', csr, false) end.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, /common name "the_certname" does not match expected certname "not_the_certname"/ ) end describe "when validating the CN" do before :all do Puppet[:keylength] = 1024 Puppet[:passfile] = '/f00' @signing_key = Puppet::SSL::Key.new('my_signing_key') @signing_key.generate end [ 'completely_okay', 'sure, why not? :)', 'so+many(things)-are=allowed.', 'this"is#just&madness%you[see]', 'and even a (an?) \\!', 'waltz, nymph, for quick jigs vex bud.', '{552c04ca-bb1b-11e1-874b-60334b04494e}' ].each do |name| it "should accept #{name.inspect}" do csr = Puppet::SSL::CertificateRequest.new(name) csr.generate(@signing_key) @ca.check_internal_signing_policies(name, csr, false) end end [ 'super/bad', "not\neven\tkind\rof", "ding\adong\a", "hidden\b\b\b\b\b\bmessage", "\xE2\x98\x83 :(" ].each do |name| it "should reject #{name.inspect}" do # We aren't even allowed to make objects with these names, so let's # stub that to simulate an invalid one coming from outside Puppet Puppet::SSL::CertificateRequest.stubs(:validate_certname) csr = Puppet::SSL::CertificateRequest.new(name) csr.generate(@signing_key) expect do @ca.check_internal_signing_policies(name, csr, false) end.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, /subject contains unprintable or non-ASCII characters/ ) end end end it "accepts numeric OIDs under the ppRegCertExt subtree" do exts = [{ 'oid' => '1.3.6.1.4.1.34380.1.1.1', 'value' => '657e4780-4cf5-11e3-8f96-0800200c9a66'}] @request.stubs(:request_extensions).returns exts expect { @ca.check_internal_signing_policies(@name, @request, false) }.to_not raise_error end it "accepts short name OIDs under the ppRegCertExt subtree" do exts = [{ 'oid' => 'pp_uuid', 'value' => '657e4780-4cf5-11e3-8f96-0800200c9a66'}] @request.stubs(:request_extensions).returns exts expect { @ca.check_internal_signing_policies(@name, @request, false) }.to_not raise_error end it "accepts OIDs under the ppPrivCertAttrs subtree" do exts = [{ 'oid' => '1.3.6.1.4.1.34380.1.2.1', 'value' => 'private extension'}] @request.stubs(:request_extensions).returns exts expect { @ca.check_internal_signing_policies(@name, @request, false) }.to_not raise_error end it "should reject a critical extension that isn't on the whitelist" do @request.stubs(:request_extensions).returns [{ "oid" => "banana", "value" => "yumm", "critical" => true }] expect { @ca.check_internal_signing_policies(@name, @request, false) }.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, /request extensions that are not permitted/ ) end it "should reject a non-critical extension that isn't on the whitelist" do @request.stubs(:request_extensions).returns [{ "oid" => "peach", "value" => "meh", "critical" => false }] expect { @ca.check_internal_signing_policies(@name, @request, false) }.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, /request extensions that are not permitted/ ) end it "should reject non-whitelist extensions even if a valid extension is present" do @request.stubs(:request_extensions).returns [{ "oid" => "peach", "value" => "meh", "critical" => false }, { "oid" => "subjectAltName", "value" => "DNS:foo", "critical" => true }] expect { @ca.check_internal_signing_policies(@name, @request, false) }.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, /request extensions that are not permitted/ ) end it "should reject a subjectAltName for a non-DNS value" do @request.stubs(:subject_alt_names).returns ['DNS:foo', 'email:bar@example.com'] expect { @ca.check_internal_signing_policies(@name, @request, true) }.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, /subjectAltName outside the DNS label space/ ) end it "should reject a wildcard subject" do @request.content.stubs(:subject). returns(OpenSSL::X509::Name.new([["CN", "*.local"]])) expect { @ca.check_internal_signing_policies('*.local', @request, false) }.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, /subject contains a wildcard/ ) end it "should reject a wildcard subjectAltName" do @request.stubs(:subject_alt_names).returns ['DNS:foo', 'DNS:*.bar'] expect { @ca.check_internal_signing_policies(@name, @request, true) }.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, /subjectAltName contains a wildcard/ ) end end it "should create a certificate instance with the content set to the newly signed x509 certificate" do @serial = 10 @ca.stubs(:next_serial).returns @serial Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request Puppet::SSL::Certificate.indirection.stubs :save Puppet::SSL::Certificate.expects(:new).with(@name).returns @cert @ca.sign(@name) end it "should return the certificate instance" do @ca.stubs(:next_serial).returns @serial Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request Puppet::SSL::Certificate.indirection.stubs :save @ca.sign(@name).should equal(@cert) end it "should add the certificate to its inventory" do @ca.stubs(:next_serial).returns @serial @inventory.expects(:add).with(@cert) Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request Puppet::SSL::Certificate.indirection.stubs :save @ca.sign(@name) end it "should have a method for triggering autosigning of available CSRs" do @ca.should respond_to(:autosign) end describe "when autosigning certificates" do let(:csr) { Puppet::SSL::CertificateRequest.new("host") } describe "using the autosign setting" do let(:autosign) { File.expand_path("/auto/sign") } it "should do nothing if autosign is disabled" do Puppet[:autosign] = false @ca.expects(:sign).never @ca.autosign(csr) end it "should do nothing if no autosign.conf exists" do Puppet[:autosign] = autosign non_existent_file = Puppet::FileSystem::MemoryFile.a_missing_file(autosign) Puppet::FileSystem.overlay(non_existent_file) do @ca.expects(:sign).never @ca.autosign(csr) end end describe "and autosign is enabled and the autosign.conf file exists" do let(:store) { stub 'store', :allow => nil, :allowed? => false } before do Puppet[:autosign] = autosign end describe "when creating the AuthStore instance to verify autosigning" do it "should create an AuthStore with each line in the configuration file allowed to be autosigned" do Puppet::FileSystem.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\ntwo\n")) do Puppet::Network::AuthStore.stubs(:new).returns store store.expects(:allow).with("one") store.expects(:allow).with("two") @ca.autosign(csr) end end it "should reparse the autosign configuration on each call" do Puppet::FileSystem.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one")) do Puppet::Network::AuthStore.stubs(:new).times(2).returns store @ca.autosign(csr) @ca.autosign(csr) end end it "should ignore comments" do Puppet::FileSystem.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\n#two\n")) do Puppet::Network::AuthStore.stubs(:new).returns store store.expects(:allow).with("one") @ca.autosign(csr) end end it "should ignore blank lines" do Puppet::FileSystem.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\n\n")) do Puppet::Network::AuthStore.stubs(:new).returns store store.expects(:allow).with("one") @ca.autosign(csr) end end end end end describe "using the autosign command setting" do let(:cmd) { File.expand_path('/autosign_cmd') } let(:autosign_cmd) { mock 'autosign_command' } let(:autosign_executable) { Puppet::FileSystem::MemoryFile.an_executable(cmd) } before do Puppet[:autosign] = cmd Puppet::SSL::CertificateAuthority::AutosignCommand.stubs(:new).returns autosign_cmd end it "autosigns the CSR if the autosign command returned true" do Puppet::FileSystem.overlay(autosign_executable) do autosign_cmd.expects(:allowed?).with(csr).returns true @ca.expects(:sign).with('host') @ca.autosign(csr) end end it "doesn't autosign the CSR if the autosign_command returned false" do Puppet::FileSystem.overlay(autosign_executable) do autosign_cmd.expects(:allowed?).with(csr).returns false @ca.expects(:sign).never @ca.autosign(csr) end end end end end describe "when managing certificate clients" do before do Puppet.settings.stubs(:use) Puppet::SSL::CertificateAuthority.any_instance.stubs(:password?).returns true stub_ca_host Puppet::SSL::Host.expects(:new).returns @host Puppet::SSL::CertificateAuthority.any_instance.stubs(:host).returns @host @cacert = mock 'certificate' @cacert.stubs(:content).returns "cacertificate" @ca = Puppet::SSL::CertificateAuthority.new end it "should be able to list waiting certificate requests" do req1 = stub 'req1', :name => "one" req2 = stub 'req2', :name => "two" Puppet::SSL::CertificateRequest.indirection.expects(:search).with("*").returns [req1, req2] @ca.waiting?.should == %w{one two} end it "should delegate removing hosts to the Host class" do Puppet::SSL::Host.expects(:destroy).with("myhost") @ca.destroy("myhost") end it "should be able to verify certificates" do @ca.should respond_to(:verify) end it "should list certificates as the sorted list of all existing signed certificates" do cert1 = stub 'cert1', :name => "cert1" cert2 = stub 'cert2', :name => "cert2" Puppet::SSL::Certificate.indirection.expects(:search).with("*").returns [cert1, cert2] @ca.list.should == %w{cert1 cert2} end it "should list the full certificates" do cert1 = stub 'cert1', :name => "cert1" cert2 = stub 'cert2', :name => "cert2" Puppet::SSL::Certificate.indirection.expects(:search).with("*").returns [cert1, cert2] @ca.list_certificates.should == [cert1, cert2] end describe "and printing certificates" do it "should return nil if the certificate cannot be found" do Puppet::SSL::Certificate.indirection.expects(:find).with("myhost").returns nil @ca.print("myhost").should be_nil end it "should print certificates by calling :to_text on the host's certificate" do cert1 = stub 'cert1', :name => "cert1", :to_text => "mytext" Puppet::SSL::Certificate.indirection.expects(:find).with("myhost").returns cert1 @ca.print("myhost").should == "mytext" end end describe "and fingerprinting certificates" do before :each do @cert = stub 'cert', :name => "cert", :fingerprint => "DIGEST" Puppet::SSL::Certificate.indirection.stubs(:find).with("myhost").returns @cert Puppet::SSL::CertificateRequest.indirection.stubs(:find).with("myhost") end it "should raise an error if the certificate or CSR cannot be found" do Puppet::SSL::Certificate.indirection.expects(:find).with("myhost").returns nil Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myhost").returns nil expect { @ca.fingerprint("myhost") }.to raise_error end it "should try to find a CSR if no certificate can be found" do Puppet::SSL::Certificate.indirection.expects(:find).with("myhost").returns nil Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myhost").returns @cert @cert.expects(:fingerprint) @ca.fingerprint("myhost") end it "should delegate to the certificate fingerprinting" do @cert.expects(:fingerprint) @ca.fingerprint("myhost") end it "should propagate the digest algorithm to the certificate fingerprinting system" do @cert.expects(:fingerprint).with(:digest) @ca.fingerprint("myhost", :digest) end end describe "and verifying certificates" do let(:cacert) { File.expand_path("/ca/cert") } before do @store = stub 'store', :verify => true, :add_file => nil, :purpose= => nil, :add_crl => true, :flags= => nil OpenSSL::X509::Store.stubs(:new).returns @store @cert = stub 'cert', :content => "mycert" Puppet::SSL::Certificate.indirection.stubs(:find).returns @cert @crl = stub('crl', :content => "mycrl") @ca.stubs(:crl).returns @crl end it "should fail if the host's certificate cannot be found" do Puppet::SSL::Certificate.indirection.expects(:find).with("me").returns(nil) expect { @ca.verify("me") }.to raise_error(ArgumentError) end it "should create an SSL Store to verify" do OpenSSL::X509::Store.expects(:new).returns @store @ca.verify("me") end it "should add the CA Certificate to the store" do Puppet[:cacert] = cacert @store.expects(:add_file).with cacert @ca.verify("me") end it "should add the CRL to the store if the crl is enabled" do @store.expects(:add_crl).with "mycrl" @ca.verify("me") end it "should set the store purpose to OpenSSL::X509::PURPOSE_SSL_CLIENT" do Puppet[:cacert] = cacert @store.expects(:add_file).with cacert @ca.verify("me") end it "should set the store flags to check the crl" do @store.expects(:flags=).with OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK @ca.verify("me") end it "should use the store to verify the certificate" do @cert.expects(:content).returns "mycert" @store.expects(:verify).with("mycert").returns true @ca.verify("me") end it "should fail if the verification returns false" do @cert.expects(:content).returns "mycert" @store.expects(:verify).with("mycert").returns false expect { @ca.verify("me") }.to raise_error end describe "certificate_is_alive?" do it "should return false if verification fails" do @cert.expects(:content).returns "mycert" @store.expects(:verify).with("mycert").returns false @ca.certificate_is_alive?(@cert).should be_false end it "should return true if verification passes" do @cert.expects(:content).returns "mycert" @store.expects(:verify).with("mycert").returns true @ca.certificate_is_alive?(@cert).should be_true end it "should used a cached instance of the x509 store" do OpenSSL::X509::Store.stubs(:new).returns(@store).once @cert.expects(:content).returns "mycert" @store.expects(:verify).with("mycert").returns true @ca.certificate_is_alive?(@cert) @ca.certificate_is_alive?(@cert) end end end describe "and revoking certificates" do before do @crl = mock 'crl' @ca.stubs(:crl).returns @crl @ca.stubs(:next_serial).returns 10 @real_cert = stub 'real_cert', :serial => 15 @cert = stub 'cert', :content => @real_cert Puppet::SSL::Certificate.indirection.stubs(:find).returns @cert end it "should fail if the certificate revocation list is disabled" do @ca.stubs(:crl).returns false expect { @ca.revoke('ca_testing') }.to raise_error(ArgumentError) end it "should delegate the revocation to its CRL" do @ca.crl.expects(:revoke) @ca.revoke('host') end it "should get the serial number from the local certificate if it exists" do @ca.crl.expects(:revoke).with { |serial, key| serial == 15 } Puppet::SSL::Certificate.indirection.expects(:find).with("host").returns @cert @ca.revoke('host') end it "should get the serial number from inventory if no local certificate exists" do real_cert = stub 'real_cert', :serial => 15 cert = stub 'cert', :content => real_cert Puppet::SSL::Certificate.indirection.expects(:find).with("host").returns nil - @ca.inventory.expects(:serial).with("host").returns 16 + @ca.inventory.expects(:serials).with("host").returns [16] @ca.crl.expects(:revoke).with { |serial, key| serial == 16 } @ca.revoke('host') end + it "should revoke all serials matching a name" do + real_cert = stub 'real_cert', :serial => 15 + cert = stub 'cert', :content => real_cert + Puppet::SSL::Certificate.indirection.expects(:find).with("host").returns nil + + @ca.inventory.expects(:serials).with("host").returns [16, 20, 25] + + @ca.crl.expects(:revoke).with { |serial, key| serial == 16 } + @ca.crl.expects(:revoke).with { |serial, key| serial == 20 } + @ca.crl.expects(:revoke).with { |serial, key| serial == 25 } + @ca.revoke('host') + end + + it "should raise an error if no certificate match" do + real_cert = stub 'real_cert', :serial => 15 + cert = stub 'cert', :content => real_cert + Puppet::SSL::Certificate.indirection.expects(:find).with("host").returns nil + + @ca.inventory.expects(:serials).with("host").returns [] + + @ca.crl.expects(:revoke).never + expect { @ca.revoke('host') }.to raise_error + end + context "revocation by serial number (#16798)" do it "revokes when given a lower case hexadecimal formatted string" do @ca.crl.expects(:revoke).with { |serial, key| serial == 15 } Puppet::SSL::Certificate.indirection.expects(:find).with("0xf").returns nil @ca.revoke('0xf') end it "revokes when given an upper case hexadecimal formatted string" do @ca.crl.expects(:revoke).with { |serial, key| serial == 15 } Puppet::SSL::Certificate.indirection.expects(:find).with("0xF").returns nil @ca.revoke('0xF') end it "handles very large serial numbers" do bighex = '0x4000000000000000000000000000000000000000' bighex_int = 365375409332725729550921208179070754913983135744 @ca.crl.expects(:revoke).with(bighex_int, anything) Puppet::SSL::Certificate.indirection.expects(:find).with(bighex).returns nil @ca.revoke(bighex) end end end it "should be able to generate a complete new SSL host" do @ca.should respond_to(:generate) end end end require 'puppet/indirector/memory' module CertificateAuthorityGenerateSpecs describe "CertificateAuthority.generate" do def expect_to_increment_serial_file Puppet.settings.setting(:serial).expects(:exclusive_open) end def expect_to_sign_a_cert expect_to_increment_serial_file end def expect_to_write_the_ca_password Puppet.settings.setting(:capass).expects(:open).with('w') end def expect_ca_initialization expect_to_write_the_ca_password expect_to_sign_a_cert end INDIRECTED_CLASSES = [ Puppet::SSL::Certificate, Puppet::SSL::CertificateRequest, Puppet::SSL::CertificateRevocationList, Puppet::SSL::Key, ] INDIRECTED_CLASSES.each do |const| class const::Memory < Puppet::Indirector::Memory # @return Array of all the indirector's values # # This mirrors Puppet::Indirector::SslFile#search which returns all files # in the directory. def search(request) return @instances.values end end end before do Puppet::SSL::Inventory.stubs(:new).returns(stub("Inventory", :add => nil)) INDIRECTED_CLASSES.each { |const| const.indirection.terminus_class = :memory } end after do INDIRECTED_CLASSES.each do |const| const.indirection.terminus_class = :file const.indirection.termini.clear end end describe "when generating certificates" do let(:ca) { Puppet::SSL::CertificateAuthority.new } before do expect_ca_initialization end it "should fail if a certificate already exists for the host" do cert = Puppet::SSL::Certificate.new('pre.existing') Puppet::SSL::Certificate.indirection.save(cert) expect { ca.generate(cert.name) }.to raise_error(ArgumentError, /a certificate already exists/i) end describe "that do not yet exist" do let(:cn) { "new.host" } def expect_cert_does_not_exist(cn) expect( Puppet::SSL::Certificate.indirection.find(cn) ).to be_nil end before do expect_to_sign_a_cert expect_cert_does_not_exist(cn) end it "should return the created certificate" do cert = ca.generate(cn) expect( cert ).to be_kind_of(Puppet::SSL::Certificate) expect( cert.name ).to eq(cn) end it "should not have any subject_alt_names by default" do cert = ca.generate(cn) expect( cert.subject_alt_names ).to be_empty end it "should have subject_alt_names if passed dns_alt_names" do cert = ca.generate(cn, :dns_alt_names => 'foo,bar') expect( cert.subject_alt_names ).to match_array(["DNS:#{cn}",'DNS:foo','DNS:bar']) end context "if autosign is false" do before do Puppet[:autosign] = false end it "should still generate and explicitly sign the request" do cert = nil cert = ca.generate(cn) expect(cert.name).to eq(cn) end end context "if autosign is true (Redmine #6112)" do def run_mode_must_be_master_for_autosign_to_be_attempted Puppet.stubs(:run_mode).returns(Puppet::Util::RunMode[:master]) end before do Puppet[:autosign] = true run_mode_must_be_master_for_autosign_to_be_attempted Puppet::Util::Log.level = :info end it "should generate a cert without attempting to sign again" do cert = ca.generate(cn) expect(cert.name).to eq(cn) expect(@logs.map(&:message)).to include("Autosigning #{cn}") end end end end end end diff --git a/spec/unit/ssl/inventory_spec.rb b/spec/unit/ssl/inventory_spec.rb index 6e4fbd340..879fd90d1 100755 --- a/spec/unit/ssl/inventory_spec.rb +++ b/spec/unit/ssl/inventory_spec.rb @@ -1,137 +1,150 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/ssl/inventory' describe Puppet::SSL::Inventory, :unless => Puppet.features.microsoft_windows? do let(:cert_inventory) { File.expand_path("/inven/tory") } before do @class = Puppet::SSL::Inventory end describe "when initializing" do it "should set its path to the inventory file" do Puppet[:cert_inventory] = cert_inventory @class.new.path.should == cert_inventory end end describe "when managing an inventory" do before do Puppet[:cert_inventory] = cert_inventory Puppet::FileSystem.stubs(:exist?).with(cert_inventory).returns true @inventory = @class.new @cert = mock 'cert' end describe "and creating the inventory file" do it "re-adds all of the existing certificates" do inventory_file = StringIO.new Puppet.settings.setting(:cert_inventory).stubs(:open).yields(inventory_file) cert1 = Puppet::SSL::Certificate.new("cert1") cert1.content = stub 'cert1', :serial => 2, :not_before => Time.now, :not_after => Time.now, :subject => "/CN=smocking" cert2 = Puppet::SSL::Certificate.new("cert2") cert2.content = stub 'cert2', :serial => 3, :not_before => Time.now, :not_after => Time.now, :subject => "/CN=mocking bird" Puppet::SSL::Certificate.indirection.expects(:search).with("*").returns [cert1, cert2] @inventory.rebuild expect(inventory_file.string).to match(/\/CN=smocking/) expect(inventory_file.string).to match(/\/CN=mocking bird/) end end describe "and adding a certificate" do it "should use the Settings to write to the file" do Puppet.settings.setting(:cert_inventory).expects(:open).with("a") @inventory.add(@cert) end it "should add formatted certificate information to the end of the file" do cert = Puppet::SSL::Certificate.new("mycert") cert.content = @cert fh = StringIO.new Puppet.settings.setting(:cert_inventory).expects(:open).with("a").yields(fh) @inventory.expects(:format).with(@cert).returns "myformat" @inventory.add(@cert) expect(fh.string).to eq("myformat") end end describe "and formatting a certificate" do before do @cert = stub 'cert', :not_before => Time.now, :not_after => Time.now, :subject => "mycert", :serial => 15 end it "should print the serial number as a 4 digit hex number in the first field" do @inventory.format(@cert).split[0].should == "0x000f" # 15 in hex end it "should print the not_before date in '%Y-%m-%dT%H:%M:%S%Z' format in the second field" do @cert.not_before.expects(:strftime).with('%Y-%m-%dT%H:%M:%S%Z').returns "before_time" @inventory.format(@cert).split[1].should == "before_time" end it "should print the not_after date in '%Y-%m-%dT%H:%M:%S%Z' format in the third field" do @cert.not_after.expects(:strftime).with('%Y-%m-%dT%H:%M:%S%Z').returns "after_time" @inventory.format(@cert).split[2].should == "after_time" end it "should print the subject in the fourth field" do @inventory.format(@cert).split[3].should == "mycert" end it "should add a carriage return" do @inventory.format(@cert).should =~ /\n$/ end it "should produce a line consisting of the serial number, start date, expiration date, and subject" do # Just make sure our serial and subject bracket the lines. @inventory.format(@cert).should =~ /^0x.+mycert$/ end end it "should be able to find a given host's serial number" do @inventory.should respond_to(:serial) end describe "and finding a serial number" do it "should return nil if the inventory file is missing" do Puppet::FileSystem.expects(:exist?).with(cert_inventory).returns false @inventory.serial(:whatever).should be_nil end it "should return the serial number from the line matching the provided name" do File.expects(:readlines).with(cert_inventory).returns ["0x00f blah blah /CN=me\n", "0x001 blah blah /CN=you\n"] @inventory.serial("me").should == 15 end it "should return the number as an integer" do File.expects(:readlines).with(cert_inventory).returns ["0x00f blah blah /CN=me\n", "0x001 blah blah /CN=you\n"] @inventory.serial("me").should == 15 end end + + describe "and finding all serial numbers" do + it "should return nil if the inventory file is missing" do + Puppet::FileSystem.expects(:exist?).with(cert_inventory).returns false + @inventory.serials(:whatever).should be_empty + end + + it "should return the all the serial numbers from the lines matching the provided name" do + File.expects(:readlines).with(cert_inventory).returns ["0x00f blah blah /CN=me\n", "0x001 blah blah /CN=you\n", "0x002 blah blah /CN=me\n"] + + @inventory.serials("me").should == [15, 2] + end + end end end