summaryrefslogtreecommitdiff
path: root/app/models/client_certificate.rb
blob: 6b579858194b8c76098b20df84c02f20000c5bcc (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
126
127
128
129
130
#
# Model for certificates
#
# 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(options = {})
    cert = CertificateAuthority::Certificate.new

    # set subject
    cert.subject.common_name = common_name(options[:prefix])

    # set expiration
    cert.not_before = last_month
    cert.not_after = expiry

    # 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
    self.cert = cert
  end

  def to_s
    self.key.to_pem + self.cert.to_pem
  end

  def fingerprint
    OpenSSL::Digest::SHA1.hexdigest(openssl_cert.to_der).scan(/../).join(':')
  end

  def expiry
    @expiry ||= months_from_yesterday(APP_CONFIG[:client_cert_lifespan])
  end

  private

  def openssl_cert
    cert.openssl_body
  end

  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

  def common_name(prefix = nil)
    [prefix, random_common_name].join
  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*60*60
    Time.utc t.year, t.month, t.day
  end

  def last_month
    t = Time.now - 24*60*60*30
    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