Skip to content

Commit

Permalink
Better CRL support
Browse files Browse the repository at this point in the history
CRLs can now be generated from both Certificates and SerialNumbers.
This is mostly for convenience if you don't want to keep the entire
certificate around or want a lighter-weight way of doing it.

There's also a new Revocable module to organize revocation-centric
attributes.

Additionally, the tests have been shorn up and the testing modulus
reduced to 768 to speed things up.
  • Loading branch information
cchandler committed Sep 16, 2012
1 parent 5e3df97 commit 99bcba0
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 36 deletions.
18 changes: 17 additions & 1 deletion README.rdoc
Expand Up @@ -214,6 +214,22 @@ Similarly, reading a CSR in is as simple as providing the PEM formatted version

Once you have the CSR in the form of a +SigningRequest+ you can transform it to a +Certificate+ with +to_cert+. At this point it works just like any other certificate. You'll have to provide a serial number to actually sign it.

= Certificate Revocation Lists (CRLs)

Revocation lists let clients know when a certificate in the wild should no longer be trusted.

Like end-user certificates, CRLs have to be signed by a signing authority to be valid. Additionally, you will need to furnish a +nextUpdate+ value that indicates to the client when it should look for updates to the CRL and how long it should consider a cached value valid.

Ideally you would place the result CRL somewhere generally accessible on the Internet and reference the URI in the +crlDistributionPoints+ extension on issued certificates.

crl = CertificateAuthority::CertificateRevocationList.new
crl << certificate # Some CertificateAuthority::Certificate
crl << serial_number # Also works with plain CertificateAuthority::SerialNumber
crl.parent = root_certificate # A valid root
crl.next_update = (60 * 60 * 10) # 10 Hours
crl.sign!
crl.to_pem

= PKCS#11 Support

If you happen to have a PKCS#11 compliant hardware token you can use +certificate_authority+ to maintain private key materials in hardware security modules. At this point the scope of operating that hardware is out of scope of this README but it's there and it is supported.
Expand Down Expand Up @@ -247,7 +263,7 @@ Also of note, I have gotten these to work with 32-bit copies of Ubuntu 10.10 and
= Coming Soon

* More PKCS#11 hardware (I need driver support from the manufacturers)
* Actual CRL/OCSP support
* Actual OCSP support

= Todone

Expand Down
1 change: 1 addition & 0 deletions lib/certificate_authority.rb
Expand Up @@ -6,6 +6,7 @@

#Internal modules
require 'certificate_authority/signing_entity'
require 'certificate_authority/revocable'
require 'certificate_authority/distinguished_name'
require 'certificate_authority/serial_number'
require 'certificate_authority/key_material'
Expand Down
3 changes: 1 addition & 2 deletions lib/certificate_authority/certificate.rb
@@ -1,14 +1,13 @@
module CertificateAuthority
class Certificate
# include SigningEntity
include ActiveModel::Validations
include Revocable

attr_accessor :distinguished_name
attr_accessor :serial_number
attr_accessor :key_material
attr_accessor :not_before
attr_accessor :not_after
attr_accessor :revoked_at
attr_accessor :extensions
attr_accessor :openssl_body

Expand Down
36 changes: 27 additions & 9 deletions lib/certificate_authority/certificate_revocation_list.rb
Expand Up @@ -17,20 +17,34 @@ def initialize
self.next_update = 60 * 60 * 4 # 4 hour default
end

def <<(cert)
raise "Only revoked certificates can be added to a CRL" unless cert.revoked?
self.certificates << cert
def <<(revocable)
case revocable
when Revocable
raise "Only revoked entities can be added to a CRL" unless revocable.revoked?
self.certificates << revocable
when OpenSSL::X509::Certificate
raise "Not implemented yet"
else
raise "#{revocable.class} cannot be included in a CRL"
end
end

def sign!
def sign!(signing_profile={})
raise "No parent entity has been set!" if self.parent.nil?
raise "Invalid CRL" unless self.valid?

revocations = self.certificates.collect do |certificate|
revocations = self.certificates.collect do |revocable|
revocation = OpenSSL::X509::Revoked.new
x509_cert = OpenSSL::X509::Certificate.new(certificate.to_pem)
revocation.serial = x509_cert.serial
revocation.time = certificate.revoked_at

