summaryrefslogtreecommitdiff
path: root/certs/app/models/client_certificate.rb
blob: 6abc1eeea07fe931e4f4a3f593a459f910f91103 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#
# 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

  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

  before_validation :generate, :on => :create

  validates :key, :presence => true
  validates :cert, :presence => true

  design do
  end

  class << self
    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(APP_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(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