diff --git a/lib/puppet/ssl/certificate.rb b/lib/puppet/ssl/certificate.rb index 47ae64cce..719a741a7 100644 --- a/lib/puppet/ssl/certificate.rb +++ b/lib/puppet/ssl/certificate.rb @@ -1,40 +1,40 @@ require 'puppet/ssl/base' # Manage certificates themselves. This class has no # 'generate' method because the CA is responsible # for turning CSRs into certificates; we can only # retrieve them from the CA (or not, as is often # the case). class Puppet::SSL::Certificate < Puppet::SSL::Base # This is defined from the base class wraps OpenSSL::X509::Certificate extend Puppet::Indirector indirects :certificate, :terminus_class => :file # Convert a string into an instance. def self.from_s(string) instance = wrapped_class.new(string) name = instance.subject.to_s.sub(/\/CN=/i, '').downcase result = new(name) result.content = instance result end # Because of how the format handler class is included, this # can't be in the base class. def self.supported_formats [:s] end - def alternate_names + def subject_alt_names alts = content.extensions.find{|ext| ext.oid == "subjectAltName"} return [] unless alts alts.value.split(/\s*,\s*/) end def expiration return nil unless content content.not_after end end diff --git a/lib/puppet/ssl/certificate_authority/interface.rb b/lib/puppet/ssl/certificate_authority/interface.rb index aa8aa7f05..797b2f751 100644 --- a/lib/puppet/ssl/certificate_authority/interface.rb +++ b/lib/puppet/ssl/certificate_authority/interface.rb @@ -1,179 +1,179 @@ # This class is basically a hidden class that knows how to act # on the CA. It's only used by the 'puppetca' executable, and its # job is to provide a CLI-like interface to the CA class. module Puppet module SSL class CertificateAuthority class Interface INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify, :fingerprint] class InterfaceError < ArgumentError; end attr_reader :method, :subjects, :digest, :options # Actually perform the work. def apply(ca) unless subjects or method == :list raise ArgumentError, "You must provide hosts or :all when using #{method}" end begin return send(method, ca) if respond_to?(method) (subjects == :all ? ca.list : subjects).each do |host| ca.send(method, host) end rescue InterfaceError raise rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not call #{method}: #{detail}" end end def generate(ca) raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all subjects.each do |host| ca.generate(host, options) end end def initialize(method, options) self.method = method self.subjects = options.delete(:to) @digest = options.delete(:digest) || :MD5 @options = options end # List the hosts. def list(ca) signed = ca.list requests = ca.waiting? case subjects when :all hosts = [signed, requests].flatten when :signed hosts = signed.flatten when nil hosts = requests else hosts = subjects end certs = {:signed => {}, :invalid => {}, :request => {}} return if hosts.empty? hosts.uniq.sort.each do |host| begin ca.verify(host) unless requests.include?(host) rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => details verify_error = details.to_s end if verify_error cert = Puppet::SSL::Certificate.indirection.find(host) certs[:invalid][host] = [cert, verify_error] elsif signed.include?(host) cert = Puppet::SSL::Certificate.indirection.find(host) certs[:signed][host] = cert else req = Puppet::SSL::CertificateRequest.indirection.find(host) certs[:request][host] = req end end names = certs.values.map(&:keys).flatten name_width = names.sort_by(&:length).last.length rescue 0 output = [:request, :signed, :invalid].map do |type| next if certs[type].empty? certs[type].map do |host,info| format_host(ca, host, type, info, name_width) end end.flatten.compact.sort.join("\n") puts output end def format_host(ca, host, type, info, width) certish, verify_error = info alt_names = case type when :signed - certish.alternate_names + certish.subject_alt_names when :request certish.subject_alt_names || [] else [] end alt_names.delete(host) alt_str = "(alt names: #{alt_names.join(', ')})" unless alt_names.empty? glyph = {:signed => '+', :request => ' ', :invalid => '-'}[type] name = host.ljust(width) fingerprint = "(#{ca.fingerprint(host, @digest)})" explanation = "(#{verify_error})" if verify_error [glyph, name, fingerprint, alt_str, explanation].compact.join(' ') end # Set the method to apply. def method=(method) raise ArgumentError, "Invalid method #{method} to apply" unless INTERFACE_METHODS.include?(method) @method = method end # Print certificate information. def print(ca) (subjects == :all ? ca.list : subjects).each do |host| if value = ca.print(host) puts value else Puppet.err "Could not find certificate for #{host}" end end end # Print certificate information. def fingerprint(ca) (subjects == :all ? ca.list + ca.waiting?: subjects).each do |host| if value = ca.fingerprint(host, @digest) puts "#{host} #{value}" else Puppet.err "Could not find certificate for #{host}" end end end # Sign a given certificate. def sign(ca) list = subjects == :all ? ca.waiting? : subjects raise InterfaceError, "No waiting certificate requests to sign" if list.empty? list.each do |host| ca.sign(host, options[:allow_dns_alt_names]) end end # Set the list of hosts we're operating on. Also supports keywords. def subjects=(value) unless value == :all or value == :signed or value.is_a?(Array) raise ArgumentError, "Subjects must be an array or :all; not #{value}" end value = nil if value.is_a?(Array) and value.empty? @subjects = value end end end end end diff --git a/spec/unit/ssl/certificate_authority/interface_spec.rb b/spec/unit/ssl/certificate_authority/interface_spec.rb index b3e208222..ac91c48e8 100755 --- a/spec/unit/ssl/certificate_authority/interface_spec.rb +++ b/spec/unit/ssl/certificate_authority/interface_spec.rb @@ -1,371 +1,371 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/ssl/certificate_authority' shared_examples_for "a normal interface method" do it "should call the method on the CA for each host specified if an array was provided" do @ca.expects(@method).with("host1") @ca.expects(@method).with("host2") @applier = Puppet::SSL::CertificateAuthority::Interface.new(@method, :to => %w{host1 host2}) @applier.apply(@ca) end it "should call the method on the CA for all existing certificates if :all was provided" do @ca.expects(:list).returns %w{host1 host2} @ca.expects(@method).with("host1") @ca.expects(@method).with("host2") @applier = Puppet::SSL::CertificateAuthority::Interface.new(@method, :to => :all) @applier.apply(@ca) end end describe Puppet::SSL::CertificateAuthority::Interface do before do @class = Puppet::SSL::CertificateAuthority::Interface end describe "when initializing" do it "should set its method using its settor" do instance = @class.new(:generate, :to => :all) instance.method.should == :generate end it "should set its subjects using the settor" do instance = @class.new(:generate, :to => :all) instance.subjects.should == :all end it "should set the digest if given" do interface = @class.new(:generate, :to => :all, :digest => :digest) interface.digest.should == :digest end it "should set the digest to md5 if none given" do interface = @class.new(:generate, :to => :all) interface.digest.should == :MD5 end end describe "when setting the method" do it "should set the method" do instance = @class.new(:generate, :to => :all) instance.method = :list instance.method.should == :list end it "should fail if the method isn't a member of the INTERFACE_METHODS array" do lambda { @class.new(:thing, :to => :all) }.should raise_error(ArgumentError, /Invalid method thing to apply/) end end describe "when setting the subjects" do it "should set the subjects" do instance = @class.new(:generate, :to => :all) instance.subjects = :signed instance.subjects.should == :signed end it "should fail if the subjects setting isn't :all or an array" do lambda { @class.new(:generate, :to => "other") }.should raise_error(ArgumentError, /Subjects must be an array or :all; not other/) end end it "should have a method for triggering the application" do @class.new(:generate, :to => :all).should respond_to(:apply) end describe "when applying" do before do # We use a real object here, because :verify can't be stubbed, apparently. @ca = Object.new end it "should raise InterfaceErrors" do @applier = @class.new(:revoke, :to => :all) @ca.expects(:list).raises Puppet::SSL::CertificateAuthority::Interface::InterfaceError lambda { @applier.apply(@ca) }.should raise_error(Puppet::SSL::CertificateAuthority::Interface::InterfaceError) end it "should log non-Interface failures rather than failing" do @applier = @class.new(:revoke, :to => :all) @ca.expects(:list).raises ArgumentError Puppet.expects(:err) lambda { @applier.apply(@ca) }.should_not raise_error end describe "with an empty array specified and the method is not list" do it "should fail" do @applier = @class.new(:sign, :to => []) lambda { @applier.apply(@ca) }.should raise_error(ArgumentError) end end describe ":generate" do it "should fail if :all was specified" do @applier = @class.new(:generate, :to => :all) lambda { @applier.apply(@ca) }.should raise_error(ArgumentError) end it "should call :generate on the CA for each host specified" do @applier = @class.new(:generate, :to => %w{host1 host2}) @ca.expects(:generate).with("host1", {}) @ca.expects(:generate).with("host2", {}) @applier.apply(@ca) end end describe ":verify" do before { @method = :verify } #it_should_behave_like "a normal interface method" it "should call the method on the CA for each host specified if an array was provided" do # LAK:NOTE Mocha apparently doesn't allow you to mock :verify, but I'm confident this works in real life. end it "should call the method on the CA for all existing certificates if :all was provided" do # LAK:NOTE Mocha apparently doesn't allow you to mock :verify, but I'm confident this works in real life. end end describe ":destroy" do before { @method = :destroy } it_should_behave_like "a normal interface method" end describe ":revoke" do before { @method = :revoke } it_should_behave_like "a normal interface method" end describe ":sign" do describe "and an array of names was provided" do let(:applier) { @class.new(:sign, @options.merge(:to => %w{host1 host2})) } it "should sign the specified waiting certificate requests" do @options = {:allow_dns_alt_names => false} @ca.expects(:sign).with("host1", false) @ca.expects(:sign).with("host2", false) applier.apply(@ca) end it "should sign the certificate requests with alt names if specified" do @options = {:allow_dns_alt_names => true} @ca.expects(:sign).with("host1", true) @ca.expects(:sign).with("host2", true) applier.apply(@ca) end end describe "and :all was provided" do it "should sign all waiting certificate requests" do @ca.stubs(:waiting?).returns(%w{cert1 cert2}) @ca.expects(:sign).with("cert1", nil) @ca.expects(:sign).with("cert2", nil) @applier = @class.new(:sign, :to => :all) @applier.apply(@ca) end it "should fail if there are no waiting certificate requests" do @ca.stubs(:waiting?).returns([]) @applier = @class.new(:sign, :to => :all) lambda { @applier.apply(@ca) }.should raise_error(Puppet::SSL::CertificateAuthority::Interface::InterfaceError) end end end describe ":list" do before :each do - certish = stub('certish', :alternate_names => [], :subject_alt_names => nil) + certish = stub('certish', :subject_alt_names => [], :subject_alt_names => nil) Puppet::SSL::Certificate.indirection.stubs(:find).returns certish Puppet::SSL::CertificateRequest.indirection.stubs(:find).returns certish @ca.expects(:waiting?).returns %w{host1 host2 host3} @ca.expects(:list).returns %w{host4 host5 host6} @ca.stubs(:fingerprint).returns "fingerprint" @ca.stubs(:verify) end describe "and an empty array was provided" do it "should print all certificate requests" do applier = @class.new(:list, :to => []) applier.expects(:puts).with(<<-OUTPUT.chomp) host1 (fingerprint) host2 (fingerprint) host3 (fingerprint) OUTPUT applier.apply(@ca) end end describe "and :all was provided" do it "should print a string containing all certificate requests and certificates" do @ca.stubs(:verify).with("host4").raises(Puppet::SSL::CertificateAuthority::CertificateVerificationError.new(23), "certificate revoked") applier = @class.new(:list, :to => :all) applier.expects(:puts).with(<<-OUTPUT.chomp) host1 (fingerprint) host2 (fingerprint) host3 (fingerprint) + host5 (fingerprint) + host6 (fingerprint) - host4 (fingerprint) (certificate revoked) OUTPUT applier.apply(@ca) end end describe "and :signed was provided" do it "should print a string containing all signed certificate requests and certificates" do applier = @class.new(:list, :to => :signed) applier.expects(:puts).with(<<-OUTPUT.chomp) + host4 (fingerprint) + host5 (fingerprint) + host6 (fingerprint) OUTPUT applier.apply(@ca) end it "should include subject alt names if they are on the certificate request" do request = stub 'request', :subject_alt_names => ["DNS:foo", "DNS:bar"] Puppet::SSL::CertificateRequest.indirection.stubs(:find).returns(request) applier = @class.new(:list, :to => ['host1']) applier.expects(:puts).with(<<-OUTPUT.chomp) host1 (fingerprint) (alt names: DNS:foo, DNS:bar) OUTPUT applier.apply(@ca) end end describe "and an array of names was provided" do it "should print all named hosts" do applier = @class.new(:list, :to => %w{host1 host2 host4 host5}) applier.expects(:puts).with(<<-OUTPUT.chomp) host1 (fingerprint) host2 (fingerprint) + host4 (fingerprint) + host5 (fingerprint) OUTPUT applier.apply(@ca) end end end describe ":print" do describe "and :all was provided" do it "should print all certificates" do @ca.expects(:list).returns %w{host1 host2} @applier = @class.new(:print, :to => :all) @ca.expects(:print).with("host1").returns "h1" @applier.expects(:puts).with "h1" @ca.expects(:print).with("host2").returns "h2" @applier.expects(:puts).with "h2" @applier.apply(@ca) end end describe "and an array of names was provided" do it "should print each named certificate if found" do @applier = @class.new(:print, :to => %w{host1 host2}) @ca.expects(:print).with("host1").returns "h1" @applier.expects(:puts).with "h1" @ca.expects(:print).with("host2").returns "h2" @applier.expects(:puts).with "h2" @applier.apply(@ca) end it "should log any named but not found certificates" do @applier = @class.new(:print, :to => %w{host1 host2}) @ca.expects(:print).with("host1").returns "h1" @applier.expects(:puts).with "h1" @ca.expects(:print).with("host2").returns nil Puppet.expects(:err).with { |msg| msg.include?("host2") } @applier.apply(@ca) end end end describe ":fingerprint" do it "should fingerprint with the set digest algorithm" do @applier = @class.new(:fingerprint, :to => %w{host1}, :digest => :digest) @ca.expects(:fingerprint).with("host1", :digest).returns "fingerprint1" @applier.expects(:puts).with "host1 fingerprint1" @applier.apply(@ca) end describe "and :all was provided" do it "should fingerprint all certificates (including waiting ones)" do @ca.expects(:list).returns %w{host1} @ca.expects(:waiting?).returns %w{host2} @applier = @class.new(:fingerprint, :to => :all) @ca.expects(:fingerprint).with("host1", :MD5).returns "fingerprint1" @applier.expects(:puts).with "host1 fingerprint1" @ca.expects(:fingerprint).with("host2", :MD5).returns "fingerprint2" @applier.expects(:puts).with "host2 fingerprint2" @applier.apply(@ca) end end describe "and an array of names was provided" do it "should print each named certificate if found" do @applier = @class.new(:fingerprint, :to => %w{host1 host2}) @ca.expects(:fingerprint).with("host1", :MD5).returns "fingerprint1" @applier.expects(:puts).with "host1 fingerprint1" @ca.expects(:fingerprint).with("host2", :MD5).returns "fingerprint2" @applier.expects(:puts).with "host2 fingerprint2" @applier.apply(@ca) end end end end end diff --git a/spec/unit/ssl/certificate_spec.rb b/spec/unit/ssl/certificate_spec.rb index 01729d71a..af4e3d594 100755 --- a/spec/unit/ssl/certificate_spec.rb +++ b/spec/unit/ssl/certificate_spec.rb @@ -1,154 +1,154 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/ssl/certificate' describe Puppet::SSL::Certificate do before do @class = Puppet::SSL::Certificate end after do @class.instance_variable_set("@ca_location", nil) end it "should be extended with the Indirector module" do @class.singleton_class.should be_include(Puppet::Indirector) end it "should indirect certificate" do @class.indirection.name.should == :certificate end it "should only support the text format" do @class.supported_formats.should == [:s] end describe "when converting from a string" do it "should create a certificate instance with its name set to the certificate subject and its content set to the extracted certificate" do cert = stub 'certificate', :subject => "/CN=Foo.madstop.com" OpenSSL::X509::Certificate.expects(:new).with("my certificate").returns(cert) mycert = stub 'sslcert' mycert.expects(:content=).with(cert) @class.expects(:new).with("foo.madstop.com").returns mycert @class.from_s("my certificate") end it "should create multiple certificate instances when asked" do cert1 = stub 'cert1' @class.expects(:from_s).with("cert1").returns cert1 cert2 = stub 'cert2' @class.expects(:from_s).with("cert2").returns cert2 @class.from_multiple_s("cert1\n---\ncert2").should == [cert1, cert2] end end describe "when converting to a string" do before do @certificate = @class.new("myname") end it "should return an empty string when it has no certificate" do @certificate.to_s.should == "" end it "should convert the certificate to pem format" do certificate = mock 'certificate', :to_pem => "pem" @certificate.content = certificate @certificate.to_s.should == "pem" end it "should be able to convert multiple instances to a string" do cert2 = @class.new("foo") @certificate.expects(:to_s).returns "cert1" cert2.expects(:to_s).returns "cert2" @class.to_multiple_s([@certificate, cert2]).should == "cert1\n---\ncert2" end end describe "when managing instances" do before do @certificate = @class.new("myname") end it "should have a name attribute" do @certificate.name.should == "myname" end it "should convert its name to a string and downcase it" do @class.new(:MyName).name.should == "myname" end it "should have a content attribute" do @certificate.should respond_to(:content) end - describe "#alternate_names" do + describe "#subject_alt_names" do it "should list all alternate names when the extension is present" do key = Puppet::SSL::Key.new('quux') key.generate csr = Puppet::SSL::CertificateRequest.new('quux') csr.generate(key, :dns_alt_names => 'foo, bar,baz') raw_csr = csr.content cert = Puppet::SSL::CertificateFactory.build('server', csr, raw_csr, 14) certificate = @class.from_s(cert.to_pem) certificate.subject_alt_names. should =~ ['DNS:foo', 'DNS:bar', 'DNS:baz', 'DNS:quux'] end it "should return an empty list of names if the extension is absent" do key = Puppet::SSL::Key.new('quux') key.generate csr = Puppet::SSL::CertificateRequest.new('quux') csr.generate(key) raw_csr = csr.content cert = Puppet::SSL::CertificateFactory.build('client', csr, raw_csr, 14) certificate = @class.from_s(cert.to_pem) - certificate.alternate_names.should == [] + certificate.subject_alt_names.should == [] end end it "should return a nil expiration if there is no actual certificate" do @certificate.stubs(:content).returns nil @certificate.expiration.should be_nil end it "should use the expiration of the certificate as its expiration date" do cert = stub 'cert' @certificate.stubs(:content).returns cert cert.expects(:not_after).returns "sometime" @certificate.expiration.should == "sometime" end it "should be able to read certificates from disk" do path = "/my/path" File.expects(:read).with(path).returns("my certificate") certificate = mock 'certificate' OpenSSL::X509::Certificate.expects(:new).with("my certificate").returns(certificate) @certificate.read(path).should equal(certificate) @certificate.content.should equal(certificate) end it "should have a :to_text method that it delegates to the actual key" do real_certificate = mock 'certificate' real_certificate.expects(:to_text).returns "certificatetext" @certificate.content = real_certificate @certificate.to_text.should == "certificatetext" end end end