certificate_authority 0.1.2 → 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 -27
  7. data/README.rdoc +184 -89
  8. data/Rakefile +6 -41
  9. data/certificate_authority.gemspec +22 -81
  10. data/lib/certificate_authority.rb +7 -6
  11. data/lib/certificate_authority/certificate.rb +151 -71
  12. data/lib/certificate_authority/certificate_revocation_list.rb +46 -26
  13. data/lib/certificate_authority/core_extensions.rb +46 -0
  14. data/lib/certificate_authority/distinguished_name.rb +84 -17
  15. data/lib/certificate_authority/extensions.rb +483 -96
  16. data/lib/certificate_authority/key_material.rb +75 -21
  17. data/lib/certificate_authority/ocsp_handler.rb +99 -29
  18. data/lib/certificate_authority/pkcs11_key_material.rb +13 -15
  19. data/lib/certificate_authority/revocable.rb +14 -0
  20. data/lib/certificate_authority/serial_number.rb +18 -5
  21. data/lib/certificate_authority/signing_entity.rb +5 -7
  22. data/lib/certificate_authority/signing_request.rb +91 -0
  23. data/lib/certificate_authority/validations.rb +31 -0
  24. data/lib/certificate_authority/version.rb +3 -0
  25. metadata +96 -94
  26. data/VERSION.yml +0 -5
  27. data/spec/spec_helper.rb +0 -4
  28. data/spec/units/certificate_authority_spec.rb +0 -4
  29. data/spec/units/certificate_revocation_list_spec.rb +0 -68
  30. data/spec/units/certificate_spec.rb +0 -351
  31. data/spec/units/distinguished_name_spec.rb +0 -38
  32. data/spec/units/extensions_spec.rb +0 -53
  33. data/spec/units/key_material_spec.rb +0 -96
  34. data/spec/units/ocsp_handler_spec.rb +0 -104
  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
data/Rakefile CHANGED
@@ -1,35 +1,9 @@
1
- require 'rubygems'
2
- require 'bundler'
3
- require 'rspec'
4
- require 'rspec/core/rake_task'
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
5
4
 
6
- begin
7
- Bundler.setup(:default, :development)
8
- rescue Bundler::BundlerError => e
9
- $stderr.puts e.message
10
- $stderr.puts "Run `bundle install` to install missing gems"
11
- exit e.status_code
12
- end
13
-
14
- require 'rake'
15
-
16
- desc 'Default: run specs.'
17
- task :default => :spec
18
-
19
- require 'jeweler'
20
- Jeweler::Tasks.new do |gem|
21
- # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
22
- gem.name = "certificate_authority"
23
- gem.homepage = "http://github.com/cchandler/certificate_authority"
24
- gem.license = "MIT"
25
- gem.summary = 'Ruby gem for managing the core functions outlined in RFC-3280 for PKI'
26
- # gem.description = ''
27
- gem.email = "chris@flatterline.com"
28
- gem.authors = ["Chris Chandler"]
29
-
30
- # gem.add_dependency('activemodel', '3.0.6')
31
- end
32
- Jeweler::RubygemsDotOrgTasks.new
5
+ desc "Default: run specs."
6
+ task default: %i[spec]
33
7
 
34
8
  task :spec do
35
9
  Rake::Task["spec:units"].invoke
@@ -38,15 +12,6 @@ end
38
12
  namespace :spec do
39
13
  desc "Run unit specs."
40
14
  RSpec::Core::RakeTask.new(:units) do |t|
41
- t.rspec_opts = ['--colour --format progress --tag ~pkcs11']
15
+ t.rspec_opts = ["--colour --format progress --tag ~pkcs11"]
42
16
  end
43
-
44
- desc "Run integration specs."
45
- RSpec::Core::RakeTask.new(:integrations) do |t|
46
- t.rspec_opts = ['--colour --format progress']
47
- end
48
- end
49
-
50
- RSpec::Core::RakeTask.new(:doc) do |t|
51
- t.rspec_opts = ['--format specdoc ']
52
17
  end
@@ -1,87 +1,28 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
- # -*- encoding: utf-8 -*-
1
+ require File.expand_path("lib/certificate_authority/version", __dir__)
5
2
 
