diff options
| author | lutter <lutter@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-08-21 21:54:13 +0000 |
|---|---|---|
| committer | lutter <lutter@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-08-21 21:54:13 +0000 |
| commit | 7ade561e75853116baef15f3750e3563e6a6faaf (patch) | |
| tree | a64b6e3f1cb2038386f60fb2774bb5a191ebc24e | |
| parent | c6fc6c56cea381c7bdf15e8610a28a4c6924ecf5 (diff) | |
| download | puppet-7ade561e75853116baef15f3750e3563e6a6faaf.tar.gz puppet-7ade561e75853116baef15f3750e3563e6a6faaf.tar.xz puppet-7ade561e75853116baef15f3750e3563e6a6faaf.zip | |
Support for certificate revocation and checking connections on the server against the CRL
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@1475 980ebf18-57e1-0310-9a29-db15c13687c0
| -rw-r--r-- | lib/puppet/server.rb | 17 | ||||
| -rw-r--r-- | lib/puppet/sslcertificates/ca.rb | 107 | ||||
| -rwxr-xr-x | test/certmgr/certmgr.rb | 55 | ||||
| -rw-r--r-- | test/server/server.rb | 115 |
4 files changed, 236 insertions, 58 deletions
diff --git a/lib/puppet/server.rb b/lib/puppet/server.rb index 107bf83a2..4a949b712 100644 --- a/lib/puppet/server.rb +++ b/lib/puppet/server.rb @@ -44,6 +44,22 @@ module Puppet @authconfig end + + # Read the CA cert and CRL and populate an OpenSSL::X509::Store + # with them, with flags appropriate for checking client + # certificates for revocation + def x509store + unless File.exist?(Puppet[:cacrl]) + raise Puppet::Error, "Could not find CRL" + end + crl = OpenSSL::X509::CRL.new(File.read(Puppet[:cacrl])) + store = OpenSSL::X509::Store.new + store.purpose = OpenSSL::X509::PURPOSE_ANY + store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK + store.add_cert(@cacert) + store.add_crl(crl) + return store + end def initialize(hash = {}) Puppet.info "Starting server for Puppet version %s" % Puppet.version @@ -97,6 +113,7 @@ module Puppet end end + hash[:SSLCertificateStore] = x509store hash[:SSLCertificate] = @cert hash[:SSLPrivateKey] = @key hash[:SSLStartImmediately] = true diff --git a/lib/puppet/sslcertificates/ca.rb b/lib/puppet/sslcertificates/ca.rb index 72b073378..89b7b183c 100644 --- a/lib/puppet/sslcertificates/ca.rb +++ b/lib/puppet/sslcertificates/ca.rb @@ -1,6 +1,6 @@ class Puppet::SSLCertificates::CA Certificate = Puppet::SSLCertificates::Certificate - attr_accessor :keyfile, :file, :config, :dir, :cert + attr_accessor :keyfile, :file, :config, :dir, :cert, :crl Puppet.setdefaults(:ca, :cadir => { :default => "$ssldir/ca", @@ -26,6 +26,12 @@ class Puppet::SSLCertificates::CA :group => "$group", :desc => "The CA public key." }, + :cacrl => { :default => "$cadir/ca_crl.pem", + :owner => "$user", + :group => "$group", + :mode => 0664, + :desc => "The certificate revocation list (CRL) for the CA." + }, :caprivatedir => { :default => "$cadir/private", :owner => "$user", :group => "$group", @@ -130,6 +136,7 @@ class Puppet::SSLCertificates::CA end self.getcert + init_crl unless FileTest.exists?(@config[:serial]) Puppet.config.write(:serial) do |f| f << "%04X" % 1 @@ -223,7 +230,6 @@ class Puppet::SSLCertificates::CA Puppet.config.write(:cacert) do |f| f.puts @cert.to_pem end - @key = cert.key return cert end @@ -278,34 +284,12 @@ class Puppet::SSLCertificates::CA raise Puppet::Error, "CSR sign verification failed" end - # i should probably check key length... - - # read the ca cert in - cacert = OpenSSL::X509::Certificate.new( - File.read(@config[:cacert]) - ) - - cakey = nil - if @config[:password] - cakey = OpenSSL::PKey::RSA.new( - File.read(@config[:cakey]), @config[:password] - ) - else - cakey = OpenSSL::PKey::RSA.new( - File.read(@config[:cakey]) - ) - end - - unless cacert.check_private_key(cakey) - raise Puppet::Error, "CA Certificate is invalid" - end - serial = File.read(@config[:serial]).chomp.hex newcert = Puppet::SSLCertificates.mkcert( :type => :server, :name => csr.subject, :days => @config[:ca_days], - :issuer => cacert, + :issuer => @cert, :serial => serial, :publickey => csr.public_key ) @@ -315,11 +299,11 @@ class Puppet::SSLCertificates::CA f << "%04X" % (serial + 1) end - newcert.sign(cakey, OpenSSL::Digest::SHA1.new) + sign_with_key(newcert) self.storeclientcert(newcert) - return [newcert, cacert] + return [newcert, @cert] end # Store the client's CSR for later signing. This is called from @@ -338,6 +322,20 @@ class Puppet::SSLCertificates::CA end end + # Revoke the certificate with serial number SERIAL issued by this + # CA. The REASON must be one of the OpenSSL::OCSP::REVOKED_* reasons + def revoke(serial, reason = OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE) + time = Time.now + 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) + @crl.add_revoked(revoked) + store_crl + end + # Store the certificate that we generate. def storeclientcert(cert) host = thing2name(cert) @@ -352,6 +350,61 @@ class Puppet::SSLCertificates::CA f.print cert.to_pem end end + + private + def init_crl + if FileTest.exists?(@config[:cacrl]) + @crl = OpenSSL::X509::CRL.new( + File.read(@config[:cacrl]) + ) + else + # Create new CRL + @crl = OpenSSL::X509::CRL.new + @crl.issuer = @cert.subject + @crl.version = 1 + store_crl + @crl + end + end + + def store_crl + # Increment the crlNumber + e = @crl.extensions.find { |e| e.oid == 'crlNumber' } + ext = @crl.extensions.reject { |e| e.oid == 'crlNumber' } + crlNum = OpenSSL::ASN1::Integer(e ? e.value.to_i + 1 : 0) + ext << OpenSSL::X509::Extension.new("crlNumber", crlNum) + @crl.extensions = ext + + # Set last/next update + now = Time.now + @crl.last_update = now + # Keep CRL valid for 5 years + @crl.next_update = now + 5 * 365*24*60*60 + + sign_with_key(@crl) + Puppet.config.write(:cacrl) do |f| + f.puts @crl.to_pem + end + end + + def sign_with_key(signable, digest = OpenSSL::Digest::SHA1.new) + cakey = nil + if @config[:password] + cakey = OpenSSL::PKey::RSA.new( + File.read(@config[:cakey]), @config[:password] + ) + else + cakey = OpenSSL::PKey::RSA.new( + File.read(@config[:cakey]) + ) + end + + unless @cert.check_private_key(cakey) + raise Puppet::Error, "CA Certificate is invalid" + end + + signable.sign(cakey, digest) + end end # $Id$ diff --git a/test/certmgr/certmgr.rb b/test/certmgr/certmgr.rb index 8c88fe4d6..66376fcea 100755 --- a/test/certmgr/certmgr.rb +++ b/test/certmgr/certmgr.rb @@ -261,4 +261,59 @@ class TestCertMgr < Test::Unit::TestCase } assert_nil(cert) end + + def test_crl + ca = mkCA() + h1 = mkSignedCert(ca, "host1.example.com") + h2 = mkSignedCert(ca, "host2.example.com") + + assert(ca.cert.verify(ca.cert.public_key)) + assert(h1.verify(ca.cert.public_key)) + assert(h2.verify(ca.cert.public_key)) + + crl = ca.crl + assert_not_nil(crl) + + store = mkStore(ca) + assert( store.verify(ca.cert)) + assert( store.verify(h1, [ca.cert])) + assert( store.verify(h2, [ca.cert])) + + ca.revoke(h1.serial) + + # Recreate the CA from disk + ca = mkCA() + store = mkStore(ca) + assert( store.verify(ca.cert)) + assert(!store.verify(h1, [ca.cert])) + assert( store.verify(h2, [ca.cert])) + + ca.revoke(h2.serial) + assert_equal(1, ca.crl.extensions.size) + + File::open("/tmp/crl.pem", "w") { |f| f.write(ca.crl.to_pem) } + # Recreate the CA from disk + ca = mkCA() + store = mkStore(ca) + assert( store.verify(ca.cert)) + assert(!store.verify(h1, [ca.cert])) + assert(!store.verify(h2, [ca.cert])) + end + + def mkSignedCert(ca, host) + cert = mkcert(host) + assert_nothing_raised { + signedcert, cacert = ca.sign(cert.mkcsr) + return signedcert + } + end + + def mkStore(ca) + store = OpenSSL::X509::Store.new + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK + store.add_cert(ca.cert) + store.add_crl(ca.crl) + store + end end diff --git a/test/server/server.rb b/test/server/server.rb index ea74a0a1c..e279d8440 100644 --- a/test/server/server.rb +++ b/test/server/server.rb @@ -24,44 +24,16 @@ class TestServer < Test::Unit::TestCase # we have to use fork here, because we apparently can't use threads # to talk to other threads def test_connect_with_fork - server = nil Puppet[:autosign] = true - - # create a server just serving status - assert_nothing_raised() { - server = Puppet::Server.new( - :Port => @@port, - :Handlers => { - :CA => {}, # so that certs autogenerate - :Status => nil - } - ) - - } - - # and fork - serverpid = fork { - assert_nothing_raised() { - trap(:INT) { server.shutdown } - server.start - } - } - @@tmppids << serverpid + serverpid, server = mk_status_server # create a status client, and verify it can talk - client = nil - assert_nothing_raised() { - client = Puppet::Client::StatusClient.new( - :Server => "localhost", - :Port => @@port - ) - } - retval = nil + client = mk_status_client + retval = nil assert_nothing_raised() { retval = client.status } - assert_equal(1, retval) end @@ -145,4 +117,85 @@ class TestServer < Test::Unit::TestCase } assert(FileTest.exists?(server.pidfile), "PID file was not created") end + + + # Test that a client whose cert has been revoked really can't connect + def test_certificate_revocation + Puppet[:autosign] = true + + serverpid, server = mk_status_server + + client = mk_status_client + + status = nil + assert_nothing_raised() { + status = client.status + } + assert_equal(1, status) + client.shutdown + + # Revoke the client's cert + ca = Puppet::SSLCertificates::CA.new() + fqdn = client.fqdn + ca.revoke(ca.getclientcert(fqdn)[0].serial) + + # Restart the server + @@port += 1 + Puppet[:autosign] = false + kill_and_wait(serverpid, server.pidfile) + serverpid, server = mk_status_server + + client = mk_status_client + # This time the client should be denied + assert_raise(Puppet::NetworkClientError) { + client.status + } + end + + def mk_status_client + client = nil + # Otherwise, the client initalization will trip over itself + # since elements created in the last run are still around + Puppet::Type::allclear + + assert_nothing_raised() { + client = Puppet::Client::StatusClient.new( + :Server => "localhost", + :Port => @@port + ) + } + client + end + + def mk_status_server + server = nil + assert_nothing_raised() { + server = Puppet::Server.new( + :Port => @@port, + :Handlers => { + :CA => {}, # so that certs autogenerate + :Status => nil + } + ) + + } + pid = fork { + assert_nothing_raised() { + trap(:INT) { server.shutdown } + server.start + } + } + @@tmppids << pid + [pid, server] + end + + def kill_and_wait(pid, file) + %x{kill -INT #{pid} 2>/dev/null} + count = 0 + while count < 30 && File::exist?(file) + count += 1 + sleep(1) + end + assert(count < 30, "Killing server #{pid} failed") + end end |