## We really just need a serial number, now we have to dig it out
case revocable
when Certificate
x509_cert = OpenSSL::X509::Certificate.new(revocable.to_pem)
revocation.serial = x509_cert.serial
when SerialNumber
revocation.serial = revocable.number
end
revocation.time = revocable.revoked_at
revocation
end

Expand All @@ -44,7 +58,11 @@ def sign!
crl.next_update = Time.now + self.next_update

signing_cert = OpenSSL::X509::Certificate.new(self.parent.to_pem)
digest = OpenSSL::Digest::Digest.new("SHA512")
if signing_profile["digest"].nil?
digest = OpenSSL::Digest::Digest.new("SHA512")
else
digest = OpenSSL::Digest::Digest.new(signing_profile["digest"])
end
crl.issuer = signing_cert.subject
self.crl_body = crl.sign(self.parent.key_material.private_key, digest)

Expand Down
14 changes: 14 additions & 0 deletions lib/certificate_authority/revocable.rb
@@ -0,0 +1,14 @@
module CertificateAuthority
module Revocable
attr_accessor :revoked_at

def revoke!(time=Time.now)
@revoked_at = time
end

def revoked?
# If we have a time, then we're revoked
!@revoked_at.nil?
end
end
end
1 change: 1 addition & 0 deletions lib/certificate_authority/serial_number.rb
@@ -1,6 +1,7 @@
module CertificateAuthority
class SerialNumber
include ActiveModel::Validations
include Revocable

attr_accessor :number

Expand Down
67 changes: 62 additions & 5 deletions spec/units/certificate_revocation_list_spec.rb
Expand Up @@ -7,17 +7,21 @@
@root_certificate = CertificateAuthority::Certificate.new
@root_certificate.signing_entity = true
@root_certificate.subject.common_name = "CRL Root"
@root_certificate.key_material.generate_key(1024)
@root_certificate.key_material.generate_key(768)
@root_certificate.serial_number.number = 1
@root_certificate.sign!

@certificate = CertificateAuthority::Certificate.new
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.subject.common_name = "http://bogusSite.com"
@certificate.parent = @root_certificate
@certificate.serial_number.number = 2
@certificate.sign!

@serial_number = CertificateAuthority::SerialNumber.new
@serial_number.revoked_at = Time.now
@serial_number.number = 5

@crl.parent = @root_certificate
@certificate.revoked_at = Time.now
end
Expand Down Expand Up @@ -50,6 +54,62 @@
OpenSSL::X509::CRL.new(@crl.to_pem).should_not be_nil
end

it "should be able to mix Certificates and SerialNumbers for convenience" do
@crl << @certificate
@crl << @serial_number
@crl.parent = @root_certificate
@crl.sign!
openssl_csr = OpenSSL::X509::CRL.new(@crl.to_pem)
openssl_csr.revoked.size.should == 2
end

it "should have the correct number of entities" do
@crl << @certificate
@crl.parent = @root_certificate
@crl.sign!
openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
openssl_clr.revoked.should be_a(Array)
openssl_clr.revoked.size.should == 1
end

it "should have the serial numbers of revoked entities" do
@crl << @certificate
@crl << @serial_number
@crl.parent = @root_certificate
@crl.sign!
openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
openssl_clr.revoked.should be_a(Array)
openssl_clr.revoked.first.serial.should == @certificate.serial_number.number
openssl_clr.revoked.last.serial.should == @serial_number.number
end

it "should be valid according to OpenSSL and signer" do
@crl << @certificate
@crl.parent = @root_certificate
@crl.sign!
openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
openssl_root = OpenSSL::X509::Certificate.new(@root_certificate.to_pem)
openssl_clr.verify(openssl_root.public_key).should be_true
end

describe "Digests" do
it "should use SHA512 by default" do
@crl << @certificate
@crl.parent = @root_certificate
@crl.sign!
openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
openssl_clr.signature_algorithm.should == "sha512WithRSAEncryption"
end

it "should support alternate digests supported by OpenSSL" do
@crl << @certificate
@crl.parent = @root_certificate
@crl.sign!({"digest" => "SHA1"})
openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
openssl_clr.signature_algorithm.should == "sha1WithRSAEncryption"
end
end

