Skip to content

Commit

Permalink
Lots of random improvements to the certificate_authority gem
Browse files Browse the repository at this point in the history
* Use https:// for rubygems.org gem source
* Add activesupport gem for useful helper functions
* Cap not_before and not_after to the hour
* Use UTC for not_before and not_after
* Add support for setting the criticality of extensions
* Add support for producing a CSR based from an extension
* Add support for importing data from X.509 certificate
* Instead of having to list each extension manually, use ObjectSpace magic to dynamically find them
* Add support for parsing extension information from existing certificates
* Add new serialNumber option to the distinguished name
* Convert openssl_identifier method to use a constant
* Correct documentation for AuthorityKeyIdentifier
* Add support for caIssuers in AuthorityInfoAccess extension
* Remove clientAuth from default extendedKeyUsage extension usage
* Add support for deprecated nsComment and nsCertType extensions
* Initialize the serial_number to be a random number (2^128)
* Add support for parsing extension information from existing CSRs
* Require a private key before trying to produce a valid CSR
* Add/update some tests

TODO: Tests for all my changes
  • Loading branch information
reedloden committed Jun 5, 2013
1 parent 58161e4 commit e79424f
Show file tree
Hide file tree
Showing 13 changed files with 441 additions and 109 deletions.
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
source 'http://rubygems.org'
source 'https://rubygems.org'

gem 'activemodel', ">= 3.0.6"
gem 'activesupport', ">= 3.0.6"

group :development do
gem 'rspec'
Expand Down
41 changes: 21 additions & 20 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
GEM
remote: http://rubygems.org/
remote: https://rubygems.org/
specs:
activemodel (3.2.8)
activesupport (= 3.2.8)
activemodel (3.2.13)
activesupport (= 3.2.13)
builder (~> 3.0.0)
activesupport (3.2.8)
i18n (~> 0.6)
activesupport (3.2.13)
i18n (= 0.6.1)
multi_json (~> 1.0)
builder (3.0.0)
diff-lcs (1.1.3)
builder (3.0.4)
diff-lcs (1.2.4)
git (1.2.5)
i18n (0.6.0)
i18n (0.6.1)
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)
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)
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)

PLATFORMS
ruby

DEPENDENCIES
activemodel (>= 3.0.6)
activesupport (>= 3.0.6)
jeweler (>= 1.5.2)
rspec
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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 = ''
Expand Down
5 changes: 4 additions & 1 deletion certificate_authority.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Gem::Specification.new do |s|
"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"
Expand All @@ -72,15 +72,18 @@ Gem::Specification.new do |s|

if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<activemodel>, [">= 3.0.6"])
s.add_runtime_dependency(%q<activesupport>, [">= 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<activesupport>, [">= 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<activesupport>, [">= 3.0.6"])
s.add_dependency(%q<rspec>, [">= 0"])
s.add_dependency(%q<jeweler>, [">= 1.5.2"])
end
Expand Down
71 changes: 44 additions & 27 deletions lib/certificate_authority/certificate.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'active_support/all'

module CertificateAuthority
class Certificate
include ActiveModel::Validations
Expand Down Expand Up @@ -32,8 +34,8 @@ def initialize
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 = Time.now.change(:min => 0).utc
self.not_after = Time.now.change(:min => 0).utc + 1.year
self.parent = self
self.extensions = load_extensions()

Expand All @@ -46,7 +48,7 @@ def sign!(signing_profile={})
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
Expand Down Expand Up @@ -85,7 +87,7 @@ def sign!(signing_profile={})
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

Expand Down Expand Up @@ -116,6 +118,34 @@ def to_pem
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
Expand Down Expand Up @@ -145,28 +175,10 @@ def merge_profile_with_extensions(signing_profile={})
def load_extensions
extension_hash = {}

temp_extensions = []
basic_constraints = CertificateAuthority::Extensions::BasicConstraints.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|
extension_hash[extension.openssl_identifier] = extension
ObjectSpace.each_object(Module) do |m|
next if m == CertificateAuthority::Extensions::ExtensionAPI
next unless m.ancestors.include?(CertificateAuthority::Extensions::ExtensionAPI)
extension_hash[m::OPENSSL_IDENTIFIER] = m.new
end

extension_hash
Expand All @@ -192,7 +204,12 @@ def self.from_openssl 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
ObjectSpace.each_object(Module) do |m|
next if m == CertificateAuthority::Extensions::ExtensionAPI
next unless m.ancestors.include?(CertificateAuthority::Extensions::ExtensionAPI)
o,v,c = (openssl_cert.extensions.detect { |e| e.to_a.first == m::OPENSSL_IDENTIFIER } || []).to_a
certificate.extensions[m::OPENSSL_IDENTIFIER] = m.parse(v, c) if v
end
certificate
end

Expand Down
5 changes: 5 additions & 0 deletions lib/certificate_authority/distinguished_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ class DistinguishedName
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("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?
Expand Down
Loading

0 comments on commit e79424f

Please sign in to comment.