[DRE-commits] [ruby-certificate-authority] 02/07: New upstream version 0.2.0~6dd483bf
Micah Anderson
micah at moszumanska.debian.org
Mon Dec 5 16:01:57 UTC 2016
This is an automated email from the git hooks/post-receive script.
micah pushed a commit to branch master
in repository ruby-certificate-authority.
commit db8d471673b1e641577febd29bd2c69377bf5015
Author: Micah Anderson <micah at riseup.net>
Date: Thu Dec 1 23:07:52 2016 -0500
New upstream version 0.2.0~6dd483bf
---
.gitignore | 6 +
.travis.yml | 3 +
Gemfile | 13 +-
Gemfile.lock | 61 +--
README.rdoc | 93 ++++-
Rakefile | 6 +-
VERSION.yml | 4 +-
certificate_authority.gemspec | 39 +-
lib/certificate_authority.rb | 5 +-
lib/certificate_authority/certificate.rb | 127 ++++--
.../certificate_revocation_list.rb | 48 ++-
lib/certificate_authority/core_extensions.rb | 46 +++
lib/certificate_authority/distinguished_name.rb | 80 +++-
lib/certificate_authority/extensions.rb | 460 +++++++++++++++++++--
lib/certificate_authority/key_material.rb | 39 +-
lib/certificate_authority/ocsp_handler.rb | 77 +++-
lib/certificate_authority/pkcs11_key_material.rb | 2 -
lib/certificate_authority/revocable.rb | 14 +
lib/certificate_authority/serial_number.rb | 17 +-
lib/certificate_authority/signing_request.rb | 91 ++++
lib/certificate_authority/validations.rb | 31 ++
metadata.yml | 111 -----
spec/samples/certs/DigiCertHighAssuranceEVCA-1.pem | 115 ++++++
spec/samples/certs/apple_wwdr_issued_cert.pem | 112 +++++
spec/samples/certs/apple_wwdr_issuer.pem | 89 ++++
spec/samples/certs/ca.crt | 130 ++++++
spec/samples/certs/ca.key | 51 +++
spec/samples/certs/client.crt | 78 ++++
spec/samples/certs/client.csr | 11 +
spec/samples/certs/client.key | 15 +
spec/samples/certs/github.com.pem | 122 ++++++
spec/samples/certs/server.crt | 79 ++++
spec/samples/certs/server.csr | 13 +
spec/samples/certs/server.key | 15 +
spec/spec_helper.rb | 12 +-
spec/units/certificate_revocation_list_spec.rb | 67 ++-
spec/units/certificate_spec.rb | 134 ++++--
spec/units/distinguished_name_spec.rb | 17 +-
spec/units/extensions_spec.rb | 80 +++-
spec/units/key_material_spec.rb | 89 +++-
spec/units/ocsp_handler_spec.rb | 121 +++++-
spec/units/signing_entity_spec.rb | 2 +-
spec/units/signing_request_spec.rb | 154 +++++++
spec/units/working_with_openssl_spec.rb | 136 ++++++
44 files changed, 2671 insertions(+), 344 deletions(-)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5533fdb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+pkg
+development
+.bundle
+.rvmrc
+coverage
+doc
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..11a115a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: ruby
+rvm:
+ - 1.9.3
diff --git a/Gemfile b/Gemfile
index 3f97a03..a1c7893 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,9 +1,12 @@
-source 'http://rubygems.org'
-
-gem 'activemodel', ">= 3.0.6"
+source 'https://rubygems.org'
group :development do
- gem 'rspec'
+ gem 'rspec'
gem "jeweler", ">= 1.5.2"
- #gem "rcov", ">= 0"
end
+
+group :test do
+ gem 'rake'
+ gem 'coveralls', require: false
+end
+
diff --git a/Gemfile.lock b/Gemfile.lock
index 0439385..caa748b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,39 +1,54 @@
GEM
- remote: http://rubygems.org/
+ remote: https://rubygems.org/
specs:
- activemodel (3.2.8)
- activesupport (= 3.2.8)
- builder (~> 3.0.0)
- activesupport (3.2.8)
- i18n (~> 0.6)
- multi_json (~> 1.0)
- builder (3.0.0)
- diff-lcs (1.1.3)
+ coveralls (0.7.0)
+ multi_json (~> 1.3)
+ rest-client
+ simplecov (>= 0.7)
+ term-ansicolor
+ thor
+ diff-lcs (1.2.4)
+ docile (1.1.2)
git (1.2.5)
- i18n (0.6.0)
jeweler (1.8.4)
bundler (~> 1.0)
git (>= 1.2.5)
rake
rdoc
- json (1.7.4)
- multi_json (1.3.6)
- rake (0.9.2.2)
- rdoc (3.12)
+ json (1.8.0)
+ mime-types (2.0)
+ multi_json (1.7.4)
+ rake (10.0.4)
+ rdoc (4.0.1)
json (~> 1.4)
- rspec (2.11.0)
- rspec-core (~> 2.11.0)
- rspec-expectations (~> 2.11.0)
- rspec-mocks (~> 2.11.0)
- rspec-core (2.11.1)
- rspec-expectations (2.11.2)
- diff-lcs (~> 1.1.3)
- rspec-mocks (2.11.2)
+ rest-client (1.6.7)
+ mime-types (>= 1.16)
+ rspec (2.13.0)
+ rspec-core (~> 2.13.0)
+ rspec-expectations (~> 2.13.0)
+ rspec-mocks (~> 2.13.0)
+ rspec-core (2.13.1)
+ rspec-expectations (2.13.0)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rspec-mocks (2.13.1)
+ simplecov (0.8.2)
+ docile (~> 1.1.0)
+ multi_json
+ simplecov-html (~> 0.8.0)
+ simplecov-html (0.8.0)
+ term-ansicolor (1.2.2)
+ tins (~> 0.8)
+ thor (0.18.1)
+ tins (0.13.1)
PLATFORMS
ruby
DEPENDENCIES
- activemodel (>= 3.0.6)
+ coveralls
jeweler (>= 1.5.2)
+ rake
rspec
+
+BUNDLED WITH
+ 1.11.2
diff --git a/README.rdoc b/README.rdoc
index 979a2b1..7a44122 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -1,5 +1,9 @@
= CertificateAuthority - Because it shouldn't be this damned complicated
+{<img src="https://travis-ci.org/cchandler/certificate_authority.png?branch=master" alt="Build Status" />}[https://travis-ci.org/cchandler/certificate_authority]
+{<img src="https://codeclimate.com/github/cchandler/certificate_authority.png" alt="Code Climate" />}[https://codeclimate.com/github/cchandler/certificate_authority]
+{<img src="https://coveralls.io/repos/cchandler/certificate_authority/badge.png?branch=master" alt="Code Coverage" />}[https://coveralls.io/r/cchandler/certificate_authority]
+
This is meant to provide a (more) programmer-friendly implementation of all the basic functionality contained in RFC-3280 to implement your own certificate authority.
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.
@@ -60,7 +64,7 @@ Maybe you don't want to actually sign certificates with your super-secret root c
signing_profile = {"extensions" => {"keyUsage" => {"usage" => ["critical", "keyCertSign"] }} }
intermediate.sign!(signing_profile)
-All we have to do is create another certificate like we did with the root. In this example we gave it the next available serial number which for us, was 2. We then generate (and save!) key material for this new entity. Even the +signing_entity+ is set to true so this certificate can sign other certificates. The difference here is that the +parent+ field is set to the root. Going forward, whatever entity you want to sign a certificate, you set that entity to be the parent. In this case [...]
+All we have to do is create another certificate like we did with the root. In this example we gave it the next available serial number, which for us, was 2. We then generate (and save!) key material for this new entity. Even the +signing_entity+ is set to true so this certificate can sign other certificates. The difference here is that the +parent+ field is set to the root. Going forward, whatever entity you want to sign a certificate, you set that entity to be the parent. In this cas [...]
= Creating new certificates (in general)
@@ -193,6 +197,78 @@ These CPSs define what vetting criteria and maintenance practices are required t
[user_notice] This is a nested field containing explicit human readable text if you want to embed a notice in the certificate body related to certification practices. It contains nested attributes of +explicit_text+ for the notice, +organization+ and +notice_numbers+. Refer to the RFC for specific implications of how these are set, but whether or not browsers implement the correct specified behavior for their presence is another issue.
+= Certificate Signing Requests (CSRs)
+
+If you want certificate requestors to be able to request certificates without moving the private key you'll need to generate a CSR and submit it to the certificate authority.
+
+Here's an example of using +certificate_authority+ to generate a CSR.
+
+ csr = CertificateAuthority::SigningRequest.new
+ dn = CertificateAuthority::DistinguishedName.new
+ dn.common_name = "localhost"
+ csr.distinguished_name = dn
+ k = CertificateAuthority::MemoryKeyMaterial.new
+ k.generate_key(2048)
+ csr.key_material = k
+ csr.digest = "SHA256"
+ csr.to_x509_csr.to_pem
+
+Similarly, reading a CSR in is as simple as providing the PEM formatted version to +SigningRequest.from_x509_csr+.
+
+ csr = CertificateAuthority::SigningRequest.from_x509_csr(@pem_csr)
+
+Once you have the CSR in the form of a +SigningRequest+ you can transform it to a +Certificate+ with +to_cert+. At this point it works just like any other certificate. You'll have to provide a serial number to actually sign it.
+
+= Certificate Revocation Lists (CRLs)
+
+Revocation lists let clients know when a certificate in the wild should no longer be trusted.
+
+Like end-user certificates, CRLs have to be signed by a signing authority to be valid. Additionally, you will need to furnish a +nextUpdate+ value that indicates to the client when it should look for updates to the CRL and how long it should consider a cached value valid.
+
+Ideally you would place the result CRL somewhere generally accessible on the Internet and reference the URI in the +crlDistributionPoints+ extension on issued certificates.
+
+ crl = CertificateAuthority::CertificateRevocationList.new
+ crl << certificate # Some CertificateAuthority::Certificate
+ crl << serial_number # Also works with plain CertificateAuthority::SerialNumber
+ crl.parent = root_certificate # A valid root
+ crl.next_update = (60 * 60 * 10) # 10 Hours
+ crl.sign!
+ crl.to_pem
+
+= OCSP Support
+
+OCSP is the Online Certificate Status Protocol. It provides a mechanism to query an authority to see if a certificate is still valid without downloading an entire CRL. To use this mechanism you provide a URI in the Authority Information Access extension.
+If a client wishes to check the validity of a certificate they can query this endpoint.
+This request will only contain serial numbers, so you'll need to uniquely identify your authority in the AIA path.
+
+If a client sends you a DER encoded OCSP request you can read it out via +OCSPRequestReader+
+
+ ocsp_request_reader = CertificateAuthority::OCSPRequestReader.from_der(@ocsp_request.to_der)
+ ocsp_request_reader.serial_numbers
+
+Then, you can construct a response like this
+
+ response_builder = CertificateAuthority::OCSPResponseBuilder.from_request_reader(ocsp_request_reader)
+ response_builder.parent = root
+ response = response_builder.build_response # Returns OpenSSL::OCSP::Response
+ response.to_der
+
+The response builder will copy a (possible) nonce from the request. By default, the +OCSPResponseBuilder+ will say that every certificate is GOOD.
+You should definitely override this if you plan on revoking certificates.
+If you want to override this you'll need to supply a proc/lambda that takes a serial number and returns an array of status and reason.
+
+ response_builder = CertificateAuthority::OCSPResponseBuilder.from_request_reader(ocsp_request_reader)
+ response_builder.verification_mechanism = lambda {|certid|
+ [CertificateAuthority::OCSPResponseBuilder::REVOKED,CertificateAuthority::OCSPResponseBuilder::UNSPECIFIED]
+ }
+ response_builder.parent = root
+ response = response_builder.build_response # Response will say everything is revoked for unspecified reasons
+
+Lastly, you can configure a nextUpdate time in the response. This is the length of time for which a client may consider this response valid.
+The default is 15 minutes.
+
+ response_builder.next_update = 30 * 60 # 30 minutes
+
= PKCS#11 Support
If you happen to have a PKCS#11 compliant hardware token you can use +certificate_authority+ to maintain private key materials in hardware security modules. At this point the scope of operating that hardware is out of scope of this README but it's there and it is supported.
@@ -223,15 +299,28 @@ Your current version of OpenSSL _must_ include dynamic engine support and you wi
Also of note, I have gotten these to work with 32-bit copies of Ubuntu 10.10 and pre-Snow Leopard versions of OS X. If you are running Snow Leopard you're out of luck since none of the companies I've contacted make a 64 bit driver.
-= Coming Soon
+= Hopefully in the future
* More PKCS#11 hardware (I need driver support from the manufacturers)
+
+= Todone
+
* Support for working with CSRs to request & issue certificates
+* OCSP support
= Misc notes
* Firefox will complain about root/intermediate certificates unless both digitalSignature and keyEncipherment are specified as keyUsage attributes. Thanks diogomonica
+= Special thanks and Contributions
+
+* Diogo Monica @diogo
+* Justin Cummins @sul3n3t
+* @databus23
+* Colin Jones @trptcolin
+* Eric Monti @emonti
+* TJ Vanderpoel @bougyman
+
== Meta
Written by Chris Chandler(http://chrischandler.name)
diff --git a/Rakefile b/Rakefile
index bdcfc1e..8f4dd26 100644
--- a/Rakefile
+++ b/Rakefile
@@ -20,14 +20,12 @@ require 'jeweler'
Jeweler::Tasks.new do |gem|
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
gem.name = "certificate_authority"
- gem.homepage = "http://github.com/cchandler/certificate_authority"
+ gem.homepage = "https://github.com/cchandler/certificate_authority"
gem.license = "MIT"
gem.summary = 'Ruby gem for managing the core functions outlined in RFC-3280 for PKI'
# gem.description = ''
- gem.email = "chris at flatterline.com"
+ gem.email = "squanderingtime at gmail.com"
gem.authors = ["Chris Chandler"]
-
- # gem.add_dependency('activemodel', '3.0.6')
end
Jeweler::RubygemsDotOrgTasks.new
diff --git a/VERSION.yml b/VERSION.yml
index 295ba34..437bf68 100644
--- a/VERSION.yml
+++ b/VERSION.yml
@@ -1,5 +1,5 @@
---
:major: 0
-:minor: 1
-:patch: 6
+:minor: 2
+:patch: 0
:build:
diff --git a/certificate_authority.gemspec b/certificate_authority.gemspec
index c8c45e2..4768a5f 100644
--- a/certificate_authority.gemspec
+++ b/certificate_authority.gemspec
@@ -2,19 +2,22 @@
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
# -*- encoding: utf-8 -*-
+# stub: certificate_authority 0.2.0 ruby lib
Gem::Specification.new do |s|
s.name = "certificate_authority"
- s.version = "0.1.6"
+ s.version = "0.2.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.require_paths = ["lib"]
s.authors = ["Chris Chandler"]
- s.date = "2012-08-12"
- s.email = "chris at flatterline.com"
+ s.date = "2016-06-21"
+ s.email = "squanderingtime at gmail.com"
s.extra_rdoc_files = [
"README.rdoc"
]
s.files = [
+ ".travis.yml",
"Gemfile",
"Gemfile.lock",
"README.rdoc",
@@ -24,14 +27,30 @@ Gem::Specification.new do |s|
"lib/certificate_authority.rb",
"lib/certificate_authority/certificate.rb",
"lib/certificate_authority/certificate_revocation_list.rb",
+ "lib/certificate_authority/core_extensions.rb",
"lib/certificate_authority/distinguished_name.rb",
"lib/certificate_authority/extensions.rb",
"lib/certificate_authority/key_material.rb",
"lib/certificate_authority/ocsp_handler.rb",
"lib/certificate_authority/pkcs11_key_material.rb",
+ "lib/certificate_authority/revocable.rb",
"lib/certificate_authority/serial_number.rb",
"lib/certificate_authority/signing_entity.rb",
+ "lib/certificate_authority/signing_request.rb",
+ "lib/certificate_authority/validations.rb",
"lib/tasks/certificate_authority.rake",
+ "spec/samples/certs/DigiCertHighAssuranceEVCA-1.pem",
+ "spec/samples/certs/apple_wwdr_issued_cert.pem",
+ "spec/samples/certs/apple_wwdr_issuer.pem",
+ "spec/samples/certs/ca.crt",
+ "spec/samples/certs/ca.key",
+ "spec/samples/certs/client.crt",
+ "spec/samples/certs/client.csr",
+ "spec/samples/certs/client.key",
+ "spec/samples/certs/github.com.pem",
+ "spec/samples/certs/server.crt",
+ "spec/samples/certs/server.csr",
+ "spec/samples/certs/server.key",
"spec/spec_helper.rb",
"spec/units/certificate_authority_spec.rb",
"spec/units/certificate_revocation_list_spec.rb",
@@ -43,28 +62,26 @@ Gem::Specification.new do |s|
"spec/units/pkcs11_key_material_spec.rb",
"spec/units/serial_number_spec.rb",
"spec/units/signing_entity_spec.rb",
- "spec/units/units_helper.rb"
+ "spec/units/signing_request_spec.rb",
+ "spec/units/units_helper.rb",
+ "spec/units/working_with_openssl_spec.rb"
]
- s.homepage = "http://github.com/cchandler/certificate_authority"
+ s.homepage = "https://github.com/cchandler/certificate_authority"
s.licenses = ["MIT"]
- s.require_paths = ["lib"]
- s.rubygems_version = "1.8.15"
+ s.rubygems_version = "2.2.2"
s.summary = "Ruby gem for managing the core functions outlined in RFC-3280 for PKI"
if s.respond_to? :specification_version then
- s.specification_version = 3
+ s.specification_version = 4
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
- s.add_runtime_dependency(%q<activemodel>, [">= 3.0.6"])
s.add_development_dependency(%q<rspec>, [">= 0"])
s.add_development_dependency(%q<jeweler>, [">= 1.5.2"])
else
- s.add_dependency(%q<activemodel>, [">= 3.0.6"])
s.add_dependency(%q<rspec>, [">= 0"])
s.add_dependency(%q<jeweler>, [">= 1.5.2"])
end
else
- s.add_dependency(%q<activemodel>, [">= 3.0.6"])
s.add_dependency(%q<rspec>, [">= 0"])
s.add_dependency(%q<jeweler>, [">= 1.5.2"])
end
diff --git a/lib/certificate_authority.rb b/lib/certificate_authority.rb
index bada423..c52e4b6 100644
--- a/lib/certificate_authority.rb
+++ b/lib/certificate_authority.rb
@@ -2,10 +2,12 @@ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) ||
#Exterior requirements
require 'openssl'
-require 'active_model'
#Internal modules
+require 'certificate_authority/core_extensions'
require 'certificate_authority/signing_entity'
+require 'certificate_authority/revocable'
+require 'certificate_authority/validations'
require 'certificate_authority/distinguished_name'
require 'certificate_authority/serial_number'
require 'certificate_authority/key_material'
@@ -14,6 +16,7 @@ require 'certificate_authority/extensions'
require 'certificate_authority/certificate'
require 'certificate_authority/certificate_revocation_list'
require 'certificate_authority/ocsp_handler'
+require 'certificate_authority/signing_request'
module CertificateAuthority
end
diff --git a/lib/certificate_authority/certificate.rb b/lib/certificate_authority/certificate.rb
index 412398d..cdf432c 100644
--- a/lib/certificate_authority/certificate.rb
+++ b/lib/certificate_authority/certificate.rb
@@ -1,14 +1,13 @@
module CertificateAuthority
class Certificate
- # include SigningEntity
- include ActiveModel::Validations
+ include Validations
+ include Revocable
attr_accessor :distinguished_name
attr_accessor :serial_number
attr_accessor :key_material
attr_accessor :not_before
attr_accessor :not_after
- attr_accessor :revoked_at
attr_accessor :extensions
attr_accessor :openssl_body
@@ -16,7 +15,7 @@ module CertificateAuthority
attr_accessor :parent
- validate do |certificate|
+ def validate
errors.add :base, "Distinguished name must be valid" unless distinguished_name.valid?
errors.add :base, "Key material must be valid" unless key_material.valid?
errors.add :base, "Serial number must be valid" unless serial_number.valid?
@@ -33,8 +32,8 @@ module CertificateAuthority
self.distinguished_name = DistinguishedName.new
self.serial_number = SerialNumber.new
self.key_material = MemoryKeyMaterial.new
- self.not_before = Time.now
- self.not_after = Time.now + 60 * 60 * 24 * 365 #One year
+ self.not_before = Date.today.utc
+ self.not_after = Date.today.advance(:years => 1).utc
self.parent = self
self.extensions = load_extensions()
@@ -42,12 +41,31 @@ module CertificateAuthority
end
+=begin
+ def self.from_openssl openssl_cert
+ unless openssl_cert.is_a? OpenSSL::X509::Certificate
+ raise "Can only construct from an OpenSSL::X509::Certificate"
+ end
+
+ certificate = Certificate.new
+ # Only subject, key_material, and body are used for signing
+ certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject
+ certificate.key_material.public_key = openssl_cert.public_key
+ certificate.openssl_body = openssl_cert
+ certificate.serial_number.number = openssl_cert.serial.to_i
+ certificate.not_before = openssl_cert.not_before
+ certificate.not_after = openssl_cert.not_after
+ # TODO extensions
+ certificate
+ end
+=end
+
def sign!(signing_profile={})
raise "Invalid certificate #{self.errors.full_messages}" unless valid?
merge_profile_with_extensions(signing_profile)
openssl_cert = OpenSSL::X509::Certificate.new
- openssl_cert.version = 2
+ openssl_cert.version = 2
openssl_cert.not_before = self.not_before
openssl_cert.not_after = self.not_after
openssl_cert.public_key = self.key_material.public_key
@@ -59,7 +77,6 @@ module CertificateAuthority
require 'tempfile'
t = Tempfile.new("bullshit_conf")
- # t = File.new("/tmp/openssl.cnf")
## The config requires a file even though we won't use it
openssl_config = OpenSSL::Config.new(t.path)
@@ -86,18 +103,19 @@ module CertificateAuthority
self.extensions.keys.sort{|a,b| b<=>a}.each do |k|
e = extensions[k]
next if e.to_s.nil? or e.to_s == "" ## If the extension returns an empty string we won't include it
- ext = factory.create_ext(e.openssl_identifier, e.to_s)
+ ext = factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
openssl_cert.add_extension(ext)
end
if signing_profile["digest"].nil?
- digest = OpenSSL::Digest::Digest.new("SHA512")
+ digest = OpenSSL::Digest.new("SHA512")
else
- digest = OpenSSL::Digest::Digest.new(signing_profile["digest"])
+ digest = OpenSSL::Digest.new(signing_profile["digest"])
end
- self.openssl_body = openssl_cert.sign(parent.key_material.private_key,digest)
- t.close! if t.is_a?(Tempfile)# We can get rid of the ridiculous temp file
- self.openssl_body
+
+ self.openssl_body = openssl_cert.sign(parent.key_material.private_key, digest)
+ ensure
+ t.close! if t # We can get rid of the ridiculous temp file
end
def is_signing_entity?
@@ -117,6 +135,34 @@ module CertificateAuthority
self.openssl_body.to_pem
end
+ def to_csr
+ csr = SigningRequest.new
+ csr.distinguished_name = self.distinguished_name
+ csr.key_material = self.key_material
+ factory = OpenSSL::X509::ExtensionFactory.new
+ exts = []
+ self.extensions.keys.each do |k|
+ ## Don't copy over key identifiers for CSRs
+ next if k == "subjectKeyIdentifier" || k == "authorityKeyIdentifier"
+ e = extensions[k]
+ ## If the extension returns an empty string we won't include it
+ next if e.to_s.nil? or e.to_s == ""
+ exts << factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
+ end
+ attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)])
+ attrs = [
+ OpenSSL::X509::Attribute.new("extReq", attrval),
+ OpenSSL::X509::Attribute.new("msExtReq", attrval)
+ ]
+ csr.attributes = attrs
+ csr
+ end
+
+ def self.from_x509_cert(raw_cert)
+ openssl_cert = OpenSSL::X509::Certificate.new(raw_cert)
+ Certificate.from_openssl(openssl_cert)
+ end
+
def is_root_entity?
self.parent == self && is_signing_entity?
end
@@ -135,6 +181,16 @@ module CertificateAuthority
items = signing_config[k]
items.keys.each do |profile_item_key|
if extension.respond_to?("#{profile_item_key}=".to_sym)
+ if k == 'subjectAltName' && profile_item_key == 'emails'
+ items[profile_item_key].map do |email|
+ if email == 'email:copy'
+ fail "no email address provided for subject: #{subject.to_x509_name}" unless subject.email_address
+ "email:#{subject.email_address}"
+ else
+ email
+ end
+ end
+ end
extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] )
else
p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!"
@@ -143,30 +199,25 @@ module CertificateAuthority
end
end
+ # Enumeration of the extensions. Not the worst option since
+ # the likelihood of these needing to be updated is low at best.
+ EXTENSIONS = [
+ CertificateAuthority::Extensions::BasicConstraints,
+ CertificateAuthority::Extensions::CrlDistributionPoints,
+ CertificateAuthority::Extensions::SubjectKeyIdentifier,
+ CertificateAuthority::Extensions::AuthorityKeyIdentifier,
+ CertificateAuthority::Extensions::AuthorityInfoAccess,
+ CertificateAuthority::Extensions::KeyUsage,
+ CertificateAuthority::Extensions::ExtendedKeyUsage,
+ CertificateAuthority::Extensions::SubjectAlternativeName,
+ CertificateAuthority::Extensions::CertificatePolicies
+ ]
+
def load_extensions
extension_hash = {}
- temp_extensions = []
- basic_constraints = CertificateAuthority::Extensions::BasicContraints.new
- temp_extensions << basic_constraints
- crl_distribution_points = CertificateAuthority::Extensions::CrlDistributionPoints.new
- temp_extensions << crl_distribution_points
- subject_key_identifier = CertificateAuthority::Extensions::SubjectKeyIdentifier.new
- temp_extensions << subject_key_identifier
- authority_key_identifier = CertificateAuthority::Extensions::AuthorityKeyIdentifier.new
- temp_extensions << authority_key_identifier
- authority_info_access = CertificateAuthority::Extensions::AuthorityInfoAccess.new
- temp_extensions << authority_info_access
- key_usage = CertificateAuthority::Extensions::KeyUsage.new
- temp_extensions << key_usage
- extended_key_usage = CertificateAuthority::Extensions::ExtendedKeyUsage.new
- temp_extensions << extended_key_usage
- subject_alternative_name = CertificateAuthority::Extensions::SubjectAlternativeName.new
- temp_extensions << subject_alternative_name
- certificate_policies = CertificateAuthority::Extensions::CertificatePolicies.new
- temp_extensions << certificate_policies
-
- temp_extensions.each do |extension|
+ EXTENSIONS.each do |klass|
+ extension = klass.new
extension_hash[extension.openssl_identifier] = extension
end
@@ -193,7 +244,11 @@ module CertificateAuthority
certificate.serial_number.number = openssl_cert.serial.to_i
certificate.not_before = openssl_cert.not_before
certificate.not_after = openssl_cert.not_after
- # TODO extensions
+ EXTENSIONS.each do |klass|
+ _,v,c = (openssl_cert.extensions.detect { |e| e.to_a.first == klass::OPENSSL_IDENTIFIER } || []).to_a
+ certificate.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v
+ end
+
certificate
end
diff --git a/lib/certificate_authority/certificate_revocation_list.rb b/lib/certificate_authority/certificate_revocation_list.rb
index 4118123..cb3aaf7 100644
--- a/lib/certificate_authority/certificate_revocation_list.rb
+++ b/lib/certificate_authority/certificate_revocation_list.rb
@@ -1,36 +1,52 @@
module CertificateAuthority
class CertificateRevocationList
- include ActiveModel::Validations
+ include Validations
attr_accessor :certificates
attr_accessor :parent
attr_accessor :crl_body
attr_accessor :next_update
+ attr_accessor :last_update_skew_seconds
- validate do |crl|
- errors.add :next_update, "Next update must be a positive value" if crl.next_update < 0
- errors.add :parent, "A parent entity must be set" if crl.parent.nil?
+ def validate
+ errors.add :next_update, "Next update must be a positive value" if self.next_update < 0
+ errors.add :parent, "A parent entity must be set" if self.parent.nil?
end
def initialize
self.certificates = []
self.next_update = 60 * 60 * 4 # 4 hour default
+ self.last_update_skew_seconds = 0
end
- def <<(cert)
- raise "Only revoked certificates can be added to a CRL" unless cert.revoked?
- self.certificates << cert
+ def <<(revocable)
+ case revocable
+ when Revocable
+ raise "Only revoked entities can be added to a CRL" unless revocable.revoked?
+ self.certificates << revocable
+ when OpenSSL::X509::Certificate
+ raise "Not implemented yet"
+ else
+ raise "#{revocable.class} cannot be included in a CRL"
+ end
end
- def sign!
+ def sign!(signing_profile={})
raise "No parent entity has been set!" if self.parent.nil?
raise "Invalid CRL" unless self.valid?
- revocations = self.certificates.collect do |certificate|
+ revocations = self.certificates.collect do |revocable|
revocation = OpenSSL::X509::Revoked.new
- x509_cert = OpenSSL::X509::Certificate.new(certificate.to_pem)
- revocation.serial = x509_cert.serial
- revocation.time = certificate.revoked_at
+
+ ## We really just need a serial number, now we have to dig it out
+ case revocable
+ when Certificate
+ x509_cert = OpenSSL::X509::Certificate.new(revocable.to_pem)
+ revocation.serial = x509_cert.serial
+ when SerialNumber
+ revocation.serial = revocable.number
+ end
+ revocation.time = revocable.revoked_at
revocation
end
@@ -40,11 +56,15 @@ module CertificateAuthority
end
crl.version = 1
- crl.last_update = Time.now
+ crl.last_update = Time.now - self.last_update_skew_seconds
crl.next_update = Time.now + self.next_update
signing_cert = OpenSSL::X509::Certificate.new(self.parent.to_pem)
- digest = OpenSSL::Digest::Digest.new("SHA512")
+ if signing_profile["digest"].nil?
+ digest = OpenSSL::Digest.new("SHA512")
+ else
+ digest = OpenSSL::Digest.new(signing_profile["digest"])
+ end
crl.issuer = signing_cert.subject
self.crl_body = crl.sign(self.parent.key_material.private_key, digest)
diff --git a/lib/certificate_authority/core_extensions.rb b/lib/certificate_authority/core_extensions.rb
new file mode 100644
index 0000000..0508f9a
--- /dev/null
+++ b/lib/certificate_authority/core_extensions.rb
@@ -0,0 +1,46 @@
+#
+# ActiveSupport has these modifications. Now that we don't use ActiveSupport,
+# these are added here as a kindness.
+#
+
+require 'date'
+
+unless nil.respond_to?(:blank?)
+ class NilClass
+ def blank?
+ true
+ end
+ end
+end
+
+unless String.respond_to?(:blank?)
+ class String
+ def blank?
+ self.empty?
+ end
+ end
+end
+
+class Date
+
+ def today
+ t = Time.now.utc
+ Date.new(t.year, t.month, t.day)
+ end
+
+ def utc
+ self.to_datetime.to_time.utc
+ end
+
+ unless Date.respond_to?(:advance)
+ def advance(options)
+ options = options.dup
+ d = self
+ d = d >> options.delete(:years) * 12 if options[:years]
+ d = d >> options.delete(:months) if options[:months]
+ d = d + options.delete(:weeks) * 7 if options[:weeks]
+ d = d + options.delete(:days) if options[:days]
+ d
+ end
+ end
+end
diff --git a/lib/certificate_authority/distinguished_name.rb b/lib/certificate_authority/distinguished_name.rb
index 71111c2..3b83582 100644
--- a/lib/certificate_authority/distinguished_name.rb
+++ b/lib/certificate_authority/distinguished_name.rb
@@ -1,58 +1,106 @@
module CertificateAuthority
class DistinguishedName
- include ActiveModel::Validations
+ include Validations
- validates_presence_of :common_name
+ def validate
+ if self.common_name.nil? || self.common_name.empty?
+ errors.add :common_name, 'cannot be blank'
+ end
+ end
attr_accessor :common_name
alias :cn :common_name
+ alias :cn= :common_name=
attr_accessor :locality
alias :l :locality
+ alias :l= :locality=
attr_accessor :state
alias :s :state
+ alias :st= :state=
attr_accessor :country
alias :c :country
+ alias :c= :country=
attr_accessor :organization
alias :o :organization
+ alias :o= :organization=
attr_accessor :organizational_unit
alias :ou :organizational_unit
+ alias :ou= :organizational_unit=
+
+ attr_accessor :email_address
+ alias :emailAddress :email_address
+ alias :emailAddress= :email_address=
+
+ attr_accessor :serial_number
+ alias :serialNumber :serial_number
+ alias :serialNumber= :serial_number=
def to_x509_name
raise "Invalid Distinguished Name" unless valid?
# NB: the capitalization in the strings counts
name = OpenSSL::X509::Name.new
- name.add_entry("CN", common_name)
- name.add_entry("O", organization) unless organization.blank?
- name.add_entry("OU", organizational_unit) unless organizational_unit.blank?
+ name.add_entry("serialNumber", serial_number) unless serial_number.blank?
+ name.add_entry("C", country) unless country.blank?
name.add_entry("ST", state) unless state.blank?
name.add_entry("L", locality) unless locality.blank?
- name.add_entry("C", country) unless country.blank?
+ name.add_entry("O", organization) unless organization.blank?
+ name.add_entry("OU", organizational_unit) unless organizational_unit.blank?
+ name.add_entry("CN", common_name)
+ name.add_entry("emailAddress", email_address) unless email_address.blank?
name
end
+ def ==(other)
+ # Use the established OpenSSL comparison
+ self.to_x509_name() == other.to_x509_name()
+ end
+
def self.from_openssl openssl_name
unless openssl_name.is_a? OpenSSL::X509::Name
raise "Argument must be a OpenSSL::X509::Name"
end
- name = DistinguishedName.new
- openssl_name.to_a.each do |k,v|
- case k
- when "CN" then name.common_name = v
- when "L" then name.locality = v
- when "ST" then name.state = v
- when "C" then name.country = v
- when "O" then name.organization = v
- when "OU" then name.organizational_unit = v
+ WrappedDistinguishedName.new(openssl_name)
+ end
+ end
+
+ ## This is a significantly more complicated case. It's possible that
+ ## generically handled certificates will include custom OIDs in the
+ ## subject.
+ class WrappedDistinguishedName < DistinguishedName
+ attr_accessor :x509_name
+
+ def initialize(x509_name)
+ @x509_name = x509_name
+
+ subject = @x509_name.to_a
+ subject.each do |element|
+ field = element[0].downcase
+ value = element[1]
+ #type = element[2] ## -not used
+ method_sym = "#{field}=".to_sym
+ if self.respond_to?(method_sym)
+ self.send("#{field}=",value)
+ else
+ ## Custom OID
+ @custom_oids = true
end
end
- name
+
+ end
+
+ def to_x509_name
+ @x509_name
+ end
+
+ def custom_oids?
+ @custom_oids
end
end
end
diff --git a/lib/certificate_authority/extensions.rb b/lib/certificate_authority/extensions.rb
index 0c51552..2b9478b 100644
--- a/lib/certificate_authority/extensions.rb
+++ b/lib/certificate_authority/extensions.rb
@@ -5,6 +5,10 @@ module CertificateAuthority
raise "Implementation required"
end
+ def self.parse(value, critical)
+ raise "Implementation required"
+ end
+
def config_extensions
{}
end
@@ -12,21 +16,47 @@ module CertificateAuthority
def openssl_identifier
raise "Implementation required"
end
+
+ def ==(value)
+ raise "Implementation required"
+ end
end
- class BasicContraints
+ # Specifies whether an X.509v3 certificate can act as a CA, signing other
+ # certificates to be verified. If set, a path length constraint can also be
+ # specified.
+ # Reference: Section 4.2.1.10 of RFC3280
+ # http://tools.ietf.org/html/rfc3280#section-4.2.1.10
+ class BasicConstraints
+ OPENSSL_IDENTIFIER = "basicConstraints"
+
include ExtensionAPI
- include ActiveModel::Validations
+ include Validations
+
+ attr_accessor :critical
attr_accessor :ca
attr_accessor :path_len
- validates :ca, :inclusion => [true,false]
+
+ def validate
+ unless [true, false].include? self.critical
+ errors.add :critical, 'must be true or false'
+ end
+ unless [true, false].include? self.ca
+ errors.add :ca, 'must be true or false'
+ end
+ end
def initialize
- self.ca = false
+ @critical = false
+ @ca = false
+ end
+
+ def openssl_identifier
+ OPENSSL_IDENTIFIER
end
def is_ca?
- self.ca
+ @ca
end
def path_len=(value)
@@ -34,29 +64,54 @@ module CertificateAuthority
@path_len = value
end
- def openssl_identifier
- "basicConstraints"
+ def to_s
+ res = []
+ res << "CA:#{@ca}"
+ res << "pathlen:#{@path_len}" unless @path_len.nil?
+ res.join(',')
end
- def to_s
- result = ""
- result += "CA:#{self.ca}"
- result += ",pathlen:#{self.path_len}" unless self.path_len.nil?
- result
+ def ==(o)
+ o.class == self.class && o.state == state
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ value.split(/,\s*/).each do |v|
+ c = v.split(':', 2)
+ obj.ca = (c.last.upcase == "TRUE") if c.first == "CA"
+ obj.path_len = c.last.to_i if c.first == "pathlen"
+ end
+ obj
+ end
+
+ protected
+ def state
+ [@critical, at ca, at path_len]
end
end
+ # Specifies where CRL information be be retrieved. This extension isn't
+ # critical, but is recommended for proper CAs.
+ # Reference: Section 4.2.1.14 of RFC3280
+ # http://tools.ietf.org/html/rfc3280#section-4.2.1.14
class CrlDistributionPoints
+ OPENSSL_IDENTIFIER = "crlDistributionPoints"
+
include ExtensionAPI
- attr_accessor :uri
+ attr_accessor :critical
+ attr_accessor :uris
def initialize
- # self.uri = "http://moo.crlendPoint.example.com/something.crl"
+ @critical = false
+ @uris = []
end
def openssl_identifier
- "crlDistributionPoints"
+ OPENSSL_IDENTIFIER
end
## NB: At this time it seems OpenSSL's extension handlers don't support
@@ -69,99 +124,302 @@ module CertificateAuthority
}
end
+ # This is for legacy support. Technically it can (and probably should)
+ # be an array. But if someone is calling the old accessor we shouldn't
+ # necessarily break it.
+ def uri=(value)
+ @uris << value
+ end
+
def to_s
- return "" if self.uri.nil?
- "URI:#{self.uri}"
+ res = []
+ @uris.each do |uri|
+ res << "URI:#{uri}"
+ end
+ res.join(',')
+ end
+
+ def ==(o)
+ o.class == self.class && o.state == state
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ value.split(/,\s*/).each do |v|
+ c = v.split(':', 2)
+ obj.uris << c.last if c.first == "URI"
+ end
+ obj
+ end
+
+ protected
+ def state
+ [@critical, at uri]
end
end
+ # Identifies the public key associated with a given certificate.
+ # Should be required for "CA" certificates.
+ # Reference: Section 4.2.1.2 of RFC3280
+ # http://tools.ietf.org/html/rfc3280#section-4.2.1.2
class SubjectKeyIdentifier
+ OPENSSL_IDENTIFIER = "subjectKeyIdentifier"
+
include ExtensionAPI
+
+ attr_accessor :critical
+ attr_accessor :identifier
+
+ def initialize
+ @critical = false
+ @identifier = "hash"
+ end
+
def openssl_identifier
- "subjectKeyIdentifier"
+ OPENSSL_IDENTIFIER
end
def to_s
- "hash"
+ res = []
+ res << @identifier
+ res.join(',')
+ end
+
+ def ==(o)
+ o.class == self.class && o.state == state
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ obj.identifier = value
+ obj
+ end
+
+ protected
+ def state
+ [@critical, at identifier]
end
end
+ # Identifies the public key associated with a given private key.
+ # Reference: Section 4.2.1.1 of RFC3280
+ # http://tools.ietf.org/html/rfc3280#section-4.2.1.1
class AuthorityKeyIdentifier
+ OPENSSL_IDENTIFIER = "authorityKeyIdentifier"
+
include ExtensionAPI
+ attr_accessor :critical
+ attr_accessor :identifier
+
+ def initialize
+ @critical = false
+ @identifier = ["keyid", "issuer"]
+ end
+
def openssl_identifier
- "authorityKeyIdentifier"
+ OPENSSL_IDENTIFIER
end
def to_s
- "keyid,issuer"
+ res = []
+ res += @identifier
+ res.join(',')
+ end
+
+ def ==(o)
+ o.class == self.class && o.state == state
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ obj.identifier = value.split(/,\s*/).last.chomp
+ obj
+ end
+
+ protected
+ def state
+ [@critical, at identifier]
end
end
+ # Specifies how to access CA information and services for the CA that
+ # issued this certificate.
+ # Generally used to specify OCSP servers.
+ # Reference: Section 4.2.2.1 of RFC3280
+ # http://tools.ietf.org/html/rfc3280#section-4.2.2.1
class AuthorityInfoAccess
+ OPENSSL_IDENTIFIER = "authorityInfoAccess"
+
include ExtensionAPI
+ attr_accessor :critical
attr_accessor :ocsp
+ attr_accessor :ca_issuers
def initialize
- self.ocsp = []
+ @critical = false
+ @ocsp = []
+ @ca_issuers = []
end
def openssl_identifier
- "authorityInfoAccess"
+ OPENSSL_IDENTIFIER
end
def to_s
- return "" if self.ocsp.empty?
- "OCSP;URI:#{self.ocsp}"
+ res = []
+ res += @ocsp.map {|o| "OCSP;URI:#{o}" }
+ res += @ca_issuers.map {|c| "caIssuers;URI:#{c}" }
+ res.join(',')
+ end
+
+ def ==(o)
+ o.class == self.class && o.state == state
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ value.split("\n").each do |v|
+ if v =~ /^OCSP/
+ obj.ocsp << v.split.last
+ end
+
+ if v =~ /^CA Issuers/
+ obj.ca_issuers << v.split.last
+ end
+ end
+ obj
+ end
+
+ protected
+ def state
+ [@critical, at ocsp, at ca_issuers]
end
end
+ # Specifies the allowed usage purposes of the keypair specified in this certificate.
+ # Reference: Section 4.2.1.3 of RFC3280
+ # http://tools.ietf.org/html/rfc3280#section-4.2.1.3
+ #
+ # Note: OpenSSL when parsing an extension will return results in the form
+ # 'Digital Signature', but on signing you have to set it to 'digitalSignature'.
+ # So copying an extension from an imported cert isn't going to work yet.
class KeyUsage
+ OPENSSL_IDENTIFIER = "keyUsage"
+
include ExtensionAPI
+ attr_accessor :critical
attr_accessor :usage
def initialize
- self.usage = ["digitalSignature", "nonRepudiation"]
+ @critical = false
+ @usage = ["digitalSignature", "nonRepudiation"]
end
def openssl_identifier
- "keyUsage"
+ OPENSSL_IDENTIFIER
end
def to_s
- "#{self.usage.join(',')}"
+ res = []
+ res += @usage
+ res.join(',')
+ end
+
+ def ==(o)
+ o.class == self.class && o.state == state
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ obj.usage = value.split(/,\s*/)
+ obj
+ end
+
+ protected
+ def state
+ [@critical, at usage]
end
end
+ # Specifies even more allowed usages in addition to what is specified in
+ # the Key Usage extension.
+ # Reference: Section 4.2.1.13 of RFC3280
+ # http://tools.ietf.org/html/rfc3280#section-4.2.1.13
class ExtendedKeyUsage
+ OPENSSL_IDENTIFIER = "extendedKeyUsage"
+
include ExtensionAPI
+ attr_accessor :critical
attr_accessor :usage
def initialize
- self.usage = ["serverAuth","clientAuth"]
+ @critical = false
+ @usage = ["serverAuth"]
end
def openssl_identifier
- "extendedKeyUsage"
+ OPENSSL_IDENTIFIER
end
def to_s
- "#{self.usage.join(',')}"
+ res = []
+ res += @usage
+ res.join(',')
+ end
+
+ def ==(o)
+ o.class == self.class && o.state == state
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ obj.usage = value.split(/,\s*/)
+ obj
+ end
+
+ protected
+ def state
+ [@critical, at usage]
end
end
+ # Specifies additional "names" for which this certificate is valid.
+ # Reference: Section 4.2.1.7 of RFC3280
+ # http://tools.ietf.org/html/rfc3280#section-4.2.1.7
class SubjectAlternativeName
+ OPENSSL_IDENTIFIER = "subjectAltName"
+
include ExtensionAPI
- attr_accessor :uris, :dns_names, :ips
+ attr_accessor :critical
+ attr_accessor :uris, :dns_names, :ips, :emails
def initialize
- self.uris = []
- self.dns_names = []
- self.ips = []
+ @critical = false
+ @uris = []
+ @dns_names = []
+ @ips = []
+ @emails = []
+ end
+
+ def openssl_identifier
+ OPENSSL_IDENTIFIER
end
def uris=(value)
@@ -179,22 +437,50 @@ module CertificateAuthority
@ips = value
end
- def openssl_identifier
- "subjectAltName"
+ def emails=(value)
+ raise "Emails must be an array" unless value.is_a?(Array)
+ @emails = value
end
def to_s
- res = self.uris.map {|u| "URI:#{u}" }
- res += self.dns_names.map {|d| "DNS:#{d}" }
- res += self.ips.map {|i| "IP:#{i}" }
+ res = []
+ res += @uris.map {|u| "URI:#{u}" }
+ res += @dns_names.map {|d| "DNS:#{d}" }
+ res += @ips.map {|i| "IP:#{i}" }
+ res += @emails.map {|i| "email:#{i}" }
+ res.join(',')
+ end
+
+ def ==(o)
+ o.class == self.class && o.state == state
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ value.split(/,\s*/).each do |v|
+ c = v.split(':', 2)
+ obj.uris << c.last if c.first == "URI"
+ obj.dns_names << c.last if c.first == "DNS"
+ obj.ips << c.last if c.first == "IP"
+ obj.emails << c.last if c.first == "EMAIL"
+ end
+ obj
+ end
- return res.join(',')
+ protected
+ def state
+ [@critical, at uris, at dns_names, at ips, at emails]
end
end
class CertificatePolicies
+ OPENSSL_IDENTIFIER = "certificatePolicies"
+
include ExtensionAPI
+ attr_accessor :critical
attr_accessor :policy_identifier
attr_accessor :cps_uris
##User notice
@@ -203,12 +489,12 @@ module CertificateAuthority
attr_accessor :notice_numbers
def initialize
+ self.critical = false
@contains_data = false
end
-
def openssl_identifier
- "certificatePolicies"
+ OPENSSL_IDENTIFIER
end
def user_notice=(value={})
@@ -258,7 +544,93 @@ module CertificateAuthority
def to_s
return "" unless @contains_data
- "ia5org, at custom_policies"
+ res = []
+ res << "ia5org"
+ res += @config_extensions["custom_policies"] unless @config_extensions.nil?
+ res.join(',')
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ value.split(/,\s*/).each do |v|
+ c = v.split(':', 2)
+ obj.policy_identifier = c.last if c.first == "policyIdentifier"
+ obj.cps_uris << c.last if c.first =~ %r{CPS.\d+}
+ # TODO: explicit_text, organization, notice_numbers
+ end
+ obj
+ end
+ end
+
+ # DEPRECATED
+ # Specifics the purposes for which a certificate can be used.
+ # The basicConstraints, keyUsage, and extendedKeyUsage extensions are now used instead.
+ # https://www.openssl.org/docs/apps/x509v3_config.html#Netscape_Certificate_Type
+ class NetscapeCertificateType
+ OPENSSL_IDENTIFIER = "nsCertType"
+
+ include ExtensionAPI
+
+ attr_accessor :critical
+ attr_accessor :flags
+
+ def initialize
+ self.critical = false
+ self.flags = []
+ end
+
+ def openssl_identifier
+ OPENSSL_IDENTIFIER
+ end
+
+ def to_s
+ res = []
+ res += self.flags
+ res.join(',')
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ obj.flags = value.split(/,\s*/)
+ obj
+ end
+ end
+
+ # DEPRECATED
+ # Contains a comment which will be displayed when the certificate is viewed in some browsers.
+ # https://www.openssl.org/docs/apps/x509v3_config.html#Netscape_String_extensions_
+ class NetscapeComment
+ OPENSSL_IDENTIFIER = "nsComment"
+
+ include ExtensionAPI
+
+ attr_accessor :critical
+ attr_accessor :comment
+
+ def initialize
+ self.critical = false
+ end
+
+ def openssl_identifier
+ OPENSSL_IDENTIFIER
+ end
+
+ def to_s
+ res = []
+ res << self.comment if self.comment
+ res.join(',')
+ end
+
+ def self.parse(value, critical)
+ obj = self.new
+ return obj if value.nil?
+ obj.critical = critical
+ obj.comment = value
+ obj
end
end
diff --git a/lib/certificate_authority/key_material.rb b/lib/certificate_authority/key_material.rb
index 13d0997..ae3a530 100644
--- a/lib/certificate_authority/key_material.rb
+++ b/lib/certificate_authority/key_material.rb
@@ -15,11 +15,30 @@ module CertificateAuthority
def is_in_memory?
raise "Required implementation"
end
+
+ def self.from_x509_key_pair(pair,password=nil)
+ if password.nil?
+ key = OpenSSL::PKey::RSA.new(pair)
+ else
+ key = OpenSSL::PKey::RSA.new(pair,password)
+ end
+ mem_key = MemoryKeyMaterial.new
+ mem_key.public_key = key.public_key
+ mem_key.private_key = key
+ mem_key
+ end
+
+ def self.from_x509_public_key(public_key_pem)
+ key = OpenSSL::PKey::RSA.new(public_key_pem)
+ signing_request_key = SigningRequestKeyMaterial.new
+ signing_request_key.public_key = key.public_key
+ signing_request_key
+ end
end
class MemoryKeyMaterial
include KeyMaterial
- include ActiveModel::Validations
+ include Validations
attr_accessor :keypair
attr_accessor :private_key
@@ -28,11 +47,13 @@ module CertificateAuthority
def initialize
end
- validates_each :private_key do |record, attr, value|
- record.errors.add :private_key, "cannot be blank" if record.private_key.nil?
- end
- validates_each :public_key do |record, attr, value|
- record.errors.add :public_key, "cannot be blank" if record.public_key.nil?
+ def validate
+ if private_key.nil?
+ errors.add :private_key, "cannot be blank"
+ end
+ if public_key.nil?
+ errors.add :public_key, "cannot be blank"
+ end
end
def is_in_hardware?
@@ -61,10 +82,10 @@ module CertificateAuthority
class SigningRequestKeyMaterial
include KeyMaterial
- include ActiveModel::Validations
+ include Validations
- validates_each :public_key do |record, attr, value|
- record.errors.add :public_key, "cannot be blank" if record.public_key.nil?
+ def validate
+ errors.add :public_key, "cannot be blank" if public_key.nil?
end
attr_accessor :public_key
diff --git a/lib/certificate_authority/ocsp_handler.rb b/lib/certificate_authority/ocsp_handler.rb
index 776a714..0f2661c 100644
--- a/lib/certificate_authority/ocsp_handler.rb
+++ b/lib/certificate_authority/ocsp_handler.rb
@@ -1,6 +1,74 @@
module CertificateAuthority
+ class OCSPResponseBuilder
+ attr_accessor :ocsp_response
+ attr_accessor :verification_mechanism
+ attr_accessor :ocsp_request_reader
+ attr_accessor :parent
+ attr_accessor :next_update
+
+ GOOD = OpenSSL::OCSP::V_CERTSTATUS_GOOD
+ REVOKED = OpenSSL::OCSP::V_CERTSTATUS_REVOKED
+
+ NO_REASON=0
+ KEY_COMPROMISED=OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE
+ UNSPECIFIED=OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED
+
+ def build_response()
+ raise "Requires a parent for signing" if @parent.nil?
+ if @verification_mechanism.nil?
+ ## If no verification callback is provided we're marking it GOOD
+ @verification_mechanism = lambda {|cert_id| [GOOD,NO_REASON] }
+ end
+
+ @ocsp_request_reader.ocsp_request.certid.each do |cert_id|
+ result,reason = verification_mechanism.call(cert_id.serial)
+
+ ## cert_id, status, reason, rev_time, this update, next update, ext
+ ## - unit of time is seconds
+ ## - rev_time is currently set to "now"
+ @ocsp_response.add_status(cert_id,
+ result, reason,
+ 0, 0, @next_update, nil)
+ end
+
+ @ocsp_response.sign(OpenSSL::X509::Certificate.new(@parent.to_pem), @parent.key_material.private_key, nil, nil)
+ OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, @ocsp_response)
+ end
+
+ def self.from_request_reader(request_reader,verification_mechanism=nil)
+ response_builder = OCSPResponseBuilder.new
+ response_builder.ocsp_request_reader = request_reader
+
+ ocsp_response = OpenSSL::OCSP::BasicResponse.new
+ ocsp_response.copy_nonce(request_reader.ocsp_request)
+ response_builder.ocsp_response = ocsp_response
+ response_builder.next_update = 60*15 #Default of 15 minutes
+ response_builder
+ end
+ end
+
+ class OCSPRequestReader
+ attr_accessor :raw_ocsp_request
+ attr_accessor :ocsp_request
+
+ def serial_numbers
+ @ocsp_request.certid.collect do |cert_id|
+ cert_id.serial
+ end
+ end
+
+ def self.from_der(request_body)
+ reader = OCSPRequestReader.new
+ reader.raw_ocsp_request = request_body
+ reader.ocsp_request = OpenSSL::OCSP::Request.new(request_body)
+
+ reader
+ end
+ end
+
+ ## DEPRECATED
class OCSPHandler
- include ActiveModel::Validations
+ include Validations
attr_accessor :ocsp_request
attr_accessor :certificate_ids
@@ -10,10 +78,10 @@ module CertificateAuthority
attr_accessor :ocsp_response_body
- validate do |crl|
+ def validate
errors.add :parent, "A parent entity must be set" if parent.nil?
+ all_certificates_available
end
- validate :all_certificates_available
def initialize
self.certificates = {}
@@ -24,8 +92,7 @@ module CertificateAuthority
end
def extract_certificate_serials
- raise "No valid OCSP request was supplied" if self.ocsp_request.nil?
- openssl_request = OpenSSL::OCSP::Request.new(self.ocsp_request)
+ openssl_request = OpenSSL::OCSP::Request.new(@ocsp_request)
self.certificate_ids = openssl_request.certid.collect do |cert_id|
cert_id.serial
diff --git a/lib/certificate_authority/pkcs11_key_material.rb b/lib/certificate_authority/pkcs11_key_material.rb
index d4ebc47..8a83f0e 100644
--- a/lib/certificate_authority/pkcs11_key_material.rb
+++ b/lib/certificate_authority/pkcs11_key_material.rb
@@ -1,8 +1,6 @@
module CertificateAuthority
class Pkcs11KeyMaterial
include KeyMaterial
- include ActiveModel::Validations
- include ActiveModel::Serialization
attr_accessor :engine
attr_accessor :token_id
diff --git a/lib/certificate_authority/revocable.rb b/lib/certificate_authority/revocable.rb
new file mode 100644
index 0000000..eba5d98
--- /dev/null
+++ b/lib/certificate_authority/revocable.rb
@@ -0,0 +1,14 @@
+module CertificateAuthority
+ module Revocable
+ attr_accessor :revoked_at
+
+ def revoke!(time=Time.now)
+ @revoked_at = time
+ end
+
+ def revoked?
+ # If we have a time, then we're revoked
+ !@revoked_at.nil?
+ end
+ end
+end
diff --git a/lib/certificate_authority/serial_number.rb b/lib/certificate_authority/serial_number.rb
index c00fa21..99f3002 100644
--- a/lib/certificate_authority/serial_number.rb
+++ b/lib/certificate_authority/serial_number.rb
@@ -1,9 +1,22 @@
+require 'securerandom'
+
module CertificateAuthority
class SerialNumber
- include ActiveModel::Validations
+ include Validations
+ include Revocable
attr_accessor :number
- validates :number, :presence => true, :numericality => {:greater_than => 0}
+ def validate
+ if self.number.nil?
+ errors.add :number, "must not be empty"
+ elsif self.number.to_i <= 0
+ errors.add :number, "must be greater than zero"
+ end
+ end
+
+ def initialize
+ self.number = SecureRandom.random_number(2**128-1)
+ end
end
end
diff --git a/lib/certificate_authority/signing_request.rb b/lib/certificate_authority/signing_request.rb
new file mode 100644
index 0000000..3584dac
--- /dev/null
+++ b/lib/certificate_authority/signing_request.rb
@@ -0,0 +1,91 @@
+module CertificateAuthority
+ class SigningRequest
+ attr_accessor :distinguished_name
+ attr_accessor :key_material
+ attr_accessor :raw_body
+ attr_accessor :openssl_csr
+ attr_accessor :digest
+ attr_accessor :attributes
+
+ def initialize()
+ @attributes = []
+ end
+
+ # Fake attribute for convenience because adding
+ # alternative names on a CSR is remarkably non-trivial.
+ def subject_alternative_names=(alt_names)
+ raise "alt_names must be an Array" unless alt_names.is_a?(Array)
+
+ factory = OpenSSL::X509::ExtensionFactory.new
+ name_list = alt_names.map{|m| "DNS:#{m}"}.join(",")
+ ext = factory.create_ext("subjectAltName",name_list,false)
+ ext_set = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence([ext])])
+ attr = OpenSSL::X509::Attribute.new("extReq", ext_set)
+ @attributes << attr
+ end
+
+ def read_attributes_by_oid(*oids)
+ attributes.detect { |a| oids.include?(a.oid) }
+ end
+ protected :read_attributes_by_oid
+
+ def to_cert
+ cert = Certificate.new
+ if !@distinguished_name.nil?
+ cert.distinguished_name = @distinguished_name
+ end
+ cert.key_material = @key_material
+ if attribute = read_attributes_by_oid('extReq', 'msExtReq')
+ set = OpenSSL::ASN1.decode(attribute.value)
+ seq = set.value.first
+ seq.value.collect { |asn1ext| OpenSSL::X509::Extension.new(asn1ext).to_a }.each do |o, v, c|
+ Certificate::EXTENSIONS.each do |klass|
+ cert.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v && klass::OPENSSL_IDENTIFIER == o
+ end
+ end
+ end
+ cert
+ end
+
+ def to_pem
+ to_x509_csr.to_pem
+ end
+
+ def to_x509_csr
+ raise "Must specify a DN/subject on csr" if @distinguished_name.nil?
+ raise "Invalid DN in request" unless @distinguished_name.valid?
+ raise "CSR must have key material" if @key_material.nil?
+ raise "CSR must include a public key on key material" if @key_material.public_key.nil?
+ raise "Need a private key on key material for CSR generation" if @key_material.private_key.nil?
+
+ opensslcsr = OpenSSL::X509::Request.new
+ opensslcsr.subject = @distinguished_name.to_x509_name
+ opensslcsr.public_key = @key_material.public_key
+ opensslcsr.attributes = @attributes unless @attributes.nil?
+ opensslcsr.sign @key_material.private_key, OpenSSL::Digest.new(@digest || "SHA512")
+ opensslcsr
+ end
+
+ def self.from_x509_csr(raw_csr)
+ csr = SigningRequest.new
+ openssl_csr = OpenSSL::X509::Request.new(raw_csr)
+ csr.distinguished_name = DistinguishedName.from_openssl openssl_csr.subject
+ csr.raw_body = raw_csr
+ csr.openssl_csr = openssl_csr
+ csr.attributes = openssl_csr.attributes
+ key_material = SigningRequestKeyMaterial.new
+ key_material.public_key = openssl_csr.public_key
+ csr.key_material = key_material
+ csr
+ end
+
+ def self.from_netscape_spkac(raw_spkac)
+ openssl_spkac = OpenSSL::Netscape::SPKI.new raw_spkac
+ csr = SigningRequest.new
+ csr.raw_body = raw_spkac
+ key_material = SigningRequestKeyMaterial.new
+ key_material.public_key = openssl_spkac.public_key
+ csr
+ end
+ end
+end
diff --git a/lib/certificate_authority/validations.rb b/lib/certificate_authority/validations.rb
new file mode 100644
index 0000000..a429c96
--- /dev/null
+++ b/lib/certificate_authority/validations.rb
@@ -0,0 +1,31 @@
+#
+# This is a super simple replacement for ActiveSupport::Validations
+#
+
+module CertificateAuthority
+ class Errors < Array
+ def add(symbol, msg)
+ self.push([symbol, msg])
+ end
+ def full_messages
+ self.map {|i| i[0].to_s + ": " + i[1]}.join("\n")
+ end
+ end
+
+ module Validations
+ def valid?
+ @errors = Errors.new
+ validate
+ errors.empty?
+ end
+
+ # must be overridden
+ def validate
+ raise NotImplementedError
+ end
+
+ def errors
+ @errors ||= Errors.new
+ end
+ end
+end
diff --git a/metadata.yml b/metadata.yml
deleted file mode 100644
index 12b460a..0000000
--- a/metadata.yml
+++ /dev/null
@@ -1,111 +0,0 @@
---- !ruby/object:Gem::Specification
-name: certificate_authority
-version: !ruby/object:Gem::Version
- version: 0.1.6
- prerelease:
-platform: ruby
-authors:
-- Chris Chandler
-autorequire:
-bindir: bin
-cert_chain: []
-date: 2012-08-12 00:00:00.000000000 Z
-dependencies:
-- !ruby/object:Gem::Dependency
- name: activemodel
- requirement: &70182567433260 !ruby/object:Gem::Requirement
- none: false
- requirements:
- - - ! '>='
- - !ruby/object:Gem::Version
- version: 3.0.6
- type: :runtime
- prerelease: false
- version_requirements: *70182567433260
-- !ruby/object:Gem::Dependency
- name: rspec
- requirement: &70182567432240 !ruby/object:Gem::Requirement
- none: false
- requirements:
- - - ! '>='
- - !ruby/object:Gem::Version
- version: '0'
- type: :development
- prerelease: false
- version_requirements: *70182567432240
-- !ruby/object:Gem::Dependency
- name: jeweler
- requirement: &70182567430960 !ruby/object:Gem::Requirement
- none: false
- requirements:
- - - ! '>='
- - !ruby/object:Gem::Version
- version: 1.5.2
- type: :development
- prerelease: false
- version_requirements: *70182567430960
-description:
-email: chris at flatterline.com
-executables: []
-extensions: []
-extra_rdoc_files:
-- README.rdoc
-files:
-- Gemfile
-- Gemfile.lock
-- README.rdoc
-- Rakefile
-- VERSION.yml
-- certificate_authority.gemspec
-- lib/certificate_authority.rb
-- lib/certificate_authority/certificate.rb
-- lib/certificate_authority/certificate_revocation_list.rb
-- lib/certificate_authority/distinguished_name.rb
-- lib/certificate_authority/extensions.rb
-- lib/certificate_authority/key_material.rb
-- lib/certificate_authority/ocsp_handler.rb
-- lib/certificate_authority/pkcs11_key_material.rb
-- lib/certificate_authority/serial_number.rb
-- lib/certificate_authority/signing_entity.rb
-- lib/tasks/certificate_authority.rake
-- spec/spec_helper.rb
-- spec/units/certificate_authority_spec.rb
-- spec/units/certificate_revocation_list_spec.rb
-- spec/units/certificate_spec.rb
-- spec/units/distinguished_name_spec.rb
-- spec/units/extensions_spec.rb
-- spec/units/key_material_spec.rb
-- spec/units/ocsp_handler_spec.rb
-- spec/units/pkcs11_key_material_spec.rb
-- spec/units/serial_number_spec.rb
-- spec/units/signing_entity_spec.rb
-- spec/units/units_helper.rb
-homepage: http://github.com/cchandler/certificate_authority
-licenses:
-- MIT
-post_install_message:
-rdoc_options: []
-require_paths:
-- lib
-required_ruby_version: !ruby/object:Gem::Requirement
- none: false
- requirements:
- - - ! '>='
- - !ruby/object:Gem::Version
- version: '0'
- segments:
- - 0
- hash: -2366790866817530073
-required_rubygems_version: !ruby/object:Gem::Requirement
- none: false
- requirements:
- - - ! '>='
- - !ruby/object:Gem::Version
- version: '0'
-requirements: []
-rubyforge_project:
-rubygems_version: 1.8.15
-signing_key:
-specification_version: 3
-summary: Ruby gem for managing the core functions outlined in RFC-3280 for PKI
-test_files: []
diff --git a/spec/samples/certs/DigiCertHighAssuranceEVCA-1.pem b/spec/samples/certs/DigiCertHighAssuranceEVCA-1.pem
new file mode 100644
index 0000000..5122b60
--- /dev/null
+++ b/spec/samples/certs/DigiCertHighAssuranceEVCA-1.pem
@@ -0,0 +1,115 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 08:bb:b0:25:47:13:4b:c9:b1:10:d7:c1:a2:12:59:c5
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV Root CA
+ Validity
+ Not Before: Nov 10 00:00:00 2006 GMT
+ Not After : Nov 10 00:00:00 2021 GMT
+ Subject: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV CA-1
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (2048 bit)
+ Modulus (2048 bit):
+ 00:f3:96:62:d8:75:6e:19:ff:3f:34:7c:49:4f:31:
+ 7e:0d:04:4e:99:81:e2:b3:85:55:91:30:b1:c0:af:
+ 70:bb:2c:a8:e7:18:aa:3f:78:f7:90:68:52:86:01:
+ 88:97:e2:3b:06:65:90:aa:bd:65:76:c2:ec:be:10:
+ 5b:37:78:83:60:75:45:c6:bd:74:aa:b6:9f:a4:3a:
+ 01:50:17:c4:39:69:b9:f1:4f:ef:82:c1:ca:f3:4a:
+ db:cc:9e:50:4f:4d:40:a3:3a:90:e7:86:66:bc:f0:
+ 3e:76:28:4c:d1:75:80:9e:6a:35:14:35:03:9e:db:
+ 0c:8c:c2:28:ad:50:b2:ce:f6:91:a3:c3:a5:0a:58:
+ 49:f6:75:44:6c:ba:f9:ce:e9:ab:3a:02:e0:4d:f3:
+ ac:e2:7a:e0:60:22:05:3c:82:d3:52:e2:f3:9c:47:
+ f8:3b:d8:b2:4b:93:56:4a:bf:70:ab:3e:e9:68:c8:
+ 1d:8f:58:1d:2a:4d:5e:27:3d:ad:0a:59:2f:5a:11:
+ 20:40:d9:68:04:68:2d:f4:c0:84:0b:0a:1b:78:df:
+ ed:1a:58:dc:fb:41:5a:6d:6b:f2:ed:1c:ee:5c:32:
+ b6:5c:ec:d7:a6:03:32:a6:e8:de:b7:28:27:59:88:
+ 80:ff:7b:ad:89:58:d5:1e:14:a4:f2:b0:70:d4:a0:
+ 3e:a7
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication, Code Signing, E-mail Protection, Time Stamping
+ X509v3 Certificate Policies:
+ Policy: 2.16.840.1.114412.2.1
+ CPS: http://www.digicert.com/ssl-cps-repository.htm
+ User Notice:
+ Explicit Text:
+
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ Authority Information Access:
+ OCSP - URI:http://ocsp.digicert.com
+ CA Issuers - URI:http://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt
+
+ X509v3 CRL Distribution Points:
+ URI:http://crl3.digicert.com/DigiCertHighAssuranceEVRootCA.crl
+ URI:http://crl4.digicert.com/DigiCertHighAssuranceEVRootCA.crl
+
+ X509v3 Subject Key Identifier:
+ 4C:58:CB:25:F0:41:4F:52:F4:28:C8:81:43:9B:A6:A8:A0:E6:92:E5
+ X509v3 Authority Key Identifier:
+ keyid:B1:3E:C3:69:03:F8:BF:47:01:D4:98:26:1A:08:02:EF:63:64:2B:C3
+
+ Signature Algorithm: sha1WithRSAEncryption
+ 50:1e:43:b0:f7:4d:29:96:5b:bb:a7:d3:0a:b5:b5:d5:d0:27:
+ aa:f9:af:c7:25:d1:95:d5:2f:5a:53:bd:42:07:7e:78:49:ca:
+ 0b:eb:4c:55:e2:ea:2f:7f:49:ad:c7:ff:d1:2d:3e:9c:a0:64:
+ 2b:51:9e:91:26:28:bb:87:bb:75:7c:bc:a1:fd:66:68:2e:4c:
+ 4a:16:cc:fe:06:cf:31:ea:80:6e:e4:bd:e8:03:72:f6:25:b5:
+ 41:83:61:d0:97:0a:27:1d:b3:f7:2b:32:84:8f:5b:e7:cc:3f:
+ e2:2c:67:86:94:f4:b2:2b:6c:52:3b:67:2a:8d:58:95:00:14:
+ 46:24:ac:0b:fa:c9:8e:c7:26:80:df:d1:e1:97:e3:f8:bb:68:
+ c6:9c:bd:be:08:54:3b:10:32:7c:81:1f:2b:28:95:a8:41:0a:
+ c6:d0:30:66:b4:e9:f2:a2:00:69:20:07:ca:82:4c:1e:cf:a7:
+ 98:b8:0c:ee:cd:16:1c:be:1a:63:d4:c0:99:f6:67:b2:f0:8e:
+ 17:2d:58:c2:80:aa:5d:96:c7:b3:28:ed:f0:da:8e:b6:47:1b:
+ 8f:4e:15:f1:97:4c:0b:4b:af:81:d4:46:94:62:2c:43:a7:3c:
+ 25:48:19:63:f2:5c:aa:15:89:76:84:85:73:91:7d:28:3c:09:
+ 83:82:bc:f7
+-----BEGIN CERTIFICATE-----
+MIIG4zCCBcugAwIBAgIQCLuwJUcTS8mxENfBohJZxTANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTIxMTExMDAwMDAwMFowaTEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTEoMCYGA1UEAxMfRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+RVYgQ0EtMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPOWYth1bhn/
+PzR8SU8xfg0ETpmB4rOFVZEwscCvcLssqOcYqj9495BoUoYBiJfiOwZlkKq9ZXbC
+7L4QWzd4g2B1Rca9dKq2n6Q6AVAXxDlpufFP74LByvNK28yeUE9NQKM6kOeGZrzw
+PnYoTNF1gJ5qNRQ1A57bDIzCKK1Qss72kaPDpQpYSfZ1RGy6+c7pqzoC4E3zrOJ6
+4GAiBTyC01Li85xH+DvYskuTVkq/cKs+6WjIHY9YHSpNXic9rQpZL1oRIEDZaARo
+LfTAhAsKG3jf7RpY3PtBWm1r8u0c7lwytlzs16YDMqbo3rcoJ1mIgP97rYlY1R4U
+pPKwcNSgPqcCAwEAAaOCA4IwggN+MA4GA1UdDwEB/wQEAwIBhjA7BgNVHSUENDAy
+BggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUH
+AwgwggHEBgNVHSAEggG7MIIBtzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUH
+AgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5o
+dG0wggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0
+AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1
+AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABp
+AGcAaQBDAGUAcgB0ACAARQBWACAAQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBl
+AGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBo
+AGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAg
+AGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAg
+AGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wDwYDVR0TAQH/BAUwAwEB/zCBgwYI
+KwYBBQUHAQEEdzB1MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
+b20wTQYIKwYBBQUHMAKGQWh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NBQ2VydHMv
+RGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3J0MIGPBgNVHR8EgYcwgYQw
+QKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hBc3N1
+cmFuY2VFVlJvb3RDQS5jcmwwQKA+oDyGOmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNv
+bS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwHQYDVR0OBBYEFExY
+yyXwQU9S9CjIgUObpqig5pLlMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoIAu9j
+ZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQBQHkOw900pllu7p9MKtbXV0Ceq+a/HJdGV
+1S9aU71CB354ScoL60xV4uovf0mtx//RLT6coGQrUZ6RJii7h7t1fLyh/WZoLkxK
+Fsz+Bs8x6oBu5L3oA3L2JbVBg2HQlwonHbP3KzKEj1vnzD/iLGeGlPSyK2xSO2cq
+jViVABRGJKwL+smOxyaA39Hhl+P4u2jGnL2+CFQ7EDJ8gR8rKJWoQQrG0DBmtOny
+ogBpIAfKgkwez6eYuAzuzRYcvhpj1MCZ9mey8I4XLVjCgKpdlsezKO3w2o62RxuP
+ThXxl0wLS6+B1EaUYixDpzwlSBlj8lyqFYl2hIVzkX0oPAmDgrz3
+-----END CERTIFICATE-----
diff --git a/spec/samples/certs/apple_wwdr_issued_cert.pem b/spec/samples/certs/apple_wwdr_issued_cert.pem
new file mode 100644
index 0000000..20eb993
--- /dev/null
+++ b/spec/samples/certs/apple_wwdr_issued_cert.pem
@@ -0,0 +1,112 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 0f:50:11:d8:8f:07:09:bf
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Apple Worldwide Developer Relations Certification Authority
+ Validity
+ Not Before: Jun 26 21:18:40 2012 GMT
+ Not After : Jun 26 21:18:40 2013 GMT
+ Subject: UID=pass.void-star.com.meepedoo, CN=Pass Type ID: pass.void-star.com.meepedoo, OU=UL736KYQR9, O=Eric Monti, C=US
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (2048 bit)
+ Modulus (2048 bit):
+ 00:af:f9:a6:9f:c0:8b:a5:56:f2:b6:97:0a:86:42:
+ d0:f1:54:01:98:95:9f:d9:69:2b:9c:be:b0:b5:f4:
+ a4:ad:9e:e6:ef:8e:a5:dc:50:d0:ce:2a:89:a9:41:
+ ce:44:36:af:90:33:e7:56:76:9e:68:91:df:c6:e7:
+ b8:21:f2:d5:75:d2:2a:17:3a:9d:4a:e0:cc:d2:94:
+ 90:e7:f2:36:2f:1c:41:00:02:76:45:fe:c2:6a:fc:
+ 36:96:e7:7e:59:00:f2:85:9e:31:ff:a3:9b:a0:b8:
+ 6d:95:9e:e4:f1:c4:d0:e9:7c:70:61:52:03:39:5c:
+ b8:8a:34:69:22:82:c5:44:f9:cd:a1:25:57:26:86:
+ e4:31:d5:08:c9:9d:5f:73:44:10:21:6d:99:90:74:
+ f6:69:fb:20:de:a9:46:49:a3:a9:96:ab:66:44:e6:
+ bd:56:65:8e:7d:dd:07:7e:71:bd:13:0f:b1:50:07:
+ af:eb:71:78:af:46:d5:71:39:da:ed:f2:d1:db:8d:
+ 81:64:7f:56:c6:87:f1:7e:a5:a3:f4:9f:02:01:b9:
+ d0:36:7f:87:f2:0d:d6:93:4e:cb:d8:32:9c:5a:ea:
+ 44:07:64:6a:9f:1c:14:8e:9a:3a:0e:a3:86:2c:e8:
+ 20:2a:f1:32:e0:11:2a:13:8a:d3:4c:73:5e:ab:70:
+ fa:4d
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ Authority Information Access:
+ OCSP - URI:http://ocsp.apple.com/ocsp-wwdr03
+
+ X509v3 Subject Key Identifier:
+ 02:87:24:00:CA:53:38:8F:C3:4D:5C:80:98:E1:65:95:38:D5:2B:69
+ X509v3 Basic Constraints:
+ CA:FALSE
+ X509v3 Authority Key Identifier:
+ keyid:88:27:17:09:A9:B6:18:60:8B:EC:EB:BA:F6:47:59:C5:52:54:A3:B7
+
+ X509v3 Certificate Policies:
+ Policy: 1.2.840.113635.100.5.1
+ CPS: http://www.apple.com/appleca/
+ User Notice:
+ Explicit Text: Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.
+
+ X509v3 CRL Distribution Points:
+ URI:http://crl.apple.com/wwdrca.crl
+
+ X509v3 Key Usage:
+ Digital Signature
+ X509v3 Extended Key Usage:
+ TLS Web Client Authentication, 1.2.840.113635.100.4.14
+ 1.2.840.113635.100.6.3.2:
+ ..
+ 1.2.840.113635.100.6.1.16:
+ ..pass.void-star.com.meepedoo
+ Signature Algorithm: sha1WithRSAEncryption
+ 5d:e8:2d:5a:d7:65:9e:92:dd:bf:66:1c:04:99:08:a9:40:b7:
+ 92:dc:fa:d4:c8:8c:cf:ad:3a:99:22:34:0c:0f:72:c9:4f:7f:
+ c5:90:dc:8a:5d:47:c0:dd:ee:47:f7:01:81:6a:06:61:66:c3:
+ 44:a1:0b:96:e5:70:a2:2f:c3:bb:98:d0:bf:07:0a:3d:56:d1:
+ 95:01:08:16:6e:9a:5e:6b:45:ce:d9:b5:78:09:0f:eb:ff:11:
+ a6:9a:eb:65:f3:b3:b1:14:a5:6f:97:a1:53:31:65:a6:e0:ea:
+ e6:70:2f:df:5a:f9:b5:e4:59:2b:33:d4:a0:a3:4c:c6:61:c8:
+ 56:5a:ca:be:4e:ac:12:c7:d3:1e:e5:b6:e3:de:04:c4:63:e5:
+ 0e:33:4d:b9:33:92:7e:11:a4:ee:85:2b:65:00:7f:a5:dc:f6:
+ 19:5b:69:37:fe:61:a7:e6:45:27:c5:1c:3a:b6:46:76:fb:f3:
+ 56:93:00:2a:4f:b4:1e:d3:ed:75:8c:32:e5:09:c8:28:84:46:
+ 29:4d:db:8f:e9:6e:1c:9e:bb:76:74:6a:8f:63:d5:04:1a:b3:
+ 16:42:cc:70:4c:b1:88:e2:6d:58:bd:52:d2:3c:dc:52:9d:de:
+ 37:94:20:00:07:6e:06:48:e7:17:86:44:a2:3a:23:07:c1:74:
+ ef:6f:2a:a5
+-----BEGIN CERTIFICATE-----
+MIIF8zCCBNugAwIBAgIID1AR2I8HCb8wDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
+ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
+aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
+HhcNMTIwNjI2MjExODQwWhcNMTMwNjI2MjExODQwWjCBmDErMCkGCgmSJomT8ixk
+AQEMG3Bhc3Mudm9pZC1zdGFyLmNvbS5tZWVwZWRvbzEyMDAGA1UEAwwpUGFzcyBU
+eXBlIElEOiBwYXNzLnZvaWQtc3Rhci5jb20ubWVlcGVkb28xEzARBgNVBAsMClVM
+NzM2S1lRUjkxEzARBgNVBAoMCkVyaWMgTW9udGkxCzAJBgNVBAYTAlVTMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/mmn8CLpVbytpcKhkLQ8VQBmJWf
+2WkrnL6wtfSkrZ7m746l3FDQziqJqUHORDavkDPnVnaeaJHfxue4IfLVddIqFzqd
+SuDM0pSQ5/I2LxxBAAJ2Rf7Cavw2lud+WQDyhZ4x/6OboLhtlZ7k8cTQ6XxwYVID
+OVy4ijRpIoLFRPnNoSVXJobkMdUIyZ1fc0QQIW2ZkHT2afsg3qlGSaOplqtmROa9
+VmWOfd0HfnG9Ew+xUAev63F4r0bVcTna7fLR242BZH9WxofxfqWj9J8CAbnQNn+H
+8g3Wk07L2DKcWupEB2RqnxwUjpo6DqOGLOggKvEy4BEqE4rTTHNeq3D6TQIDAQAB
+o4ICPzCCAjswPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vb2Nz
+cC5hcHBsZS5jb20vb2NzcC13d2RyMDMwHQYDVR0OBBYEFAKHJADKUziPw01cgJjh
+ZZU41StpMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUiCcXCam2GGCL7Ou69kdZxVJU
+o7cwggEPBgNVHSAEggEGMIIBAjCB/wYJKoZIhvdjZAUBMIHxMCkGCCsGAQUFBwIB
+Fh1odHRwOi8vd3d3LmFwcGxlLmNvbS9hcHBsZWNhLzCBwwYIKwYBBQUHAgIwgbYM
+gbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1
+bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0
+ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBh
+bmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjAwBgNVHR8EKTAn
+MCWgI6Ahhh9odHRwOi8vY3JsLmFwcGxlLmNvbS93d2RyY2EuY3JsMAsGA1UdDwQE
+AwIHgDAeBgNVHSUEFzAVBggrBgEFBQcDAgYJKoZIhvdjZAQOMBAGCiqGSIb3Y2QG
+AwIEAgUAMCsGCiqGSIb3Y2QGARAEHQwbcGFzcy52b2lkLXN0YXIuY29tLm1lZXBl
+ZG9vMA0GCSqGSIb3DQEBBQUAA4IBAQBd6C1a12Wekt2/ZhwEmQipQLeS3PrUyIzP
+rTqZIjQMD3LJT3/FkNyKXUfA3e5H9wGBagZhZsNEoQuW5XCiL8O7mNC/Bwo9VtGV
+AQgWbppea0XO2bV4CQ/r/xGmmutl87OxFKVvl6FTMWWm4OrmcC/fWvm15FkrM9Sg
+o0zGYchWWsq+TqwSx9Me5bbj3gTEY+UOM025M5J+EaTuhStlAH+l3PYZW2k3/mGn
+5kUnxRw6tkZ2+/NWkwAqT7Qe0+11jDLlCcgohEYpTduP6W4cnrt2dGqPY9UEGrMW
+QsxwTLGI4m1YvVLSPNxSnd43lCAAB24GSOcXhkSiOiMHwXTvbyql
+-----END CERTIFICATE-----
diff --git a/spec/samples/certs/apple_wwdr_issuer.pem b/spec/samples/certs/apple_wwdr_issuer.pem
new file mode 100644
index 0000000..20fa897
--- /dev/null
+++ b/spec/samples/certs/apple_wwdr_issuer.pem
@@ -0,0 +1,89 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 25 (0x19)
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple Root CA
+ Validity
+ Not Before: Feb 14 18:56:35 2008 GMT
+ Not After : Feb 14 18:56:35 2016 GMT
+ Subject: C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Apple Worldwide Developer Relations Certification Authority
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (2048 bit)
+ Modulus (2048 bit):
+ 00:ca:38:54:a6:cb:56:aa:c8:24:39:48:e9:8c:ee:
+ ec:5f:b8:7f:26:91:bc:34:53:7a:ce:7c:63:80:61:
+ 77:64:5e:a5:07:23:b6:39:fe:50:2d:15:56:58:70:
+ 2d:7e:c4:6e:c1:4a:85:3e:2f:f0:de:84:1a:a1:57:
+ c9:af:7b:18:ff:6a:fa:15:12:49:15:08:19:ac:aa:
+ db:2a:32:ed:96:63:68:52:15:3d:8c:8a:ec:bf:6b:
+ 18:95:e0:03:ac:01:7d:97:05:67:ce:0e:85:95:37:
+ 6a:ed:09:b6:ae:67:cd:51:64:9f:c6:5c:d1:bc:57:
+ 6e:67:35:80:76:36:a4:87:81:6e:38:8f:d8:2b:15:
+ 4e:7b:25:d8:5a:bf:4e:83:c1:8d:d2:93:d5:1a:71:
+ b5:60:9c:9d:33:4e:55:f9:12:58:0c:86:b8:16:0d:
+ c1:e5:77:45:8d:50:48:ba:2b:2d:e4:94:85:e1:e8:
+ c4:9d:c6:68:a5:b0:a3:fc:67:7e:70:ba:02:59:4b:
+ 77:42:91:39:b9:f5:cd:e1:4c:ef:c0:3b:48:8c:a6:
+ e5:21:5d:fd:6a:6a:bb:a7:16:35:60:d2:e6:ad:f3:
+ 46:29:c9:e8:c3:8b:e9:79:c0:6a:61:67:15:b2:f0:
+ fd:e5:68:bc:62:5f:6e:cf:99:dd:ef:1b:63:fe:92:
+ 65:ab
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ 88:27:17:09:A9:B6:18:60:8B:EC:EB:BA:F6:47:59:C5:52:54:A3:B7
+ X509v3 Authority Key Identifier:
+ keyid:2B:D0:69:47:94:76:09:FE:F4:6B:8D:2E:40:A6:F7:47:4D:7F:08:5E
+
+ X509v3 CRL Distribution Points:
+ URI:http://www.apple.com/appleca/root.crl
+
+ 1.2.840.113635.100.6.2.1:
+ ..
+ Signature Algorithm: sha1WithRSAEncryption
+ da:32:00:96:c5:54:94:d3:3b:82:37:66:7d:2e:68:d5:c3:c6:
+ b8:cb:26:8c:48:90:cf:13:24:6a:46:8e:63:d4:f0:d0:13:06:
+ dd:d8:c4:c1:37:15:f2:33:13:39:26:2d:ce:2e:55:40:e3:0b:
+ 03:af:fa:12:c2:e7:0d:21:b8:d5:80:cf:ac:28:2f:ce:2d:b3:
+ 4e:af:86:19:04:c6:e9:50:dd:4c:29:47:10:23:fc:6c:bb:1b:
+ 98:6b:48:89:e1:5b:9d:de:46:db:35:85:35:ef:3e:d0:e2:58:
+ 4b:38:f4:ed:75:5a:1f:5c:70:1d:56:39:12:e5:e1:0d:11:e4:
+ 89:25:06:bd:d5:b4:15:8e:5e:d0:59:97:90:e9:4b:81:e2:df:
+ 18:af:44:74:1e:19:a0:3a:47:cc:91:1d:3a:eb:23:5a:fe:a5:
+ 2d:97:f7:7b:bb:d6:87:46:42:85:eb:52:3d:26:b2:63:a8:b4:
+ b1:ca:8f:f4:cc:e2:b3:c8:47:e0:bf:9a:59:83:fa:da:98:53:
+ 2a:82:f5:7c:65:2e:95:d9:33:5d:f5:ed:65:cc:31:37:c5:5a:
+ 04:e8:6b:e1:e7:88:03:4a:75:9e:9b:28:cb:4a:40:88:65:43:
+ 75:dd:cb:3a:25:23:c5:9e:57:f8:2e:ce:d2:a9:92:5e:73:2e:
+ 2f:25:75:15
+-----BEGIN CERTIFICATE-----
+MIIEIzCCAwugAwIBAgIBGTANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET
+MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDgwMjE0MTg1
+NjM1WhcNMTYwMjE0MTg1NjM1WjCBljELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFw
+cGxlIEluYy4xLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVs
+YXRpb25zMUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0
+aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAMo4VKbLVqrIJDlI6Yzu7F+4fyaRvDRTes58Y4Bhd2RepQcj
+tjn+UC0VVlhwLX7EbsFKhT4v8N6EGqFXya97GP9q+hUSSRUIGayq2yoy7ZZjaFIV
+PYyK7L9rGJXgA6wBfZcFZ84OhZU3au0Jtq5nzVFkn8Zc0bxXbmc1gHY2pIeBbjiP
+2CsVTnsl2Fq/ToPBjdKT1RpxtWCcnTNOVfkSWAyGuBYNweV3RY1QSLorLeSUheHo
+xJ3GaKWwo/xnfnC6AllLd0KRObn1zeFM78A7SIym5SFd/Wpqu6cWNWDS5q3zRinJ
+6MOL6XnAamFnFbLw/eVovGJfbs+Z3e8bY/6SZasCAwEAAaOBrjCBqzAOBgNVHQ8B
+Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUiCcXCam2GGCL7Ou6
+9kdZxVJUo7cwHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wNgYDVR0f
+BC8wLTAroCmgJ4YlaHR0cDovL3d3dy5hcHBsZS5jb20vYXBwbGVjYS9yb290LmNy
+bDAQBgoqhkiG92NkBgIBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEA2jIAlsVUlNM7
+gjdmfS5o1cPGuMsmjEiQzxMkakaOY9Tw0BMG3djEwTcV8jMTOSYtzi5VQOMLA6/6
+EsLnDSG41YDPrCgvzi2zTq+GGQTG6VDdTClHECP8bLsbmGtIieFbnd5G2zWFNe8+
+0OJYSzj07XVaH1xwHVY5EuXhDRHkiSUGvdW0FY5e0FmXkOlLgeLfGK9EdB4ZoDpH
+zJEdOusjWv6lLZf3e7vWh0ZChetSPSayY6i0scqP9Mzis8hH4L+aWYP62phTKoL1
+fGUuldkzXfXtZcwxN8VaBOhr4eeIA0p1npsoy0pAiGVDdd3LOiUjxZ5X+C7O0qmS
+XnMuLyV1FQ==
+-----END CERTIFICATE-----
diff --git a/spec/samples/certs/ca.crt b/spec/samples/certs/ca.crt
new file mode 100644
index 0000000..7d0820a
--- /dev/null
+++ b/spec/samples/certs/ca.crt
@@ -0,0 +1,130 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ e7:13:70:5d:01:cc:39:d2
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=AU, ST=Some-State, L=locality, O=Internet Widgits Pty Ltd, OU=section, CN=example ca/emailAddress=emailaddr at foo.com
+ Validity
+ Not Before: Sep 14 18:42:25 2012 GMT
+ Not After : Sep 14 18:42:25 2013 GMT
+ Subject: C=AU, ST=Some-State, L=locality, O=Internet Widgits Pty Ltd, OU=section, CN=example ca/emailAddress=emailaddr at foo.com
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (4096 bit)
+ Modulus (4096 bit):
+ 00:c0:b4:be:5d:80:c2:19:5f:5c:f4:64:72:2f:3c:
+ 0f:d1:6f:f2:69:74:de:ee:86:d2:a9:4e:02:85:d1:
+ e2:b0:84:c8:73:1f:e0:0d:a9:9f:60:e0:53:7f:b3:
+ 4e:ed:42:63:3d:19:09:bc:67:a0:13:32:dc:a0:25:
+ ac:09:e1:7e:ae:72:73:4f:a1:9c:76:8c:80:35:e2:
+ 75:2e:b2:4a:68:5f:03:83:bc:2e:63:1f:4a:7b:29:
+ 80:2c:3e:fe:72:7f:43:ee:7b:59:6f:fc:51:8b:42:
+ 0a:1b:b9:bb:5f:14:cd:9e:16:36:dd:05:ef:2c:21:
+ 3e:de:39:bc:27:b1:0c:06:6a:49:f8:01:ff:3a:cc:
+ 2c:5a:d9:56:0f:81:bc:6b:88:ee:83:72:d9:14:0c:
+ 34:08:98:aa:1d:35:c2:34:fd:0c:3b:5d:5a:72:c1:
+ 9b:28:ac:04:e1:30:3b:b8:ea:04:a2:48:34:a2:87:
+ 3b:1c:b8:fc:89:f6:91:35:42:e9:6e:c7:af:ab:3b:
+ f6:b7:0a:2e:e3:7f:31:45:1d:04:a5:a4:5a:f6:cd:
+ bb:62:f9:49:e0:83:b3:77:df:ee:ba:84:bf:07:fa:
+ 29:72:6d:2d:b8:e0:42:10:bd:f9:f3:e4:c6:7e:8a:
+ e8:d7:11:b8:67:14:b4:8c:f5:c4:a9:fa:d9:04:bd:
+ 17:7f:b4:d0:a3:b9:c7:df:44:2d:50:dd:ba:7c:01:
+ 11:02:23:d9:9c:4c:9a:f0:92:2f:4a:b7:0a:72:3f:
+ fc:61:6d:08:3b:cc:99:3c:65:78:92:76:63:a3:c4:
+ cb:1f:b8:ca:4a:7a:b0:c4:22:b2:9a:13:b6:ae:19:
+ 60:7a:bd:37:1b:93:0e:02:9f:22:e4:dc:21:24:11:
+ 4d:9d:9e:30:7f:7b:92:48:2d:4d:b9:1a:2a:58:c6:
+ 77:4b:70:cf:c1:a6:6d:06:f9:54:83:bf:27:73:8f:
+ cc:8b:5c:7a:d2:79:56:75:41:d4:77:5f:9c:39:30:
+ 02:0c:c5:0b:0c:32:6b:23:39:68:26:d5:f7:ef:b1:
+ 7f:1e:c6:1d:a9:39:64:15:dd:b0:df:f5:d6:cf:c0:
+ 73:b2:d0:f2:2a:f4:6e:9e:d8:9c:5b:9f:bf:ce:e7:
+ 69:80:bf:69:d2:20:3b:aa:55:49:3e:fa:50:fc:63:
+ 7e:8d:90:46:9a:7d:eb:9c:ca:17:bc:36:d7:88:e0:
+ d9:c4:29:8c:2f:bc:ce:9d:e5:35:d5:ec:d7:a6:93:
+ 49:2a:84:3b:b6:37:1e:0c:57:f1:04:bb:2f:ea:75:
+ 75:6a:de:0c:71:42:e6:e9:14:5e:ee:55:1a:1f:a4:
+ 87:21:2b:12:98:8b:17:63:c8:84:88:43:46:f4:8f:
+ b2:b7:5b
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Subject Key Identifier:
+ 28:88:9A:FA:67:E6:16:C3:67:92:46:15:0B:46:12:08:54:45:AC:3A
+ X509v3 Authority Key Identifier:
+ keyid:28:88:9A:FA:67:E6:16:C3:67:92:46:15:0B:46:12:08:54:45:AC:3A
+ DirName:/C=AU/ST=Some-State/L=locality/O=Internet Widgits Pty Ltd/OU=section/CN=example ca/emailAddress=emailaddr at foo.com
+ serial:E7:13:70:5D:01:CC:39:D2
+
+ X509v3 Basic Constraints:
+ CA:TRUE
+ Signature Algorithm: sha1WithRSAEncryption
+ 81:b5:c6:b6:4a:10:f5:8a:98:30:96:07:46:8e:bb:6b:b3:c1:
+ 2e:fa:f3:59:ca:e1:c6:85:70:b6:bc:fe:9f:77:0a:70:1a:56:
+ fb:46:71:d6:ee:90:cf:f5:a6:1d:4c:3c:24:76:32:fe:ec:55:
+ d6:c4:cd:3a:ee:dd:2d:97:b6:3b:d3:3b:3f:02:21:1c:ba:03:
+ 48:b8:8d:03:a7:b8:70:81:b6:43:82:82:5f:34:eb:b1:a7:01:
+ cf:e4:f8:f7:d0:d8:5a:b6:c4:a6:0b:4e:e2:cc:4d:be:d4:99:
+ f4:37:54:6f:a7:7d:63:7c:7f:25:34:bb:e0:f4:e2:cf:31:c3:
+ 99:4e:9e:a0:d1:f0:3b:15:8e:95:2e:76:a7:14:f0:3b:0c:e7:
+ 42:8d:12:53:f9:24:d7:f9:90:93:71:56:9a:5f:35:8e:f3:29:
+ 1e:d8:d2:71:fe:d8:d0:eb:d9:61:d5:3e:dd:d8:8f:6a:10:0d:
+ 7c:f8:2a:1d:ef:24:d0:c1:13:d7:d6:9e:f7:cc:20:62:ea:78:
+ 71:d1:7a:5f:2e:20:0d:64:38:07:1b:c9:a0:0f:3c:c9:4e:3f:
+ b1:84:8c:ae:86:ea:60:c7:84:2f:b6:d6:93:95:8d:36:9f:d2:
+ 6a:bf:5e:98:1f:f9:da:fd:97:f3:e0:a7:0b:a7:52:30:e5:10:
+ 89:c1:2c:85:75:07:a4:c7:d4:dc:b5:96:2a:c3:d5:eb:3b:7c:
+ fb:a0:55:4c:c1:65:6c:f8:29:74:8b:78:54:7d:a0:d0:9c:32:
+ 8a:87:48:90:ad:5c:41:fe:ab:16:5a:ad:44:06:1b:c6:4d:e4:
+ 01:f0:d8:c7:b1:db:bf:ca:fd:31:75:0a:01:70:00:45:e1:2d:
+ 98:42:d5:36:53:2c:f1:57:93:7d:cd:e6:24:f3:4d:c9:31:ce:
+ a6:59:fe:2c:4a:61:56:9d:ad:55:7b:43:c5:48:ee:95:1e:b4:
+ 0b:8d:4d:53:c7:a9:af:03:01:12:99:06:e8:fe:3a:63:8d:53:
+ 30:8e:5a:95:64:d4:28:04:d8:bf:72:30:a1:fb:10:a7:84:15:
+ 1b:13:a0:82:f3:38:5b:95:57:79:16:24:49:1a:1c:c5:43:90:
+ 04:dd:5f:7b:d6:67:36:e1:41:fe:9d:20:a4:08:9c:14:9d:e3:
+ 30:5c:1f:8e:46:f7:89:6b:f3:04:b9:75:b6:b6:33:49:54:99:
+ 8e:9d:b0:f5:2b:51:61:3a:48:fc:f5:73:32:cd:a2:f6:9d:2a:
+ 52:92:66:ae:a9:1f:06:b0:4d:63:29:83:e8:80:61:af:99:76:
+ bc:fd:97:d4:98:0b:49:d5:94:24:b1:34:85:31:0a:4e:6c:c3:
+ 0d:de:be:85:1d:49:41:6c
+-----BEGIN CERTIFICATE-----
+MIIG0zCCBLugAwIBAgIJAOcTcF0BzDnSMA0GCSqGSIb3DQEBBQUAMIGhMQswCQYD
+VQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTERMA8GA1UEBxMIbG9jYWxpdHkx
+ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEQMA4GA1UECxMHc2Vj
+dGlvbjETMBEGA1UEAxMKZXhhbXBsZSBjYTEgMB4GCSqGSIb3DQEJARYRZW1haWxh
+ZGRyQGZvby5jb20wHhcNMTIwOTE0MTg0MjI1WhcNMTMwOTE0MTg0MjI1WjCBoTEL
+MAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxETAPBgNVBAcTCGxvY2Fs
+aXR5MSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEDAOBgNVBAsT
+B3NlY3Rpb24xEzARBgNVBAMTCmV4YW1wbGUgY2ExIDAeBgkqhkiG9w0BCQEWEWVt
+YWlsYWRkckBmb28uY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
+wLS+XYDCGV9c9GRyLzwP0W/yaXTe7obSqU4ChdHisITIcx/gDamfYOBTf7NO7UJj
+PRkJvGegEzLcoCWsCeF+rnJzT6GcdoyANeJ1LrJKaF8Dg7wuYx9KeymALD7+cn9D
+7ntZb/xRi0IKG7m7XxTNnhY23QXvLCE+3jm8J7EMBmpJ+AH/OswsWtlWD4G8a4ju
+g3LZFAw0CJiqHTXCNP0MO11acsGbKKwE4TA7uOoEokg0ooc7HLj8ifaRNULpbsev
+qzv2twou438xRR0EpaRa9s27YvlJ4IOzd9/uuoS/B/opcm0tuOBCEL358+TGforo
+1xG4ZxS0jPXEqfrZBL0Xf7TQo7nH30QtUN26fAERAiPZnEya8JIvSrcKcj/8YW0I
+O8yZPGV4knZjo8TLH7jKSnqwxCKymhO2rhlger03G5MOAp8i5NwhJBFNnZ4wf3uS
+SC1NuRoqWMZ3S3DPwaZtBvlUg78nc4/Mi1x60nlWdUHUd1+cOTACDMULDDJrIzlo
+JtX377F/HsYdqTlkFd2w3/XWz8BzstDyKvRunticW5+/zudpgL9p0iA7qlVJPvpQ
+/GN+jZBGmn3rnMoXvDbXiODZxCmML7zOneU11ezXppNJKoQ7tjceDFfxBLsv6nV1
+at4McULm6RRe7lUaH6SHISsSmIsXY8iEiENG9I+yt1sCAwEAAaOCAQowggEGMB0G
+A1UdDgQWBBQoiJr6Z+YWw2eSRhULRhIIVEWsOjCB1gYDVR0jBIHOMIHLgBQoiJr6
+Z+YWw2eSRhULRhIIVEWsOqGBp6SBpDCBoTELMAkGA1UEBhMCQVUxEzARBgNVBAgT
+ClNvbWUtU3RhdGUxETAPBgNVBAcTCGxvY2FsaXR5MSEwHwYDVQQKExhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQxEDAOBgNVBAsTB3NlY3Rpb24xEzARBgNVBAMTCmV4
+YW1wbGUgY2ExIDAeBgkqhkiG9w0BCQEWEWVtYWlsYWRkckBmb28uY29tggkA5xNw
+XQHMOdIwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAgEAgbXGtkoQ9YqY
+MJYHRo67a7PBLvrzWcrhxoVwtrz+n3cKcBpW+0Zx1u6Qz/WmHUw8JHYy/uxV1sTN
+Ou7dLZe2O9M7PwIhHLoDSLiNA6e4cIG2Q4KCXzTrsacBz+T499DYWrbEpgtO4sxN
+vtSZ9DdUb6d9Y3x/JTS74PTizzHDmU6eoNHwOxWOlS52pxTwOwznQo0SU/kk1/mQ
+k3FWml81jvMpHtjScf7Y0OvZYdU+3diPahANfPgqHe8k0MET19ae98wgYup4cdF6
+Xy4gDWQ4BxvJoA88yU4/sYSMrobqYMeEL7bWk5WNNp/Sar9emB/52v2X8+CnC6dS
+MOUQicEshXUHpMfU3LWWKsPV6zt8+6BVTMFlbPgpdIt4VH2g0JwyiodIkK1cQf6r
+FlqtRAYbxk3kAfDYx7Hbv8r9MXUKAXAAReEtmELVNlMs8VeTfc3mJPNNyTHOpln+
+LEphVp2tVXtDxUjulR60C41NU8eprwMBEpkG6P46Y41TMI5alWTUKATYv3IwofsQ
+p4QVGxOggvM4W5VXeRYkSRocxUOQBN1fe9ZnNuFB/p0gpAicFJ3jMFwfjkb3iWvz
+BLl1trYzSVSZjp2w9StRYTpI/PVzMs2i9p0qUpJmrqkfBrBNYymD6IBhr5l2vP2X
+1JgLSdWUJLE0hTEKTmzDDd6+hR1JQWw=
+-----END CERTIFICATE-----
diff --git a/spec/samples/certs/ca.key b/spec/samples/certs/ca.key
new file mode 100644
index 0000000..3c6d1ee
--- /dev/null
+++ b/spec/samples/certs/ca.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEAwLS+XYDCGV9c9GRyLzwP0W/yaXTe7obSqU4ChdHisITIcx/g
+DamfYOBTf7NO7UJjPRkJvGegEzLcoCWsCeF+rnJzT6GcdoyANeJ1LrJKaF8Dg7wu
+Yx9KeymALD7+cn9D7ntZb/xRi0IKG7m7XxTNnhY23QXvLCE+3jm8J7EMBmpJ+AH/
+OswsWtlWD4G8a4jug3LZFAw0CJiqHTXCNP0MO11acsGbKKwE4TA7uOoEokg0ooc7
+HLj8ifaRNULpbsevqzv2twou438xRR0EpaRa9s27YvlJ4IOzd9/uuoS/B/opcm0t
+uOBCEL358+TGforo1xG4ZxS0jPXEqfrZBL0Xf7TQo7nH30QtUN26fAERAiPZnEya
+8JIvSrcKcj/8YW0IO8yZPGV4knZjo8TLH7jKSnqwxCKymhO2rhlger03G5MOAp8i
+5NwhJBFNnZ4wf3uSSC1NuRoqWMZ3S3DPwaZtBvlUg78nc4/Mi1x60nlWdUHUd1+c
+OTACDMULDDJrIzloJtX377F/HsYdqTlkFd2w3/XWz8BzstDyKvRunticW5+/zudp
+gL9p0iA7qlVJPvpQ/GN+jZBGmn3rnMoXvDbXiODZxCmML7zOneU11ezXppNJKoQ7
+tjceDFfxBLsv6nV1at4McULm6RRe7lUaH6SHISsSmIsXY8iEiENG9I+yt1sCAwEA
+AQKCAgAo+AQkwtQBKuoLNzOjYSSHxUIHM4aVtWoh/mjA5H9KQeCPwS4UGYS9xtNZ
+qdhUzrFkcudD+8/nZP/MuFWcACm7kq97NYObHIHBcvSwyczR5alMn3xJLITcLFWI
+kpfr5ayejfDUwxLfBVo6zMDOFREl09k0IifX/PVtr16WHajN0FkLdfk6GeAwLFaE
+k3NodUMoBQmrnnCNh09bSGuScl3gXRd2oDyJaBDdgzCfPnlfuvQdvZxOnfFqr1Xt
+ud0A5UkuoV/xSCPxz7+8zs+HG6sPH2wAPbl8FuPXz27kjoZCfufC4P1AecTx5EG4
+nWGp04Ru/OB0Yc2EzldSP/dVb5IIAEeIzeqOC1dKh+m5BL6+KCMW4RA2VRmtVcSO
+Xu8/uLNw3coWj8OdUg4JJLWIuY17puKyhmMuZqXSwF5qcSCD2kGMtgdIFevHGLDy
+NCXIxAhnp/mWGJJvkRPxe7NVZSKGh7nfGGFbr2GAsTX5XZetk6ye/A+oNrUM4UP+
+WQNtD5m2R1IPIp7f8NdTiSpAsKrgzDVEKSdXW/DObRm64DdRINuxGkrjDTOR6B+M
+HmcKQdMZPVyzWeFHF+JyR4+GVx2OZ/5lzEPTHO8thiA1nn1ov/RYZwuwrl8EKDt6
+jgU26C+1TD3lL9smBdekOB8EtQT2EcuE7hwS6ca4Sryj5RYCmQKCAQEA/DTBSczZ
+HtPcHs75IiHfUXGoJB943LIR4cqrWKO2xy16MpamNQy8yBzet7pgXmrxixtar13B
+SpPozQNwlKD43rLSG77soY9eZcn2SQCOlcuekdRczR2jp4cnIo3bTXOm5OTz55EK
+WTLGkTirlcGCPFHv866qrmVQF+cIo3qtszrwuJGkOWTfU8X9zSN+AU+KwAqam5C5
+ZbkcXFNwI2FOud5nQsOrjJ9Aivrw1CbHvHqAiZzZXjZmMNurH/cLX9jregxoiTAv
+wXsS1Apvvq0kvWJ9QrRBNyRsZ6qEt0ABgf2tl6+4t+S9bCmS2L6RzqjrxbkVPoM2
+ChHX+8XkwuVUHwKCAQEAw5rao4MU4itSeaWLrny5oo57pICyY3zF5c5HySpaWyA8
+JksxYCvNDR1aFZjCdOG9zOf5vLieniyTK+Tl3FF5P6OxSJOcQ9UOXaoTKny1Kw9W
+BMTbR0COLXsJSNvjNyZSyoLgOOLTZj11S+X/W1vb+Xi0kkx4SS6Tg3i5N+0Fl2Wa
+Mgucy/pgq+JBTlgKZ1Rjpna1T5ibUn0pT6fxgZ2U+FAD0cYNyaCjVmzMbiIkLUId
+jJEDGYXenyoIFI0rrJvF/0nl98BZjsT9+GNs0ZlejS0rfn2+DWlrLur3aqG2i3Yr
+L9nWZ51bSB7UhHALN2t2qd9etKWmz4YbuFHPvHOVRQKCAQEAgmdMoboXcYcdw5hE
+3M8ixtu7kqHrPkGcWWEPY4+SzD3JdyrJ2ZgybE3xIpJtjaRCLCkCpLYXYVZFAuwK
+Y+8vfwZ6+PmpJIgayQq6G1j8YJud680gBraSjeal54ntoIhx/Nwc+NjXvvMwFJp2
+rcIWctXy+c6QVgfwd8tvfgfKlGefW++COGLdzlULO+xkFI1qMo3JDzKviddCwMIr
+sz93E0fZoH3Hz7fwCWxi5W7/y4aTu5OsGLiL8itCug5khTSF3N9ZlcWii6n1PEoB
+KVghLQMlvT2ykq50ls1mPdIMdYgTH+Et43eUMb71PLicb3yMG/ns8Bur71z2jinu
+dI9bBQKCAQBk30XjTuUFIbw9mX6oNA/zYbEni2rzXVQdB70DY8EG/1+li99hrhTn
+r3xWaNnXNtcPhY3HohnCjlAzMa7MaIOzqvHw8JaEcKog6WVK4tb25sjAWtiOLR9l
+Gu8V0LejKDNH1ihVjbvhHM6RnoGKlpuhUnskeyUI8GkIQsiZq7TXd4EGT/DDTFJw
+MZTmFwb+dImTPeKQsq1e48bbGku0QRSi3XiqxI01ro6tMhxWq2qmoFLmu52ymtPM
+lvtlxcuBDzATUAO1OU+2Daa/Yl6q1IHrIiEs8SGCfxvULT38knq1/vGUkq077+00
+CxojVjiiktu2DMglNswIdytyaVZM4/pVAoIBAQDmXzA7dptH9tOrp5/pYO4C8Fk0
+4NB7OgKrhtE8YQtfghQn1O0FjvNT/o1+Gf9mf40Np7OtVulhZ56fQupmkk56TAJu
+P1YDVDRPLOvMkYeIsqqv6HnXGQaS6Bv91wrqwWPjLDD3pH+AdjP27SnObzARjRUq
+/s5LmI+u87rCldkGLG4qO4sjptWRlU8pIsLWQ3NckXINT7i8wwXzEfOcu7IzKWg6
+sdzMWxc7Vt/4WE5m+9DLkpk3sD9dqELHry5lGWbpLHhfoVXoRdoFNVmyi2ckdr0J
+FJ6hkWkgQ4ee+yHyL3FfYRbGIolqaVSCd8/CvvDZ/24eS3/7TpCzf8SUqtL6
+-----END RSA PRIVATE KEY-----
diff --git a/spec/samples/certs/client.crt b/spec/samples/certs/client.crt
new file mode 100644
index 0000000..b481376
--- /dev/null
+++ b/spec/samples/certs/client.crt
@@ -0,0 +1,78 @@
+Certificate:
+ Data:
+ Version: 1 (0x0)
+ Serial Number: 2 (0x2)
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=AU, ST=Some-State, L=locality, O=Internet Widgits Pty Ltd, OU=section, CN=example ca/emailAddress=emailaddr at foo.com
+ Validity
+ Not Before: Sep 14 18:43:15 2012 GMT
+ Not After : Sep 14 18:43:15 2013 GMT
+ Subject: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=client.example.com
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (1024 bit)
+ Modulus (1024 bit):
+ 00:dd:81:af:cb:67:4f:9d:2b:37:9f:bb:52:6a:17:
+ f3:25:ba:ca:6d:23:45:94:dc:08:9b:cd:82:56:79:
+ da:0b:47:c9:d0:3f:a8:16:bb:56:77:3a:85:a9:ab:
+ 20:f3:1e:2d:5c:fa:b2:23:90:86:81:bf:a9:f6:10:
+ 85:55:3f:c6:aa:7e:fa:43:96:08:c0:fa:e8:2f:cb:
+ e5:2d:01:d1:12:22:a2:3f:7a:c5:75:4e:6f:47:d3:
+ 2e:7a:fe:17:6e:4f:a3:20:60:5b:46:57:51:69:ce:
+ 8f:33:87:05:df:1e:cb:fb:32:43:2a:71:6c:44:d1:
+ a6:2d:98:41:e4:88:36:9a:f1
+ Exponent: 65537 (0x10001)
+ Signature Algorithm: sha1WithRSAEncryption
+ 9e:7f:e1:05:a7:9a:86:6c:0d:6e:37:76:ae:2e:5b:78:e5:22:
+ 6f:52:40:36:e8:7e:cf:20:77:4f:3b:b4:7b:7c:72:46:68:df:
+ fe:04:87:16:9f:cd:6e:0f:79:01:25:cf:ca:5e:b8:47:31:9e:
+ f1:20:44:26:78:79:40:00:57:cc:a6:a8:39:67:ef:01:d7:65:
+ 1c:dd:5d:8f:6e:48:43:f4:03:48:46:8f:08:95:cf:27:3f:30:
+ da:82:a7:33:82:5f:82:cb:e6:2c:f0:25:e8:87:3b:e5:bf:82:
+ 79:72:a9:10:45:69:3c:7f:f0:dd:c9:50:6e:02:c9:05:16:cf:
+ c3:58:15:3f:a6:32:ec:80:4f:88:b4:72:d2:5f:70:62:24:98:
+ e5:99:c8:a7:d9:dd:0c:b0:cb:9c:70:d1:6a:44:21:d9:d7:65:
+ a6:71:6f:60:64:7e:28:de:5d:98:42:6d:aa:fc:32:f9:1c:d6:
+ 5c:d7:b6:15:18:79:09:80:7e:d7:9e:74:16:a5:80:39:6d:93:
+ 8e:8e:4e:c5:8c:f5:4c:ea:d3:fd:12:bc:fa:fc:b8:e2:2c:30:
+ 52:f4:eb:ad:d9:56:e9:84:e9:a8:df:a3:16:fa:d2:1e:74:49:
+ 5d:d1:24:10:f2:2e:c4:b9:4a:a9:2d:3d:a4:70:6d:24:00:26:
+ 46:bf:e2:98:16:4d:c8:55:40:a7:ab:76:b3:c6:a7:72:46:2d:
+ 9b:fd:a2:ca:b8:62:9c:59:53:cc:64:ef:60:76:10:c8:c9:e7:
+ 51:11:82:d4:81:04:73:e9:af:df:2d:c4:c7:2d:e4:17:d4:e2:
+ 10:82:68:56:ae:7a:f2:3c:60:b7:59:29:39:6a:56:86:94:fc:
+ 93:2b:5b:f0:ac:80:1d:c7:c5:b7:27:36:94:1c:ad:e9:1c:6b:
+ f3:8a:2a:6c:c8:ce:69:52:b2:42:d9:b9:e7:8e:a3:d4:18:07:
+ a1:db:bf:54:3c:ec:2e:68:7f:cf:d6:71:8f:3e:99:88:e4:ea:
+ 7f:98:22:3a:31:68:24:a5:47:23:e2:d6:21:8f:1f:5f:a7:9a:
+ 12:10:ba:6d:ac:22:e7:97:95:93:a2:b5:1c:f8:c8:86:1a:ad:
+ 32:ff:64:4f:25:8d:d5:25:29:46:85:30:bc:c4:86:41:1b:6b:
+ 24:7e:04:b6:eb:46:39:55:9c:4d:84:86:2f:bf:11:26:a9:40:
+ 3d:2d:f4:90:22:05:7f:27:3b:13:d1:86:17:70:05:e1:68:be:
+ 12:ce:c5:30:7b:0a:1b:7b:8a:89:e6:e7:9a:9c:b0:8b:c1:f8:
+ c3:b6:0a:4c:41:da:fe:cd:ea:ce:89:3b:a5:8d:30:90:93:93:
+ 9b:85:b6:61:46:8d:69:f2
+-----BEGIN CERTIFICATE-----
+MIID9DCCAdwCAQIwDQYJKoZIhvcNAQEFBQAwgaExCzAJBgNVBAYTAkFVMRMwEQYD
+VQQIEwpTb21lLVN0YXRlMREwDwYDVQQHEwhsb2NhbGl0eTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQLEwdzZWN0aW9uMRMwEQYDVQQD
+EwpleGFtcGxlIGNhMSAwHgYJKoZIhvcNAQkBFhFlbWFpbGFkZHJAZm9vLmNvbTAe
+Fw0xMjA5MTQxODQzMTVaFw0xMzA5MTQxODQzMTVaMGIxCzAJBgNVBAYTAkFVMRMw
+EQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0
+eSBMdGQxGzAZBgNVBAMTEmNsaWVudC5leGFtcGxlLmNvbTCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEA3YGvy2dPnSs3n7tSahfzJbrKbSNFlNwIm82CVnnaC0fJ
+0D+oFrtWdzqFqasg8x4tXPqyI5CGgb+p9hCFVT/Gqn76Q5YIwProL8vlLQHREiKi
+P3rFdU5vR9Muev4Xbk+jIGBbRldRac6PM4cF3x7L+zJDKnFsRNGmLZhB5Ig2mvEC
+AwEAATANBgkqhkiG9w0BAQUFAAOCAgEAnn/hBaeahmwNbjd2ri5beOUib1JANuh+
+zyB3Tzu0e3xyRmjf/gSHFp/Nbg95ASXPyl64RzGe8SBEJnh5QABXzKaoOWfvAddl
+HN1dj25IQ/QDSEaPCJXPJz8w2oKnM4JfgsvmLPAl6Ic75b+CeXKpEEVpPH/w3clQ
+bgLJBRbPw1gVP6Yy7IBPiLRy0l9wYiSY5ZnIp9ndDLDLnHDRakQh2ddlpnFvYGR+
+KN5dmEJtqvwy+RzWXNe2FRh5CYB+1550FqWAOW2Tjo5OxYz1TOrT/RK8+vy44iww
+UvTrrdlW6YTpqN+jFvrSHnRJXdEkEPIuxLlKqS09pHBtJAAmRr/imBZNyFVAp6t2
+s8anckYtm/2iyrhinFlTzGTvYHYQyMnnURGC1IEEc+mv3y3Exy3kF9TiEIJoVq56
+8jxgt1kpOWpWhpT8kytb8KyAHcfFtyc2lByt6Rxr84oqbMjOaVKyQtm5546j1BgH
+odu/VDzsLmh/z9Zxjz6ZiOTqf5giOjFoJKVHI+LWIY8fX6eaEhC6bawi55eVk6K1
+HPjIhhqtMv9kTyWN1SUpRoUwvMSGQRtrJH4EtutGOVWcTYSGL78RJqlAPS30kCIF
+fyc7E9GGF3AF4Wi+Es7FMHsKG3uKiebnmpywi8H4w7YKTEHa/s3qzok7pY0wkJOT
+m4W2YUaNafI=
+-----END CERTIFICATE-----
diff --git a/spec/samples/certs/client.csr b/spec/samples/certs/client.csr
new file mode 100644
index 0000000..68856e1
--- /dev/null
+++ b/spec/samples/certs/client.csr
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBojCCAQsCAQAwYjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx
+ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEbMBkGA1UEAxMSY2xp
+ZW50LmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdga/L
+Z0+dKzefu1JqF/MlusptI0WU3AibzYJWedoLR8nQP6gWu1Z3OoWpqyDzHi1c+rIj
+kIaBv6n2EIVVP8aqfvpDlgjA+ugvy+UtAdESIqI/esV1Tm9H0y56/hduT6MgYFtG
+V1Fpzo8zhwXfHsv7MkMqcWxE0aYtmEHkiDaa8QIDAQABoAAwDQYJKoZIhvcNAQEF
+BQADgYEARSbIBSvvhvqX7zMBap+RcQfMdXbSQTI3iNVSEOoUtfuGEJOmkHrWwsz0
+ZfKv/qC9LBeWD+yqDeKbuRNJEla2oIInUfs3FINYLsm3jufsuBpVPY1OOglq91VD
+v1zBodLbTvbHIHKStsMfNaS9lKYZ/PycWNXxhQhpZZAeGBqk3mY=
+-----END CERTIFICATE REQUEST-----
diff --git a/spec/samples/certs/client.key b/spec/samples/certs/client.key
new file mode 100644
index 0000000..5a87945
--- /dev/null
+++ b/spec/samples/certs/client.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDdga/LZ0+dKzefu1JqF/MlusptI0WU3AibzYJWedoLR8nQP6gW
+u1Z3OoWpqyDzHi1c+rIjkIaBv6n2EIVVP8aqfvpDlgjA+ugvy+UtAdESIqI/esV1
+Tm9H0y56/hduT6MgYFtGV1Fpzo8zhwXfHsv7MkMqcWxE0aYtmEHkiDaa8QIDAQAB
+AoGBAIdkD4mqWhVdJyCxJMzIWsyDAdv3pT45x+FDmhk1XbtrY8WwQxOx6kXyNWTh
+vsAbf+rHKT9nxW9lMYO/0V+sHcdRtG0NEXPfB8pX7LEsaHpRPHkVoiWpRlCL/maM
+ci85RPsATlDkiOn1luysfk2PHy5aSKG0RkLS7lSkahTQOn9JAkEA8goUnDdh5hVm
+7o2npeZG66Zb2mnm1l8aO9LJ76u6L/jmTx5aSRXP0aBiDb1PXJZedqhd/MdPcYEI
+QsFCIoT/iwJBAOpIbAK15eWsubUme/UoCbfNpM4H3jQXbSODgJ83nwmqr2slrX9m
+soz/+2nZl6/TL344xxTmDChFGivHdh7JXvMCQBoHi3/hVN3xn0g4Y7crtKTTFz29
+9d1IDQIyARWNWlCea+ZGVV9WwSrCHMlteoNyiGYqZTEyHhEO11yWfA5KT1ECQQCY
+bFHJWaqeyMdxsf4Hu+rGqIZGfRv17B/XcSDndXqFAYVrQnIkZx5XWduqPCTSAaXu
+iuYLFLhoIr0qKnURBpY9AkEAy9JOf5tqc1jFndLIgVtXzM5KptIFHr1ZA7VSfrdY
+ozt9adbCTXfNTTLqAK0N7A2F7T3APEbpPuR2a7TpzawRWA==
+-----END RSA PRIVATE KEY-----
diff --git a/spec/samples/certs/github.com.pem b/spec/samples/certs/github.com.pem
new file mode 100644
index 0000000..74dda62
--- /dev/null
+++ b/spec/samples/certs/github.com.pem
@@ -0,0 +1,122 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 0e:77:76:8a:5d:07:f0:e5:79:59:ca:2a:9d:50:82:b5
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV CA-1
+ Validity
+ Not Before: May 27 00:00:00 2011 GMT
+ Not After : Jul 29 12:00:00 2013 GMT
+ Subject: businessCategory=Private Organization/1.3.6.1.4.1.311.60.2.1.3=US/1.3.6.1.4.1.311.60.2.1.2=California/serialNumber=C3268102, C=US, ST=California, L=San Francisco, O=GitHub, Inc., CN=github.com
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (2048 bit)
+ Modulus (2048 bit):
+ 00:ed:d3:89:c3:5d:70:72:09:f3:33:4f:1a:72:74:
+ d9:b6:5a:95:50:bb:68:61:9f:f7:fb:1f:19:e1:da:
+ 04:31:af:15:7c:1a:7f:f9:73:af:1d:e5:43:2b:56:
+ 09:00:45:69:4a:e8:c4:5b:df:c2:77:52:51:19:5b:
+ d1:2b:d9:39:65:36:a0:32:19:1c:41:73:fb:32:b2:
+ 3d:9f:98:ec:82:5b:0b:37:64:39:2c:b7:10:83:72:
+ cd:f0:ea:24:4b:fa:d9:94:2e:c3:85:15:39:a9:3a:
+ f6:88:da:f4:27:89:a6:95:4f:84:a2:37:4e:7c:25:
+ 78:3a:c9:83:6d:02:17:95:78:7d:47:a8:55:83:ee:
+ 13:c8:19:1a:b3:3c:f1:5f:fe:3b:02:e1:85:fb:11:
+ 66:ab:09:5d:9f:4c:43:f0:c7:24:5e:29:72:28:ce:
+ d4:75:68:4f:24:72:29:ae:39:28:fc:df:8d:4f:4d:
+ 83:73:74:0c:6f:11:9b:a7:dd:62:de:ff:e2:eb:17:
+ e6:ff:0c:bf:c0:2d:31:3b:d6:59:a2:f2:dd:87:4a:
+ 48:7b:6d:33:11:14:4d:34:9f:32:38:f6:c8:19:9d:
+ f1:b6:3d:c5:46:ef:51:0b:8a:c6:33:ed:48:61:c4:
+ 1d:17:1b:bd:7c:b6:67:e9:39:cf:a5:52:80:0a:f4:
+ ea:cd
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Authority Key Identifier:
+ keyid:4C:58:CB:25:F0:41:4F:52:F4:28:C8:81:43:9B:A6:A8:A0:E6:92:E5
+
+ X509v3 Subject Key Identifier:
+ 87:D1:8F:19:6E:E4:87:6F:53:8C:77:91:07:50:DF:A3:BF:55:47:20
+ X509v3 Subject Alternative Name:
+ DNS:github.com, DNS:www.github.com
+ Authority Information Access:
+ OCSP - URI:http://ocsp.digicert.com
+ CA Issuers - URI:http://www.digicert.com/CACerts/DigiCertHighAssuranceEVCA-1.crt
+
+ X509v3 Basic Constraints: critical
+ CA:FALSE
+ X509v3 CRL Distribution Points:
+ URI:http://crl3.digicert.com/ev2009a.crl
+ URI:http://crl4.digicert.com/ev2009a.crl
+
+ X509v3 Certificate Policies:
+ Policy: 2.16.840.1.114412.2.1
+ CPS: http://www.digicert.com/ssl-cps-repository.htm
+ User Notice:
+ Explicit Text:
+
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ Netscape Cert Type:
+ SSL Client, SSL Server
+ X509v3 Key Usage: critical
+ Digital Signature, Key Encipherment
+ Signature Algorithm: sha1WithRSAEncryption
+ 14:52:71:1f:86:9d:6d:35:3e:86:bb:66:1a:8b:85:98:b9:00:
+ 4c:cb:42:b5:46:fc:06:e7:44:39:c8:e8:52:d8:11:14:23:b3:
+ 72:96:e9:14:94:9e:2f:00:28:f7:d5:04:45:40:00:c6:f4:57:
+ 42:42:de:09:89:97:11:0d:14:5c:6b:bd:0b:f7:18:a3:5f:67:
+ 02:f3:09:38:63:bf:c1:12:9d:30:ba:8e:a5:54:74:59:53:67:
+ a1:1b:50:5b:26:da:fd:13:7e:59:17:bf:49:ef:94:7e:45:a4:
+ fd:3a:49:32:f0:6a:ff:89:8d:a9:61:a9:aa:9b:96:46:c8:1c:
+ e0:18:1c:e6:fb:82:f4:0a:ab:52:a6:ca:e8:54:22:d9:db:2a:
+ 3d:5a:22:7b:80:ea:07:05:d4:f9:c7:f0:53:59:5f:bb:77:7e:
+ de:93:70:41:4e:23:cb:78:79:79:c4:2e:ea:d7:66:2a:18:f7:
+ d1:c5:7c:e2:12:78:82:8d:1d:ec:82:9e:01:a2:e5:02:be:78:
+ a1:b9:59:58:c5:4c:6f:4f:a5:31:b4:49:5b:5e:98:1e:2e:38:
+ f6:19:c4:39:a2:4a:fb:79:05:b8:f2:59:e5:26:12:70:ad:c0:
+ e8:75:23:1f:18:d1:0b:e0:9f:65:e4:c3:d7:49:87:5b:72:6c:
+ b1:2f:ac:6f
+-----BEGIN CERTIFICATE-----
+MIIHKjCCBhKgAwIBAgIQDnd2il0H8OV5WcoqnVCCtTANBgkqhkiG9w0BAQUFADBp
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSgwJgYDVQQDEx9EaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBDQS0xMB4XDTExMDUyNzAwMDAwMFoXDTEzMDcyOTEyMDAwMFowgcoxHTAb
+BgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGCNzwCAQMTAlVT
+MRswGQYLKwYBBAGCNzwCAQITCkNhbGlmb3JuaWExETAPBgNVBAUTCEMzMjY4MTAy
+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2Fu
+IEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYDVQQDEwpnaXRo
+dWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7dOJw11wcgnz
+M08acnTZtlqVULtoYZ/3+x8Z4doEMa8VfBp/+XOvHeVDK1YJAEVpSujEW9/Cd1JR
+GVvRK9k5ZTagMhkcQXP7MrI9n5jsglsLN2Q5LLcQg3LN8OokS/rZlC7DhRU5qTr2
+iNr0J4mmlU+EojdOfCV4OsmDbQIXlXh9R6hVg+4TyBkaszzxX/47AuGF+xFmqwld
+n0xD8MckXilyKM7UdWhPJHIprjko/N+NT02Dc3QMbxGbp91i3v/i6xfm/wy/wC0x
+O9ZZovLdh0pIe20zERRNNJ8yOPbIGZ3xtj3FRu9RC4rGM+1IYcQdFxu9fLZn6TnP
+pVKACvTqzQIDAQABo4IDajCCA2YwHwYDVR0jBBgwFoAUTFjLJfBBT1L0KMiBQ5um
+qKDmkuUwHQYDVR0OBBYEFIfRjxlu5IdvU4x3kQdQ36O/VUcgMCUGA1UdEQQeMByC
+CmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMIGBBggrBgEFBQcBAQR1MHMwJAYI
+KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBLBggrBgEFBQcwAoY/
+aHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ0FDZXJ0cy9EaWdpQ2VydEhpZ2hBc3N1
+cmFuY2VFVkNBLTEuY3J0MAwGA1UdEwEB/wQCMAAwYQYDVR0fBFowWDAqoCigJoYk
+aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL2V2MjAwOWEuY3JsMCqgKKAmhiRodHRw
+Oi8vY3JsNC5kaWdpY2VydC5jb20vZXYyMDA5YS5jcmwwggHEBgNVHSAEggG7MIIB
+tzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cuZGln
+aWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFkBggrBgEFBQcCAjCC
+AVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABp
+AGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBw
+AHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQ
+AC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQBy
+AHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0
+ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwBy
+AHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBl
+AG4AYwBlAC4wHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBEGCWCGSAGG
++EIBAQQEAwIGwDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQEFBQADggEBABRS
+cR+GnW01Poa7ZhqLhZi5AEzLQrVG/AbnRDnI6FLYERQjs3KW6RSUni8AKPfVBEVA
+AMb0V0JC3gmJlxENFFxrvQv3GKNfZwLzCThjv8ESnTC6jqVUdFlTZ6EbUFsm2v0T
+flkXv0nvlH5FpP06STLwav+JjalhqaqblkbIHOAYHOb7gvQKq1KmyuhUItnbKj1a
+InuA6gcF1PnH8FNZX7t3ft6TcEFOI8t4eXnELurXZioY99HFfOISeIKNHeyCngGi
+5QK+eKG5WVjFTG9PpTG0SVtemB4uOPYZxDmiSvt5BbjyWeUmEnCtwOh1Ix8Y0Qvg
+n2Xkw9dJh1tybLEvrG8=
+-----END CERTIFICATE-----
+
diff --git a/spec/samples/certs/server.crt b/spec/samples/certs/server.crt
new file mode 100644
index 0000000..f986ab3
--- /dev/null
+++ b/spec/samples/certs/server.crt
@@ -0,0 +1,79 @@
+Certificate:
+ Data:
+ Version: 1 (0x0)
+ Serial Number: 1 (0x1)
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=AU, ST=Some-State, L=locality, O=Internet Widgits Pty Ltd, OU=section, CN=example ca/emailAddress=emailaddr at foo.com
+ Validity
+ Not Before: Sep 14 18:43:03 2012 GMT
+ Not After : Sep 14 18:43:03 2013 GMT
+ Subject: C=AU, ST=Some-State, L=locality, O=Internet Widgits Pty Ltd, OU=ouname, CN=server.example.com/emailAddress=email at example.com
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (1024 bit)
+ Modulus (1024 bit):
+ 00:c2:6b:ec:81:d7:3f:94:09:f0:a6:f1:78:65:79:
+ a7:28:26:5c:b7:96:86:5b:77:9a:be:67:7a:16:e0:
+ a2:c8:8d:9c:ec:5d:9a:56:0e:c7:f4:8b:11:01:df:
+ d2:94:80:ba:87:2b:cf:d4:33:4a:06:21:3b:c6:31:
+ b5:fc:b4:30:f6:fd:28:58:5f:18:11:46:08:fa:12:
+ 75:82:c2:e9:e6:65:9a:e4:fe:8d:f0:36:63:91:06:
+ bb:43:f6:72:50:5c:8b:00:e3:53:2a:09:34:06:e6:
+ 05:16:30:d0:f8:96:3b:b0:7d:b6:8d:ef:5f:b0:c1:
+ 31:bc:77:d4:a9:93:31:d7:f1
+ Exponent: 65537 (0x10001)
+ Signature Algorithm: sha1WithRSAEncryption
+ aa:c7:e4:d5:c5:33:e8:75:43:01:23:9f:ae:91:c3:17:dc:54:
+ d5:34:65:e3:76:ec:00:e4:71:06:70:84:10:2d:ae:ea:21:90:
+ 70:27:ec:86:15:97:e5:e4:10:62:19:08:b5:56:86:4f:84:ef:
+ 43:c7:86:06:33:8a:61:bb:71:ac:f9:e1:d2:f0:08:83:32:bb:
+ 73:a0:fd:39:66:8f:a3:d9:bb:59:bb:c2:cc:5e:8f:56:fc:72:
+ b2:42:da:d2:31:1b:98:be:0d:0c:8d:1a:8e:12:fe:7f:ef:5b:
+ cf:93:b7:e1:ba:c8:a0:c4:de:60:4f:74:ea:12:9d:3a:ea:81:
+ 28:b3:ed:14:6e:22:00:23:56:b0:ef:d4:7e:6b:7d:4a:fb:7e:
+ 3b:c2:a8:9e:84:42:43:ad:6d:b1:41:78:75:a6:32:46:5c:98:
+ c2:e5:3b:d0:dd:cc:17:35:7b:f9:54:25:ef:38:07:82:dc:a3:
+ 32:69:bb:15:28:71:a9:c7:a3:8e:55:29:61:04:eb:ee:05:e9:
+ 9f:4b:f6:c7:6e:9b:02:19:e9:0c:5e:66:a1:65:fe:ae:6e:25:
+ a4:a4:31:3c:40:0d:b9:f4:c2:44:40:23:65:85:58:33:5a:0d:
+ 84:3d:24:71:43:0f:65:69:28:75:de:ae:b3:b7:82:a4:09:f1:
+ b7:21:8f:5d:76:66:4c:d7:08:19:80:68:a0:9f:33:df:46:a7:
+ ab:7f:45:4d:1f:d1:45:54:8f:53:b9:da:77:86:b3:e2:b2:7e:
+ 72:a2:6e:ad:08:01:2a:05:79:d7:ba:a2:17:c7:82:72:58:7c:
+ 4d:fb:b9:0e:09:54:24:1e:34:e0:ae:32:d7:0a:00:1b:23:e4:
+ 95:1b:8e:28:6c:7b:31:55:ad:6b:bb:e0:76:d3:2e:d2:14:0c:
+ 02:9a:b5:65:ce:54:c7:28:08:7b:85:3c:43:00:09:c3:90:4e:
+ b0:9c:57:f0:66:d2:18:95:ce:4e:18:f0:81:f1:16:a1:b0:ca:
+ a8:85:33:c4:8c:b5:06:9c:eb:e6:5b:b9:13:31:14:53:83:3c:
+ 8f:0e:02:56:f9:b7:07:d5:a0:66:8a:8a:06:ee:c7:46:a2:e6:
+ b2:f5:ef:be:f5:ac:e0:a3:fa:9e:1d:03:b1:40:0f:b8:8c:bb:
+ ab:f0:13:55:db:05:65:eb:8a:f7:03:86:e0:a5:bb:f2:ae:ab:
+ f4:32:7b:2c:28:56:fb:7d:30:b3:c1:71:c7:52:3c:a9:ee:a0:
+ 82:6f:b6:90:54:b7:5b:f2:74:11:4d:40:89:e4:cb:c3:50:b8:
+ f7:e6:98:bd:95:7e:1c:8f:b9:e8:9f:ef:d6:ca:3b:77:40:6a:
+ dc:1e:51:74:c0:32:53:61
+-----BEGIN CERTIFICATE-----
+MIIEOzCCAiMCAQEwDQYJKoZIhvcNAQEFBQAwgaExCzAJBgNVBAYTAkFVMRMwEQYD
+VQQIEwpTb21lLVN0YXRlMREwDwYDVQQHEwhsb2NhbGl0eTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQLEwdzZWN0aW9uMRMwEQYDVQQD
+EwpleGFtcGxlIGNhMSAwHgYJKoZIhvcNAQkBFhFlbWFpbGFkZHJAZm9vLmNvbTAe
+Fw0xMjA5MTQxODQzMDNaFw0xMzA5MTQxODQzMDNaMIGoMQswCQYDVQQGEwJBVTET
+MBEGA1UECBMKU29tZS1TdGF0ZTERMA8GA1UEBxMIbG9jYWxpdHkxITAfBgNVBAoT
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UECxMGb3VuYW1lMRswGQYD
+VQQDExJzZXJ2ZXIuZXhhbXBsZS5jb20xIDAeBgkqhkiG9w0BCQEWEWVtYWlsQGV4
+YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCa+yB1z+UCfCm
+8XhleacoJly3loZbd5q+Z3oW4KLIjZzsXZpWDsf0ixEB39KUgLqHK8/UM0oGITvG
+MbX8tDD2/ShYXxgRRgj6EnWCwunmZZrk/o3wNmORBrtD9nJQXIsA41MqCTQG5gUW
+MND4ljuwfbaN71+wwTG8d9SpkzHX8QIDAQABMA0GCSqGSIb3DQEBBQUAA4ICAQCq
+x+TVxTPodUMBI5+ukcMX3FTVNGXjduwA5HEGcIQQLa7qIZBwJ+yGFZfl5BBiGQi1
+VoZPhO9Dx4YGM4phu3Gs+eHS8AiDMrtzoP05Zo+j2btZu8LMXo9W/HKyQtrSMRuY
+vg0MjRqOEv5/71vPk7fhusigxN5gT3TqEp066oEos+0UbiIAI1aw79R+a31K+347
+wqiehEJDrW2xQXh1pjJGXJjC5TvQ3cwXNXv5VCXvOAeC3KMyabsVKHGpx6OOVSlh
+BOvuBemfS/bHbpsCGekMXmahZf6ubiWkpDE8QA259MJEQCNlhVgzWg2EPSRxQw9l
+aSh13q6zt4KkCfG3IY9ddmZM1wgZgGignzPfRqerf0VNH9FFVI9Tudp3hrPisn5y
+om6tCAEqBXnXuqIXx4JyWHxN+7kOCVQkHjTgrjLXCgAbI+SVG44obHsxVa1ru+B2
+0y7SFAwCmrVlzlTHKAh7hTxDAAnDkE6wnFfwZtIYlc5OGPCB8RahsMqohTPEjLUG
+nOvmW7kTMRRTgzyPDgJW+bcH1aBmiooG7sdGouay9e++9azgo/qeHQOxQA+4jLur
+8BNV2wVl64r3A4bgpbvyrqv0MnssKFb7fTCzwXHHUjyp7qCCb7aQVLdb8nQRTUCJ
+5MvDULj35pi9lX4cj7non+/Wyjt3QGrcHlF0wDJTYQ==
+-----END CERTIFICATE-----
diff --git a/spec/samples/certs/server.csr b/spec/samples/certs/server.csr
new file mode 100644
index 0000000..c12c9b1
--- /dev/null
+++ b/spec/samples/certs/server.csr
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIB6TCCAVICAQAwgagxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRl
+MREwDwYDVQQHEwhsb2NhbGl0eTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
+dHkgTHRkMQ8wDQYDVQQLEwZvdW5hbWUxGzAZBgNVBAMTEnNlcnZlci5leGFtcGxl
+LmNvbTEgMB4GCSqGSIb3DQEJARYRZW1haWxAZXhhbXBsZS5jb20wgZ8wDQYJKoZI
+hvcNAQEBBQADgY0AMIGJAoGBAMJr7IHXP5QJ8KbxeGV5pygmXLeWhlt3mr5nehbg
+osiNnOxdmlYOx/SLEQHf0pSAuocrz9QzSgYhO8Yxtfy0MPb9KFhfGBFGCPoSdYLC
+6eZlmuT+jfA2Y5EGu0P2clBciwDjUyoJNAbmBRYw0PiWO7B9to3vX7DBMbx31KmT
+MdfxAgMBAAGgADANBgkqhkiG9w0BAQUFAAOBgQAyoSojahrYbGK+6QbxOO7i5Ufm
+VHlhVBbPFfmYDrpWjoRlKVwk1iRNi/3ijQi3oPONk19wRh/A0gD0DOiKi3fz2m5K
+gaFLIRcBy25EYVeBic39A6b69SiXQoHv00f5CBHNSHLk4hc30vGIWifexU8ehwJJ
+TlHmdHkECni6w0eDmg==
+-----END CERTIFICATE REQUEST-----
diff --git a/spec/samples/certs/server.key b/spec/samples/certs/server.key
new file mode 100644
index 0000000..bded07a
--- /dev/null
+++ b/spec/samples/certs/server.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDCa+yB1z+UCfCm8XhleacoJly3loZbd5q+Z3oW4KLIjZzsXZpW
+Dsf0ixEB39KUgLqHK8/UM0oGITvGMbX8tDD2/ShYXxgRRgj6EnWCwunmZZrk/o3w
+NmORBrtD9nJQXIsA41MqCTQG5gUWMND4ljuwfbaN71+wwTG8d9SpkzHX8QIDAQAB
+AoGBALX9FBvOAsNuMofyjEJgh6m7jxqCmi3QXVdRwSTdDUMx2+wdCjT4DQ/JhRf+
+DT3Y6cFRr27wu5/VSACT08hCW3mVgE5WcsmxvxPeaWMQCAUoHW4I9bmpvf/1AksZ
+x36N4GwdEvFjDGlM8B1ndW3qLUp6e5iDFUFB7veQGoA6WR/hAkEA39yivn0ThKH/
+9a8glJMBDVmnzNXQveBX+Y0aiJSNTT+rBfDRgWxR8A60z1Xl1iHtGG1j1tRXvDL3
+2u2w4d1lnQJBAN5VTMFkf7ojTvPh0+QHkc7b3d0IRruojEIG3A1ZdUj4WaOqnOBs
+xk6EWsF5YbCBTWH42qWAq/EBXaJcPbyIdWUCQF25882LcpOSfCcyJpLuJX+gbPf/
+AYGuH0dVg6lxgOO553H6TM1CO+AlWCCC11LbK3iRvD5i80TRliJsaCV426UCQEAD
+RS8lNVUtV00GhxBPUZ7CVPWPrXXYSFG2UeMSD5+ryXtC4xoGl24B03OC9CpygAom
+MSWXj2m7X+8gKbI/g7UCQEc0Ne6+4T0NsCz9Dw2TWqIvi+WbK65veyFC212OJ5Wh
+qEF4SY3WXkxJk4Y0ElQARMz6DojpwHI5PtAaYswe8wI=
+-----END RSA PRIVATE KEY-----
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4d2e02d..25daaab 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,4 +1,14 @@
require 'rubygems'
require 'rspec'
+require 'pathname'
+
+require 'coveralls'
+Coveralls.wear!
+
+SPECDIR = Pathname(__FILE__).dirname
+require SPECDIR.join('..', 'lib', 'certificate_authority').to_s
+
+def sample_file(name)
+ SPECDIR.join("samples", name)
+end
-require File.dirname(__FILE__) + '/../lib/certificate_authority'
\ No newline at end of file
diff --git a/spec/units/certificate_revocation_list_spec.rb b/spec/units/certificate_revocation_list_spec.rb
index 0c3e8ed..af69b38 100644
--- a/spec/units/certificate_revocation_list_spec.rb
+++ b/spec/units/certificate_revocation_list_spec.rb
@@ -7,17 +7,21 @@ describe CertificateAuthority::CertificateRevocationList do
@root_certificate = CertificateAuthority::Certificate.new
@root_certificate.signing_entity = true
@root_certificate.subject.common_name = "CRL Root"
- @root_certificate.key_material.generate_key(1024)
+ @root_certificate.key_material.generate_key(768)
@root_certificate.serial_number.number = 1
@root_certificate.sign!
@certificate = CertificateAuthority::Certificate.new
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.subject.common_name = "http://bogusSite.com"
@certificate.parent = @root_certificate
@certificate.serial_number.number = 2
@certificate.sign!
+ @serial_number = CertificateAuthority::SerialNumber.new
+ @serial_number.revoked_at = Time.now
+ @serial_number.number = 5
+
@crl.parent = @root_certificate
@certificate.revoked_at = Time.now
end
@@ -50,6 +54,62 @@ describe CertificateAuthority::CertificateRevocationList do
OpenSSL::X509::CRL.new(@crl.to_pem).should_not be_nil
end
+ it "should be able to mix Certificates and SerialNumbers for convenience" do
+ @crl << @certificate
+ @crl << @serial_number
+ @crl.parent = @root_certificate
+ @crl.sign!
+ openssl_csr = OpenSSL::X509::CRL.new(@crl.to_pem)
+ openssl_csr.revoked.size.should == 2
+ end
+
+ it "should have the correct number of entities" do
+ @crl << @certificate
+ @crl.parent = @root_certificate
+ @crl.sign!
+ openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
+ openssl_clr.revoked.should be_a(Array)
+ openssl_clr.revoked.size.should == 1
+ end
+
+ it "should have the serial numbers of revoked entities" do
+ @crl << @certificate
+ @crl << @serial_number
+ @crl.parent = @root_certificate
+ @crl.sign!
+ openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
+ openssl_clr.revoked.should be_a(Array)
+ openssl_clr.revoked.first.serial.should == @certificate.serial_number.number
+ openssl_clr.revoked.last.serial.should == @serial_number.number
+ end
+
+ it "should be valid according to OpenSSL and signer" do
+ @crl << @certificate
+ @crl.parent = @root_certificate
+ @crl.sign!
+ openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
+ openssl_root = OpenSSL::X509::Certificate.new(@root_certificate.to_pem)
+ openssl_clr.verify(openssl_root.public_key).should be_true
+ end
+
+ describe "Digests" do
+ it "should use SHA512 by default" do
+ @crl << @certificate
+ @crl.parent = @root_certificate
+ @crl.sign!
+ openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
+ openssl_clr.signature_algorithm.should == "sha512WithRSAEncryption"
+ end
+
+ it "should support alternate digests supported by OpenSSL" do
+ @crl << @certificate
+ @crl.parent = @root_certificate
+ @crl.sign!({"digest" => "SHA1"})
+ openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem)
+ openssl_clr.signature_algorithm.should == "sha1WithRSAEncryption"
+ end
+ end
+
describe "Next update" do
it "should be able to set a 'next_update' value" do
@crl.next_update = (60 * 60 * 10) # 10 Hours
@@ -61,8 +121,5 @@ describe CertificateAuthority::CertificateRevocationList do
@crl.next_update = - (60 * 60 * 10)
lambda{@crl.sign!}.should raise_error
end
-
end
-
-
end
diff --git a/spec/units/certificate_spec.rb b/spec/units/certificate_spec.rb
index 8158e54..d07abd6 100644
--- a/spec/units/certificate_spec.rb
+++ b/spec/units/certificate_spec.rb
@@ -36,7 +36,7 @@ describe CertificateAuthority::Certificate do
it "should be able to self-sign" do
@certificate.serial_number.number = 1
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.sign!
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
cert.subject.to_s.should == cert.issuer.to_s
@@ -45,7 +45,7 @@ describe CertificateAuthority::Certificate do
it "should have the basicContraint CA:TRUE" do
@certificate.serial_number.number = 1
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.sign!
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
cert.extensions.map{|i| [i.oid,i.value] }.select{|i| i.first == "basicConstraints"}.first[1].should == "CA:TRUE"
@@ -57,7 +57,7 @@ describe CertificateAuthority::Certificate do
@different_cert = CertificateAuthority::Certificate.new
@different_cert.signing_entity = true
@different_cert.subject.common_name = "chrischandler.name root"
- @different_cert.key_material.generate_key(1024)
+ @different_cert.key_material.generate_key(768)
@different_cert.serial_number.number = 2
@different_cert.sign! #self-signed
@certificate.parent = @different_cert
@@ -79,7 +79,7 @@ describe CertificateAuthority::Certificate do
it "should correctly be signed by a parent certificate" do
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.signing_entity = true
@certificate.serial_number.number = 1
@certificate.sign!
@@ -89,7 +89,7 @@ describe CertificateAuthority::Certificate do
it "should have the basicContraint CA:TRUE" do
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.signing_entity = true
@certificate.serial_number.number = 3
@certificate.sign!
@@ -104,7 +104,7 @@ describe CertificateAuthority::Certificate do
@different_cert = CertificateAuthority::Certificate.new
@different_cert.signing_entity = true
@different_cert.subject.common_name = "chrischandler.name root"
- @different_cert.key_material.generate_key(1024)
+ @different_cert.key_material.generate_key(768)
@different_cert.serial_number.number = 1
@different_cert.sign! #self-signed
@certificate.parent = @different_cert
@@ -120,7 +120,7 @@ describe CertificateAuthority::Certificate do
it "should have the basicContraint CA:FALSE" do
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.signing_entity = false
@certificate.serial_number.number = 1
@certificate.sign!
@@ -139,7 +139,7 @@ describe CertificateAuthority::Certificate do
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
@certificate.sign!
end
@@ -154,7 +154,7 @@ describe CertificateAuthority::Certificate do
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
@signing_profile = {
"extensions" => {
@@ -175,7 +175,7 @@ describe CertificateAuthority::Certificate do
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
end
@@ -190,13 +190,23 @@ describe CertificateAuthority::Certificate do
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
cert.extensions.map(&:oid).include?("subjectAltName").should be_false
end
+
+ it 'should replace email:copy with email address' do
+ @certificate.subject.email_address = 'foo at bar.com'
+ @certificate.sign!(
+ { "extensions" => { "subjectAltName" => { 'emails' => %w[copy fubar at bar.com] } } }
+ )
+ cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
+ alt = cert.extensions.select { |e| e.oid == 'subjectAltName' }.first
+ alt.value.should == 'email:foo at bar.com, email:fubar at bar.com'
+ end
end
describe "AuthorityInfoAccess" do
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
end
@@ -211,12 +221,12 @@ describe CertificateAuthority::Certificate do
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
end
it "should have a crlDistributionPoint if specified" do
- @certificate.sign!({"extensions" => {"crlDistributionPoints" => {"uri" => ["http://crlThingy.com"]}}})
+ @certificate.sign!({"extensions" => {"crlDistributionPoints" => {"uris" => ["http://crlThingy.com"]}}})
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
cert.extensions.map(&:oid).include?("crlDistributionPoints").should be_true
end
@@ -233,7 +243,7 @@ describe CertificateAuthority::Certificate do
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
end
@@ -250,21 +260,28 @@ describe CertificateAuthority::Certificate do
cert.extensions.map(&:oid).include?("certificatePolicies").should be_true
end
- it "should contain a nested userNotice if specified" do
- pending
- # @certificate.sign!({
- # "extensions" => {
- # "certificatePolicies" => {
- # "policy_identifier" => "1.3.5.7",
- # "cps_uris" => ["http://my.host.name/", "http://my.your.name/"],
- # "user_notice" => {
- # "explicit_text" => "Testing!", "organization" => "RSpec Test organization name", "notice_numbers" => "1,2,3,4"
- # }
- # }
- # }
- # })
- # cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
- # cert.extensions.map(&:oid).include?("certificatePolicies").should be_true
+ pending "should contain a nested userNotice if specified" do
+ #pending
+ @certificate.sign!({
+ "extensions" => {
+ "certificatePolicies" => {
+ "policy_identifier" => "1.3.5.7",
+ "cps_uris" => ["http://my.host.name/", "http://my.your.name/"],
+ "user_notice" => {
+ "explicit_text" => "Testing explicit text!", "organization" => "RSpec Test organization name", "notice_numbers" => "1,2,3,4"
+ }
+ }
+ }
+ })
+ cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
+ cert.extensions.map(&:oid).include?("certificatePolicies").should be_true
+ ## Checking OIDs after they've run through OpenSSL is a pain...
+ ## The nicely structured data will be flattened to a single String
+ cert.extensions.each do |ext|
+ if ext.oid == "certificatePolicies"
+ ext.to_a[1].should include("Testing explicit text!")
+ end
+ end
end
it "should NOT include a certificatePolicy if not specified" do
@@ -275,7 +292,7 @@ describe CertificateAuthority::Certificate do
end
- it "should support BasicContraints" do
+ it "should support BasicConstraints" do
cert = OpenSSL::X509::Certificate.new(@certificate.to_pem)
cert.extensions.map(&:oid).include?("basicConstraints").should be_true
end
@@ -312,7 +329,7 @@ describe CertificateAuthority::Certificate do
before(:each) do
@certificate = CertificateAuthority::Certificate.new
@certificate.subject.common_name = "chrischandler.name"
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.serial_number.number = 1
@signing_profile = {
@@ -321,7 +338,7 @@ describe CertificateAuthority::Certificate do
"crlDistributionPoints" => {"uri" => "http://notme.com/other.crl" },
"subjectKeyIdentifier" => {},
"authorityKeyIdentifier" => {},
- "authorityInfoAccess" => {"ocsp" => ["http://youFillThisOut/ocsp/"] },
+ "authorityInfoAccess" => {"ocsp" => ["http://youFillThisOut/ocsp/"], "ca_issuers" => ["http://me.com/other.crt"] },
"keyUsage" => {"usage" => ["digitalSignature","nonRepudiation"] },
"extendedKeyUsage" => {"usage" => [ "serverAuth","clientAuth"]},
"subjectAltName" => {"uris" => ["http://subdomains.youFillThisOut/"]},
@@ -384,6 +401,51 @@ CERT
@small_cert.key_material.private_key = "data"
@small_cert.should be_valid
end
+
+ it "should check to make sure that if a certificate had extensions they were imported" do
+ cert_path = File.join(File.dirname(__FILE__),"..","samples","certs","github.com.pem")
+ openssl_cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
+ @cert_with_extensions = CertificateAuthority::Certificate.from_openssl(openssl_cert)
+
+ expected_basicConstraints = CertificateAuthority::Extensions::BasicConstraints.new
+ expected_basicConstraints.critical = true
+ expected_basicConstraints.ca = false
+ @cert_with_extensions.extensions["basicConstraints"].should == expected_basicConstraints
+
+ expected_crlDistributionPoints = CertificateAuthority::Extensions::CrlDistributionPoints.new
+ expected_crlDistributionPoints.uris = ["http://crl3.digicert.com/ev2009a.crl","http://crl4.digicert.com/ev2009a.crl"]
+ @cert_with_extensions.extensions["crlDistributionPoints"].should == expected_crlDistributionPoints
+
+ expected_subjectAlt = CertificateAuthority::Extensions::SubjectAlternativeName.new
+ expected_subjectAlt.dns_names =["github.com", "www.github.com"]
+ @cert_with_extensions.extensions["subjectAltName"].should == expected_subjectAlt
+
+ expected_subjectKeyIdentifier = CertificateAuthority::Extensions::SubjectKeyIdentifier.new
+ expected_subjectKeyIdentifier.identifier = "87:D1:8F:19:6E:E4:87:6F:53:8C:77:91:07:50:DF:A3:BF:55:47:20"
+ @cert_with_extensions.extensions["subjectKeyIdentifier"].should == expected_subjectKeyIdentifier
+
+ expected_authorityKeyIdentifier = CertificateAuthority::Extensions::AuthorityKeyIdentifier.new
+ expected_authorityKeyIdentifier.identifier = "keyid:4C:58:CB:25:F0:41:4F:52:F4:28:C8:81:43:9B:A6:A8:A0:E6:92:E5"
+ @cert_with_extensions.extensions["authorityKeyIdentifier"].should == expected_authorityKeyIdentifier
+
+ expected_authorityInfoAccess = CertificateAuthority::Extensions::AuthorityInfoAccess.new
+ expected_authorityInfoAccess.ocsp << "URI:http://ocsp.digicert.com"
+ expected_authorityInfoAccess.ca_issuers << "URI:http://www.digicert.com/CACerts/DigiCertHighAssuranceEVCA-1.crt"
+ @cert_with_extensions.extensions["authorityInfoAccess"].should == expected_authorityInfoAccess
+
+ expected_keyUsage = CertificateAuthority::Extensions::KeyUsage.new
+ expected_keyUsage.critical = true
+ # This one is goofy. Though you have to tell openssl 'digitalSignature'
+ # it will parse and return 'Digital Signature' even though those should
+ # be identical.
+ expected_keyUsage.usage = ["Digital Signature", "Key Encipherment"]
+ @cert_with_extensions.extensions["keyUsage"].should == expected_keyUsage
+
+ expected_extendedKeyUsage = CertificateAuthority::Extensions::ExtendedKeyUsage.new
+ # Same asymmetric specify vs parse as above
+ expected_extendedKeyUsage.usage = ["TLS Web Server Authentication", "TLS Web Client Authentication"]
+ @cert_with_extensions.extensions["extendedKeyUsage"].should == expected_extendedKeyUsage
+ end
end
it "should have a distinguished name" do
@@ -415,13 +477,15 @@ CERT
end
it "should default to one year validity" do
- @certificate.not_after.should < Time.now + 65 * 60 * 24 * 365 and
- @certificate.not_after.should > Time.now + 55 * 60 * 24 * 365
+ day = 60 * 60 * 24
+ year = day * 365
+ @certificate.not_after.should < Time.now + year + day and
+ @certificate.not_after.should > Time.now + year - day
end
it "should be able to have a revoked at time" do
@certificate.revoked?.should be_false
- @certificate.revoked_at = Time.now
+ @certificate.revoked_at = Time.now.utc
@certificate.revoked?.should be_true
end
diff --git a/spec/units/distinguished_name_spec.rb b/spec/units/distinguished_name_spec.rb
index 136a007..0ea5706 100644
--- a/spec/units/distinguished_name_spec.rb
+++ b/spec/units/distinguished_name_spec.rb
@@ -12,6 +12,8 @@ describe CertificateAuthority::DistinguishedName do
@distinguished_name.respond_to?(:o).should be_true
@distinguished_name.respond_to?(:ou).should be_true
@distinguished_name.respond_to?(:c).should be_true
+ @distinguished_name.respond_to?(:emailAddress).should be_true
+ @distinguished_name.respond_to?(:serialNumber).should be_true
end
it "should provide human-readable equivalents to the distinguished name common attributes" do
@@ -21,6 +23,8 @@ describe CertificateAuthority::DistinguishedName do
@distinguished_name.respond_to?(:organization).should be_true
@distinguished_name.respond_to?(:organizational_unit).should be_true
@distinguished_name.respond_to?(:country).should be_true
+ @distinguished_name.respond_to?(:email_address).should be_true
+ @distinguished_name.respond_to?(:serial_number).should be_true
end
it "should require a common name" do
@@ -55,5 +59,16 @@ describe CertificateAuthority::DistinguishedName do
it "should create an equivalent object" do
@dn.to_x509_name.to_s.split('/').should =~ @name.to_s.split('/')
end
+
+ end
+
+ describe CertificateAuthority::WrappedDistinguishedName do
+ it "should mark the DN as having custom OIDs if there's an unknown subject element" do
+ OpenSSL::ASN1::ObjectId.register("2.3.4.5","testing","testingCustomOIDs")
+ subject = "/testingCustomOIDs=custom/CN=justincummins.name/L=on my laptop/ST=relaxed/C=as/O=programmer/OU=using this code"
+ @name = OpenSSL::X509::Name.parse subject
+ @dn = CertificateAuthority::DistinguishedName.from_openssl @name
+ @dn.custom_oids?.should be_true
+ end
end
-end
\ No newline at end of file
+end
diff --git a/spec/units/extensions_spec.rb b/spec/units/extensions_spec.rb
index 72e6e92..a4618ec 100644
--- a/spec/units/extensions_spec.rb
+++ b/spec/units/extensions_spec.rb
@@ -1,32 +1,39 @@
require File.dirname(__FILE__) + '/units_helper'
describe CertificateAuthority::Extensions do
- describe CertificateAuthority::Extensions::BasicContraints do
+ describe CertificateAuthority::Extensions::BasicConstraints do
it "should only allow true/false" do
- basic_constraints = CertificateAuthority::Extensions::BasicContraints.new
+ basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new
basic_constraints.valid?.should be_true
basic_constraints.ca = "moo"
basic_constraints.valid?.should be_false
end
it "should respond to :path_len" do
- basic_constraints = CertificateAuthority::Extensions::BasicContraints.new
+ basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new
basic_constraints.respond_to?(:path_len).should be_true
end
it "should raise an error if :path_len isn't a non-negative integer" do
- basic_constraints = CertificateAuthority::Extensions::BasicContraints.new
+ basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new
lambda {basic_constraints.path_len = "moo"}.should raise_error
lambda {basic_constraints.path_len = -1}.should raise_error
lambda {basic_constraints.path_len = 1.5}.should raise_error
end
it "should generate a proper OpenSSL extension string" do
- basic_constraints = CertificateAuthority::Extensions::BasicContraints.new
+ basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new
basic_constraints.ca = true
basic_constraints.path_len = 2
basic_constraints.to_s.should == "CA:true,pathlen:2"
end
+
+ it "should parse values from a proper OpenSSL extension string" do
+ basic_constraints = CertificateAuthority::Extensions::BasicConstraints.parse("CA:true,pathlen:2", true)
+ basic_constraints.critical.should be_true
+ basic_constraints.ca.should be_true
+ basic_constraints.path_len.should == 2
+ end
end
describe CertificateAuthority::Extensions::SubjectAlternativeName do
@@ -49,6 +56,13 @@ describe CertificateAuthority::Extensions do
subjectAltName.to_s.should == "URI:http://localhost.altname.example.com,URI:http://other.example.com"
end
+ it "should parse URIs from a proper OpenSSL extension string" do
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("URI:http://localhost.altname.example.com", false)
+ subjectAltName.uris.should == ["http://localhost.altname.example.com"]
+
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("URI:http://localhost.altname.example.com,URI:http://other.example.com", false)
+ subjectAltName.uris.should == ["http://localhost.altname.example.com", "http://other.example.com"]
+ end
it "should respond to :dns_names" do
subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new
@@ -69,6 +83,14 @@ describe CertificateAuthority::Extensions do
subjectAltName.to_s.should == "DNS:localhost.altname.example.com,DNS:other.example.com"
end
+ it "should parse DNS names from a proper OpenSSL extension string" do
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com", false)
+ subjectAltName.dns_names.should == ["localhost.altname.example.com"]
+
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com,DNS:other.example.com", false)
+ subjectAltName.dns_names.should == ["localhost.altname.example.com", "other.example.com"]
+ end
+
it "should respond to :ips" do
subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new
subjectAltName.respond_to?(:ips).should be_true
@@ -88,6 +110,32 @@ describe CertificateAuthority::Extensions do
subjectAltName.to_s.should == "IP:1.2.3.4,IP:5.6.7.8"
end
+ it "should parse IPs from a proper OpenSSL extension string" do
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("IP:1.2.3.4", false)
+ subjectAltName.ips.should == ["1.2.3.4"]
+
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("IP:1.2.3.4,IP:5.6.7.8", false)
+ subjectAltName.ips.should == ["1.2.3.4", "5.6.7.8"]
+ end
+
+ describe 'emails' do
+ let(:subject) { CertificateAuthority::Extensions::SubjectAlternativeName.new }
+
+ it "should require 'emails' to be an Array" do
+ expect {
+ subject.emails = "not an array"
+ }.to raise_error "Emails must be an array"
+ end
+
+ it "should generate a proper OpenSSL extension string for emails" do
+ subject.emails = ["copy"]
+ subject.to_s.should == "email:copy"
+
+ subject.emails = ["copy", "foo at bar.com"]
+ subject.to_s.should == "email:copy,email:foo at bar.com"
+ end
+ end
+
it "should generate a proper OpenSSL extension string for URIs IPs and DNS names together" do
subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new
subjectAltName.ips = ["1.2.3.4"]
@@ -107,9 +155,29 @@ describe CertificateAuthority::Extensions do
subjectAltName.uris = ["http://localhost.altname.example.com", "http://other.altname.example.com"]
subjectAltName.to_s.should == "URI:http://localhost.altname.example.com,URI:http://other.altname.example.com,DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4,IP:5.6.7.8"
-
end
+ it "should parse URIs IPs and DNS names together from a proper OpenSSL extension string" do
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("IP:1.2.3.4", false)
+ subjectAltName.ips.should == ["1.2.3.4"]
+
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com,IP:1.2.3.4", false)
+ subjectAltName.dns_names.should == ["localhost.altname.example.com"]
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4", false)
+ subjectAltName.dns_names.should == ["localhost.altname.example.com", "other.example.com"]
+
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4,IP:5.6.7.8", false)
+ subjectAltName.ips.should == ["1.2.3.4", "5.6.7.8"]
+
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("URI:http://localhost.altname.example.com,DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4,IP:5.6.7.8", false)
+ subjectAltName.uris.should == ["http://localhost.altname.example.com"]
+
+ subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("URI:http://localhost.altname.example.com,URI:http://other.altname.example.com,DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4,IP:5.6.7.8", false)
+ subjectAltName.uris.should == ["http://localhost.altname.example.com", "http://other.altname.example.com"]
+
+ subjectAltName.emails= ["copy", "foo at bar.com"]
+ subjectAltName.to_s.should == "URI:http://localhost.altname.example.com,URI:http://other.altname.example.com,DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4,IP:5.6.7.8,email:copy,email:foo@bar.com"
+ end
end
end
diff --git a/spec/units/key_material_spec.rb b/spec/units/key_material_spec.rb
index 4f4e64a..8398f03 100644
--- a/spec/units/key_material_spec.rb
+++ b/spec/units/key_material_spec.rb
@@ -15,6 +15,85 @@ describe CertificateAuthority::KeyMaterial do
@key_material.is_in_memory?.should be_true
end
end
+
+ describe "reading keys from PEM" do
+ before(:each) do
+ @key_pair=<<EOF
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQCxiGVfRrf90CHmvXa+XYWE4m7LZ1slc6cxIYyIgZuQ5T8AeqUa
+kbyYY4wMUR2gZ4pDPs/WGs8fW66q23qmHSr1bQ6HaL8znbD7UL/IiiyiW8I11orb
+rhimIx1A606qi8/0gQc+H851gzUusd5xgKP2X+oPxYx3VG3dpksLnNK1IwIDAQAB
+AoGAfrNNRbX+0dGcoERPXoT4KWJAmEHnNs9XXyUGWtXE5J/3Wqws8M1Zv5gr9w5d
+CoFal6tYQQFZGJQiECYbXjoq0VT8ApWfuO/mCXyXfmnLFEU8EJjmXtXzn2yyPfoY
+At7O8QwvG0bwtw1SqNf7cRtlOEIqoLMtdyaVv4C5ffyheIECQQDVdVf2Sk113Kke
+PREzEb6XZ0n2ugSG8fWJh2QKUI4RXhg7bDzHhSpexeKsJdoet8NJOUEsXMoqLSzK
+bBnSD63RAkEA1OogtDCkpwkvqC63a7hyDP7qRVHFuVeSA1fu+6BFS0xblkgvcPXT
+J7WbWYcP+lqcLjXWeFsqe5qS6sDCsAhsswJAIumZZHgMqU1Y/9AfIwow8RR8vXT5
+TpT+gur5CtLYGbEZJ4bxffSi1HNrOprKTSHjN/O8XCQlELboz4bUxk24MQJAcsaX
+xKsoR4dTMoWkiSRQDyNoJOA1B3nmk3jWsryuPi42fSgCsxFBt/lVeoitm1c3NE3/
+hLgYibNFGdm52e1gswJBAMwYuImbl6AVLv0Y41smxIkvfAzlyNfTAsp7GqLoMhYN
+q/0KoyI2Ge3+NnmJI/eaiYs8qC2HjrgdX9ZDSUCWfpQ=
+-----END RSA PRIVATE KEY-----
+EOF
+ @public_key=<<EOF
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxiGVfRrf90CHmvXa+XYWE4m7L
+Z1slc6cxIYyIgZuQ5T8AeqUakbyYY4wMUR2gZ4pDPs/WGs8fW66q23qmHSr1bQ6H
+aL8znbD7UL/IiiyiW8I11orbrhimIx1A606qi8/0gQc+H851gzUusd5xgKP2X+oP
+xYx3VG3dpksLnNK1IwIDAQAB
+-----END PUBLIC KEY-----
+EOF
+
+ @encrypted_key_pair=<<EOF
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,EF5CCB3A64C0A6DB57FB924A3ED5B9A6
+
+qziIbWkeBVYS8Lhs3DqW6BeL6/rLDOyLcei604qx/SsmQZfNPb6SfRnq5EWqWk8V
+kHHA9cPb8x8bRNdGiajg4NPwXHlkEBUJkeM8C24MCvPEztM7jooPtmrRR8ilz3m+
+62LyvyzO6LIHonoqOq/jQwfYFOInPTqpwW/drzkFxuh2Mc7enVnU9e+Z5Pnk6I8g
+Sv7Nmh1kMEArzu4vSFBFalBFJkFOXjysOUMhQ7SJwKHmTbKEqcI5Le5NKT708BMa
+xnDzWQQgvKjiOSKz71Gq2QT3hwqV1bgwV015G0xlpWpBhemey4pr6L3AxysWFujO
+z1fHe0MaV6dbQNwymljwdE5R+9jzU8uEmdo8QrrRnzUnetr01uUcWgOUqnKmuinq
+WMl7Odvep6hoBbqSMfESJZJA9+XPBXfs4FHE1Ri8riAPoGLl484Lj91cVqBqVXZG
+tht39kXNYROOqBZ7pU9fNuTFhDHvAoH9ydFD6YpTRZALHTgSdyZ5IbhmOiJAlW3b
+nhAZtMwwcTYJIF8xyR+8GAdAG/dIDFM+tRmeB1aRkSd6IgxLMh52SFv/vSglEwYj
+PYHvhcpId93yYO34vCGT4tGsZ2j2VpZqAyu5cl8K6Hq8dTYzH2jkfSdGQ9a8vCKG
+jSlK4vK91ZFdO8sTCTlQd6+jUXH3B4wTUsA4key7yUGLXMr1jmapRQJIuhCUnQQn
+B9Py5RdGlGW2ycVaGh08n4LmG/OTJuLb+xTStm5w4iLmB+ynDjIxpfQjvX98hzMh
+G35pgQ2GdKs+NByYXZKz0OHT2NAHkKpoO7rPlzTpMLgUtAGmH7rLEeOUz6TscUDc
+-----END RSA PRIVATE KEY-----
+EOF
+ end
+
+ it "should include a means of reading an RSA keypair" do
+ key = CertificateAuthority::KeyMaterial.from_x509_key_pair(@key_pair)
+ key.public_key.should_not be_nil
+ key.public_key.should be_a(OpenSSL::PKey::RSA)
+ key.private_key.should_not be_nil
+ key.private_key.should be_a(OpenSSL::PKey::RSA)
+ end
+
+ it "should include a means of reading encrypted RSA keypairs" do
+ key = CertificateAuthority::KeyMaterial.from_x509_key_pair(@encrypted_key_pair,"meow")
+ key.public_key.should_not be_nil
+ key.public_key.should be_a(OpenSSL::PKey::RSA)
+ key.private_key.should_not be_nil
+ key.private_key.should be_a(OpenSSL::PKey::RSA)
+ end
+
+ it "should raise an exception if you read an encrypted keypair w/ bad password" do
+ lambda {
+ key = CertificateAuthority::KeyMaterial.from_x509_key_pair(@encrypted_key_pair,"wrong")
+ }.should raise_error
+ end
+
+ it "should include a means of reading a public-only PEM formatted key" do
+ key = CertificateAuthority::KeyMaterial.from_x509_public_key(@public_key)
+ key.public_key.should_not be_nil
+ key.public_key.should be_a(OpenSSL::PKey::RSA)
+ end
+ end
end
describe CertificateAuthority::MemoryKeyMaterial do
@@ -23,21 +102,21 @@ describe CertificateAuthority::MemoryKeyMaterial do
end
it "should be able to generate an RSA key" do
- @key_material.generate_key(1024).should_not be_nil
+ @key_material.generate_key(768).should_not be_nil
end
it "should generate a proper OpenSSL::PKey::RSA" do
- @key_material.generate_key(1024).class.should == OpenSSL::PKey::RSA
+ @key_material.generate_key(768).class.should == OpenSSL::PKey::RSA
end
it "should be able to specify the size of the modulus to generate" do
- @key_material.generate_key(1024).should_not be_nil
+ @key_material.generate_key(768).should_not be_nil
end
describe "with generated key" do
before(:all) do
@key_material_in_memory = CertificateAuthority::MemoryKeyMaterial.new
- @key_material_in_memory.generate_key(1024)
+ @key_material_in_memory.generate_key(768)
end
it "should be able to retrieve the private key" do
@@ -51,7 +130,7 @@ describe CertificateAuthority::MemoryKeyMaterial do
it "should not validate without public and private keys" do
@key_material.valid?.should be_false
- @key_material.generate_key(1024)
+ @key_material.generate_key(768)
@key_material.valid?.should be_true
pub = @key_material.public_key
@key_material.public_key = nil
diff --git a/spec/units/ocsp_handler_spec.rb b/spec/units/ocsp_handler_spec.rb
index 067fa0d..fd6663d 100644
--- a/spec/units/ocsp_handler_spec.rb
+++ b/spec/units/ocsp_handler_spec.rb
@@ -1,18 +1,132 @@
require File.dirname(__FILE__) + '/units_helper'
+describe CertificateAuthority::OCSPRequestReader do
+ before(:each) do
+ @root_certificate = CertificateAuthority::Certificate.new
+ @root_certificate.signing_entity = true
+ @root_certificate.subject.common_name = "OCSP Root"
+ @root_certificate.key_material.generate_key(768)
+ @root_certificate.serial_number.number = 1
+ @root_certificate.sign!
+
+ @certificate = CertificateAuthority::Certificate.new
+ @certificate.key_material.generate_key(768)
+ @certificate.subject.common_name = "http://questionablesite.com"
+ @certificate.parent = @root_certificate
+ @certificate.serial_number.number = 2
+ @certificate.sign!
+
+ @ocsp_request = OpenSSL::OCSP::Request.new
+ openssl_cert_issuer = OpenSSL::X509::Certificate.new(@root_certificate.to_pem)
+ openssl_cert_subject = OpenSSL::X509::Certificate.new(@certificate.to_pem)
+
+ cert_id = OpenSSL::OCSP::CertificateId.new(openssl_cert_subject, openssl_cert_issuer)
+ @ocsp_request.add_certid(cert_id)
+ @ocsp_request_reader = CertificateAuthority::OCSPRequestReader.from_der(@ocsp_request.to_der)
+ end
+
+ it "should read in the DER encoded body" do
+ @ocsp_request_reader.should_not be_nil
+ end
+
+ it "should read out certificate serial numbers" do
+ @ocsp_request_reader.serial_numbers.should == [2]
+ end
+end
+
+describe CertificateAuthority::OCSPResponseBuilder do
+ before(:each) do
+ @root_certificate = CertificateAuthority::Certificate.new
+ @root_certificate.signing_entity = true
+ @root_certificate.subject.common_name = "OCSP Root"
+ @root_certificate.key_material.generate_key(768)
+ @root_certificate.serial_number.number = 1
+ @root_certificate.sign!({"extensions" => {"keyUsage" => {"usage" => ["critical", "keyCertSign"] }} })
+
+ @certificate = CertificateAuthority::Certificate.new
+ @certificate.key_material.generate_key(768)
+ @certificate.subject.common_name = "http://questionablesite.com"
+ @certificate.parent = @root_certificate
+ @certificate.serial_number.number = 2
+ @certificate.sign!
+
+ @ocsp_request = OpenSSL::OCSP::Request.new
+ @ocsp_request.add_nonce
+ openssl_cert_issuer = OpenSSL::X509::Certificate.new(@root_certificate.to_pem)
+ openssl_cert_subject = OpenSSL::X509::Certificate.new(@certificate.to_pem)
+
+ cert_id = OpenSSL::OCSP::CertificateId.new(openssl_cert_subject, openssl_cert_issuer)
+ @ocsp_request.add_certid(cert_id)
+ @ocsp_request_reader = CertificateAuthority::OCSPRequestReader.from_der(@ocsp_request.to_der)
+
+ @response_builder = CertificateAuthority::OCSPResponseBuilder.from_request_reader(@ocsp_request_reader)
+ @response_builder.parent = @root_certificate
+ end
+
+ it "should build from a OCSPRequestReader" do
+ @response_builder.should_not be_nil
+ @response_builder.should be_a(CertificateAuthority::OCSPResponseBuilder)
+ end
+
+ it "should build a response" do
+ response = @response_builder.build_response
+ response.should be_a(OpenSSL::OCSP::Response)
+ end
+
+ it "should verify against the root" do
+ response = @response_builder.build_response
+ root_cert = OpenSSL::X509::Certificate.new(@root_certificate.to_pem)
+ store = OpenSSL::X509::Store.new
+ store.add_cert(root_cert)
+ response.basic.verify([root_cert],store).should be_true
+ end
+
+ it "should have a configurable nextUpdate" do
+ time = 30 * 60 # 30 minutes
+ @response_builder.next_update=time
+ response = @response_builder.build_response
+ response.basic.status.each do |status|
+ ## 3 seconds of wabble is OK
+ status[5].should be_within(3).of(status[4] + time)
+ end
+ end
+
+ describe "verification mechanisms" do
+ it "should support an everything's OK default (though somewhat useless)" do
+ response = @response_builder.build_response
+ response.basic.status.each do |status|
+ status[1].should == OpenSSL::OCSP::V_CERTSTATUS_GOOD
+ end
+ end
+
+ it "should support an overridable verification mechanism callback" do
+ verification = lambda {|serial_number|
+ [CertificateAuthority::OCSPResponseBuilder::REVOKED,CertificateAuthority::OCSPResponseBuilder::UNSPECIFIED]
+ }
+ @response_builder.verification_mechanism = verification
+ response = @response_builder.build_response
+
+ response.basic.status.each do |status|
+ status[1].should == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
+ end
+ end
+ end
+end
+
+
+## DEPRECATED
describe CertificateAuthority::OCSPHandler do
before(:each) do
@ocsp_handler = CertificateAuthority::OCSPHandler.new
-
@root_certificate = CertificateAuthority::Certificate.new
@root_certificate.signing_entity = true
@root_certificate.subject.common_name = "OCSP Root"
- @root_certificate.key_material.generate_key(1024)
+ @root_certificate.key_material.generate_key(768)
@root_certificate.serial_number.number = 1
@root_certificate.sign!
@certificate = CertificateAuthority::Certificate.new
- @certificate.key_material.generate_key(1024)
+ @certificate.key_material.generate_key(768)
@certificate.subject.common_name = "http://questionablesite.com"
@certificate.parent = @root_certificate
@certificate.serial_number.number = 2
@@ -24,6 +138,7 @@ describe CertificateAuthority::OCSPHandler do
cert_id = OpenSSL::OCSP::CertificateId.new(openssl_cert_subject, openssl_cert_issuer)
@ocsp_request.add_certid(cert_id)
+
@ocsp_handler.ocsp_request = @ocsp_request.to_der
end
diff --git a/spec/units/signing_entity_spec.rb b/spec/units/signing_entity_spec.rb
index 8951c22..a38e78c 100644
--- a/spec/units/signing_entity_spec.rb
+++ b/spec/units/signing_entity_spec.rb
@@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/units_helper'
describe CertificateAuthority::SigningEntity do
-end
\ No newline at end of file
+end
diff --git a/spec/units/signing_request_spec.rb b/spec/units/signing_request_spec.rb
new file mode 100644
index 0000000..4b770c7
--- /dev/null
+++ b/spec/units/signing_request_spec.rb
@@ -0,0 +1,154 @@
+require File.dirname(__FILE__) + '/units_helper'
+
+describe CertificateAuthority::SigningRequest do
+ before(:each) do
+ @pem_csr =<<EOF
+-----BEGIN CERTIFICATE REQUEST-----
+MIICwDCCAagCAQAwezELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xHjAcBgNVBAoTFUNlcnRpZmljYXRlIEF1
+dGhvcml0eTEfMB0GA1UEAxMWd3d3LmNocmlzY2hhbmRsZXIubmFtZTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMCYdPHqyAafolv+tDmCPVj8R11aZSqG
+h44W2AF7OOhTYiiyJaudBU4uYryJHeWVMnL2I9uxyvzDqBSfjwIU3bQAAvoqWdlS
+qa/V5kLa5CM4nSYvdBErvpxEyd6neAEgtPepPqKWGve8WRziuL0it/TopBTl4eOl
+yqHrXTa3I98qBWS28Iifxpz9SXcCaXXHmMmK9KN0Y+BVCJZFVHtPoTLNIxF+nu6S
+SieWtGXVe71pDDumndjuYsn3vw+q0Oc8v79AYb0ltdU/lc6ptoSQ5dG0NRG9OA0v
+hKntu8TvzgOD6IunJ2ttuLLZ3OqWIwZHi6KrahxOjwoMHVpGdzVLrxkCAwEAAaAA
+MA0GCSqGSIb3DQEBBQUAA4IBAQA9Vc/WPbGWieetcaz6uDToFSJZhXyhRKuiMDwJ
+cBYWiDlzpNXPTsrWnaDr2kySHpLvlrl3GPpMTvTOO1QYfYmX+adgHmZAwezBsI4a
+NBnaAcI4Qv8p+v7ZxHa3yr78Mxj08Yoihd0/f7Ls5XFUppYpwNoYiKYroOMNPEuu
+TJC3u6zMEQH8wtHUy3Ii3Ho+MlXlz/DynlOAPmq6EpnMAwh8fMSbMtwTJeVcU34d
+m7FwfCvp/120RQLdKaB7zYffcwJUBLTSRKIYkWl9lAC4MlhLUfLmYnJi19Gj/SJZ
+jX2pfrub2mscWVhEw+kxYakXh31KnroCYN9I3WGWNYi9ysbi
+-----END CERTIFICATE REQUEST-----
+EOF
+ end
+
+ it "should generate from a PEM CSR" do
+ csr = CertificateAuthority::SigningRequest.from_x509_csr(@pem_csr)
+ csr.should_not be_nil
+ csr.should be_a(CertificateAuthority::SigningRequest)
+ end
+
+ it "should generate a proper DN from the CSR" do
+ csr = CertificateAuthority::SigningRequest.from_x509_csr(@pem_csr)
+ expected_dn = CertificateAuthority::DistinguishedName.new
+ expected_dn.country = "US"
+ expected_dn.organization = "Certificate Authority"
+ expected_dn.common_name = "www.chrischandler.name"
+ expected_dn.locality = "San Francisco"
+ expected_dn.state = "California"
+ csr.distinguished_name.should == expected_dn
+ end
+
+ it "should expose the underlying OpenSSL CSR" do
+ csr = CertificateAuthority::SigningRequest.from_x509_csr(@pem_csr)
+ csr.openssl_csr.should be_a(OpenSSL::X509::Request)
+ end
+
+ it "should expose the PEM encoded original CSR" do
+ csr = CertificateAuthority::SigningRequest.from_x509_csr(@pem_csr)
+ csr.raw_body.should == @pem_csr
+ csr.raw_body.should be_a(String)
+ end
+
+ describe "transforming to a certificate" do
+ before(:each) do
+ @csr = CertificateAuthority::SigningRequest.from_x509_csr(@pem_csr)
+ @cert = @csr.to_cert
+ end
+
+ it "should allow transformation to a certificate" do
+ cert = @csr.to_cert
+ cert.should_not be_nil
+ cert.should be_a(CertificateAuthority::Certificate)
+ end
+
+ it "should be signable w/ a serial number" do
+ root = CertificateAuthority::Certificate.new
+ root.signing_entity = true
+ root.subject.common_name = "chrischandler.name root"
+ root.key_material.generate_key(1024)
+ root.serial_number.number = 2
+ root.sign!
+ @cert.serial_number.number = 5
+ @cert.parent = root
+ result_cert = @cert.sign!
+ result_cert.should be_a(OpenSSL::X509::Certificate)
+ ## Verify the subjects and public key match
+ @csr.distinguished_name.to_x509_name.should == result_cert.subject
+ @csr.key_material.public_key.to_pem.should == result_cert.public_key.to_pem
+ end
+ end
+
+ describe "Netscape SPKAC" do
+ before(:each) do
+ @spkac =<<EOF
+MIICQDCCASgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVDEqzj21++aMWvN6zzwDXKKpR9g5hIeAPYqbUdaPFePhtz7R73l7fVxDeQZDQpQxTqts0/w0wEa/A1ehHCtAkDoTYzjwX8G0Gkb90poA156I8b4Cl1Q2veKbLsaOsMWItlXSU6HULQ5McfYfvEaPmIKiIr0UIFdzMDcy9TnY854w9TcQVvLJZcQkaM3dy/p9W4gg0a9hBJwwFUUR2UV/nEEi+++HbsOE46Z7Y3qoQhLrL4DNUrXUPDVeqac1SmNfKTA71QADbezWDfKi9habHHGXqk18i2Pl6uA2mpNPuSWnEHbQONgnfeoWZBvMWkwlolaBeWhGSmgcL/HqaRLlFAgMBAAEWADANBgkqhkiG9w0BAQQFAAOCAQEAp8wOvrl2QG9p1PS19dnrh4l0JWNAPB+d1kc64xUG6FAfGCKnOHzdDndTJfEE [...]
+EOF
+ end
+
+ it "should process a netscape SPKAC" do
+ @csr = CertificateAuthority::SigningRequest.from_netscape_spkac(@spkac)
+ @csr.should be_a(CertificateAuthority::SigningRequest)
+ end
+ end
+
+ describe "Generating CSRs" do
+ before(:each) do
+ @key_pair =<<EOF
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQCxiGVfRrf90CHmvXa+XYWE4m7LZ1slc6cxIYyIgZuQ5T8AeqUa
+kbyYY4wMUR2gZ4pDPs/WGs8fW66q23qmHSr1bQ6HaL8znbD7UL/IiiyiW8I11orb
+rhimIx1A606qi8/0gQc+H851gzUusd5xgKP2X+oPxYx3VG3dpksLnNK1IwIDAQAB
+AoGAfrNNRbX+0dGcoERPXoT4KWJAmEHnNs9XXyUGWtXE5J/3Wqws8M1Zv5gr9w5d
+CoFal6tYQQFZGJQiECYbXjoq0VT8ApWfuO/mCXyXfmnLFEU8EJjmXtXzn2yyPfoY
+At7O8QwvG0bwtw1SqNf7cRtlOEIqoLMtdyaVv4C5ffyheIECQQDVdVf2Sk113Kke
+PREzEb6XZ0n2ugSG8fWJh2QKUI4RXhg7bDzHhSpexeKsJdoet8NJOUEsXMoqLSzK
+bBnSD63RAkEA1OogtDCkpwkvqC63a7hyDP7qRVHFuVeSA1fu+6BFS0xblkgvcPXT
+J7WbWYcP+lqcLjXWeFsqe5qS6sDCsAhsswJAIumZZHgMqU1Y/9AfIwow8RR8vXT5
+TpT+gur5CtLYGbEZJ4bxffSi1HNrOprKTSHjN/O8XCQlELboz4bUxk24MQJAcsaX
+xKsoR4dTMoWkiSRQDyNoJOA1B3nmk3jWsryuPi42fSgCsxFBt/lVeoitm1c3NE3/
+hLgYibNFGdm52e1gswJBAMwYuImbl6AVLv0Y41smxIkvfAzlyNfTAsp7GqLoMhYN
+q/0KoyI2Ge3+NnmJI/eaiYs8qC2HjrgdX9ZDSUCWfpQ=
+-----END RSA PRIVATE KEY-----
+EOF
+ @csr = CertificateAuthority::SigningRequest.new
+ dn = CertificateAuthority::DistinguishedName.new
+ dn.common_name = "localhost"
+ @csr.distinguished_name = dn
+
+ k = CertificateAuthority::KeyMaterial.from_x509_key_pair(@key_pair)
+ @csr.key_material = k
+ end
+
+ it "should generate a CSR" do
+ expected =<<EOF
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBUDCBugIAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEAsYhlX0a3/dAh5r12vl2FhOJuy2dbJXOnMSGMiIGbkOU/AHql
+GpG8mGOMDFEdoGeKQz7P1hrPH1uuqtt6ph0q9W0Oh2i/M52w+1C/yIosolvCNdaK
+264YpiMdQOtOqovP9IEHPh/OdYM1LrHecYCj9l/qD8WMd1Rt3aZLC5zStSMCAwEA
+ATANBgkqhkiG9w0BAQ0FAAOBgQACWe6Eyl5XDKgvUS9PUBnATpag2joaRDgc1qxU
+FTkA7VX3GcnDFqnu1bj8kE7Ej7KBUybSJoSlfZrTxT1GsZ1tubzBeWsYdY1LctU2
+5a/fyqvMg/m2DQaMK5oupJNuAvihmVCM0I1qjmDregeAqz94iki8YgAbG6q/NnyT
+YK3KbQ==
+-----END CERTIFICATE REQUEST-----
+EOF
+ @csr.to_pem.should == expected
+ end
+
+ it "should generate a signed CSR" do
+ @csr.digest = "SHA256"
+ @csr.to_x509_csr.signature_algorithm.should == "sha256WithRSAEncryption"
+ end
+
+ it "should generate a CSR w/ a subjectAlternativeName extension" do
+ alt_names = ["abc.com","somethingelse.com"]
+ @csr.subject_alternative_names = alt_names
+
+ expected_subjectAlt = CertificateAuthority::Extensions::SubjectAlternativeName.new
+ expected_subjectAlt.dns_names =["abc.com", "somethingelse.com"]
+ @csr.to_cert.extensions["subjectAltName"] == expected_subjectAlt
+ end
+ end
+end
diff --git a/spec/units/working_with_openssl_spec.rb b/spec/units/working_with_openssl_spec.rb
new file mode 100644
index 0000000..3af4fbf
--- /dev/null
+++ b/spec/units/working_with_openssl_spec.rb
@@ -0,0 +1,136 @@
+require File.dirname(__FILE__) + '/units_helper'
+
+describe "Using OpenSSL" do
+
+ shared_examples_for "an ossl issuer and its signed cert" do
+ it "should issue a certificate verified by the issuer" do
+ @signed.verify(@issuer.public_key ).should be_true
+ end
+
+ it "should issue a certificate with a matching issuer subject string" do
+ @signed.issuer.to_s.should == @issuer.subject.to_s
+ end
+
+ it "should issue a certificate with a matching issuer subject openssl name" do
+ @signed.issuer.should == @issuer.subject
+ end
+
+ end
+
+ context "Signing CSRs" do
+ shared_examples_for "a csr operation" do
+ before :all do
+ @ca = sample_file("certs/ca.crt").read
+ @ca_key = sample_file("certs/ca.key").read
+
+ @issuer = OpenSSL::X509::Certificate.new(@ca)
+ issuer_ca = CertificateAuthority::Certificate.from_openssl(@issuer)
+ issuer_ca.key_material.private_key = OpenSSL::PKey::RSA.new(@ca_key)
+
+ @our_csr = CertificateAuthority::SigningRequest.from_x509_csr(@csr_pem)
+ signed = @our_csr.to_cert
+ signed.parent = issuer_ca
+ signed.serial_number.number = 2
+
+ signed.sign!
+
+ @cert = @signed = OpenSSL::X509::Certificate.new(signed.to_pem)
+ end
+ it_should_behave_like "an ossl issuer and its signed cert"
+ end
+
+ context "With a server CSR" do
+ before :all do
+ @csr_pem = sample_file("certs/server.csr").read
+ end
+
+ it_should_behave_like "a csr operation"
+ end
+
+ context "With a client CSR" do
+ before :all do
+ @csr_pem = sample_file("certs/client.csr").read
+ end
+
+ it_should_behave_like "a csr operation"
+ end
+ end
+
+ context "Handling externally supplied CAs and certs" do
+ shared_examples_for "comparing a pair of openssl certs" do
+ context "using openssl" do
+ before :all do
+ @issuer = @ca
+ @signed = @cert
+ end
+ it_should_behave_like "an ossl issuer and its signed cert"
+ end
+
+ context "using certificate_authority" do
+ before :all do
+ # from openssl
+ @our_ca = CertificateAuthority::Certificate.from_openssl(@ca)
+ @our_cert = CertificateAuthority::Certificate.from_openssl(@cert)
+
+ # and back
+ @issuer = OpenSSL::X509::Certificate.new(@our_ca.to_pem)
+ @signed = OpenSSL::X509::Certificate.new(@our_cert.to_pem)
+ end
+
+ it "should match the original ca's distinguished name" do
+ @our_ca.distinguished_name.to_x509_name.should == @ca.subject
+ end
+
+ it "should match the original openssl ca" do
+ back = OpenSSL::X509::Certificate.new(@our_ca.to_pem)
+ back.subject.should == @ca.subject
+ end
+
+ it "should match the original cert's distinguished name" do
+ @our_cert.distinguished_name.to_x509_name.should == @cert.subject
+ end
+
+ it "should match the original openssl cert" do
+ back = OpenSSL::X509::Certificate.new(@our_cert.to_pem)
+ back.subject.should == @cert.subject
+ end
+
+ it_should_behave_like "an ossl issuer and its signed cert"
+ end
+ end
+
+ context "A custom CA signing a client cert" do
+ before :all do
+ @ca = OpenSSL::X509::Certificate.new(sample_file("certs/ca.crt").read)
+ @cert = OpenSSL::X509::Certificate.new(sample_file("certs/client.crt").read)
+ end
+
+ it_should_behave_like "comparing a pair of openssl certs"
+ end
+
+ context "A custom CA signing a server cert" do
+ before :all do
+ @ca = OpenSSL::X509::Certificate.new(sample_file("certs/ca.crt").read)
+ @cert = OpenSSL::X509::Certificate.new(sample_file("certs/server.crt").read)
+ end
+
+ it_should_behave_like "comparing a pair of openssl certs"
+ end
+
+ context "Github's signer" do
+ before :all do
+ @ca = OpenSSL::X509::Certificate.new(sample_file("certs/DigiCertHighAssuranceEVCA-1.pem").read)
+ @cert = OpenSSL::X509::Certificate.new(sample_file("certs/github.com.pem").read)
+ end
+ it_should_behave_like "comparing a pair of openssl certs"
+ end
+
+ context "Apple's WWDR signer" do
+ before :all do
+ @ca = OpenSSL::X509::Certificate.new(sample_file("certs/apple_wwdr_issuer.pem").read)
+ @cert = OpenSSL::X509::Certificate.new(sample_file("certs/apple_wwdr_issued_cert.pem").read)
+ end
+ it_should_behave_like "comparing a pair of openssl certs"
+ end
+ end
+end
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-certificate-authority.git
More information about the Pkg-ruby-extras-commits
mailing list