certificate_authority 0.1.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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