certificate_authority 0.1.5 → 1.1.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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +26 -0
  3. data/.gitignore +6 -0
  4. data/.rspec +3 -0
  5. data/Gemfile +2 -8
  6. data/Gemfile.lock +71 -27
  7. data/README.rdoc +91 -2
  8. data/Rakefile +6 -41
  9. data/certificate_authority.gemspec +22 -83
  10. data/lib/certificate_authority/certificate.rb +139 -49
  11. data/lib/certificate_authority/certificate_revocation_list.rb +34 -14
  12. data/lib/certificate_authority/core_extensions.rb +46 -0
  13. data/lib/certificate_authority/distinguished_name.rb +64 -16
  14. data/lib/certificate_authority/extensions.rb +417 -45
  15. data/lib/certificate_authority/key_material.rb +30 -9
  16. data/lib/certificate_authority/ocsp_handler.rb +75 -5
  17. data/lib/certificate_authority/pkcs11_key_material.rb +0 -2
  18. data/lib/certificate_authority/revocable.rb +14 -0
  19. data/lib/certificate_authority/serial_number.rb +15 -2
  20. data/lib/certificate_authority/signing_request.rb +91 -0
  21. data/lib/certificate_authority/validations.rb +31 -0
  22. data/lib/certificate_authority/version.rb +3 -0
  23. data/lib/certificate_authority.rb +6 -5
  24. metadata +76 -71
  25. data/VERSION.yml +0 -5
  26. data/spec/spec_helper.rb +0 -4
  27. data/spec/units/certificate_authority_spec.rb +0 -4
  28. data/spec/units/certificate_revocation_list_spec.rb +0 -68
  29. data/spec/units/certificate_spec.rb +0 -428
  30. data/spec/units/distinguished_name_spec.rb +0 -59
  31. data/spec/units/extensions_spec.rb +0 -115
  32. data/spec/units/key_material_spec.rb +0 -100
  33. data/spec/units/ocsp_handler_spec.rb +0 -104
  34. data/spec/units/pkcs11_key_material_spec.rb +0 -41
  35. data/spec/units/serial_number_spec.rb +0 -20
  36. data/spec/units/signing_entity_spec.rb +0 -4
  37. data/spec/units/units_helper.rb +0 -1
@@ -1,14 +1,13 @@
1
1
  module CertificateAuthority
2
2
  class Certificate
3
- # include SigningEntity
4
- include ActiveModel::Validations
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 do |certificate|
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 = Time.now
37
- self.not_after = Time.now + 60 * 60 * 24 * 365 #One year
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 = 2
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
@@ -57,12 +75,6 @@ module CertificateAuthority
57
75
  openssl_cert.subject = self.distinguished_name.to_x509_name
58
76
  openssl_cert.issuer = parent.distinguished_name.to_x509_name
59
77
 
60
- require 'tempfile'
61
- t = Tempfile.new("bullshit_conf")
62
- # t = File.new("/tmp/openssl.cnf")
63
- ## The config requires a file even though we won't use it
64
- openssl_config = OpenSSL::Config.new(t.path)
65
-
66
78
  factory = OpenSSL::X509::ExtensionFactory.new
67
79
  factory.subject_certificate = openssl_cert
68
80
 
@@ -73,31 +85,23 @@ module CertificateAuthority
73
85
  factory.issuer_certificate = parent.openssl_body
74
86
  end
75
87
 
76
- self.extensions.keys.each do |k|
77
- config_extensions = extensions[k].config_extensions
78
- openssl_config = merge_options(openssl_config,config_extensions)
79
- end
80
-
81
- # p openssl_config.sections
82
-
83
- factory.config = openssl_config
88
+ factory.config = build_openssl_config
84
89
 
85
90
  # Order matters: e.g. for self-signed, subjectKeyIdentifier must come before authorityKeyIdentifier
86
91
  self.extensions.keys.sort{|a,b| b<=>a}.each do |k|
87
92
  e = extensions[k]
88
93
  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)
94
+ ext = factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
90
95
  openssl_cert.add_extension(ext)
91
96
  end
92
97
 
93
98
  if signing_profile["digest"].nil?