6
- Gem::Specification.new do |s|
7
- s.name = %q{certificate_authority}
8
- s.version = "0.1.2"
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "certificate_authority"
5
+ spec.version = CertificateAuthority::VERSION
6
+ spec.authors = ["Chris Chandler"]
7
+ spec.email = ["squanderingtime@gmail.com"]
9
8
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Chris Chandler"]
12
- s.date = %q{2011-04-21}
13
- s.email = %q{chris@flatterline.com}
14
- s.extra_rdoc_files = [
15
- "README.rdoc"
16
- ]
17
- s.files = [
18
- "Gemfile",
19
- "Gemfile.lock",
20
- "README.rdoc",
21
- "Rakefile",
22
- "VERSION.yml",
23
- "certificate_authority.gemspec",
24
- "lib/certificate_authority.rb",
25
- "lib/certificate_authority/certificate.rb",
26
- "lib/certificate_authority/certificate_revocation_list.rb",
27
- "lib/certificate_authority/distinguished_name.rb",
28
- "lib/certificate_authority/extensions.rb",
29
- "lib/certificate_authority/key_material.rb",
30
- "lib/certificate_authority/ocsp_handler.rb",
31
- "lib/certificate_authority/pkcs11_key_material.rb",
32
- "lib/certificate_authority/serial_number.rb",
33
- "lib/certificate_authority/signing_entity.rb",
34
- "lib/tasks/certificate_authority.rake",
35
- "spec/spec_helper.rb",
36
- "spec/units/certificate_authority_spec.rb",
37
- "spec/units/certificate_revocation_list_spec.rb",
38
- "spec/units/certificate_spec.rb",
39
- "spec/units/distinguished_name_spec.rb",
40
- "spec/units/extensions_spec.rb",
41
- "spec/units/key_material_spec.rb",
42
- "spec/units/ocsp_handler_spec.rb",
43
- "spec/units/serial_number_spec.rb",
44
- "spec/units/signing_entity_spec.rb",
45
- "spec/units/units_helper.rb"
46
- ]
47
- s.homepage = %q{http://github.com/cchandler/certificate_authority}
48
- s.licenses = ["MIT"]
49
- s.require_paths = ["lib"]
50
- s.rubygems_version = %q{1.7.2}
51
- s.summary = %q{Ruby gem for managing the core functions outlined in RFC-3280 for PKI}
52
- s.test_files = [
53
- "spec/spec_helper.rb",
54
- "spec/units/certificate_authority_spec.rb",
55
- "spec/units/certificate_revocation_list_spec.rb",
56
- "spec/units/certificate_spec.rb",
57
- "spec/units/distinguished_name_spec.rb",
58
- "spec/units/extensions_spec.rb",
59
- "spec/units/key_material_spec.rb",
60
- "spec/units/ocsp_handler_spec.rb",
61
- "spec/units/serial_number_spec.rb",
62
- "spec/units/signing_entity_spec.rb",
63
- "spec/units/units_helper.rb"
64
- ]
9
+ spec.summary = "Ruby gem for managing the core functions outlined in RFC-3280 for PKI"
10
+ spec.homepage = "https://github.com/cchandler/certificate_authority"
11
+ spec.license = "MIT"
65
12
 
66
- if s.respond_to? :specification_version then
67
- s.specification_version = 3
13
+ spec.metadata["homepage_uri"] = "https://github.com/cchandler/certificate_authority"
14
+ spec.metadata["source_code_uri"] = "https://github.com/cchandler/certificate_authority"
68
15
 
69
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
70
- s.add_runtime_dependency(%q<activemodel>, ["~> 3.0.6"])
71
- s.add_development_dependency(%q<rspec>, [">= 0"])
72
- s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
73
- s.add_development_dependency(%q<rcov>, [">= 0"])
74
- else
75
- s.add_dependency(%q<activemodel>, ["~> 3.0.6"])
76
- s.add_dependency(%q<rspec>, [">= 0"])
77
- s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
78
- s.add_dependency(%q<rcov>, [">= 0"])
79
- end
80
- else
81
- s.add_dependency(%q<activemodel>, ["~> 3.0.6"])
82
- s.add_dependency(%q<rspec>, [">= 0"])
83
- s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
84
- s.add_dependency(%q<rcov>, [">= 0"])
16
+ spec.files = Dir.chdir(__dir__) do
17
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec/)}) }
85
18
  end
86
- end
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = ">= 2.4"
87
22
 
23
+ spec.add_development_dependency "coveralls"
24
+ spec.add_development_dependency "pry"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "rubocop"
28
+ end
@@ -1,11 +1,11 @@
1
- $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
-
3
- #Exterior requirements
1
+ # Exterior requirements
4
2
  require 'openssl'
5
- require 'active_model'
6
3
 
7
- #Internal modules
4
+ # Internal modules
5
+ require 'certificate_authority/core_extensions'
8
6
  require 'certificate_authority/signing_entity'
