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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +2 -8
  6. data/Gemfile.lock +71 -31
  7. data/README.rdoc +91 -2
  8. data/Rakefile +6 -41
  9. data/certificate_authority.gemspec +22 -66
  10. data/lib/certificate_authority.rb +6 -5
  11. data/lib/certificate_authority/certificate.rb +91 -36
  12. data/lib/certificate_authority/certificate_revocation_list.rb +34 -14
  13. data/lib/certificate_authority/core_extensions.rb +46 -0
  14. data/lib/certificate_authority/distinguished_name.rb +64 -16
  15. data/lib/certificate_authority/extensions.rb +417 -45
  16. data/lib/certificate_authority/key_material.rb +30 -9
  17. data/lib/certificate_authority/ocsp_handler.rb +75 -5
  18. data/lib/certificate_authority/pkcs11_key_material.rb +0 -2
  19. data/lib/certificate_authority/revocable.rb +14 -0
  20. data/lib/certificate_authority/serial_number.rb +15 -2
  21. data/lib/certificate_authority/signing_request.rb +91 -0
  22. data/lib/certificate_authority/validations.rb +31 -0
  23. data/lib/certificate_authority/version.rb +3 -0
  24. metadata +76 -48
  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
@@ -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::Digest.new("SHA512")
111
+ digest = OpenSSL::Digest.new("SHA512")
95
112
  else
96
- digest = OpenSSL::Digest::Digest.new(signing_profile["digest"])
113
+ digest = OpenSSL::Digest.new(signing_profile["digest"])
97
114
  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
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
- 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|
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
- # TODO extensions
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 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