certificate_authority 0.1.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/.travis.yml +11 -0
- data/Gemfile +2 -8
- data/Gemfile.lock +71 -31
- data/README.rdoc +91 -2
- data/Rakefile +6 -41
- data/certificate_authority.gemspec +22 -66
- data/lib/certificate_authority.rb +6 -5
- data/lib/certificate_authority/certificate.rb +91 -36
- data/lib/certificate_authority/certificate_revocation_list.rb +34 -14
- data/lib/certificate_authority/core_extensions.rb +46 -0
- data/lib/certificate_authority/distinguished_name.rb +64 -16
- data/lib/certificate_authority/extensions.rb +417 -45
- data/lib/certificate_authority/key_material.rb +30 -9
- data/lib/certificate_authority/ocsp_handler.rb +75 -5
- data/lib/certificate_authority/pkcs11_key_material.rb +0 -2
- data/lib/certificate_authority/revocable.rb +14 -0
- data/lib/certificate_authority/serial_number.rb +15 -2
- data/lib/certificate_authority/signing_request.rb +91 -0
- data/lib/certificate_authority/validations.rb +31 -0
- data/lib/certificate_authority/version.rb +3 -0
- metadata +76 -48
- data/VERSION.yml +0 -5
- data/spec/spec_helper.rb +0 -4
- data/spec/units/certificate_authority_spec.rb +0 -4
- data/spec/units/certificate_revocation_list_spec.rb +0 -68
- data/spec/units/certificate_spec.rb +0 -428
- data/spec/units/distinguished_name_spec.rb +0 -59
- data/spec/units/extensions_spec.rb +0 -115
- data/spec/units/key_material_spec.rb +0 -100
- data/spec/units/ocsp_handler_spec.rb +0 -104
- data/spec/units/pkcs11_key_material_spec.rb +0 -41
- data/spec/units/serial_number_spec.rb +0 -20
- data/spec/units/signing_entity_spec.rb +0 -4
- data/spec/units/units_helper.rb +0 -1
@@ -1,14 +1,13 @@
|
|
1
1
|
module CertificateAuthority
|
2
2
|
class Certificate
|
3
|
-
|
4
|
-
include
|
3
|
+
include Validations
|
4
|
+
include Revocable
|
5
5
|
|
6
6
|
attr_accessor :distinguished_name
|
7
7
|
attr_accessor :serial_number
|
8
8
|
attr_accessor :key_material
|
9
9
|
attr_accessor :not_before
|
10
10
|
attr_accessor :not_after
|
11
|
-
attr_accessor :revoked_at
|
12
11
|
attr_accessor :extensions
|
13
12
|
attr_accessor :openssl_body
|
14
13
|
|
@@ -16,7 +15,7 @@ module CertificateAuthority
|
|
16
15
|
|
17
16
|
attr_accessor :parent
|
18
17
|
|
19
|
-
validate
|
18
|
+
def validate
|
20
19
|
errors.add :base, "Distinguished name must be valid" unless distinguished_name.valid?
|
21
20
|
errors.add :base, "Key material must be valid" unless key_material.valid?
|
22
21
|
errors.add :base, "Serial number must be valid" unless serial_number.valid?
|
@@ -33,8 +32,8 @@ module CertificateAuthority
|
|
33
32
|
self.distinguished_name = DistinguishedName.new
|
34
33
|
self.serial_number = SerialNumber.new
|
35
34
|
self.key_material = MemoryKeyMaterial.new
|
36
|
-
self.not_before =
|
37
|
-
self.not_after =
|
35
|
+
self.not_before = Date.today.utc
|
36
|
+
self.not_after = Date.today.advance(:years => 1).utc
|
38
37
|
self.parent = self
|
39
38
|
self.extensions = load_extensions()
|
40
39
|
|
@@ -42,12 +41,31 @@ module CertificateAuthority
|
|
42
41
|
|
43
42
|
end
|
44
43
|
|
44
|
+
=begin
|
45
|
+
def self.from_openssl openssl_cert
|
46
|
+
unless openssl_cert.is_a? OpenSSL::X509::Certificate
|
47
|
+
raise "Can only construct from an OpenSSL::X509::Certificate"
|
48
|
+
end
|
49
|
+
|
50
|
+
certificate = Certificate.new
|
51
|
+
# Only subject, key_material, and body are used for signing
|
52
|
+
certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject
|
53
|
+
certificate.key_material.public_key = openssl_cert.public_key
|
54
|
+
certificate.openssl_body = openssl_cert
|
55
|
+
certificate.serial_number.number = openssl_cert.serial.to_i
|
56
|
+
certificate.not_before = openssl_cert.not_before
|
57
|
+
certificate.not_after = openssl_cert.not_after
|
58
|
+
# TODO extensions
|
59
|
+
certificate
|
60
|
+
end
|
61
|
+
=end
|
62
|
+
|
45
63
|
def sign!(signing_profile={})
|
46
64
|
raise "Invalid certificate #{self.errors.full_messages}" unless valid?
|
47
65
|
merge_profile_with_extensions(signing_profile)
|
48
66
|
|
49
67
|
openssl_cert = OpenSSL::X509::Certificate.new
|
50
|
-
openssl_cert.version
|
68
|
+
openssl_cert.version = 2
|
51
69
|
openssl_cert.not_before = self.not_before
|
52
70
|
openssl_cert.not_after = self.not_after
|
53
71
|
openssl_cert.public_key = self.key_material.public_key
|
@@ -59,7 +77,6 @@ module CertificateAuthority
|
|
59
77
|
|
60
78
|
require 'tempfile'
|
61
79
|
t = Tempfile.new("bullshit_conf")
|
62
|
-
# t = File.new("/tmp/openssl.cnf")
|
63
80
|
## The config requires a file even though we won't use it
|
64
81
|
openssl_config = OpenSSL::Config.new(t.path)
|
65
82
|
|
@@ -86,18 +103,19 @@ module CertificateAuthority
|
|
86
103
|
self.extensions.keys.sort{|a,b| b<=>a}.each do |k|
|
87
104
|
e = extensions[k]
|
88
105
|
next if e.to_s.nil? or e.to_s == "" ## If the extension returns an empty string we won't include it
|
89
|
-
ext = factory.create_ext(e.openssl_identifier, e.to_s)
|
106
|
+
ext = factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
|
90
107
|
openssl_cert.add_extension(ext)
|
91
108
|
end
|
92
109
|
|
93
110
|
if signing_profile["digest"].nil?
|
94
|
-
digest = OpenSSL::Digest
|
111
|
+
digest = OpenSSL::Digest.new("SHA512")
|
95
112
|
else
|
96
|
-
digest = OpenSSL::Digest
|
113
|
+
digest = OpenSSL::Digest.new(signing_profile["digest"])
|
97
114
|
end
|
98
|
-
|
99
|
-
|
100
|
-
|
115
|
+
|
116
|
+
self.openssl_body = openssl_cert.sign(parent.key_material.private_key, digest)
|
117
|
+
ensure
|
118
|
+
t.close! if t # We can get rid of the ridiculous temp file
|
101
119
|
end
|
102
120
|
|
103
121
|
def is_signing_entity?
|
@@ -117,6 +135,34 @@ module CertificateAuthority
|
|
117
135
|
self.openssl_body.to_pem
|
118
136
|
end
|
119
137
|
|
138
|
+
def to_csr
|
139
|
+
csr = SigningRequest.new
|
140
|
+
csr.distinguished_name = self.distinguished_name
|
141
|
+
csr.key_material = self.key_material
|
142
|
+
factory = OpenSSL::X509::ExtensionFactory.new
|
143
|
+
exts = []
|
144
|
+
self.extensions.keys.each do |k|
|
145
|
+
## Don't copy over key identifiers for CSRs
|
146
|
+
next if k == "subjectKeyIdentifier" || k == "authorityKeyIdentifier"
|
147
|
+
e = extensions[k]
|
148
|
+
## If the extension returns an empty string we won't include it
|
149
|
+
next if e.to_s.nil? or e.to_s == ""
|
150
|
+
exts << factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
|
151
|
+
end
|
152
|
+
attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)])
|
153
|
+
attrs = [
|
154
|
+
OpenSSL::X509::Attribute.new("extReq", attrval),
|
155
|
+
OpenSSL::X509::Attribute.new("msExtReq", attrval)
|
156
|
+
]
|
157
|
+
csr.attributes = attrs
|
158
|
+
csr
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.from_x509_cert(raw_cert)
|
162
|
+
openssl_cert = OpenSSL::X509::Certificate.new(raw_cert)
|
163
|
+
Certificate.from_openssl(openssl_cert)
|
164
|
+
end
|
165
|
+
|
120
166
|
def is_root_entity?
|
121
167
|
self.parent == self && is_signing_entity?
|
122
168
|
end
|
@@ -135,6 +181,16 @@ module CertificateAuthority
|
|
135
181
|
items = signing_config[k]
|
136
182
|
items.keys.each do |profile_item_key|
|
137
183
|
if extension.respond_to?("#{profile_item_key}=".to_sym)
|
184
|
+
if k == 'subjectAltName' && profile_item_key == 'emails'
|
185
|
+
items[profile_item_key].map do |email|
|
186
|
+
if email == 'email:copy'
|
187
|
+
fail "no email address provided for subject: #{subject.to_x509_name}" unless subject.email_address
|
188
|
+
"email:#{subject.email_address}"
|
189
|
+
else
|
190
|
+
email
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
138
194
|
extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] )
|
139
195
|
else
|
140
196
|
p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!"
|
@@ -143,30 +199,25 @@ module CertificateAuthority
|
|
143
199
|
end
|
144
200
|
end
|
145
201
|
|
202
|
+
# Enumeration of the extensions. Not the worst option since
|
203
|
+
# the likelihood of these needing to be updated is low at best.
|
204
|
+
EXTENSIONS = [
|
205
|
+
CertificateAuthority::Extensions::BasicConstraints,
|
206
|
+
CertificateAuthority::Extensions::CrlDistributionPoints,
|
207
|
+
CertificateAuthority::Extensions::SubjectKeyIdentifier,
|
208
|
+
CertificateAuthority::Extensions::AuthorityKeyIdentifier,
|
209
|
+
CertificateAuthority::Extensions::AuthorityInfoAccess,
|
210
|
+
CertificateAuthority::Extensions::KeyUsage,
|
211
|
+
CertificateAuthority::Extensions::ExtendedKeyUsage,
|
212
|
+
CertificateAuthority::Extensions::SubjectAlternativeName,
|
213
|
+
CertificateAuthority::Extensions::CertificatePolicies
|
214
|
+
]
|
215
|
+
|
146
216
|
def load_extensions
|
147
217
|
extension_hash = {}
|
148
218
|
|
149
|
-
|
150
|
-
|
151
|
-
temp_extensions << basic_constraints
|
152
|
-
crl_distribution_points = CertificateAuthority::Extensions::CrlDistributionPoints.new
|
153
|
-
temp_extensions << crl_distribution_points
|
154
|
-
subject_key_identifier = CertificateAuthority::Extensions::SubjectKeyIdentifier.new
|
155
|
-
temp_extensions << subject_key_identifier
|
156
|
-
authority_key_identifier = CertificateAuthority::Extensions::AuthorityKeyIdentifier.new
|
157
|
-
temp_extensions << authority_key_identifier
|
158
|
-
authority_info_access = CertificateAuthority::Extensions::AuthorityInfoAccess.new
|
159
|
-
temp_extensions << authority_info_access
|
160
|
-
key_usage = CertificateAuthority::Extensions::KeyUsage.new
|
161
|
-
temp_extensions << key_usage
|
162
|
-
extended_key_usage = CertificateAuthority::Extensions::ExtendedKeyUsage.new
|
163
|
-
temp_extensions << extended_key_usage
|
164
|
-
subject_alternative_name = CertificateAuthority::Extensions::SubjectAlternativeName.new
|
165
|
-
temp_extensions << subject_alternative_name
|
166
|
-
certificate_policies = CertificateAuthority::Extensions::CertificatePolicies.new
|
167
|
-
temp_extensions << certificate_policies
|
168
|
-
|
169
|
-
temp_extensions.each do |extension|
|
219
|
+
EXTENSIONS.each do |klass|
|
220
|
+
extension = klass.new
|
170
221
|
extension_hash[extension.openssl_identifier] = extension
|
171
222
|
end
|
172
223
|
|
@@ -193,7 +244,11 @@ module CertificateAuthority
|
|
193
244
|
certificate.serial_number.number = openssl_cert.serial.to_i
|
194
245
|
certificate.not_before = openssl_cert.not_before
|
195
246
|
certificate.not_after = openssl_cert.not_after
|
196
|
-
|
247
|
+
EXTENSIONS.each do |klass|
|
248
|
+
_,v,c = (openssl_cert.extensions.detect { |e| e.to_a.first == klass::OPENSSL_IDENTIFIER } || []).to_a
|
249
|
+
certificate.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v
|
250
|
+
end
|
251
|
+
|
197
252
|
certificate
|
198
253
|
end
|
199
254
|
|
@@ -1,36 +1,52 @@
|
|
1
1
|
module CertificateAuthority
|
2
2
|
class CertificateRevocationList
|
3
|
-
include
|
3
|
+
include Validations
|
4
4
|
|
5
5
|
attr_accessor :certificates
|
6
6
|
attr_accessor :parent
|
7
7
|
attr_accessor :crl_body
|
8
8
|
attr_accessor :next_update
|
9
|
+
attr_accessor :last_update_skew_seconds
|
9
10
|
|
10
|
-
validate
|
11
|
-
errors.add :next_update, "Next update must be a positive value" if
|
12
|
-
errors.add :parent, "A parent entity must be set" if
|
11
|
+
def validate
|
12
|
+
errors.add :next_update, "Next update must be a positive value" if self.next_update < 0
|
13
|
+
errors.add :parent, "A parent entity must be set" if self.parent.nil?
|
13
14
|
end
|
14
15
|
|
15
16
|
def initialize
|
16
17
|
self.certificates = []
|
17
18
|
self.next_update = 60 * 60 * 4 # 4 hour default
|
19
|
+
self.last_update_skew_seconds = 0
|
18
20
|
end
|
19
21
|
|
20
|
-
def <<(
|
21
|
-
|
22
|
-
|
22
|
+
def <<(revocable)
|
23
|
+
case revocable
|
24
|
+
when Revocable
|
25
|
+
raise "Only revoked entities can be added to a CRL" unless revocable.revoked?
|
26
|
+
self.certificates << revocable
|
27
|
+
when OpenSSL::X509::Certificate
|
28
|
+
raise "Not implemented yet"
|
29
|
+
else
|
30
|
+
raise "#{revocable.class} cannot be included in a CRL"
|
31
|
+
end
|
23
32
|
end
|
24
33
|
|
25
|
-
def sign!
|
34
|
+
def sign!(signing_profile={})
|
26
35
|
raise "No parent entity has been set!" if self.parent.nil?
|
27
36
|
raise "Invalid CRL" unless self.valid?
|
28
37
|
|
29
|
-
revocations = self.certificates.collect do |
|
38
|
+
revocations = self.certificates.collect do |revocable|
|
30
39
|
revocation = OpenSSL::X509::Revoked.new
|
31
|
-
|
32
|
-
|
33
|
-
|
40
|
+
|
41
|
+
## We really just need a serial number, now we have to dig it out
|
42
|
+
case revocable
|
43
|
+
when Certificate
|
44
|
+
x509_cert = OpenSSL::X509::Certificate.new(revocable.to_pem)
|
45
|
+
revocation.serial = x509_cert.serial
|
46
|
+
when SerialNumber
|
47
|
+
revocation.serial = revocable.number
|
48
|
+
end
|
49
|
+
revocation.time = revocable.revoked_at
|
34
50
|
revocation
|
35
51
|
end
|
36
52
|
|
@@ -40,11 +56,15 @@ module CertificateAuthority
|
|
40
56
|
end
|
41
57
|
|
42
58
|
crl.version = 1
|
43
|
-
crl.last_update = Time.now
|
59
|
+
crl.last_update = Time.now - self.last_update_skew_seconds
|
44
60
|
crl.next_update = Time.now + self.next_update
|
45
61
|
|
46
62
|
signing_cert = OpenSSL::X509::Certificate.new(self.parent.to_pem)
|
47
|
-
|
63
|
+
if signing_profile["digest"].nil?
|
64
|
+
digest = OpenSSL::Digest.new("SHA512")
|
65
|
+
else
|
66
|
+
digest = OpenSSL::Digest.new(signing_profile["digest"])
|
67
|
+
end
|
48
68
|
crl.issuer = signing_cert.subject
|
49
69
|
self.crl_body = crl.sign(self.parent.key_material.private_key, digest)
|
50
70
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#
|
2
|
+
# ActiveSupport has these modifications. Now that we don't use ActiveSupport,
|
3
|
+
# these are added here as a kindness.
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'date'
|
7
|
+
|
8
|
+
unless nil.respond_to?(:blank?)
|
9
|
+
class NilClass
|
10
|
+
def blank?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
unless String.respond_to?(:blank?)
|
17
|
+
class String
|
18
|
+
def blank?
|
19
|
+
self.empty?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Date
|
25
|
+
|
26
|
+
def today
|
27
|
+
t = Time.now.utc
|
28
|
+
Date.new(t.year, t.month, t.day)
|
29
|
+
end
|
30
|
+
|
31
|
+
def utc
|
32
|
+
self.to_datetime.to_time.utc
|
33
|
+
end
|
34
|
+
|
35
|
+
unless Date.respond_to?(:advance)
|
36
|
+
def advance(options)
|
37
|
+
options = options.dup
|
38
|
+
d = self
|
39
|
+
d = d >> options.delete(:years) * 12 if options[:years]
|
40
|
+
d = d >> options.delete(:months) if options[:months]
|
41
|
+
d = d + options.delete(:weeks) * 7 if options[:weeks]
|
42
|
+
d = d + options.delete(:days) if options[:days]
|
43
|
+
d
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,58 +1,106 @@
|
|
1
1
|
module CertificateAuthority
|
2
2
|
class DistinguishedName
|
3
|
-
include
|
3
|
+
include Validations
|
4
4
|
|
5
|
-
|
5
|
+
def validate
|
6
|
+
if self.common_name.nil? || self.common_name.empty?
|
7
|
+
errors.add :common_name, 'cannot be blank'
|
8
|
+
end
|
9
|
+
end
|
6
10
|
|
7
11
|
attr_accessor :common_name
|
8
12
|
alias :cn :common_name
|
13
|
+
alias :cn= :common_name=
|
9
14
|
|
10
15
|
attr_accessor :locality
|
11
16
|
alias :l :locality
|
17
|
+
alias :l= :locality=
|
12
18
|
|
13
19
|
attr_accessor :state
|
14
20
|
alias :s :state
|
21
|
+
alias :st= :state=
|
15
22
|
|
16
23
|
attr_accessor :country
|
17
24
|
alias :c :country
|
25
|
+
alias :c= :country=
|
18
26
|
|
19
27
|
attr_accessor :organization
|
20
28
|
alias :o :organization
|
29
|
+
alias :o= :organization=
|
21
30
|
|
22
31
|
attr_accessor :organizational_unit
|
23
32
|
alias :ou :organizational_unit
|
33
|
+
alias :ou= :organizational_unit=
|
34
|
+
|
35
|
+
attr_accessor :email_address
|
36
|
+
alias :emailAddress :email_address
|
37
|
+
alias :emailAddress= :email_address=
|
38
|
+
|
39
|
+
attr_accessor :serial_number
|
40
|
+
alias :serialNumber :serial_number
|
41
|
+
alias :serialNumber= :serial_number=
|
24
42
|
|
25
43
|
def to_x509_name
|
26
44
|
raise "Invalid Distinguished Name" unless valid?
|
27
45
|
|
28
46
|
# NB: the capitalization in the strings counts
|
29
47
|
name = OpenSSL::X509::Name.new
|
30
|
-
name.add_entry("
|
31
|
-
name.add_entry("
|
32
|
-
name.add_entry("OU", organizational_unit) unless organizational_unit.blank?
|
48
|
+
name.add_entry("serialNumber", serial_number) unless serial_number.blank?
|
49
|
+
name.add_entry("C", country) unless country.blank?
|
33
50
|
name.add_entry("ST", state) unless state.blank?
|
34
51
|
name.add_entry("L", locality) unless locality.blank?
|
35
|
-
name.add_entry("
|
52
|
+
name.add_entry("O", organization) unless organization.blank?
|
53
|
+
name.add_entry("OU", organizational_unit) unless organizational_unit.blank?
|
54
|
+
name.add_entry("CN", common_name)
|
55
|
+
name.add_entry("emailAddress", email_address) unless email_address.blank?
|
36
56
|
name
|
37
57
|
end
|
38
58
|
|
59
|
+
def ==(other)
|
60
|
+
# Use the established OpenSSL comparison
|
61
|
+
self.to_x509_name() == other.to_x509_name()
|
62
|
+
end
|
63
|
+
|
39
64
|
def self.from_openssl openssl_name
|
40
65
|
unless openssl_name.is_a? OpenSSL::X509::Name
|
41
66
|
raise "Argument must be a OpenSSL::X509::Name"
|
42
67
|
end
|
43
68
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
69
|
+
WrappedDistinguishedName.new(openssl_name)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
## This is a significantly more complicated case. It's possible that
|
74
|
+
## generically handled certificates will include custom OIDs in the
|
75
|
+
## subject.
|
76
|
+
class WrappedDistinguishedName < DistinguishedName
|
77
|
+
attr_accessor :x509_name
|
78
|
+
|
79
|
+
def initialize(x509_name)
|
80
|
+
@x509_name = x509_name
|
81
|
+
|
82
|
+
subject = @x509_name.to_a
|
83
|
+
subject.each do |element|
|
84
|
+
field = element[0].downcase
|
85
|
+
value = element[1]
|
86
|
+
#type = element[2] ## -not used
|
87
|
+
method_sym = "#{field}=".to_sym
|
88
|
+
if self.respond_to?(method_sym)
|
89
|
+
self.send("#{field}=",value)
|
90
|
+
else
|
91
|
+
## Custom OID
|
92
|
+
@custom_oids = true
|
53
93
|
end
|
54
94
|
end
|
55
|
-
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_x509_name
|
99
|
+
@x509_name
|
100
|
+
end
|
101
|
+
|
102
|
+
def custom_oids?
|
103
|
+
@custom_oids
|
56
104
|
end
|
57
105
|
end
|
58
106
|
end
|