certificate_authority 0.1.1

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.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'activemodel'
4
+
5
+ #group :development do
6
+ gem 'rspec'
7
+ gem "jeweler", "~> 1.5.2"
8
+ gem "rcov", ">= 0"
9
+ #end
data/Gemfile.lock ADDED
@@ -0,0 +1,35 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.0.6)
5
+ activesupport (= 3.0.6)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.5.0)
8
+ activesupport (3.0.6)
9
+ builder (2.1.2)
10
+ diff-lcs (1.1.2)
11
+ git (1.2.5)
12
+ i18n (0.5.0)
13
+ jeweler (1.5.2)
14
+ bundler (~> 1.0.0)
15
+ git (>= 1.2.5)
16
+ rake
17
+ rake (0.8.7)
18
+ rcov (0.9.9)
19
+ rspec (2.5.0)
20
+ rspec-core (~> 2.5.0)
21
+ rspec-expectations (~> 2.5.0)
22
+ rspec-mocks (~> 2.5.0)
23
+ rspec-core (2.5.1)
24
+ rspec-expectations (2.5.0)
25
+ diff-lcs (~> 1.1.2)
26
+ rspec-mocks (2.5.0)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ activemodel
33
+ jeweler (~> 1.5.2)
34
+ rcov
35
+ rspec
data/README.rdoc ADDED
@@ -0,0 +1,68 @@
1
+ = CertificateAuthority - Because it shouldn't be this damned complicated
2
+
3
+ This is meant to provide a programmer-friendly implementation of all the basic functionality contained in RFC-3280 to implement your own certificate authority.
4
+
5
+ You can generate root certificates, intermediate certificates, and terminal certificates. You can also generate/manage Certificate Revocation Lists (CRLs) and Online Certificate Status Protocol (OCSP) messages.
6
+
7
+ Because this library is built using the native Ruby bindings for OpenSSL it also supports PKCS#11 cryptographic hardware for secure maintenance of private key materials.
8
+
9
+ = The important parts
10
+ Coming soon.
11
+
12
+ = Examples
13
+
14
+ == Creating a self-signed certificate/root (probably what you want)
15
+
16
+ require 'certificate_authority'
17
+ root = CertificateAuthority::Certificate.new
18
+ root.subject.common_name "http://mydomain.com"
19
+ root.key_material.generate_key
20
+ root.signing_entity = true
21
+ root.sign!
22
+
23
+ == Creating an intermediate certificate (much less common use-case)
24
+
25
+ require 'certificate_authority'
26
+ root = CertificateAuthority::Certificate.new
27
+ root.subject.common_name "My snazzy root!"
28
+ root.key_material.generate_key
29
+ root.signing_entity = true
30
+ root.sign!
31
+
32
+ intermediate = CertificateAuthority::Certificate.new
33
+ intermediate.subject.common_name "My snazzy intermediate!"
34
+ intermediate.key_material.generate_key
35
+ intermediate.signing_entity = true
36
+ intermediate.parent = root
37
+ intermediate.sign!
38
+
39
+ == Creating a terminal (non-signing) cert
40
+
41
+ require 'certificate_authority'
42
+ plain_cert = CertificateAuthority::Certificate.new
43
+ plain_cert.subject.common_name "http://mydomain.com"
44
+ plain_cert.key_material.generate_key
45
+ plain_cert.parent = root # or intermediate
46
+ plain_cert.sign!
47
+
48
+ == Getting the certificate body
49
+
50
+ ...
51
+ certificate.sign!
52
+ certificate.to_pem # <= Returns a PEM formatted string of your certificate
53
+ certificate.key_material.private_key.to_pem # <= If you need the private key (and it's in memory)
54
+
55
+ = Coming Soon
56
+
57
+ * More PKCS#11 hardware (I need driver support from the manufacturers)
58
+ * Configurable V3 extensions for all the extended functionality
59
+
60
+ == Meta
61
+
62
+ Written by Chris Chandler(http://chrischandler.name) of Flatterline(http://flatterline.com)
63
+
64
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
65
+
66
+ Main page: http://github.com/cchandler/certificateauthority
67
+
68
+ Issue tracking: https://github.com/cchandler/certificateauthority/issues
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'rspec'
4
+ require 'rspec/core/rake_task'
5
+
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
33
+
34
+ task :spec do
35
+ Rake::Task["spec:units"].invoke
36
+ end
37
+
38
+ namespace :spec do
39
+ desc "Run unit specs."
40
+ RSpec::Core::RakeTask.new(:units) do |t|
41
+ t.rspec_opts = ['--colour --format progress --tag ~pkcs11']
42
+ 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
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :build:
5
+ :patch: 1
@@ -0,0 +1,90 @@
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 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{certificate_authority}
8
+ s.version = "0.1.1"
9
+
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-20}
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
+ ]
65
+
66
+ if s.respond_to? :specification_version then
67
+ s.specification_version = 3
68
+
69
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
70
+ s.add_runtime_dependency(%q<activemodel>, [">= 0"])
71
+ s.add_runtime_dependency(%q<rspec>, [">= 0"])
72
+ s.add_runtime_dependency(%q<jeweler>, ["~> 1.5.2"])
73
+ s.add_runtime_dependency(%q<rcov>, [">= 0"])
74
+ s.add_runtime_dependency(%q<activemodel>, ["= 3.0.6"])
75
+ else
76
+ s.add_dependency(%q<activemodel>, [">= 0"])
77
+ s.add_dependency(%q<rspec>, [">= 0"])
78
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
79
+ s.add_dependency(%q<rcov>, [">= 0"])
80
+ s.add_dependency(%q<activemodel>, ["= 3.0.6"])
81
+ end
82
+ else
83
+ s.add_dependency(%q<activemodel>, [">= 0"])
84
+ s.add_dependency(%q<rspec>, [">= 0"])
85
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
86
+ s.add_dependency(%q<rcov>, [">= 0"])
87
+ s.add_dependency(%q<activemodel>, ["= 3.0.6"])
88
+ end
89
+ end
90
+
@@ -0,0 +1,176 @@
1
+ module CertificateAuthority
2
+ class Certificate
3
+ # include SigningEntity
4
+ include ActiveModel::Validations
5
+
6
+ attr_accessor :distinguished_name
7
+ attr_accessor :serial_number
8
+ attr_accessor :key_material
9
+ attr_accessor :not_before
10
+ attr_accessor :not_after
11
+ attr_accessor :revoked_at
12
+ attr_accessor :extensions
13
+ attr_accessor :openssl_body
14
+
15
+ alias :subject :distinguished_name #Same thing as the DN
16
+
17
+ attr_accessor :parent
18
+
19
+ validate do |certificate|
20
+ 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?
22
+ errors.add :base, "Serial number must be valid" unless serial_number.valid?
23
+ errors.add :base, "Extensions must be valid" unless extensions.each do |item|
24
+ return true unless item.respond_to?(:valid?)
25
+ item.valid?
26
+ end
27
+ end
28
+
29
+ def initialize
30
+ self.distinguished_name = DistinguishedName.new
31
+ self.serial_number = SerialNumber.new
32
+ 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.parent = self
36
+ self.extensions = load_extensions()
37
+
38
+ self.signing_entity = false
39
+
40
+ end
41
+
42
+ def sign!(signing_profile={})
43
+ raise "Invalid certificate" unless valid?
44
+ merge_profile_with_extensions(signing_profile)
45
+
46
+ openssl_cert = OpenSSL::X509::Certificate.new
47
+ openssl_cert.version = 2
48
+ openssl_cert.not_before = self.not_before
49
+ openssl_cert.not_after = self.not_after
50
+ openssl_cert.public_key = self.key_material.public_key
51
+
52
+ openssl_cert.serial = self.serial_number.number
53
+
54
+ openssl_cert.subject = self.distinguished_name.to_x509_name
55
+ openssl_cert.issuer = parent.distinguished_name.to_x509_name
56
+
57
+ require 'tempfile'
58
+ t = Tempfile.new("bullshit_conf")
59
+ # t = File.new("/tmp/openssl.cnf")
60
+ ## The config requires a file even though we won't use it
61
+ openssl_config = OpenSSL::Config.new(t.path)
62
+
63
+ factory = OpenSSL::X509::ExtensionFactory.new
64
+ factory.subject_certificate = openssl_cert
65
+
66
+ #NB: If the parent doesn't have an SSL body we're making this a self-signed cert
67
+ if parent.openssl_body.nil?
68
+ factory.issuer_certificate = openssl_cert
69
+ else
70
+ factory.issuer_certificate = parent.openssl_body
71
+ end
72
+
73
+ self.extensions.keys.each do |k|
74
+ config_extensions = extensions[k].config_extensions
75
+ openssl_config = merge_options(openssl_config,config_extensions)
76
+ end
77
+
78
+ # p openssl_config.sections
79
+
80
+ factory.config = openssl_config
81
+
82
+ self.extensions.keys.each do |k|
83
+ e = extensions[k]
84
+ 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)
86
+ openssl_cert.add_extension(ext)
87
+ 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
93
+ end
94
+
95
+ def is_signing_entity?
96
+ self.extensions["basicConstraints"].ca
97
+ end
98
+
99
+ def signing_entity=(signing)
100
+ self.extensions["basicConstraints"].ca = signing
101
+ end
102
+
103
+ def revoked?
104
+ !self.revoked_at.nil?
105
+ end
106
+
107
+ def to_pem
108
+ raise "Certificate has no signed body" if self.openssl_body.nil?
109
+ self.openssl_body.to_pem
110
+ end
111
+
112
+ def is_root_entity?
113
+ self.parent == self && is_signing_entity?
114
+ end
115
+
116
+ def is_intermediate_entity?
117
+ (self.parent != self) && is_signing_entity?
118
+ end
119
+
120
+ private
121
+
122
+ def merge_profile_with_extensions(signing_profile={})
123
+ return self.extensions if signing_profile["extensions"].nil?
124
+ signing_config = signing_profile["extensions"]
125
+ signing_config.keys.each do |k|
126
+ extension = self.extensions[k]
127
+ items = signing_config[k]
128
+ items.keys.each do |profile_item_key|
129
+ if extension.respond_to?("#{profile_item_key}=".to_sym)
130
+ extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] )
131
+ else
132
+ p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!"
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ def load_extensions
139
+ 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|
162
+ extension_hash[extension.openssl_identifier] = extension
163
+ end
164
+
165
+ extension_hash
166
+ end
167
+
168
+ def merge_options(config,hash)
169
+ hash.keys.each do |k|
170
+ config[k] = hash[k]
171
+ end
172
+ config
173
+ end
174
+
175
+ end
176
+ end
@@ -0,0 +1,59 @@
1
+ module CertificateAuthority
2
+ class CertificateRevocationList
3
+ include ActiveModel::Validations
4
+
5
+ attr_accessor :certificates
6
+ attr_accessor :parent
7
+ attr_accessor :crl_body
8
+ attr_accessor :next_update
9
+
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?
13
+ end
14
+
15
+ def initialize
16
+ self.certificates = []
17
+ self.next_update = 60 * 60 * 4 # 4 hour default
18
+ end
19
+
20
+ def <<(cert)
21
+ raise "Only revoked certificates can be added to a CRL" unless cert.revoked?
22
+ self.certificates << cert
23
+ end
24
+
25
+ def sign!
26
+ raise "No parent entity has been set!" if self.parent.nil?
27
+ raise "Invalid CRL" unless self.valid?
28
+
29
+ revocations = self.certificates.collect do |certificate|
30
+ 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
34
+ revocation
35
+ end
36
+
37
+ crl = OpenSSL::X509::CRL.new
38
+ revocations.each do |revocation|
39
+ crl.add_revoked(revocation)
40
+ end
41
+
42
+ crl.version = 1
43
+ crl.last_update = Time.now
44
+ crl.next_update = Time.now + self.next_update
45
+
46
+ signing_cert = OpenSSL::X509::Certificate.new(self.parent.to_pem)
47
+ digest = OpenSSL::Digest::Digest.new("SHA512")
48
+ crl.issuer = signing_cert.subject
49
+ self.crl_body = crl.sign(self.parent.key_material.private_key, digest)
50
+
51
+ self.crl_body
52
+ end
53
+
54
+ def to_pem
55
+ raise "No signed CRL body" if self.crl_body.nil?
56
+ self.crl_body.to_pem
57
+ end
58
+ end#CertificateRevocationList
59
+ end