94
- digest = OpenSSL::Digest::Digest.new("SHA512")
99
+ digest = OpenSSL::Digest.new("SHA512")
95
100
  else
96
- digest = OpenSSL::Digest::Digest.new(signing_profile["digest"])
101
+ digest = OpenSSL::Digest.new(signing_profile["digest"])
97
102
  end
98
- self.openssl_body = openssl_cert.sign(parent.key_material.private_key,digest)
99
- t.close! if t.is_a?(Tempfile)# We can get rid of the ridiculous temp file
100
- self.openssl_body
103
+
104
+ self.openssl_body = openssl_cert.sign(parent.key_material.private_key, digest)
101
105
  end
102
106
 
103
107
  def is_signing_entity?
@@ -117,6 +121,34 @@ module CertificateAuthority
117
121
  self.openssl_body.to_pem
118
122
  end
119
123
 
124
+ def to_csr
125
+ csr = SigningRequest.new
126
+ csr.distinguished_name = self.distinguished_name
127
+ csr.key_material = self.key_material
128
+ factory = OpenSSL::X509::ExtensionFactory.new
129
+ exts = []
130
+ self.extensions.keys.each do |k|
131
+ ## Don't copy over key identifiers for CSRs
132
+ next if k == "subjectKeyIdentifier" || k == "authorityKeyIdentifier"
133
+ e = extensions[k]
134
+ ## If the extension returns an empty string we won't include it
135
+ next if e.to_s.nil? or e.to_s == ""
136
+ exts << factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
137
+ end
138
+ attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)])
139
+ attrs = [
140
+ OpenSSL::X509::Attribute.new("extReq", attrval),
141
+ OpenSSL::X509::Attribute.new("msExtReq", attrval)
142
+ ]
143
+ csr.attributes = attrs
144
+ csr
145
+ end
146
+
147
+ def self.from_x509_cert(raw_cert)
148
+ openssl_cert = OpenSSL::X509::Certificate.new(raw_cert)
149
+ Certificate.from_openssl(openssl_cert)
150
+ end
151
+
120
152
  def is_root_entity?
121
153
  self.parent == self && is_signing_entity?
122
154
  end
@@ -135,6 +167,16 @@ module CertificateAuthority
135
167
  items = signing_config[k]
136
168
  items.keys.each do |profile_item_key|
137
169
  if extension.respond_to?("#{profile_item_key}=".to_sym)
170
+ if k == 'subjectAltName' && profile_item_key == 'emails'
171
+ items[profile_item_key].map do |email|
172
+ if email == 'email:copy'
173
+ fail "no email address provided for subject: #{subject.to_x509_name}" unless subject.email_address
174
+ "email:#{subject.email_address}"
175
+ else
176
+ email
177
+ end
178
+ end
179
+ end
138
180
  extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] )
139
181
  else
140
182
  p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!"
@@ -143,36 +185,80 @@ module CertificateAuthority
143
185
  end
144
186
  end
145
187
 
188
+ # Enumeration of the extensions. Not the worst option since
189
+ # the likelihood of these needing to be updated is low at best.
190
+ EXTENSIONS = [
191
+ CertificateAuthority::Extensions::BasicConstraints,
192
+ CertificateAuthority::Extensions::CrlDistributionPoints,
193
+ CertificateAuthority::Extensions::SubjectKeyIdentifier,
194
+ CertificateAuthority::Extensions::AuthorityKeyIdentifier,
195
+ CertificateAuthority::Extensions::AuthorityInfoAccess,
196
+ CertificateAuthority::Extensions::KeyUsage,
197
+ CertificateAuthority::Extensions::ExtendedKeyUsage,
198
+ CertificateAuthority::Extensions::SubjectAlternativeName,
199
+ CertificateAuthority::Extensions::CertificatePolicies
200
+ ]
201
+
146
202
  def load_extensions
147
203
  extension_hash = {}
148
204
 
149
- temp_extensions = []
150
- basic_constraints = CertificateAuthority::Extensions::BasicContraints.new
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|
205
+ EXTENSIONS.each do |klass|
206
+ extension = klass.new
170
207
  extension_hash[extension.openssl_identifier] = extension
171
208
  end
172
209
 
173
210
  extension_hash
174
211
  end
