diff --git a/lib/puppet/ssl/certificate_revocation_list.rb b/lib/puppet/ssl/certificate_revocation_list.rb index 14f93ae2b..e0fb3a068 100644 --- a/lib/puppet/ssl/certificate_revocation_list.rb +++ b/lib/puppet/ssl/certificate_revocation_list.rb @@ -1,105 +1,107 @@ require 'puppet/ssl/base' require 'puppet/indirector' # Manage the CRL. class Puppet::SSL::CertificateRevocationList < Puppet::SSL::Base FIVE_YEARS = 5 * 365*24*60*60 wraps OpenSSL::X509::CRL extend Puppet::Indirector indirects :certificate_revocation_list, :terminus_class => :file # Convert a string into an instance. def self.from_s(string) crl = new('foo') # The name doesn't matter crl.content = wrapped_class.new(string) crl end # Because of how the format handler class is included, this # can't be in the base class. def self.supported_formats [:s] end # Knows how to create a CRL with our system defaults. def generate(cert, cakey) Puppet.info "Creating a new certificate revocation list" create_crl_issued_by(cert) start_at_initial_crl_number update_valid_time_range_to_start_at(Time.now) sign_with(cakey) @content end # The name doesn't actually matter; there's only one CRL. # We just need the name so our Indirector stuff all works more easily. def initialize(fakename) @name = "crl" end # Revoke the certificate with serial number SERIAL issued by this # CA, then write the CRL back to disk. The REASON must be one of the # OpenSSL::OCSP::REVOKED_* reasons def revoke(serial, cakey, reason = OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE) Puppet.notice "Revoked certificate with serial #{serial}" time = Time.now add_certitificate_revocation_for(serial, reason, time) update_to_next_crl_number update_valid_time_range_to_start_at(time) sign_with(cakey) Puppet::SSL::CertificateRevocationList.indirection.save(self) end private def create_crl_issued_by(cert) @content = wrapped_class.new @content.issuer = cert.subject @content.version = 1 end def start_at_initial_crl_number @content.extensions = [crl_number_of(0)] end def add_certitificate_revocation_for(serial, reason, time) revoked = OpenSSL::X509::Revoked.new revoked.serial = serial revoked.time = time enum = OpenSSL::ASN1::Enumerated(reason) ext = OpenSSL::X509::Extension.new("CRLReason", enum) revoked.add_extension(ext) @content.add_revoked(revoked) end def update_valid_time_range_to_start_at(time) - @content.last_update = time + # The CRL is not valid if the time of checking == the time of last_update. + # So to have it valid right now we need to say that it was updated one second ago. + @content.last_update = time - 1 @content.next_update = time + FIVE_YEARS end def update_to_next_crl_number @content.extensions = with_next_crl_number_from(@content.extensions) end def with_next_crl_number_from(existing_extensions) existing_crl_num = existing_extensions.find { |e| e.oid == 'crlNumber' } new_crl_num = existing_crl_num ? existing_crl_num.value.to_i + 1 : 0 extensions_without_crl_num = existing_extensions.reject { |e| e.oid == 'crlNumber' } extensions_without_crl_num + [crl_number_of(new_crl_num)] end def crl_number_of(number) OpenSSL::X509::Extension.new('crlNumber', OpenSSL::ASN1::Integer(number)) end def sign_with(cakey) @content.sign(cakey, OpenSSL::Digest::SHA1.new) end end diff --git a/spec/unit/ssl/certificate_revocation_list_spec.rb b/spec/unit/ssl/certificate_revocation_list_spec.rb index 99058b353..5b8e47b48 100755 --- a/spec/unit/ssl/certificate_revocation_list_spec.rb +++ b/spec/unit/ssl/certificate_revocation_list_spec.rb @@ -1,167 +1,167 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/ssl/certificate_revocation_list' describe Puppet::SSL::CertificateRevocationList do before do @cert = stub 'cert', :subject => "mysubject" @key = stub 'key', :private? => true @class = Puppet::SSL::CertificateRevocationList 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 CRL instance with its name set to 'foo' and its content set to the extracted CRL" do crl = stub 'crl' OpenSSL::X509::CRL.expects(:new).returns(crl) mycrl = stub 'sslcrl' mycrl.expects(:content=).with(crl) @class.expects(:new).with("foo").returns mycrl @class.from_s("my crl").should == mycrl end end describe "when an instance" do before do @class.any_instance.stubs(:read_or_generate) @crl = @class.new("whatever") end it "should always use 'crl' for its name" do @crl.name.should == "crl" end it "should have a content attribute" do @crl.should respond_to(:content) end end describe "when generating the crl" do before do @real_crl = mock 'crl' @real_crl.stub_everything OpenSSL::X509::CRL.stubs(:new).returns(@real_crl) @class.any_instance.stubs(:read_or_generate) @crl = @class.new("crl") end it "should set its issuer to the subject of the passed certificate" do @real_crl.expects(:issuer=).with(@cert.subject) @crl.generate(@cert, @key) end it "should set its version to 1" do @real_crl.expects(:version=).with(1) @crl.generate(@cert, @key) end it "should create an instance of OpenSSL::X509::CRL" do OpenSSL::X509::CRL.expects(:new).returns(@real_crl) @crl.generate(@cert, @key) end # The next three tests aren't good, but at least they # specify the behaviour. it "should add an extension for the CRL number" do @real_crl.expects(:extensions=) @crl.generate(@cert, @key) end it "should set the last update time" do @real_crl.expects(:last_update=) @crl.generate(@cert, @key) end it "should set the next update time" do @real_crl.expects(:next_update=) @crl.generate(@cert, @key) end it "should sign the CRL" do @real_crl.expects(:sign).with { |key, digest| key == @key } @crl.generate(@cert, @key) end it "should set the content to the generated crl" do @crl.generate(@cert, @key) @crl.content.should equal(@real_crl) end it "should return the generated crl" do @crl.generate(@cert, @key).should equal(@real_crl) end end # This test suite isn't exactly complete, because the # SSL stuff is very complicated. It just hits the high points. describe "when revoking a certificate" do before do @class.wrapped_class.any_instance.stubs(:issuer=) @class.wrapped_class.any_instance.stubs(:sign) @crl = @class.new("crl") @crl.generate(@cert, @key) @crl.content.stubs(:sign) Puppet::SSL::CertificateRevocationList.indirection.stubs :save @key = mock 'key' end it "should require a serial number and the CA's private key" do lambda { @crl.revoke }.should raise_error(ArgumentError) end it "should default to OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE as the revocation reason" do # This makes it a bit more of an integration test than we'd normally like, but that's life # with openssl. reason = OpenSSL::ASN1::Enumerated(OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE) OpenSSL::ASN1.expects(:Enumerated).with(OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE).returns reason @crl.revoke(1, @key) end - it "should mark the CRL as updated" do + it "should mark the CRL as updated at a time that makes it valid now" do time = Time.now Time.stubs(:now).returns time - @crl.content.expects(:last_update=).with(time) + @crl.content.expects(:last_update=).with(time - 1) @crl.revoke(1, @key) end it "should mark the CRL valid for five years" do time = Time.now Time.stubs(:now).returns time @crl.content.expects(:next_update=).with(time + (5 * 365*24*60*60)) @crl.revoke(1, @key) end it "should sign the CRL with the CA's private key and a digest instance" do @crl.content.expects(:sign).with { |key, digest| key == @key and digest.is_a?(OpenSSL::Digest::SHA1) } @crl.revoke(1, @key) end it "should save the CRL" do Puppet::SSL::CertificateRevocationList.indirection.expects(:save).with(@crl, nil) @crl.revoke(1, @key) end end end