7
+ require 'certificate_authority/revocable'
8
+ require 'certificate_authority/validations'
9
9
  require 'certificate_authority/distinguished_name'
10
10
  require 'certificate_authority/serial_number'
11
11
  require 'certificate_authority/key_material'
@@ -14,6 +14,7 @@ require 'certificate_authority/extensions'
14
14
  require 'certificate_authority/certificate'
15
15
  require 'certificate_authority/certificate_revocation_list'
16
16
  require 'certificate_authority/ocsp_handler'
17
+ require 'certificate_authority/signing_request'
17
18
 
18
19
  module CertificateAuthority
19
- end
20
+ end
@@ -1,68 +1,88 @@
1
1
  module CertificateAuthority
2
2
  class Certificate
3
- # include SigningEntity
4
- include ActiveModel::Validations
5
-
3
+ include Validations
4
+ include Revocable
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
+
15
14
  alias :subject :distinguished_name #Same thing as the DN
16
-
15
+
17
16
  attr_accessor :parent
18
-
19
- validate do |certificate|
17
+
18
+ def validate
20
19
  errors.add :base, "Distinguished name must be valid" unless distinguished_name.valid?
21
- errors.add :base, "Key material name must be valid" unless key_material.valid?
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?
23
22
  errors.add :base, "Extensions must be valid" unless extensions.each do |item|
24
- return true unless item.respond_to?(:valid?)
25
- item.valid?
23
+ unless item.respond_to?(:valid?)
24
+ true
25
+ else
26
+ item.valid?
27
+ end
26
28
  end
27
29
  end
28
-
30
+
29
31
  def initialize
30
32
  self.distinguished_name = DistinguishedName.new
31
33
  self.serial_number = SerialNumber.new
32
34
  self.key_material = MemoryKeyMaterial.new
33
- self.not_before = Time.now
34
- 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
35
37
  self.parent = self
36
38
  self.extensions = load_extensions()
37
-
39
+
38
40
  self.signing_entity = false
39
-
41
+
40
42
  end
41
-
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
+
42
63
  def sign!(signing_profile={})
43
64
  raise "Invalid certificate #{self.errors.full_messages}" unless valid?
44
65
  merge_profile_with_extensions(signing_profile)
45
-
66
+
46
67
  openssl_cert = OpenSSL::X509::Certificate.new
47
- openssl_cert.version = 2
68
+ openssl_cert.version = 2
48
69
  openssl_cert.not_before = self.not_before
49
70
  openssl_cert.not_after = self.not_after
50
71
  openssl_cert.public_key = self.key_material.public_key
51
-
72
+
52
73
  openssl_cert.serial = self.serial_number.number
53
-
74
+
54
75
  openssl_cert.subject = self.distinguished_name.to_x509_name
55
76
  openssl_cert.issuer = parent.distinguished_name.to_x509_name
56
-
77
+
57
78
  require 'tempfile'
58
79
  t = Tempfile.new("bullshit_conf")
59
- # t = File.new("/tmp/openssl.cnf")
60
80
  ## The config requires a file even though we won't use it
61
81
  openssl_config = OpenSSL::Config.new(t.path)
62
-
82
+
63
83
  factory = OpenSSL::X509::ExtensionFactory.new
64
84
  factory.subject_certificate = openssl_cert
65
-
85
+
66
86
  #NB: If the parent doesn't have an SSL body we're making this a self-signed cert
67
87
  if parent.openssl_body.nil?
68
88
  factory.issuer_certificate = openssl_cert
@@ -74,51 +94,85 @@ module CertificateAuthority
74
94
  config_extensions = extensions[k].config_extensions
75
95
  openssl_config = merge_options(openssl_config,config_extensions)
76
96
  end
77
-
97
+
78
98
  # p openssl_config.sections
79
-
99
+
80
100
  factory.config = openssl_config
81
-
82
- self.extensions.keys.each do |k|
101
+
102
+ # Order matters: e.g. for self-signed, subjectKeyIdentifier must come before authorityKeyIdentifier
103
+ self.extensions.keys.sort{|a,b| b<=>a}.each do |k|
83
104
  e = extensions[k]
84
105
  next if e.to_s.nil? or e.to_s == "" ## If the extension returns an empty string we won't include it
85
- ext = factory.create_ext(e.openssl_identifier, e.to_s)
106
+ ext = factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
86
107
  openssl_cert.add_extension(ext)
87
108
  end
