diff options
-rw-r--r-- | certs/app/controllers/certs_controller.rb | 5 | ||||
-rw-r--r-- | certs/app/models/client_certificate.rb | 105 | ||||
-rw-r--r-- | certs/app/models/leap_ca/cert.rb | 56 | ||||
-rw-r--r-- | certs/leap_web_certs.gemspec | 1 | ||||
-rw-r--r-- | certs/test/files/ca.crt | 14 | ||||
-rw-r--r-- | certs/test/files/ca.key | 18 | ||||
-rw-r--r-- | certs/test/functional/certs_controller_test.rb | 2 | ||||
-rw-r--r-- | certs/test/unit/cert_pool_test.rb | 52 | ||||
-rw-r--r-- | certs/test/unit/cert_test.rb | 39 | ||||
-rw-r--r-- | certs/test/unit/client_certificate_test.rb | 14 | ||||
-rw-r--r-- | config/defaults.yml | 16 |
11 files changed, 170 insertions, 152 deletions
diff --git a/certs/app/controllers/certs_controller.rb b/certs/app/controllers/certs_controller.rb index d81aea0..6db270c 100644 --- a/certs/app/controllers/certs_controller.rb +++ b/certs/app/controllers/certs_controller.rb @@ -4,11 +4,8 @@ class CertsController < ApplicationController # GET /cert def show - @cert = LeapCA::Cert.pick_from_pool + @cert = ClientCertificate.new render :text => @cert.key + @cert.cert, :content_type => 'text/plain' - rescue RECORD_NOT_FOUND - flash[:error] = t(:cert_pool_empty) - redirect_to root_path end end diff --git a/certs/app/models/client_certificate.rb b/certs/app/models/client_certificate.rb new file mode 100644 index 0000000..b2b8c0d --- /dev/null +++ b/certs/app/models/client_certificate.rb @@ -0,0 +1,105 @@ +# +# Model for certificates stored in CouchDB. +# +# This file must be loaded after Config has been loaded. +# +require 'base64' +require 'digest/md5' +require 'openssl' +require 'certificate_authority' +require 'date' + +class ClientCertificate + + attr_accessor :key # the client private RSA key + attr_accessor :cert # the client x509 certificate, signed by the CA + + # + # generate the private key and client certificate + # + def initialize + cert = CertificateAuthority::Certificate.new + + # set subject + cert.subject.common_name = random_common_name + + # set expiration + cert.not_before = yesterday + cert.not_after = months_from_yesterday(APP_CONFIG[:client_cert_lifespan]) + + # generate key + cert.serial_number.number = cert_serial_number + cert.key_material.generate_key(APP_CONFIG[:client_cert_bit_size]) + + # sign + cert.parent = ClientCertificate.root_ca + cert.sign! client_signing_profile + + self.key = cert.key_material.private_key.to_pem + self.cert = cert.to_pem + end + + private + + def self.root_ca + @root_ca ||= begin + crt = File.read(APP_CONFIG[:ca_cert_path]) + key = File.read(APP_CONFIG[:ca_key_path]) + openssl_cert = OpenSSL::X509::Certificate.new(crt) + cert = CertificateAuthority::Certificate.from_openssl(openssl_cert) + cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, APP_CONFIG[:ca_key_password]) + cert + end + end + + # + # For cert serial numbers, we need a non-colliding number less than 160 bits. + # md5 will do nicely, since there is no need for a secure hash, just a short one. + # (md5 is 128 bits) + # + def cert_serial_number + Digest::MD5.hexdigest("#{rand(10**10)} -- #{Time.now}").to_i(16) + end + + # + # for the random common name, we need a text string that will be unique across all certs. + # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid + # + def random_common_name + cert_serial_number.to_s(36) + end + + def client_signing_profile + { + "digest" => APP_CONFIG[:client_cert_hash], + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature"] + }, + "extendedKeyUsage" => { + "usage" => ["clientAuth"] + } + } + } + end + + ## + ## TIME HELPERS + ## + ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet + ## are behind UTC. + ## + + def yesterday + t = Time.now - 24*24*60 + Time.utc t.year, t.month, t.day + end + + def months_from_yesterday(num) + t = yesterday + date = Date.new t.year, t.month, t.day + date = date >> num # >> is months in the future operator + Time.utc date.year, date.month, date.day + end + +end diff --git a/certs/app/models/leap_ca/cert.rb b/certs/app/models/leap_ca/cert.rb deleted file mode 100644 index 9d4f15e..0000000 --- a/certs/app/models/leap_ca/cert.rb +++ /dev/null @@ -1,56 +0,0 @@ -# -# Model for certificates stored in CouchDB. -# -# This file must be loaded after Config has been loaded. -# - -module LeapCA - class Cert < CouchRest::Model::Base - -# No config yet. use_database LeapCA::Config.db_name - use_database 'client_certificates' - - timestamps! - - property :key, String # the client private RSA key - property :cert, String # the client x509 certificate, signed by the CA - property :valid_until, Time # expiration time of the client certificate - property :random, Float, :accessible => false # used to help pick a random cert by the webapp - - before_validation :set_random, :on => :create - - validates :key, :presence => true - validates :cert, :presence => true - validates :random, :presence => true - validates :random, :numericality => {:greater_than => 0, :less_than => 1} - - design do - view :by_random - end - - def set_random - self.random = rand - end - - class << self - def sample - self.by_random.startkey(rand).first || self.by_random.first - end - - def pick_from_pool - cert = self.sample - raise RECORD_NOT_FOUND unless cert - cert.destroy - return cert - rescue RESOURCE_NOT_FOUND - retry if self.by_random.count > 0 - raise RECORD_NOT_FOUND - end - - def valid_attributes_hash - {:key => "ABCD", :cert => "A123"} - end - end - - end -end diff --git a/certs/leap_web_certs.gemspec b/certs/leap_web_certs.gemspec index 15a45be..531afda 100644 --- a/certs/leap_web_certs.gemspec +++ b/certs/leap_web_certs.gemspec @@ -16,5 +16,6 @@ Gem::Specification.new do |s| s.test_files = Dir["test/**/*"] s.add_dependency "leap_web_core", LeapWeb::VERSION + s.add_dependency "certificate_authority" end diff --git a/certs/test/files/ca.crt b/certs/test/files/ca.crt new file mode 100644 index 0000000..cade598 --- /dev/null +++ b/certs/test/files/ca.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICPDCCAYmgAwIBAgIEUKCI4DANBgkqhkiG9w0BAQsFADAkMSIwIAYDVQQDExlS +b290IENBIGZvciBydW5uaW5nIHRlc3RzMB4XDTEyMTExMjA1MjgwMFoXDTEzMTEx +MjA1MjgwMFowJDEiMCAGA1UEAxMZUm9vdCBDQSBmb3IgcnVubmluZyB0ZXN0czCB +uzANBgkqhkiG9w0BAQEFAAOBqQAwgaUCgZ0ApeqCGQOmiHxCFxsfUKmBV6ruOYar +EsepFAycTmmakXBjNj4B9Pd3gE3Cc56rvkq0uxluRvqspzpEOQpCg8M5fkft/fxS +acw+ackj3ys7r0MrXgL66QeLnNGe8+RjBO8UHb3OPx547hqUHVg+3HqSCdn9cGQX +9//EJrnSJsLuZw9ktkN4Ytyd1deZo6AkiIeCyz0HxKQBIhdJAPRlAgMBAAGjQzBB +MA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcEADAdBgNVHQ4EFgQUBe1l +BbuGErEkHLffGvkY5dDOH1YwDQYJKoZIhvcNAQELBQADgZ0ADpudncToYPS183w8 +c68dObCCvNfv/FTBg4ihCLW6PapADYuvXmCvXgHflylET+rFdcrnUfl+XjNT5IjF +ImUyyOnCiy7scRgY+9qrEb7neH4CopGZKkWBTadZLu0QZqMcsWyAZBzaI8tBwL+G ++ylSgw3xTSf/HFjmTJAlDzUieV4DufrPqz7Yx0GrTswdJOcccc/PWUvQIU1GXvto +-----END CERTIFICATE----- diff --git a/certs/test/files/ca.key b/certs/test/files/ca.key new file mode 100644 index 0000000..d266ef7 --- /dev/null +++ b/certs/test/files/ca.key @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIC2gIBAAKBnQCl6oIZA6aIfEIXGx9QqYFXqu45hqsSx6kUDJxOaZqRcGM2PgH0 +93eATcJznqu+SrS7GW5G+qynOkQ5CkKDwzl+R+39/FJpzD5pySPfKzuvQyteAvrp +B4uc0Z7z5GME7xQdvc4/HnjuGpQdWD7cepIJ2f1wZBf3/8QmudImwu5nD2S2Q3hi +3J3V15mjoCSIh4LLPQfEpAEiF0kA9GUCAwEAAQKBnAKz9FSgqO42Sq6tBBtAolkh +nBSXK2L4mmTiOQr/UMOnzLtN0qMBWRK1Bu2dRcz+0zztEs0t45wsfdS0DxYDGy+s +elBrSOhs/w34IeZ5LM6xY0u4HZDmhn0pQNo6QZcFICr0GkkYdmWDlkLvIeJ/u6+q +nmyqAQXvj3R4nA7hrKUXzJjfvN3RYrhLN+/T41zLybeJ5vLZQK3jJSiIjQJPAMhS +HTIbYTUi2pxYVSwJDY4S2klTdroNGvTCkqcTRcB4Ms70FGLPZ6+ZumrkbSohHUsj +gDRRy3e4fjA9qMSQynVr2gkUobsR0tAdQGVOKwJPANQIUPaTc2ouNYNLAiHoAXoL +qAcF5g7/vtlMOwr+16EYoG7bLbiEie7nBfg9zz/VUnvOEy6pZ89YvsZOMlGicsRs ++tfUM1g/u0ZFEoQPrwJOC6bbE+ML0G9qj9WDfsA4DZ+DGujD6yZ//uSiax1v3TYg +nnEMDoNJ4KjscvM+dkjez1QNTP3E+/27OUsc2fIiFJplYEnW7m6m+Hv7FulpAk8A +tiASk0oiV/ErLARw53jmU9PRV378lqOcZgAxswclZo3FuJLxmc3WwOuV2B4Xd+gf +epKPLYR708GR1Lp0RGS6GfjWGi9+ju3nSbuo5OCnAk5yun/UvDdtnZ6fXo9aF22/ +yoiztru7yhJdVrMx3PbbndfN2y9ctqcd6CD5fIQdyZ4K8eTr686RjH8C0XP095Ib +an3AO/TQG1c4yE2hSvQ= +-----END RSA PRIVATE KEY----- diff --git a/certs/test/functional/certs_controller_test.rb b/certs/test/functional/certs_controller_test.rb index 3d6946e..75256ca 100644 --- a/certs/test/functional/certs_controller_test.rb +++ b/certs/test/functional/certs_controller_test.rb @@ -13,7 +13,7 @@ class CertsControllerTest < ActionController::TestCase test "should send cert" do login cert = stub :cert => "adsf", :key => "key" - LeapCA::Cert.expects(:pick_from_pool).returns(cert) + ClientCertificate.expects(:new).returns(cert) get :show assert_response :success assert_equal cert.key + cert.cert, @response.body diff --git a/certs/test/unit/cert_pool_test.rb b/certs/test/unit/cert_pool_test.rb deleted file mode 100644 index 06f7ce0..0000000 --- a/certs/test/unit/cert_pool_test.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'test_helper' - -class CertPoolTest < ActiveSupport::TestCase - - setup do - 2.times { LeapCA::Cert.create(LeapCA::Cert.valid_attributes_hash) } - end - - teardown do - LeapCA::Cert.all.each {|c| c.destroy} - end - - test "picks random sample" do - # with 3 certs chances are pretty low we pick the same one 40 times. - LeapCA::Cert.create! LeapCA::Cert.valid_attributes_hash - picked = [] - first = LeapCA::Cert.sample.id - current = LeapCA::Cert.sample.id - 40.times do - break if current != first - current = LeapCA::Cert.sample.id - end - assert_not_equal current, first - end - - test "picks cert from the pool" do - assert_difference "LeapCA::Cert.count", -1 do - cert = LeapCA::Cert.pick_from_pool - end - end - - test "err's out if all certs have been destroyed" do - sample = LeapCA::Cert.first.tap{|c| c.destroy} - LeapCA::Cert.all.each {|c| c.destroy} - assert_raises RECORD_NOT_FOUND do - LeapCA::Cert.expects(:sample).returns(sample) - cert = LeapCA::Cert.pick_from_pool - end - end - - test "picks other cert if first pick has been destroyed" do - first = LeapCA::Cert.first.tap{|c| c.destroy} - second = LeapCA::Cert.first - LeapCA::Cert.expects(:sample).at_least_once. - returns(first). - then.returns(second) - cert = LeapCA::Cert.pick_from_pool - assert_equal second, cert - assert_nil LeapCA::Cert.first - end - -end diff --git a/certs/test/unit/cert_test.rb b/certs/test/unit/cert_test.rb deleted file mode 100644 index 0b21d0b..0000000 --- a/certs/test/unit/cert_test.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'test_helper' - -class CertTest < ActiveSupport::TestCase - - setup do - @sample = LeapCA::Cert.new LeapCA::Cert.valid_attributes_hash - end - - test "stub cert for testing is valid" do - assert @sample.valid? - end - - test "setting random on create validation" do - @sample.random = "asdf" - assert @sample.valid? - assert @sample.random.is_a? Float - assert @sample.random >= 0 - assert @sample.random < 1 - end - - test "validates random" do - @sample.save # make sure we are past the on_create - assert @sample.valid? - ["asdf", 1, 2, -0.1, nil, "asdf"].each do |invalid| - @sample.random = invalid - assert !@sample.valid?, "#{invalid} should not be a valid value for random" - end - end - - test "validates key" do - @sample.key = nil - assert !@sample.valid?, "Cert should require key" - end - - test "validates cert" do - @sample.cert = nil - assert !@sample.valid?, "Cert should require cert" - end -end diff --git a/certs/test/unit/client_certificate_test.rb b/certs/test/unit/client_certificate_test.rb new file mode 100644 index 0000000..492a44a --- /dev/null +++ b/certs/test/unit/client_certificate_test.rb @@ -0,0 +1,14 @@ +require 'test_helper' + +class ClientCertificateTest < ActiveSupport::TestCase + + setup do + @sample = ClientCertificate.new + end + + test "new cert has all we need" do + assert @sample.key + assert @sample.cert + end + +end diff --git a/config/defaults.yml b/config/defaults.yml index 4ffa2c9..f5a7c07 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -1,11 +1,27 @@ +dev_ca: &dev_ca + ca_key_path: "./certs/test/files/ca.key" + ca_key_password: nil + ca_cert_path: "./certs/test/files/ca.crt" + +cert_options: &cert_options + client_cert_lifespan: 2 + client_cert_bit_size: 2024 + client_cert_hash: "SHA256" + development: + <<: *dev_ca + <<: *cert_options admins: [admin, admin2] domain: develop.me test: + <<: *dev_ca + <<: *cert_options admins: [admin, admin2] domain: test.me + production: + <<: *cert_options admins: [] domain: deploy.me |