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
|