certificate_authority_sonian 0.1.7
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 +9 -0
- data/Gemfile.lock +39 -0
- data/README.rdoc +243 -0
- data/Rakefile +52 -0
- data/VERSION.yml +5 -0
- data/certificate_authority.gemspec +71 -0
- data/lib/certificate_authority.rb +19 -0
- data/lib/certificate_authority/certificate.rb +215 -0
- data/lib/certificate_authority/certificate_revocation_list.rb +59 -0
- data/lib/certificate_authority/distinguished_name.rb +58 -0
- data/lib/certificate_authority/extensions.rb +266 -0
- data/lib/certificate_authority/key_material.rb +135 -0
- data/lib/certificate_authority/ocsp_handler.rb +77 -0
- data/lib/certificate_authority/pkcs11_key_material.rb +65 -0
- data/lib/certificate_authority/serial_number.rb +9 -0
- data/lib/certificate_authority/signing_entity.rb +16 -0
- data/lib/tasks/certificate_authority.rake +23 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/units/certificate_authority_spec.rb +4 -0
- data/spec/units/certificate_revocation_list_spec.rb +68 -0
- data/spec/units/certificate_spec.rb +428 -0
- data/spec/units/distinguished_name_spec.rb +59 -0
- data/spec/units/extensions_spec.rb +115 -0
- data/spec/units/key_material_spec.rb +154 -0
- data/spec/units/ocsp_handler_spec.rb +104 -0
- data/spec/units/pkcs11_key_material_spec.rb +41 -0
- data/spec/units/serial_number_spec.rb +20 -0
- data/spec/units/signing_entity_spec.rb +4 -0
- data/spec/units/units_helper.rb +1 -0
- metadata +123 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
module CertificateAuthority
|
2
|
+
module KeyMaterial
|
3
|
+
def public_key
|
4
|
+
raise "Required implementation"
|
5
|
+
end
|
6
|
+
|
7
|
+
def private_key
|
8
|
+
raise "Required implementation"
|
9
|
+
end
|
10
|
+
|
11
|
+
def is_in_hardware?
|
12
|
+
raise "Required implementation"
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_in_memory?
|
16
|
+
raise "Required implementation"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class MemoryKeyMaterial
|
21
|
+
include KeyMaterial
|
22
|
+
include ActiveModel::Validations
|
23
|
+
|
24
|
+
attr_accessor :keypair
|
25
|
+
attr_accessor :private_key
|
26
|
+
attr_accessor :public_key
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
end
|
30
|
+
|
31
|
+
validates_each :private_key do |record, attr, value|
|
32
|
+
record.errors.add :private_key, "cannot be blank" if record.private_key.nil?
|
33
|
+
end
|
34
|
+
validates_each :public_key do |record, attr, value|
|
35
|
+
record.errors.add :public_key, "cannot be blank" if record.public_key.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Boolean]
|
39
|
+
def is_in_hardware?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Boolean]
|
44
|
+
def is_in_memory?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param modulus_bits [Integer] number of bits to generate the key with
|
49
|
+
# @return [OpenSSL::Pkey::RSA]
|
50
|
+
def generate_key(modulus_bits=2048)
|
51
|
+
self.keypair = OpenSSL::PKey::RSA.new(modulus_bits)
|
52
|
+
self.private_key = keypair
|
53
|
+
self.public_key = keypair.public_key
|
54
|
+
self.keypair
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [OpenSSL::Pkey::RSA]
|
58
|
+
def private_key
|
59
|
+
@private_key
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [OpenSSL::Pkey::RSA]
|
63
|
+
def public_key
|
64
|
+
@public_key
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class SigningRequestKeyMaterial
|
69
|
+
include KeyMaterial
|
70
|
+
include ActiveModel::Validations
|
71
|
+
|
72
|
+
validates_each :public_key do |record, attr, value|
|
73
|
+
record.errors.add :public_key, "cannot be blank" if record.public_key.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_accessor :public_key
|
77
|
+
attr_reader :csr # @return [OpenSSL::X509::Request,OpenSSL::Netscape::SPKI]
|
78
|
+
attr_reader :certificate # @return [CertificateAuthority::Certificate]
|
79
|
+
|
80
|
+
# @param request [OpenSSL::X508::Request,OpenSSL::Netscape::SPKI,String] a signing request
|
81
|
+
def initialize(request=nil)
|
82
|
+
if request.is_a?(OpenSSL::X509::Request) || request.is_a?(OpenSSL::Netscape::SPKI)
|
83
|
+
@csr = request
|
84
|
+
raise "Invalid certificate signing request" unless @csr.verify(@csr.public_key)
|
85
|
+
self.public_key = @csr.public_key
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Given a root certificate and a key, will generate a signed certificate
|
90
|
+
# @param root_cert [CertificateAuthority::Certificate] the parent certificate (CA)
|
91
|
+
# @param key [OpenSSL::Pkey::RSA] the private key to sign with
|
92
|
+
# @param serial_number [Integer] the serial number for the generated certificate
|
93
|
+
# @param options [Hash{:dn => CertificateAuthority::DistinguishedName, :algorithm => OpenSSL::Digest, :not_after => Time}] :dn is required for SPKAC signing
|
94
|
+
# @return [CertificateAuthority::Certificate] A signed certificate instance
|
95
|
+
def sign_and_certify(root_cert, key, serial_number, options = {})
|
96
|
+
if csr.is_a? OpenSSL::Netscape::SPKI
|
97
|
+
raise "Must pass :dn in options to generate certificates for OpenSSL::Netscape::SPKI requests" unless options[:dn]
|
98
|
+
end
|
99
|
+
algorithm = options[:algorithm] || OpenSSL::Digest::SHA1.new
|
100
|
+
cert = OpenSSL::X509::Certificate.new
|
101
|
+
if options[:dn]
|
102
|
+
cert.subject = options[:dn].to_x509_name
|
103
|
+
else
|
104
|
+
cert.subject = csr.subject
|
105
|
+
end
|
106
|
+
cert.public_key = public_key
|
107
|
+
cert.not_before = Time.now
|
108
|
+
cert.not_after = options[:not_after] || (Time.now + 100000000)
|
109
|
+
cert.issuer = root_cert.subject.to_x509_name
|
110
|
+
cert.serial = serial_number
|
111
|
+
cert.sign key, algorithm
|
112
|
+
@certificate = CertificateAuthority::Certificate.from_openssl cert
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [Boolean]
|
116
|
+
def is_in_hardware?
|
117
|
+
false
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [Boolean]
|
121
|
+
def is_in_memory?
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [NilClass]
|
126
|
+
def private_key
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [OpenSSL::Pkey::RSA]
|
131
|
+
def public_key
|
132
|
+
@public_key
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module CertificateAuthority
|
2
|
+
class OCSPHandler
|
3
|
+
include ActiveModel::Validations
|
4
|
+
|
5
|
+
attr_accessor :ocsp_request
|
6
|
+
attr_accessor :certificate_ids
|
7
|
+
|
8
|
+
attr_accessor :certificates
|
9
|
+
attr_accessor :parent
|
10
|
+
|
11
|
+
attr_accessor :ocsp_response_body
|
12
|
+
|
13
|
+
validate do |crl|
|
14
|
+
errors.add :parent, "A parent entity must be set" if parent.nil?
|
15
|
+
end
|
16
|
+
validate :all_certificates_available
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
self.certificates = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(cert)
|
23
|
+
self.certificates[cert.serial_number.number.to_s] = cert
|
24
|
+
end
|
25
|
+
|
26
|
+
def extract_certificate_serials
|
27
|
+
raise "No valid OCSP request was supplied" if self.ocsp_request.nil?
|
28
|
+
openssl_request = OpenSSL::OCSP::Request.new(self.ocsp_request)
|
29
|
+
|
30
|
+
self.certificate_ids = openssl_request.certid.collect do |cert_id|
|
31
|
+
cert_id.serial
|
32
|
+
end
|
33
|
+
|
34
|
+
self.certificate_ids
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def response
|
39
|
+
raise "Invalid response" unless valid?
|
40
|
+
|
41
|
+
openssl_ocsp_response = OpenSSL::OCSP::BasicResponse.new
|
42
|
+
openssl_ocsp_request = OpenSSL::OCSP::Request.new(self.ocsp_request)
|
43
|
+
openssl_ocsp_response.copy_nonce(openssl_ocsp_request)
|
44
|
+
|
45
|
+
openssl_ocsp_request.certid.each do |cert_id|
|
46
|
+
certificate = self.certificates[cert_id.serial.to_s]
|
47
|
+
|
48
|
+
openssl_ocsp_response.add_status(cert_id,
|
49
|
+
OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0,
|
50
|
+
0, 0, 30, nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
openssl_ocsp_response.sign(OpenSSL::X509::Certificate.new(self.parent.to_pem), self.parent.key_material.private_key, nil, nil)
|
55
|
+
final_response = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, openssl_ocsp_response)
|
56
|
+
self.ocsp_response_body = final_response
|
57
|
+
self.ocsp_response_body
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_der
|
61
|
+
raise "No signed OCSP response body available" if self.ocsp_response_body.nil?
|
62
|
+
self.ocsp_response_body.to_der
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def all_certificates_available
|
68
|
+
openssl_ocsp_request = OpenSSL::OCSP::Request.new(self.ocsp_request)
|
69
|
+
|
70
|
+
openssl_ocsp_request.certid.each do |cert_id|
|
71
|
+
certificate = self.certificates[cert_id.serial.to_s]
|
72
|
+
errors.add(:base, "Certificate #{cert_id.serial} has not been added yet") if certificate.nil?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module CertificateAuthority
|
2
|
+
class Pkcs11KeyMaterial
|
3
|
+
include KeyMaterial
|
4
|
+
include ActiveModel::Validations
|
5
|
+
include ActiveModel::Serialization
|
6
|
+
|
7
|
+
attr_accessor :engine
|
8
|
+
attr_accessor :token_id
|
9
|
+
attr_accessor :pkcs11_lib
|
10
|
+
attr_accessor :openssl_pkcs11_engine_lib
|
11
|
+
attr_accessor :pin
|
12
|
+
|
13
|
+
def initialize(attributes = {})
|
14
|
+
@attributes = attributes
|
15
|
+
initialize_engine
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_in_hardware?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def is_in_memory?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_key(modulus_bits=1024)
|
27
|
+
puts "Key generation is not currently supported in hardware"
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def private_key
|
32
|
+
initialize_engine
|
33
|
+
self.engine.load_private_key(self.token_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
def public_key
|
37
|
+
initialize_engine
|
38
|
+
self.engine.load_public_key(self.token_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def initialize_engine
|
44
|
+
## We're going to return early and try again later if params weren't passed in
|
45
|
+
## at initialization. Any attempt at getting a public/private key will try
|
46
|
+
## again.
|
47
|
+
return false if self.openssl_pkcs11_engine_lib.nil? or self.pkcs11_lib.nil?
|
48
|
+
return self.engine unless self.engine.nil?
|
49
|
+
OpenSSL::Engine.load
|
50
|
+
|
51
|
+
pkcs11 = OpenSSL::Engine.by_id("dynamic") do |e|
|
52
|
+
e.ctrl_cmd("SO_PATH",self.openssl_pkcs11_engine_lib)
|
53
|
+
e.ctrl_cmd("ID","pkcs11")
|
54
|
+
e.ctrl_cmd("LIST_ADD","1")
|
55
|
+
e.ctrl_cmd("LOAD")
|
56
|
+
e.ctrl_cmd("PIN",self.pin) unless self.pin.nil? or self.pin == ""
|
57
|
+
e.ctrl_cmd("MODULE_PATH",self.pkcs11_lib)
|
58
|
+
end
|
59
|
+
|
60
|
+
self.engine = pkcs11
|
61
|
+
pkcs11
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CertificateAuthority
|
2
|
+
module SigningEntity
|
3
|
+
|
4
|
+
def self.included(mod)
|
5
|
+
mod.class_eval do
|
6
|
+
attr_accessor :signing_entity
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def signing_entity=(val)
|
11
|
+
raise "invalid param" unless [true,false].include?(val)
|
12
|
+
@signing_entity = val
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'certificate_authority'
|
2
|
+
|
3
|
+
namespace :certificate_authority do
|
4
|
+
desc "Generate a quick self-signed cert"
|
5
|
+
task :self_signed do
|
6
|
+
|
7
|
+
cn = "http://localhost"
|
8
|
+
cn = ENV['DOMAIN'] unless ENV['DOMAIN'].nil?
|
9
|
+
|
10
|
+
root = CertificateAuthority::Certificate.new
|
11
|
+
root.subject.common_name= cn
|
12
|
+
root.key_material.generate_key
|
13
|
+
root.signing_entity = true
|
14
|
+
root.valid?
|
15
|
+
root.sign!
|
16
|
+
|
17
|
+
print "Your cert for #{cn}\n"
|
18
|
+
print root.to_pem
|
19
|
+
|
20
|
+
print "Your private key\n"
|
21
|
+
print root.key_material.private_key.to_pem
|
22
|
+
end
|
23
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/units_helper'
|
2
|
+
|
3
|
+
describe CertificateAuthority::CertificateRevocationList do
|
4
|
+
before(:each) do
|
5
|
+
@crl = CertificateAuthority::CertificateRevocationList.new
|
6
|
+
|
7
|
+
@root_certificate = CertificateAuthority::Certificate.new
|
8
|
+
@root_certificate.signing_entity = true
|
9
|
+
@root_certificate.subject.common_name = "CRL Root"
|
10
|
+
@root_certificate.key_material.generate_key(1024)
|
11
|
+
@root_certificate.serial_number.number = 1
|
12
|
+
@root_certificate.sign!
|
13
|
+
|
14
|
+
@certificate = CertificateAuthority::Certificate.new
|
15
|
+
@certificate.key_material.generate_key(1024)
|
16
|
+
@certificate.subject.common_name = "http://bogusSite.com"
|
17
|
+
@certificate.parent = @root_certificate
|
18
|
+
@certificate.serial_number.number = 2
|
19
|
+
@certificate.sign!
|
20
|
+
|
21
|
+
@crl.parent = @root_certificate
|
22
|
+
@certificate.revoked_at = Time.now
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should accept a list of certificates" do
|
26
|
+
@crl << @certificate
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should complain if you add a certificate without a revocation time" do
|
30
|
+
@certificate.revoked_at = nil
|
31
|
+
lambda{ @crl << @certificate}.should raise_error
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should have a 'parent' that will be responsible for signing" do
|
35
|
+
@crl.parent = @root_certificate
|
36
|
+
@crl.parent.should_not be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should raise an error if you try and sign a CRL without attaching a parent" do
|
40
|
+
@crl.parent = nil
|
41
|
+
lambda { @crl.sign! }.should raise_error
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should be able to generate a proper CRL" do
|
45
|
+
@crl << @certificate
|
46
|
+
lambda {@crl.to_pem}.should raise_error
|
47
|
+
@crl.parent = @root_certificate
|
48
|
+
@crl.sign!
|
49
|
+
@crl.to_pem.should_not be_nil
|
50
|
+
OpenSSL::X509::CRL.new(@crl.to_pem).should_not be_nil
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "Next update" do
|
54
|
+
it "should be able to set a 'next_update' value" do
|
55
|
+
@crl.next_update = (60 * 60 * 10) # 10 Hours
|
56
|
+
@crl.next_update.should_not be_nil
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should throw an error if we try and sign up with a negative next_update" do
|
60
|
+
@crl.sign!
|
61
|
+
@crl.next_update = - (60 * 60 * 10)
|
62
|
+
lambda{@crl.sign!}.should raise_error
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,428 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/units_helper'
|
2
|
+
|
3
|
+
describe CertificateAuthority::Certificate do
|
4
|
+
before(:each) do
|
5
|
+
@certificate = CertificateAuthority::Certificate.new
|
6
|
+
end
|
7
|
+
|
8
|
+
describe CertificateAuthority::SigningEntity do
|
9
|
+
it "should behave as a signing entity" do
|
10
|
+
@certificate.respond_to?(:is_signing_entity?).should be_true
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should only be a signing entity if it's identified as a CA", :rfc3280 => true do
|
14
|
+
@certificate.is_signing_entity?.should be_false
|
15
|
+
@certificate.signing_entity = true
|
16
|
+
@certificate.is_signing_entity?.should be_true
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "Root certificates" do
|
20
|
+
before(:each) do
|
21
|
+
@certificate.signing_entity = true
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should be able to be identified as a root certificate" do
|
25
|
+
@certificate.is_root_entity?.should be_true
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should only be a root certificate if the parent entity is itself", :rfc3280 => true do
|
29
|
+
@certificate.parent.should == @certificate
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should be a root certificate by default" do
|
33
|
+
@certificate.is_root_entity?.should be_true
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should be able to self-sign" do
|
37
|
+
@certificate.serial_number.number = 1
|
38
|
+
@certificate.subject.common_name = "chrischandler.name"
|
39
|
+
@certificate.key_material.generate_key(1024)
|
40
|
+
@certificate.sign!
|
41
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
42
|
+
cert.subject.to_s.should == cert.issuer.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should have the basicContraint CA:TRUE" do
|
46
|
+
@certificate.serial_number.number = 1
|
47
|
+
@certificate.subject.common_name = "chrischandler.name"
|
48
|
+
@certificate.key_material.generate_key(1024)
|
49
|
+
@certificate.sign!
|
50
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
51
|
+
cert.extensions.map{|i| [i.oid,i.value] }.select{|i| i.first == "basicConstraints"}.first[1].should == "CA:TRUE"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "Intermediate certificates" do
|
56
|
+
before(:each) do
|
57
|
+
@different_cert = CertificateAuthority::Certificate.new
|
58
|
+
@different_cert.signing_entity = true
|
59
|
+
@different_cert.subject.common_name = "chrischandler.name root"
|
60
|
+
@different_cert.key_material.generate_key(1024)
|
61
|
+
@different_cert.serial_number.number = 2
|
62
|
+
@different_cert.sign! #self-signed
|
63
|
+
@certificate.parent = @different_cert
|
64
|
+
@certificate.signing_entity = true
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be able to be identified as an intermediate certificate" do
|
68
|
+
@certificate.is_intermediate_entity?.should be_true
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should not be identified as a root" do
|
72
|
+
@certificate.is_root_entity?.should be_false
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should only be an intermediate certificate if the parent is a different entity" do
|
76
|
+
@certificate.parent.should_not == @certificate
|
77
|
+
@certificate.parent.should_not be_nil
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should correctly be signed by a parent certificate" do
|
81
|
+
@certificate.subject.common_name = "chrischandler.name"
|
82
|
+
@certificate.key_material.generate_key(1024)
|
83
|
+
@certificate.signing_entity = true
|
84
|
+
@certificate.serial_number.number = 1
|
85
|
+
@certificate.sign!
|
86
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
87
|
+
cert.subject.to_s.should_not == cert.issuer.to_s
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should have the basicContraint CA:TRUE" do
|
91
|
+
@certificate.subject.common_name = "chrischandler.name"
|
92
|
+
@certificate.key_material.generate_key(1024)
|
93
|
+
@certificate.signing_entity = true
|
94
|
+
@certificate.serial_number.number = 3
|
95
|
+
@certificate.sign!
|
96
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
97
|
+
cert.extensions.map{|i| [i.oid,i.value] }.select{|i| i.first == "basicConstraints"}.first[1].should == "CA:TRUE"
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "Terminal certificates" do
|
103
|
+
before(:each) do
|
104
|
+
@different_cert = CertificateAuthority::Certificate.new
|
105
|
+
@different_cert.signing_entity = true
|
106
|
+
@different_cert.subject.common_name = "chrischandler.name root"
|
107
|
+
@different_cert.key_material.generate_key(1024)
|
108
|
+
@different_cert.serial_number.number = 1
|
109
|
+
@different_cert.sign! #self-signed
|
110
|
+
@certificate.parent = @different_cert
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should not be identified as an intermediate certificate" do
|
114
|
+
@certificate.is_intermediate_entity?.should be_false
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should not be identified as a root" do
|
118
|
+
@certificate.is_root_entity?.should be_false
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should have the basicContraint CA:FALSE" do
|
122
|
+
@certificate.subject.common_name = "chrischandler.name"
|
123
|
+
@certificate.key_material.generate_key(1024)
|
124
|
+
@certificate.signing_entity = false
|
125
|
+
@certificate.serial_number.number = 1
|
126
|
+
@certificate.sign!
|
127
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
128
|
+
cert.extensions.map{|i| [i.oid,i.value] }.select{|i| i.first == "basicConstraints"}.first[1].should == "CA:FALSE"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
it "should be able to be identified as a root certificate" do
|
134
|
+
@certificate.respond_to?(:is_root_entity?).should be_true
|
135
|
+
end
|
136
|
+
end #End of SigningEntity
|
137
|
+
|
138
|
+
describe "Signed certificates" do
|
139
|
+
before(:each) do
|
140
|
+
@certificate = CertificateAuthority::Certificate.new
|
141
|
+
@certificate.subject.common_name = "chrischandler.name"
|
142
|
+
@certificate.key_material.generate_key(1024)
|
143
|
+
@certificate.serial_number.number = 1
|
144
|
+
@certificate.sign!
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should have a PEM encoded certificate body available" do
|
148
|
+
@certificate.to_pem.should_not be_nil
|
149
|
+
OpenSSL::X509::Certificate.new(@certificate.to_pem).should_not be_nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "X.509 V3 Extensions on Signed Certificates" do
|
154
|
+
before(:each) do
|
155
|
+
@certificate = CertificateAuthority::Certificate.new
|
156
|
+
@certificate.subject.common_name = "chrischandler.name"
|
157
|
+
@certificate.key_material.generate_key(1024)
|
158
|
+
@certificate.serial_number.number = 1
|
159
|
+
@signing_profile = {
|
160
|
+
"extensions" => {
|
161
|
+
"subjectAltName" => {"uris" => ["www.chrischandler.name"]},
|
162
|
+
"certificatePolicies" => {
|
163
|
+
"policy_identifier" => "1.3.5.7",
|
164
|
+
"cps_uris" => ["http://my.host.name/", "http://my.your.name/"],
|
165
|
+
"user_notice" => {
|
166
|
+
"explicit_text" => "Testing!", "organization" => "RSpec Test organization name", "notice_numbers" => "1,2,3,4"
|
167
|
+
}
|
168
|
+
}
|
169
|
+
}
|
170
|
+
}
|
171
|
+
@certificate.sign!(@signing_profile)
|
172
|
+
end
|
173
|
+
|
174
|
+
describe "SubjectAltName" do
|
175
|
+
before(:each) do
|
176
|
+
@certificate = CertificateAuthority::Certificate.new
|
177
|
+
@certificate.subject.common_name = "chrischandler.name"
|
178
|
+
@certificate.key_material.generate_key(1024)
|
179
|
+
@certificate.serial_number.number = 1
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should have a subjectAltName if specified" do
|
183
|
+
@certificate.sign!({"extensions" => {"subjectAltName" => {"uris" => ["www.chrischandler.name"]}}})
|
184
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
185
|
+
cert.extensions.map(&:oid).include?("subjectAltName").should be_true
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should NOT have a subjectAltName if one was not specified" do
|
189
|
+
@certificate.sign!
|
190
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
191
|
+
cert.extensions.map(&:oid).include?("subjectAltName").should be_false
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "AuthorityInfoAccess" do
|
196
|
+
before(:each) do
|
197
|
+
@certificate = CertificateAuthority::Certificate.new
|
198
|
+
@certificate.subject.common_name = "chrischandler.name"
|
199
|
+
@certificate.key_material.generate_key(1024)
|
200
|
+
@certificate.serial_number.number = 1
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should have an authority info access if specified" do
|
204
|
+
@certificate.sign!({"extensions" => {"authorityInfoAccess" => {"ocsp" => ["www.chrischandler.name"]}}})
|
205
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
206
|
+
cert.extensions.map(&:oid).include?("authorityInfoAccess").should be_true
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe "CrlDistributionPoints" do
|
211
|
+
before(:each) do
|
212
|
+
@certificate = CertificateAuthority::Certificate.new
|
213
|
+
@certificate.subject.common_name = "chrischandler.name"
|
214
|
+
@certificate.key_material.generate_key(1024)
|
215
|
+
@certificate.serial_number.number = 1
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should have a crlDistributionPoint if specified" do
|
219
|
+
@certificate.sign!({"extensions" => {"crlDistributionPoints" => {"uri" => ["http://crlThingy.com"]}}})
|
220
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
221
|
+
cert.extensions.map(&:oid).include?("crlDistributionPoints").should be_true
|
222
|
+
end
|
223
|
+
|
224
|
+
it "should NOT have a crlDistributionPoint if one was not specified" do
|
225
|
+
@certificate.sign!
|
226
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
227
|
+
cert.extensions.map(&:oid).include?("crlDistributionPoints").should be_false
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
describe "CertificatePolicies" do
|
233
|
+
before(:each) do
|
234
|
+
@certificate = CertificateAuthority::Certificate.new
|
235
|
+
@certificate.subject.common_name = "chrischandler.name"
|
236
|
+
@certificate.key_material.generate_key(1024)
|
237
|
+
@certificate.serial_number.number = 1
|
238
|
+
end
|
239
|
+
|
240
|
+
it "should have a certificatePolicy if specified" do
|
241
|
+
@certificate.sign!({
|
242
|
+
"extensions" => {
|
243
|
+
"certificatePolicies" => {
|
244
|
+
"policy_identifier" => "1.3.5.7",
|
245
|
+
"cps_uris" => ["http://my.host.name/", "http://my.your.name/"]
|
246
|
+
}
|
247
|
+
}
|
248
|
+
})
|
249
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
250
|
+
cert.extensions.map(&:oid).include?("certificatePolicies").should be_true
|
251
|
+
end
|
252
|
+
|
253
|
+
it "should contain a nested userNotice if specified" do
|
254
|
+
pending
|
255
|
+
# @certificate.sign!({
|
256
|
+
# "extensions" => {
|
257
|
+
# "certificatePolicies" => {
|
258
|
+
# "policy_identifier" => "1.3.5.7",
|
259
|
+
# "cps_uris" => ["http://my.host.name/", "http://my.your.name/"],
|
260
|
+
# "user_notice" => {
|
261
|
+
# "explicit_text" => "Testing!", "organization" => "RSpec Test organization name", "notice_numbers" => "1,2,3,4"
|
262
|
+
# }
|
263
|
+
# }
|
264
|
+
# }
|
265
|
+
# })
|
266
|
+
# cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
267
|
+
# cert.extensions.map(&:oid).include?("certificatePolicies").should be_true
|
268
|
+
end
|
269
|
+
|
270
|
+
it "should NOT include a certificatePolicy if not specified" do
|
271
|
+
@certificate.sign!
|
272
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
273
|
+
cert.extensions.map(&:oid).include?("certificatePolicies").should be_false
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
it "should support BasicContraints" do
|
279
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
280
|
+
cert.extensions.map(&:oid).include?("basicConstraints").should be_true
|
281
|
+
end
|
282
|
+
|
283
|
+
it "should support subjectKeyIdentifier" do
|
284
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
285
|
+
cert.extensions.map(&:oid).include?("subjectKeyIdentifier").should be_true
|
286
|
+
end
|
287
|
+
|
288
|
+
it "should support authorityKeyIdentifier" do
|
289
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
290
|
+
cert.extensions.map(&:oid).include?("authorityKeyIdentifier").should be_true
|
291
|
+
end
|
292
|
+
|
293
|
+
it "should order subjectKeyIdentifier before authorityKeyIdentifier" do
|
294
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
295
|
+
cert.extensions.map(&:oid).select do |oid|
|
296
|
+
["subjectKeyIdentifier", "authorityKeyIdentifier"].include?(oid)
|
297
|
+
end.should == ["subjectKeyIdentifier", "authorityKeyIdentifier"]
|
298
|
+
end
|
299
|
+
|
300
|
+
it "should support keyUsage" do
|
301
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
302
|
+
cert.extensions.map(&:oid).include?("keyUsage").should be_true
|
303
|
+
end
|
304
|
+
|
305
|
+
it "should support extendedKeyUsage" do
|
306
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
307
|
+
cert.extensions.map(&:oid).include?("extendedKeyUsage").should be_true
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe "Signing profile" do
|
312
|
+
before(:each) do
|
313
|
+
@certificate = CertificateAuthority::Certificate.new
|
314
|
+
@certificate.subject.common_name = "chrischandler.name"
|
315
|
+
@certificate.key_material.generate_key(1024)
|
316
|
+
@certificate.serial_number.number = 1
|
317
|
+
|
318
|
+
@signing_profile = {
|
319
|
+
"extensions" => {
|
320
|
+
"basicConstraints" => {"ca" => false},
|
321
|
+
"crlDistributionPoints" => {"uri" => "http://notme.com/other.crl" },
|
322
|
+
"subjectKeyIdentifier" => {},
|
323
|
+
"authorityKeyIdentifier" => {},
|
324
|
+
"authorityInfoAccess" => {"ocsp" => ["http://youFillThisOut/ocsp/"] },
|
325
|
+
"keyUsage" => {"usage" => ["digitalSignature","nonRepudiation"] },
|
326
|
+
"extendedKeyUsage" => {"usage" => [ "serverAuth","clientAuth"]},
|
327
|
+
"subjectAltName" => {"uris" => ["http://subdomains.youFillThisOut/"]},
|
328
|
+
"certificatePolicies" => {
|
329
|
+
"policy_identifier" => "1.3.5.8", "cps_uris" => ["http://my.host.name/", "http://my.your.name/"], "user_notice" => {
|
330
|
+
"explicit_text" => "Explicit Text Here", "organization" => "Organization name", "notice_numbers" => "1,2,3,4"
|
331
|
+
}
|
332
|
+
}
|
333
|
+
}
|
334
|
+
}
|
335
|
+
end
|
336
|
+
|
337
|
+
it "should be able to sign with an optional policy hash" do
|
338
|
+
@certificate.sign!(@signing_profile)
|
339
|
+
end
|
340
|
+
|
341
|
+
it "should support a default signing digest of SHA512" do
|
342
|
+
@certificate.sign!(@signing_profile)
|
343
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
344
|
+
cert.signature_algorithm.should == "sha512WithRSAEncryption"
|
345
|
+
end
|
346
|
+
|
347
|
+
it "should support a configurable digest algorithm" do
|
348
|
+
@signing_profile.merge!({"digest" => "SHA1"})
|
349
|
+
@certificate.sign!(@signing_profile)
|
350
|
+
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
|
351
|
+
cert.signature_algorithm.should == "sha1WithRSAEncryption"
|
352
|
+
end
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
describe "from_openssl" do
|
357
|
+
before(:each) do
|
358
|
+
@pem_cert=<<CERT
|
359
|
+
-----BEGIN CERTIFICATE-----
|
360
|
+
MIICFDCCAc6gAwIBAgIJAPDLgMilKuayMA0GCSqGSIb3DQEBBQUAMEgxCzAJBgNV
|
361
|
+
BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMQowCAYDVQQKEwEgMRgwFgYDVQQD
|
362
|
+
Ew9WZXJ5IFNtYWxsIENlcnQwHhcNMTIwNTAzMDMyODI1WhcNMTMwNTAzMDMyODI1
|
363
|
+
WjBIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEKMAgGA1UEChMB
|
364
|
+
IDEYMBYGA1UEAxMPVmVyeSBTbWFsbCBDZXJ0MEwwDQYJKoZIhvcNAQEBBQADOwAw
|
365
|
+
OAIxAN6+33+WQ3FBMt+vMhshxOj+8W7V64pDKCJ3pVlnSn36imBWqrN0AGWX8qjv
|
366
|
+
S+GzGwIDAQABo4GqMIGnMB0GA1UdDgQWBBRMUQ/HpPrAkKOufS5h+xPtEuzyWDB4
|
367
|
+
BgNVHSMEcTBvgBRMUQ/HpPrAkKOufS5h+xPtEuzyWKFMpEowSDELMAkGA1UEBhMC
|
368
|
+
VVMxEzARBgNVBAgTClNvbWUtU3RhdGUxCjAIBgNVBAoTASAxGDAWBgNVBAMTD1Zl
|
369
|
+
cnkgU21hbGwgQ2VydIIJAPDLgMilKuayMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN
|
370
|
+
AQEFBQADMQAq0CsqEChn4uf6MkXYBwaAAmS3JLmagyliJe5zM3y8dZz6Em2Ugb8o
|
371
|
+
1cCaKaHJHSg=
|
372
|
+
-----END CERTIFICATE-----
|
373
|
+
CERT
|
374
|
+
@openssl_cert = OpenSSL::X509::Certificate.new @pem_cert
|
375
|
+
@small_cert = CertificateAuthority::Certificate.from_openssl @openssl_cert
|
376
|
+
end
|
377
|
+
|
378
|
+
it "should reject non-Certificate arguments" do
|
379
|
+
lambda { CertificateAuthority::Certificate.from_openssl "a string" }.should raise_error
|
380
|
+
end
|
381
|
+
|
382
|
+
it "should only be missing a private key" do
|
383
|
+
@small_cert.should_not be_valid
|
384
|
+
@small_cert.key_material.private_key = "data"
|
385
|
+
@small_cert.should be_valid
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
it "should have a distinguished name" do
|
390
|
+
@certificate.distinguished_name.should_not be_nil
|
391
|
+
end
|
392
|
+
|
393
|
+
it "should have a serial number" do
|
394
|
+
@certificate.serial_number.should_not be_nil
|
395
|
+
end
|
396
|
+
|
397
|
+
it "should have a subject" do
|
398
|
+
@certificate.subject.should_not be_nil
|
399
|
+
end
|
400
|
+
|
401
|
+
it "should be able to have a parent entity" do
|
402
|
+
@certificate.respond_to?(:parent).should be_true
|
403
|
+
end
|
404
|
+
|
405
|
+
it "should have key material" do
|
406
|
+
@certificate.key_material.should_not be_nil
|
407
|
+
end
|
408
|
+
|
409
|
+
it "should have a not_before field" do
|
410
|
+
@certificate.not_before.should_not be_nil
|
411
|
+
end
|
412
|
+
|
413
|
+
it "should have a not_after field" do
|
414
|
+
@certificate.not_after.should_not be_nil
|
415
|
+
end
|
416
|
+
|
417
|
+
it "should default to one year validity" do
|
418
|
+
@certificate.not_after.should < Time.now + 65 * 60 * 24 * 365 and
|
419
|
+
@certificate.not_after.should > Time.now + 55 * 60 * 24 * 365
|
420
|
+
end
|
421
|
+
|
422
|
+
it "should be able to have a revoked at time" do
|
423
|
+
@certificate.revoked?.should be_false
|
424
|
+
@certificate.revoked_at = Time.now
|
425
|
+
@certificate.revoked?.should be_true
|
426
|
+
end
|
427
|
+
|
428
|
+
end
|