describe "Next update" do
it "should be able to set a 'next_update' value" do
@crl.next_update = (60 * 60 * 10) # 10 Hours
Expand All @@ -61,8 +121,5 @@
@crl.next_update = - (60 * 60 * 10)
lambda{@crl.sign!}.should raise_error
end

end


end
28 changes: 14 additions & 14 deletions spec/units/certificate_spec.rb
Expand Up @@ -36,7 +36,7 @@
it "should be able to self-sign" do
@certificate.serial_number.number = 1
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.sign!
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
cert.subject.to_s.should == cert.issuer.to_s
Expand All @@ -45,7 +45,7 @@
it "should have the basicContraint CA:TRUE" do
@certificate.serial_number.number = 1
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.sign!
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
cert.extensions.map{|i| [i.oid,i.value] }.select{|i| i.first == "basicConstraints"}.first[1].should == "CA:TRUE"
Expand All @@ -57,7 +57,7 @@
@different_cert = CertificateAuthority::Certificate.new
@different_cert.signing_entity = true
@different_cert.subject.common_name = "chrischandler.name root"
@different_cert.key_material.generate_key(1024)
@different_cert.key_material.generate_key(768)
@different_cert.serial_number.number = 2
@different_cert.sign! #self-signed
@certificate.parent = @different_cert
Expand All @@ -79,7 +79,7 @@

it "should correctly be signed by a parent certificate" do
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.signing_entity = true
@certificate.serial_number.number = 1
@certificate.sign!
Expand All @@ -89,7 +89,7 @@

it "should have the basicContraint CA:TRUE" do
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.signing_entity = true
@certificate.serial_number.number = 3
@certificate.sign!
Expand All @@ -104,7 +104,7 @@
@different_cert = CertificateAuthority::Certificate.new
@different_cert.signing_entity = true
@different_cert.subject.common_name = "chrischandler.name root"
@different_cert.key_material.generate_key(1024)
@different_cert.key_material.generate_key(768)
@different_cert.serial_number.number = 1
@different_cert.sign! #self-signed
@certificate.parent = @different_cert
Expand All @@ -120,7 +120,7 @@

it "should have the basicContraint CA:FALSE" do
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.signing_entity = false
@certificate.serial_number.number = 1
@certificate.sign!
Expand All @@ -139,7 +139,7 @@
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
@certificate.sign!
end
Expand All @@ -154,7 +154,7 @@
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
@signing_profile = {
"extensions" => {
Expand All @@ -175,7 +175,7 @@
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
end

Expand All @@ -196,7 +196,7 @@
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
end

Expand All @@ -211,7 +211,7 @@
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
end

Expand All @@ -233,7 +233,7 @@
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
end

Expand Down Expand Up @@ -319,7 +319,7 @@
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
@certificate.key_material.generate_key(1024)
@certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1

@signing_profile = {
Expand Down
10 changes: 5 additions & 5 deletions spec/units/key_material_spec.rb
Expand Up @@ -102,21 +102,21 @@
end

it "should be able to generate an RSA key" do
@key_material.generate_key(1024).should_not be_nil
@key_material.generate_key(768).should_not be_nil
end

it "should generate a proper OpenSSL::PKey::RSA" do
@key_material.generate_key(1024).class.should == OpenSSL::PKey::RSA
@key_material.generate_key(768).class.should == OpenSSL::PKey::RSA
end

it "should be able to specify the size of the modulus to generate" do
@key_material.generate_key(1024).should_not be_nil
@key_material.generate_key(768).should_not be_nil
end

describe "with generated key" do
before(:all) do
@key_material_in_memory = CertificateAuthority::MemoryKeyMaterial.new
@key_material_in_memory.generate_key(1024)
@key_material_in_memory.generate_key(768)
end

it "should be able to retrieve the private key" do
Expand All @@ -130,7 +130,7 @@

it "should not validate without public and private keys" do
@key_material.valid?.should be_false
@key_material.generate_key(1024)
@key_material.generate_key(768)
@key_material.valid?.should be_true
pub = @key_material.public_key
@key_material.public_key = nil
Expand Down

0 comments on commit 99bcba0

Please sign in to comment.