175
212
 
213
+ def build_openssl_config
214
+ OpenSSL::Config.parse(openssl_config_string)
215
+ end
216
+
217
+ def openssl_config_string
218
+ lines = openssl_config_without_multi_value + openssl_config_with_multi_value
219
+ return '' if lines.empty?
220
+ (["[extensions]" ]+ lines).join("\n")
221
+ end
222
+
223
+ def openssl_config_without_multi_value
224
+ no_multi_value_keys = self.extensions.keys.select { |k| extensions[k].config_extensions.empty? }
225
+
226
+ lines = no_multi_value_keys.map do |k|
227
+ value = extensions[k].to_s
228
+ value.empty? ? '' : "#{k} = #{value}"
229
+ end.reject(&:empty?)
230
+ lines
231
+ end
232
+
233
+ def openssl_config_with_multi_value
234
+ multi_value_keys = self.extensions.keys.reject { |k| extensions[k].config_extensions.empty? }
235
+ sections = {}
236
+
237
+ entries = multi_value_keys.map do |k|
238
+ sections.merge!(extensions[k].config_extensions)
239
+ value = comma_terminate(extensions[k]) + section_ref_str(extensions[k].config_extensions.keys)
240
+ "#{k} = #{value}"
241
+ end.reject(&:empty?)
242
+
243
+ section_lines = sections.keys.flat_map do |k|
244
+ section_lines(k, sections[k])
245
+ end
246
+ entries + [''] + section_lines
247
+ end
248
+
249
+ def comma_terminate(val)
250
+ s = val.to_s
251
+ s.empty? ? s : "#{s},"
252
+ end
253
+
254
+ def section_ref_str(section_names)
255
+ section_names.map { |n| "@#{n}"}.join(',')
256
+ end
257
+
258
+ def section_lines(section_name, value_hash)
259
+ ["[#{section_name}]"] + value_hash.keys.map { |k| "#{k} = #{value_hash[k]}"} + ['']
260
+ end
261
+
176
262
  def merge_options(config,hash)
177
263
  hash.keys.each do |k|
178
264
  config[k] = hash[k]
@@ -193,7 +279,11 @@ module CertificateAuthority
193
279
  certificate.serial_number.number = openssl_cert.serial.to_i
194
280
  certificate.not_before = openssl_cert.not_before
195
281
  certificate.not_after = openssl_cert.not_after
196
- # TODO extensions
282
+ EXTENSIONS.each do |klass|
283
+ _,v,c = (openssl_cert.extensions.detect { |e| e.to_a.first == klass::OPENSSL_IDENTIFIER } || []).to_a
284
+ certificate.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v
285
+ end
286
+
197
287
  certificate
198
288
  end
199
289
 
@@ -1,36 +1,52 @@
1
1
  module CertificateAuthority
2
2
  class CertificateRevocationList
3
- include ActiveModel::Validations
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 do |crl|
11
- errors.add :next_update, "Next update must be a positive value" if crl.next_update < 0
12
- errors.add :parent, "A parent entity must be set" if crl.parent.nil?
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 <<(cert)
21
- raise "Only revoked certificates can be added to a CRL" unless cert.revoked?
22
- self.certificates << cert
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 |certificate|
38
+ revocations = self.certificates.collect do |revocable|
30
39
  revocation = OpenSSL::X509::Revoked.new
31
- x509_cert = OpenSSL::X509::Certificate.new(certificate.to_pem)
32
- revocation.serial = x509_cert.serial
33
- revocation.time = certificate.revoked_at
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
- digest = OpenSSL::Digest::Digest.new("SHA512")
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 ActiveModel::Validations
3
+ include Validations
4
4
 
5
- validates_presence_of :common_name
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("CN", common_name)
31
- name.add_entry("O", organization) unless organization.blank?
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("C", country) unless country.blank?
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
- name = DistinguishedName.new
45
- openssl_name.to_a.each do |k,v|
46
- case k
47
- when "CN" then name.common_name = v
48
- when "L" then name.locality = v
49
- when "ST" then name.state = v
50
- when "C" then name.country = v
51
- when "O" then name.organization = v
52
- when "OU" then name.organizational_unit = v
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
- name
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