summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--certs/app/controllers/certs_controller.rb5
-rw-r--r--certs/app/models/client_certificate.rb105
-rw-r--r--certs/app/models/leap_ca/cert.rb56
-rw-r--r--certs/leap_web_certs.gemspec1
-rw-r--r--certs/test/files/ca.crt14
-rw-r--r--certs/test/files/ca.key18
-rw-r--r--certs/test/functional/certs_controller_test.rb2
-rw-r--r--certs/test/unit/cert_pool_test.rb52
-rw-r--r--certs/test/unit/cert_test.rb39
-rw-r--r--certs/test/unit/client_certificate_test.rb14
-rw-r--r--config/defaults.yml16
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..9cde820
--- /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[:client_ca_cert])
+ key = File.read(APP_CONFIG[:client_ca_key])
+ 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..cb8a627 100644
--- a/config/defaults.yml
+++ b/config/defaults.yml
@@ -1,11 +1,27 @@
+dev_ca: &dev_ca
+ client_ca_key: "./certs/test/files/ca.key"
+ client_ca_cert: "./certs/test/files/ca.crt"
+ ca_key_password: nil
+
+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