88
-
89
- digest = OpenSSL::Digest::Digest.new("SHA512")
90
- self.openssl_body = openssl_cert.sign(parent.key_material.private_key,digest)
91
- t.close! if t.is_a?(Tempfile)# We can get rid of the ridiculous temp file
92
- self.openssl_body
109
+
110
+ if signing_profile["digest"].nil?
111
+ digest = OpenSSL::Digest.new("SHA512")
112
+ else
113
+ digest = OpenSSL::Digest.new(signing_profile["digest"])
114
+ end
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
93
119
  end
94
-
120
+
95
121
  def is_signing_entity?
96
122
  self.extensions["basicConstraints"].ca
97
123
  end
98
-
124
+
99
125
  def signing_entity=(signing)
100
126
  self.extensions["basicConstraints"].ca = signing
101
127
  end
102
-
128
+
103
129
  def revoked?
104
130
  !self.revoked_at.nil?
105
131
  end
106
-
132
+
107
133
  def to_pem
108
134
  raise "Certificate has no signed body" if self.openssl_body.nil?
109
135
  self.openssl_body.to_pem
110
136
  end
111
-
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
+
112
166
  def is_root_entity?
113
167
  self.parent == self && is_signing_entity?
114
168
  end
115
-
169
+
116
170
  def is_intermediate_entity?
117
171
  (self.parent != self) && is_signing_entity?
118
172
  end
119
-
173
+
120
174
  private
121
-
175
+
122
176
  def merge_profile_with_extensions(signing_profile={})
123
177
  return self.extensions if signing_profile["extensions"].nil?
124
178
  signing_config = signing_profile["extensions"]
@@ -127,50 +181,76 @@ module CertificateAuthority
127
181
  items = signing_config[k]
128
182
  items.keys.each do |profile_item_key|
129
183
  if extension.respond_to?("#{profile_item_key}=".to_sym)
130
- extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] )
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
194
+ extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] )
131
195
  else
132
196
  p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!"
133
197
  end
134
198
  end
135
199
  end
136
200
  end
137
-
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
+
138
216
  def load_extensions
139
217
  extension_hash = {}
140
-
141
- temp_extensions = []
142
- basic_constraints = CertificateAuthority::Extensions::BasicContraints.new
143
- temp_extensions << basic_constraints
144
- crl_distribution_points = CertificateAuthority::Extensions::CrlDistributionPoints.new
145
- temp_extensions << crl_distribution_points
146
- subject_key_identifier = CertificateAuthority::Extensions::SubjectKeyIdentifier.new
147
- temp_extensions << subject_key_identifier
148
- authority_key_identifier = CertificateAuthority::Extensions::AuthorityKeyIdentifier.new
149
- temp_extensions << authority_key_identifier
150
- authority_info_access = CertificateAuthority::Extensions::AuthorityInfoAccess.new
151
- temp_extensions << authority_info_access
152
- key_usage = CertificateAuthority::Extensions::KeyUsage.new
153
- temp_extensions << key_usage
154
- extended_key_usage = CertificateAuthority::Extensions::ExtendedKeyUsage.new
155
- temp_extensions << extended_key_usage
156
- subject_alternative_name = CertificateAuthority::Extensions::SubjectAlternativeName.new
157
- temp_extensions << subject_alternative_name
158
- certificate_policies = CertificateAuthority::Extensions::CertificatePolicies.new
159
- temp_extensions << certificate_policies
160
-
161
- temp_extensions.each do |extension|
218
+
219
+ EXTENSIONS.each do |klass|
220
+ extension = klass.new
162
221
  extension_hash[extension.openssl_identifier] = extension
163
222
  end
164
-
223
+
165
224
  extension_hash
166
225
  end
167
-
226
+
168
227
  def merge_options(config,hash)
169
228
  hash.keys.each do |k|
170
229
  config[k] = hash[k]
171
230
  end
172
231
  config
173
232
  end
174
-
233
+
234
+ def self.from_openssl openssl_cert
235
+ unless openssl_cert.is_a? OpenSSL::X509::Certificate
236
+ raise "Can only construct from an OpenSSL::X509::Certificate"
237
+ end
238
+
239
+ certificate = Certificate.new
240
+ # Only subject, key_material, and body are used for signing
241
+ certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject
242
+ certificate.key_material.public_key = openssl_cert.public_key
243
+ certificate.openssl_body = openssl_cert
244
+ certificate.serial_number.number = openssl_cert.serial.to_i
245
+ certificate.not_before = openssl_cert.not_before
246
+ certificate.not_after = openssl_cert.not_after
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
+
252
+ certificate
253
+ end
254
+
175
255
  end
176
- end
256
+ end