diff options
| -rw-r--r-- | certs/app/models/client_certificate.rb | 148 | ||||
| -rw-r--r-- | certs/app/models/leap_ca/cert.rb | 150 | ||||
| -rw-r--r-- | certs/test/unit/client_certificate_test.rb (renamed from certs/test/unit/cert_test.rb) | 4 | 
3 files changed, 150 insertions, 152 deletions
| diff --git a/certs/app/models/client_certificate.rb b/certs/app/models/client_certificate.rb new file mode 100644 index 0000000..23b66a2 --- /dev/null +++ b/certs/app/models/client_certificate.rb @@ -0,0 +1,148 @@ +# +# 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 < 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 :generate, :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 + +  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 + +  # +  # generate the private key and client certificate +  # +  def generate +    cert = CertificateAuthority::Certificate.new + +    # set subject +    cert.subject.common_name = random_common_name + +    # set expiration +    self.valid_until = months_from_yesterday(Config.client_cert_lifespan) +    cert.not_before = yesterday +    cert.not_after = self.valid_until + +    # generate key +    cert.serial_number.number = cert_serial_number +    cert.key_material.generate_key(Config.client_cert_bit_size) + +    # sign +    cert.parent = Cert.root_ca +    cert.sign! client_signing_profile + +    self.key = cert.key_material.private_key.to_pem +    self.cert = cert.to_pem +  end + +  private + +  def set_random +    self.random = rand +  end + +  def self.root_ca +    @root_ca ||= begin +                   crt = File.read(Config.ca_cert_path) +                   key = File.read(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, 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" => 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 6c59144..0000000 --- a/certs/app/models/leap_ca/cert.rb +++ /dev/null @@ -1,150 +0,0 @@ -# -# 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' - -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 :generate, :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 - -    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 - -    # -    # generate the private key and client certificate -    # -    def generate -      cert = CertificateAuthority::Certificate.new - -      # set subject -      cert.subject.common_name = random_common_name - -      # set expiration -      self.valid_until = months_from_yesterday(Config.client_cert_lifespan) -      cert.not_before = yesterday -      cert.not_after = self.valid_until - -      # generate key -      cert.serial_number.number = cert_serial_number -      cert.key_material.generate_key(Config.client_cert_bit_size) - -      # sign -      cert.parent = Cert.root_ca -      cert.sign! client_signing_profile - -      self.key = cert.key_material.private_key.to_pem -      self.cert = cert.to_pem -    end - -    private - -    def set_random -      self.random = rand -    end - -    def self.root_ca -      @root_ca ||= begin -        crt = File.read(Config.ca_cert_path) -        key = File.read(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, 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" => 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 -end diff --git a/certs/test/unit/cert_test.rb b/certs/test/unit/client_certificate_test.rb index 0b21d0b..a721483 100644 --- a/certs/test/unit/cert_test.rb +++ b/certs/test/unit/client_certificate_test.rb @@ -1,9 +1,9 @@  require 'test_helper' -class CertTest < ActiveSupport::TestCase +class ClientCertificateTest < ActiveSupport::TestCase    setup do -    @sample = LeapCA::Cert.new LeapCA::Cert.valid_attributes_hash +    @sample = ClientCertificate.new ClientCertificate.valid_attributes_hash    end    test "stub cert for testing is valid" do | 
