├── Gemfile ├── .rspec ├── .gitignore ├── spec ├── units │ ├── certificate_authority_spec.rb │ ├── signing_entity_spec.rb │ ├── serial_number_spec.rb │ ├── pkcs11_key_material_spec.rb │ ├── distinguished_name_spec.rb │ ├── certificate_revocation_list_spec.rb │ ├── working_with_openssl_spec.rb │ ├── key_material_spec.rb │ ├── signing_request_spec.rb │ ├── ocsp_handler_spec.rb │ ├── extensions_spec.rb │ └── certificate_spec.rb ├── spec_helper.rb └── samples │ └── certs │ ├── client.csr │ ├── server.csr │ ├── server.key │ ├── client.key │ ├── ca.key │ ├── client.crt │ ├── server.crt │ ├── apple_wwdr_issuer.pem │ ├── apple_wwdr_issued_cert.pem │ ├── DigiCertHighAssuranceEVCA-1.pem │ ├── github.com.pem │ └── ca.crt ├── lib ├── certificate_authority │ ├── version.rb │ ├── revocable.rb │ ├── signing_entity.rb │ ├── serial_number.rb │ ├── validations.rb │ ├── core_extensions.rb │ ├── pkcs11_key_material.rb │ ├── certificate_revocation_list.rb │ ├── key_material.rb │ ├── distinguished_name.rb │ ├── signing_request.rb │ ├── ocsp_handler.rb │ ├── certificate.rb │ └── extensions.rb ├── tasks │ └── certificate_authority.rake └── certificate_authority.rb ├── Rakefile ├── .github └── workflows │ └── ci.yml ├── certificate_authority.gemspec ├── Gemfile.lock └── README.rdoc /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format documentation 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | development 3 | .bundle 4 | .rvmrc 5 | coverage 6 | doc 7 | -------------------------------------------------------------------------------- /spec/units/certificate_authority_spec.rb: -------------------------------------------------------------------------------- 1 | describe CertificateAuthority do 2 | end 3 | -------------------------------------------------------------------------------- /spec/units/signing_entity_spec.rb: -------------------------------------------------------------------------------- 1 | describe CertificateAuthority::SigningEntity do 2 | end 3 | -------------------------------------------------------------------------------- /lib/certificate_authority/version.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | VERSION = '1.1.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'certificate_authority' 4 | require 'pathname' 5 | require 'pry' 6 | 7 | require 'coveralls' 8 | Coveralls.wear! 9 | 10 | SAMPLES_DIR = Pathname.new(__dir__).join('samples').freeze 11 | 12 | def sample_file(name) 13 | SAMPLES_DIR.join(name) 14 | end 15 | 16 | -------------------------------------------------------------------------------- /lib/certificate_authority/revocable.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | module Revocable 3 | attr_accessor :revoked_at 4 | 5 | def revoke!(time=Time.now) 6 | @revoked_at = time 7 | end 8 | 9 | def revoked? 10 | # If we have a time, then we're revoked 11 | !@revoked_at.nil? 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/certificate_authority/signing_entity.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | module SigningEntity 3 | 4 | def self.included(mod) 5 | mod.class_eval do 6 | attr_accessor :signing_entity 7 | end 8 | end 9 | 10 | def signing_entity=(val) 11 | raise "invalid param" unless [true,false].include?(val) 12 | @signing_entity = val 13 | end 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | require "rubocop/rake_task" 4 | 5 | desc "Default: run specs." 6 | task default: %i[spec] 7 | 8 | task :spec do 9 | Rake::Task["spec:units"].invoke 10 | end 11 | 12 | namespace :spec do 13 | desc "Run unit specs." 14 | RSpec::Core::RakeTask.new(:units) do |t| 15 | t.rspec_opts = ["--colour --format progress --tag ~pkcs11"] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/certificate_authority/serial_number.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module CertificateAuthority 4 | class SerialNumber 5 | include Validations 6 | include Revocable 7 | 8 | attr_accessor :number 9 | 10 | def validate 11 | if self.number.nil? 12 | errors.add :number, "must not be empty" 13 | elsif self.number.to_i <= 0 14 | errors.add :number, "must be greater than zero" 15 | end 16 | end 17 | 18 | def initialize 19 | self.number = SecureRandom.random_number(2**128-1) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/units/serial_number_spec.rb: -------------------------------------------------------------------------------- 1 | describe CertificateAuthority::SerialNumber do 2 | before(:each) do 3 | @serial_number = CertificateAuthority::SerialNumber.new 4 | end 5 | 6 | it "should support basic integer serial numbers", :rfc3280 => true do 7 | @serial_number.number = 25 8 | expect(@serial_number).to be_valid 9 | @serial_number.number = "abc" 10 | expect(@serial_number).not_to be_valid 11 | end 12 | 13 | it "should not allow negative serial numbers", :rfc3280 => true do 14 | @serial_number.number = -5 15 | expect(@serial_number).not_to be_valid 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | ruby-version: 12 | - '3.1' 13 | - '3.0' 14 | - '2.7' 15 | - '2.6' 16 | - '2.5' 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Ruby ${{ matrix.ruby-version }} 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | bundler-cache: true # 'bundle install' and cache 25 | - name: Run tests 26 | run: bundle exec rake 27 | -------------------------------------------------------------------------------- /lib/tasks/certificate_authority.rake: -------------------------------------------------------------------------------- 1 | require 'certificate_authority' 2 | 3 | namespace :certificate_authority do 4 | desc "Generate a quick self-signed cert" 5 | task :self_signed do 6 | 7 | cn = "http://localhost" 8 | cn = ENV['DOMAIN'] unless ENV['DOMAIN'].nil? 9 | 10 | root = CertificateAuthority::Certificate.new 11 | root.subject.common_name= cn 12 | root.key_material.generate_key 13 | root.signing_entity = true 14 | root.valid? 15 | root.sign! 16 | 17 | print "Your cert for #{cn}\n" 18 | print root.to_pem 19 | 20 | print "Your private key\n" 21 | print root.key_material.private_key.to_pem 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/samples/certs/client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBojCCAQsCAQAwYjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx 3 | ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEbMBkGA1UEAxMSY2xp 4 | ZW50LmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdga/L 5 | Z0+dKzefu1JqF/MlusptI0WU3AibzYJWedoLR8nQP6gWu1Z3OoWpqyDzHi1c+rIj 6 | kIaBv6n2EIVVP8aqfvpDlgjA+ugvy+UtAdESIqI/esV1Tm9H0y56/hduT6MgYFtG 7 | V1Fpzo8zhwXfHsv7MkMqcWxE0aYtmEHkiDaa8QIDAQABoAAwDQYJKoZIhvcNAQEF 8 | BQADgYEARSbIBSvvhvqX7zMBap+RcQfMdXbSQTI3iNVSEOoUtfuGEJOmkHrWwsz0 9 | ZfKv/qC9LBeWD+yqDeKbuRNJEla2oIInUfs3FINYLsm3jufsuBpVPY1OOglq91VD 10 | v1zBodLbTvbHIHKStsMfNaS9lKYZ/PycWNXxhQhpZZAeGBqk3mY= 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /lib/certificate_authority/validations.rb: -------------------------------------------------------------------------------- 1 | # 2 | # This is a super simple replacement for ActiveSupport::Validations 3 | # 4 | 5 | module CertificateAuthority 6 | class Errors < Array 7 | def add(symbol, msg) 8 | self.push([symbol, msg]) 9 | end 10 | def full_messages 11 | self.map {|i| i[0].to_s + ": " + i[1]}.join("\n") 12 | end 13 | end 14 | 15 | module Validations 16 | def valid? 17 | @errors = Errors.new 18 | validate 19 | errors.empty? 20 | end 21 | 22 | # must be overridden 23 | def validate 24 | raise NotImplementedError 25 | end 26 | 27 | def errors 28 | @errors ||= Errors.new 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/samples/certs/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIB6TCCAVICAQAwgagxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRl 3 | MREwDwYDVQQHEwhsb2NhbGl0eTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ 4 | dHkgTHRkMQ8wDQYDVQQLEwZvdW5hbWUxGzAZBgNVBAMTEnNlcnZlci5leGFtcGxl 5 | LmNvbTEgMB4GCSqGSIb3DQEJARYRZW1haWxAZXhhbXBsZS5jb20wgZ8wDQYJKoZI 6 | hvcNAQEBBQADgY0AMIGJAoGBAMJr7IHXP5QJ8KbxeGV5pygmXLeWhlt3mr5nehbg 7 | osiNnOxdmlYOx/SLEQHf0pSAuocrz9QzSgYhO8Yxtfy0MPb9KFhfGBFGCPoSdYLC 8 | 6eZlmuT+jfA2Y5EGu0P2clBciwDjUyoJNAbmBRYw0PiWO7B9to3vX7DBMbx31KmT 9 | MdfxAgMBAAGgADANBgkqhkiG9w0BAQUFAAOBgQAyoSojahrYbGK+6QbxOO7i5Ufm 10 | VHlhVBbPFfmYDrpWjoRlKVwk1iRNi/3ijQi3oPONk19wRh/A0gD0DOiKi3fz2m5K 11 | gaFLIRcBy25EYVeBic39A6b69SiXQoHv00f5CBHNSHLk4hc30vGIWifexU8ehwJJ 12 | TlHmdHkECni6w0eDmg== 13 | -----END CERTIFICATE REQUEST----- 14 | -------------------------------------------------------------------------------- /lib/certificate_authority.rb: -------------------------------------------------------------------------------- 1 | # Exterior requirements 2 | require 'openssl' 3 | 4 | # Internal modules 5 | require 'certificate_authority/core_extensions' 6 | require 'certificate_authority/signing_entity' 7 | require 'certificate_authority/revocable' 8 | require 'certificate_authority/validations' 9 | require 'certificate_authority/distinguished_name' 10 | require 'certificate_authority/serial_number' 11 | require 'certificate_authority/key_material' 12 | require 'certificate_authority/pkcs11_key_material' 13 | require 'certificate_authority/extensions' 14 | require 'certificate_authority/certificate' 15 | require 'certificate_authority/certificate_revocation_list' 16 | require 'certificate_authority/ocsp_handler' 17 | require 'certificate_authority/signing_request' 18 | 19 | module CertificateAuthority 20 | end 21 | -------------------------------------------------------------------------------- /spec/samples/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDCa+yB1z+UCfCm8XhleacoJly3loZbd5q+Z3oW4KLIjZzsXZpW 3 | Dsf0ixEB39KUgLqHK8/UM0oGITvGMbX8tDD2/ShYXxgRRgj6EnWCwunmZZrk/o3w 4 | NmORBrtD9nJQXIsA41MqCTQG5gUWMND4ljuwfbaN71+wwTG8d9SpkzHX8QIDAQAB 5 | AoGBALX9FBvOAsNuMofyjEJgh6m7jxqCmi3QXVdRwSTdDUMx2+wdCjT4DQ/JhRf+ 6 | DT3Y6cFRr27wu5/VSACT08hCW3mVgE5WcsmxvxPeaWMQCAUoHW4I9bmpvf/1AksZ 7 | x36N4GwdEvFjDGlM8B1ndW3qLUp6e5iDFUFB7veQGoA6WR/hAkEA39yivn0ThKH/ 8 | 9a8glJMBDVmnzNXQveBX+Y0aiJSNTT+rBfDRgWxR8A60z1Xl1iHtGG1j1tRXvDL3 9 | 2u2w4d1lnQJBAN5VTMFkf7ojTvPh0+QHkc7b3d0IRruojEIG3A1ZdUj4WaOqnOBs 10 | xk6EWsF5YbCBTWH42qWAq/EBXaJcPbyIdWUCQF25882LcpOSfCcyJpLuJX+gbPf/ 11 | AYGuH0dVg6lxgOO553H6TM1CO+AlWCCC11LbK3iRvD5i80TRliJsaCV426UCQEAD 12 | RS8lNVUtV00GhxBPUZ7CVPWPrXXYSFG2UeMSD5+ryXtC4xoGl24B03OC9CpygAom 13 | MSWXj2m7X+8gKbI/g7UCQEc0Ne6+4T0NsCz9Dw2TWqIvi+WbK65veyFC212OJ5Wh 14 | qEF4SY3WXkxJk4Y0ElQARMz6DojpwHI5PtAaYswe8wI= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /spec/samples/certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDdga/LZ0+dKzefu1JqF/MlusptI0WU3AibzYJWedoLR8nQP6gW 3 | u1Z3OoWpqyDzHi1c+rIjkIaBv6n2EIVVP8aqfvpDlgjA+ugvy+UtAdESIqI/esV1 4 | Tm9H0y56/hduT6MgYFtGV1Fpzo8zhwXfHsv7MkMqcWxE0aYtmEHkiDaa8QIDAQAB 5 | AoGBAIdkD4mqWhVdJyCxJMzIWsyDAdv3pT45x+FDmhk1XbtrY8WwQxOx6kXyNWTh 6 | vsAbf+rHKT9nxW9lMYO/0V+sHcdRtG0NEXPfB8pX7LEsaHpRPHkVoiWpRlCL/maM 7 | ci85RPsATlDkiOn1luysfk2PHy5aSKG0RkLS7lSkahTQOn9JAkEA8goUnDdh5hVm 8 | 7o2npeZG66Zb2mnm1l8aO9LJ76u6L/jmTx5aSRXP0aBiDb1PXJZedqhd/MdPcYEI 9 | QsFCIoT/iwJBAOpIbAK15eWsubUme/UoCbfNpM4H3jQXbSODgJ83nwmqr2slrX9m 10 | soz/+2nZl6/TL344xxTmDChFGivHdh7JXvMCQBoHi3/hVN3xn0g4Y7crtKTTFz29 11 | 9d1IDQIyARWNWlCea+ZGVV9WwSrCHMlteoNyiGYqZTEyHhEO11yWfA5KT1ECQQCY 12 | bFHJWaqeyMdxsf4Hu+rGqIZGfRv17B/XcSDndXqFAYVrQnIkZx5XWduqPCTSAaXu 13 | iuYLFLhoIr0qKnURBpY9AkEAy9JOf5tqc1jFndLIgVtXzM5KptIFHr1ZA7VSfrdY 14 | ozt9adbCTXfNTTLqAK0N7A2F7T3APEbpPuR2a7TpzawRWA== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /lib/certificate_authority/core_extensions.rb: -------------------------------------------------------------------------------- 1 | # 2 | # ActiveSupport has these modifications. Now that we don't use ActiveSupport, 3 | # these are added here as a kindness. 4 | # 5 | 6 | require 'date' 7 | 8 | unless nil.respond_to?(:blank?) 9 | class NilClass 10 | def blank? 11 | true 12 | end 13 | end 14 | end 15 | 16 | unless String.respond_to?(:blank?) 17 | class String 18 | def blank? 19 | self.empty? 20 | end 21 | end 22 | end 23 | 24 | class Date 25 | 26 | def today 27 | t = Time.now.utc 28 | Date.new(t.year, t.month, t.day) 29 | end 30 | 31 | def utc 32 | self.to_datetime.to_time.utc 33 | end 34 | 35 | unless Date.respond_to?(:advance) 36 | def advance(options) 37 | options = options.dup 38 | d = self 39 | d = d >> options.delete(:years) * 12 if options[:years] 40 | d = d >> options.delete(:months) if options[:months] 41 | d = d + options.delete(:weeks) * 7 if options[:weeks] 42 | d = d + options.delete(:days) if options[:days] 43 | d 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /certificate_authority.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("lib/certificate_authority/version", __dir__) 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "certificate_authority" 5 | spec.version = CertificateAuthority::VERSION 6 | spec.authors = ["Chris Chandler"] 7 | spec.email = ["squanderingtime@gmail.com"] 8 | 9 | spec.summary = "Ruby gem for managing the core functions outlined in RFC-3280 for PKI" 10 | spec.homepage = "https://github.com/cchandler/certificate_authority" 11 | spec.license = "MIT" 12 | 13 | spec.metadata["homepage_uri"] = "https://github.com/cchandler/certificate_authority" 14 | spec.metadata["source_code_uri"] = "https://github.com/cchandler/certificate_authority" 15 | 16 | spec.files = Dir.chdir(__dir__) do 17 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec/)}) } 18 | end 19 | spec.require_paths = ["lib"] 20 | 21 | spec.required_ruby_version = ">= 2.4" 22 | 23 | spec.add_development_dependency "coveralls" 24 | spec.add_development_dependency "pry" 25 | spec.add_development_dependency "rake" 26 | spec.add_development_dependency "rspec" 27 | spec.add_development_dependency "rubocop" 28 | end 29 | -------------------------------------------------------------------------------- /lib/certificate_authority/pkcs11_key_material.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | class Pkcs11KeyMaterial 3 | include KeyMaterial 4 | 5 | attr_accessor :engine 6 | attr_accessor :token_id 7 | attr_accessor :pkcs11_lib 8 | attr_accessor :openssl_pkcs11_engine_lib 9 | attr_accessor :pin 10 | 11 | def initialize(attributes = {}) 12 | @attributes = attributes 13 | initialize_engine 14 | end 15 | 16 | def is_in_hardware? 17 | true 18 | end 19 | 20 | def is_in_memory? 21 | false 22 | end 23 | 24 | def generate_key(modulus_bits=1024) 25 | puts "Key generation is not currently supported in hardware" 26 | nil 27 | end 28 | 29 | def private_key 30 | initialize_engine 31 | self.engine.load_private_key(self.token_id) 32 | end 33 | 34 | def public_key 35 | initialize_engine 36 | self.engine.load_public_key(self.token_id) 37 | end 38 | 39 | private 40 | 41 | def initialize_engine 42 | ## We're going to return early and try again later if params weren't passed in 43 | ## at initialization. Any attempt at getting a public/private key will try 44 | ## again. 45 | return false if self.openssl_pkcs11_engine_lib.nil? or self.pkcs11_lib.nil? 46 | return self.engine unless self.engine.nil? 47 | OpenSSL::Engine.load 48 | 49 | pkcs11 = OpenSSL::Engine.by_id("dynamic") do |e| 50 | e.ctrl_cmd("SO_PATH",self.openssl_pkcs11_engine_lib) 51 | e.ctrl_cmd("ID","pkcs11") 52 | e.ctrl_cmd("LIST_ADD","1") 53 | e.ctrl_cmd("LOAD") 54 | e.ctrl_cmd("PIN",self.pin) unless self.pin.nil? or self.pin == "" 55 | e.ctrl_cmd("MODULE_PATH",self.pkcs11_lib) 56 | end 57 | 58 | self.engine = pkcs11 59 | pkcs11 60 | end 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/units/pkcs11_key_material_spec.rb: -------------------------------------------------------------------------------- 1 | ## Anything that requires crypto hardware needs to be tagged as 'pkcs11' 2 | describe CertificateAuthority::Pkcs11KeyMaterial, :pkcs11 => true do 3 | before(:each) do 4 | @key_material_in_hardware = CertificateAuthority::Pkcs11KeyMaterial.new 5 | @key_material_in_hardware.token_id = "46" 6 | @key_material_in_hardware.pkcs11_lib = "/usr/lib/libeTPkcs11.so" 7 | @key_material_in_hardware.openssl_pkcs11_engine_lib = "/usr/lib/engines/engine_pkcs11.so" 8 | @key_material_in_hardware.pin = "11111111" 9 | end 10 | 11 | it "should identify as being in hardware", :pkcs11 => true do 12 | expect(@key_material_in_hardware.is_in_hardware?).to be_truthy 13 | end 14 | 15 | it "should return a Pkey ref if the private key is requested", :pkcs11 => true do 16 | expect(@key_material_in_hardware.private_key.class).to eq(OpenSSL::PKey::RSA) 17 | end 18 | 19 | it "should return a Pkey ref if the public key is requested", :pkcs11 => true do 20 | expect(@key_material_in_hardware.public_key.class).to eq(OpenSSL::PKey::RSA) 21 | end 22 | 23 | it "should accept an ID for on-token objects", :pkcs11 => true do 24 | expect(@key_material_in_hardware.respond_to?(:token_id)).to be_truthy 25 | end 26 | 27 | it "should accept a path to a shared library for a PKCS11 driver", :pkcs11 => true do 28 | expect(@key_material_in_hardware.respond_to?(:pkcs11_lib)).to be_truthy 29 | end 30 | 31 | it "should accept a path to OpenSSL's dynamic PKCS11 engine (provided by libengine-pkcs11-openssl)", :pkcs11 => true do 32 | expect(@key_material_in_hardware.respond_to?(:openssl_pkcs11_engine_lib)).to be_truthy 33 | end 34 | 35 | it "should accept an optional PIN to authenticate to the token", :pkcs11 => true do 36 | expect(@key_material_in_hardware.respond_to?(:pin)).to be_truthy 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | certificate_authority (1.1.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.0) 10 | coderay (1.1.2) 11 | coveralls (0.8.23) 12 | json (>= 1.8, < 3) 13 | simplecov (~> 0.16.1) 14 | term-ansicolor (~> 1.3) 15 | thor (>= 0.19.4, < 2.0) 16 | tins (~> 1.6) 17 | diff-lcs (1.3) 18 | docile (1.3.2) 19 | json (2.3.0) 20 | method_source (1.0.0) 21 | parallel (1.19.1) 22 | parser (2.7.1.3) 23 | ast (~> 2.4.0) 24 | pry (0.13.1) 25 | coderay (~> 1.1) 26 | method_source (~> 1.0) 27 | rainbow (3.0.0) 28 | rake (13.0.1) 29 | rexml (3.2.5) 30 | rspec (3.9.0) 31 | rspec-core (~> 3.9.0) 32 | rspec-expectations (~> 3.9.0) 33 | rspec-mocks (~> 3.9.0) 34 | rspec-core (3.9.2) 35 | rspec-support (~> 3.9.3) 36 | rspec-expectations (3.9.2) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.9.0) 39 | rspec-mocks (3.9.1) 40 | diff-lcs (>= 1.2.0, < 2.0) 41 | rspec-support (~> 3.9.0) 42 | rspec-support (3.9.3) 43 | rubocop (0.84.0) 44 | parallel (~> 1.10) 45 | parser (>= 2.7.0.1) 46 | rainbow (>= 2.2.2, < 4.0) 47 | rexml 48 | rubocop-ast (>= 0.0.3) 49 | ruby-progressbar (~> 1.7) 50 | unicode-display_width (>= 1.4.0, < 2.0) 51 | rubocop-ast (0.0.3) 52 | parser (>= 2.7.0.1) 53 | ruby-progressbar (1.10.1) 54 | simplecov (0.16.1) 55 | docile (~> 1.1) 56 | json (>= 1.8, < 3) 57 | simplecov-html (~> 0.10.0) 58 | simplecov-html (0.10.2) 59 | sync (0.5.0) 60 | term-ansicolor (1.7.1) 61 | tins (~> 1.0) 62 | thor (1.0.1) 63 | tins (1.25.0) 64 | sync 65 | unicode-display_width (1.7.0) 66 | 67 | PLATFORMS 68 | ruby 69 | 70 | DEPENDENCIES 71 | certificate_authority! 72 | coveralls 73 | pry 74 | rake 75 | rspec 76 | rubocop 77 | 78 | BUNDLED WITH 79 | 2.1.4 80 | -------------------------------------------------------------------------------- /lib/certificate_authority/certificate_revocation_list.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | class CertificateRevocationList 3 | include Validations 4 | 5 | attr_accessor :certificates 6 | attr_accessor :parent 7 | attr_accessor :crl_body 8 | attr_accessor :next_update 9 | attr_accessor :last_update_skew_seconds 10 | 11 | def validate 12 | errors.add :next_update, "Next update must be a positive value" if self.next_update < 0 13 | errors.add :parent, "A parent entity must be set" if self.parent.nil? 14 | end 15 | 16 | def initialize 17 | self.certificates = [] 18 | self.next_update = 60 * 60 * 4 # 4 hour default 19 | self.last_update_skew_seconds = 0 20 | end 21 | 22 | def <<(revocable) 23 | case revocable 24 | when Revocable 25 | raise "Only revoked entities can be added to a CRL" unless revocable.revoked? 26 | self.certificates << revocable 27 | when OpenSSL::X509::Certificate 28 | raise "Not implemented yet" 29 | else 30 | raise "#{revocable.class} cannot be included in a CRL" 31 | end 32 | end 33 | 34 | def sign!(signing_profile={}) 35 | raise "No parent entity has been set!" if self.parent.nil? 36 | raise "Invalid CRL" unless self.valid? 37 | 38 | revocations = self.certificates.collect do |revocable| 39 | revocation = OpenSSL::X509::Revoked.new 40 | 41 | ## We really just need a serial number, now we have to dig it out 42 | case revocable 43 | when Certificate 44 | x509_cert = OpenSSL::X509::Certificate.new(revocable.to_pem) 45 | revocation.serial = x509_cert.serial 46 | when SerialNumber 47 | revocation.serial = revocable.number 48 | end 49 | revocation.time = revocable.revoked_at 50 | revocation 51 | end 52 | 53 | crl = OpenSSL::X509::CRL.new 54 | revocations.each do |revocation| 55 | crl.add_revoked(revocation) 56 | end 57 | 58 | crl.version = 1 59 | crl.last_update = Time.now - self.last_update_skew_seconds 60 | crl.next_update = Time.now + self.next_update 61 | 62 | signing_cert = OpenSSL::X509::Certificate.new(self.parent.to_pem) 63 | if signing_profile["digest"].nil? 64 | digest = OpenSSL::Digest.new("SHA512") 65 | else 66 | digest = OpenSSL::Digest.new(signing_profile["digest"]) 67 | end 68 | crl.issuer = signing_cert.subject 69 | self.crl_body = crl.sign(self.parent.key_material.private_key, digest) 70 | 71 | self.crl_body 72 | end 73 | 74 | def to_pem 75 | raise "No signed CRL body" if self.crl_body.nil? 76 | self.crl_body.to_pem 77 | end 78 | end#CertificateRevocationList 79 | end 80 | -------------------------------------------------------------------------------- /lib/certificate_authority/key_material.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | module KeyMaterial 3 | def public_key 4 | raise "Required implementation" 5 | end 6 | 7 | def private_key 8 | raise "Required implementation" 9 | end 10 | 11 | def is_in_hardware? 12 | raise "Required implementation" 13 | end 14 | 15 | def is_in_memory? 16 | raise "Required implementation" 17 | end 18 | 19 | def self.from_x509_key_pair(pair,password=nil) 20 | if password.nil? 21 | key = OpenSSL::PKey::RSA.new(pair) 22 | else 23 | key = OpenSSL::PKey::RSA.new(pair,password) 24 | end 25 | mem_key = MemoryKeyMaterial.new 26 | mem_key.public_key = key.public_key 27 | mem_key.private_key = key 28 | mem_key 29 | end 30 | 31 | def self.from_x509_public_key(public_key_pem) 32 | key = OpenSSL::PKey::RSA.new(public_key_pem) 33 | signing_request_key = SigningRequestKeyMaterial.new 34 | signing_request_key.public_key = key.public_key 35 | signing_request_key 36 | end 37 | end 38 | 39 | class MemoryKeyMaterial 40 | include KeyMaterial 41 | include Validations 42 | 43 | attr_accessor :keypair 44 | attr_accessor :private_key 45 | attr_accessor :public_key 46 | 47 | def initialize 48 | end 49 | 50 | def validate 51 | if private_key.nil? 52 | errors.add :private_key, "cannot be blank" 53 | end 54 | if public_key.nil? 55 | errors.add :public_key, "cannot be blank" 56 | end 57 | end 58 | 59 | def is_in_hardware? 60 | false 61 | end 62 | 63 | def is_in_memory? 64 | true 65 | end 66 | 67 | def generate_key(modulus_bits=2048) 68 | self.keypair = OpenSSL::PKey::RSA.new(modulus_bits) 69 | self.private_key = keypair 70 | self.public_key = keypair.public_key 71 | self.keypair 72 | end 73 | 74 | def private_key 75 | @private_key 76 | end 77 | 78 | def public_key 79 | @public_key 80 | end 81 | end 82 | 83 | class SigningRequestKeyMaterial 84 | include KeyMaterial 85 | include Validations 86 | 87 | def validate 88 | errors.add :public_key, "cannot be blank" if public_key.nil? 89 | end 90 | 91 | attr_accessor :public_key 92 | 93 | def initialize(request=nil) 94 | if request.is_a? OpenSSL::X509::Request 95 | raise "Invalid certificate signing request" unless request.verify request.public_key 96 | self.public_key = request.public_key 97 | end 98 | end 99 | 100 | def is_in_hardware? 101 | false 102 | end 103 | 104 | def is_in_memory? 105 | true 106 | end 107 | 108 | def private_key 109 | nil 110 | end 111 | 112 | def public_key 113 | @public_key 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/certificate_authority/distinguished_name.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | class DistinguishedName 3 | include Validations 4 | 5 | def validate 6 | if self.common_name.nil? || self.common_name.empty? 7 | errors.add :common_name, 'cannot be blank' 8 | end 9 | end 10 | 11 | attr_accessor :common_name 12 | alias :cn :common_name 13 | alias :cn= :common_name= 14 | 15 | attr_accessor :locality 16 | alias :l :locality 17 | alias :l= :locality= 18 | 19 | attr_accessor :state 20 | alias :s :state 21 | alias :st= :state= 22 | 23 | attr_accessor :country 24 | alias :c :country 25 | alias :c= :country= 26 | 27 | attr_accessor :organization 28 | alias :o :organization 29 | alias :o= :organization= 30 | 31 | attr_accessor :organizational_unit 32 | alias :ou :organizational_unit 33 | alias :ou= :organizational_unit= 34 | 35 | attr_accessor :email_address 36 | alias :emailAddress :email_address 37 | alias :emailAddress= :email_address= 38 | 39 | attr_accessor :serial_number 40 | alias :serialNumber :serial_number 41 | alias :serialNumber= :serial_number= 42 | 43 | def to_x509_name 44 | raise "Invalid Distinguished Name" unless valid? 45 | 46 | # NB: the capitalization in the strings counts 47 | name = OpenSSL::X509::Name.new 48 | name.add_entry("serialNumber", serial_number) unless serial_number.blank? 49 | name.add_entry("C", country) unless country.blank? 50 | name.add_entry("ST", state) unless state.blank? 51 | name.add_entry("L", locality) unless locality.blank? 52 | name.add_entry("O", organization) unless organization.blank? 53 | name.add_entry("OU", organizational_unit) unless organizational_unit.blank? 54 | name.add_entry("CN", common_name) 55 | name.add_entry("emailAddress", email_address) unless email_address.blank? 56 | name 57 | end 58 | 59 | def ==(other) 60 | # Use the established OpenSSL comparison 61 | self.to_x509_name() == other.to_x509_name() 62 | end 63 | 64 | def self.from_openssl openssl_name 65 | unless openssl_name.is_a? OpenSSL::X509::Name 66 | raise "Argument must be a OpenSSL::X509::Name" 67 | end 68 | 69 | WrappedDistinguishedName.new(openssl_name) 70 | end 71 | end 72 | 73 | ## This is a significantly more complicated case. It's possible that 74 | ## generically handled certificates will include custom OIDs in the 75 | ## subject. 76 | class WrappedDistinguishedName < DistinguishedName 77 | attr_accessor :x509_name 78 | 79 | def initialize(x509_name) 80 | @x509_name = x509_name 81 | 82 | subject = @x509_name.to_a 83 | subject.each do |element| 84 | field = element[0].downcase 85 | value = element[1] 86 | #type = element[2] ## -not used 87 | method_sym = "#{field}=".to_sym 88 | if self.respond_to?(method_sym) 89 | self.send("#{field}=",value) 90 | else 91 | ## Custom OID 92 | @custom_oids = true 93 | end 94 | end 95 | 96 | end 97 | 98 | def to_x509_name 99 | @x509_name 100 | end 101 | 102 | def custom_oids? 103 | @custom_oids 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/samples/certs/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEAwLS+XYDCGV9c9GRyLzwP0W/yaXTe7obSqU4ChdHisITIcx/g 3 | DamfYOBTf7NO7UJjPRkJvGegEzLcoCWsCeF+rnJzT6GcdoyANeJ1LrJKaF8Dg7wu 4 | Yx9KeymALD7+cn9D7ntZb/xRi0IKG7m7XxTNnhY23QXvLCE+3jm8J7EMBmpJ+AH/ 5 | OswsWtlWD4G8a4jug3LZFAw0CJiqHTXCNP0MO11acsGbKKwE4TA7uOoEokg0ooc7 6 | HLj8ifaRNULpbsevqzv2twou438xRR0EpaRa9s27YvlJ4IOzd9/uuoS/B/opcm0t 7 | uOBCEL358+TGforo1xG4ZxS0jPXEqfrZBL0Xf7TQo7nH30QtUN26fAERAiPZnEya 8 | 8JIvSrcKcj/8YW0IO8yZPGV4knZjo8TLH7jKSnqwxCKymhO2rhlger03G5MOAp8i 9 | 5NwhJBFNnZ4wf3uSSC1NuRoqWMZ3S3DPwaZtBvlUg78nc4/Mi1x60nlWdUHUd1+c 10 | OTACDMULDDJrIzloJtX377F/HsYdqTlkFd2w3/XWz8BzstDyKvRunticW5+/zudp 11 | gL9p0iA7qlVJPvpQ/GN+jZBGmn3rnMoXvDbXiODZxCmML7zOneU11ezXppNJKoQ7 12 | tjceDFfxBLsv6nV1at4McULm6RRe7lUaH6SHISsSmIsXY8iEiENG9I+yt1sCAwEA 13 | AQKCAgAo+AQkwtQBKuoLNzOjYSSHxUIHM4aVtWoh/mjA5H9KQeCPwS4UGYS9xtNZ 14 | qdhUzrFkcudD+8/nZP/MuFWcACm7kq97NYObHIHBcvSwyczR5alMn3xJLITcLFWI 15 | kpfr5ayejfDUwxLfBVo6zMDOFREl09k0IifX/PVtr16WHajN0FkLdfk6GeAwLFaE 16 | k3NodUMoBQmrnnCNh09bSGuScl3gXRd2oDyJaBDdgzCfPnlfuvQdvZxOnfFqr1Xt 17 | ud0A5UkuoV/xSCPxz7+8zs+HG6sPH2wAPbl8FuPXz27kjoZCfufC4P1AecTx5EG4 18 | nWGp04Ru/OB0Yc2EzldSP/dVb5IIAEeIzeqOC1dKh+m5BL6+KCMW4RA2VRmtVcSO 19 | Xu8/uLNw3coWj8OdUg4JJLWIuY17puKyhmMuZqXSwF5qcSCD2kGMtgdIFevHGLDy 20 | NCXIxAhnp/mWGJJvkRPxe7NVZSKGh7nfGGFbr2GAsTX5XZetk6ye/A+oNrUM4UP+ 21 | WQNtD5m2R1IPIp7f8NdTiSpAsKrgzDVEKSdXW/DObRm64DdRINuxGkrjDTOR6B+M 22 | HmcKQdMZPVyzWeFHF+JyR4+GVx2OZ/5lzEPTHO8thiA1nn1ov/RYZwuwrl8EKDt6 23 | jgU26C+1TD3lL9smBdekOB8EtQT2EcuE7hwS6ca4Sryj5RYCmQKCAQEA/DTBSczZ 24 | HtPcHs75IiHfUXGoJB943LIR4cqrWKO2xy16MpamNQy8yBzet7pgXmrxixtar13B 25 | SpPozQNwlKD43rLSG77soY9eZcn2SQCOlcuekdRczR2jp4cnIo3bTXOm5OTz55EK 26 | WTLGkTirlcGCPFHv866qrmVQF+cIo3qtszrwuJGkOWTfU8X9zSN+AU+KwAqam5C5 27 | ZbkcXFNwI2FOud5nQsOrjJ9Aivrw1CbHvHqAiZzZXjZmMNurH/cLX9jregxoiTAv 28 | wXsS1Apvvq0kvWJ9QrRBNyRsZ6qEt0ABgf2tl6+4t+S9bCmS2L6RzqjrxbkVPoM2 29 | ChHX+8XkwuVUHwKCAQEAw5rao4MU4itSeaWLrny5oo57pICyY3zF5c5HySpaWyA8 30 | JksxYCvNDR1aFZjCdOG9zOf5vLieniyTK+Tl3FF5P6OxSJOcQ9UOXaoTKny1Kw9W 31 | BMTbR0COLXsJSNvjNyZSyoLgOOLTZj11S+X/W1vb+Xi0kkx4SS6Tg3i5N+0Fl2Wa 32 | Mgucy/pgq+JBTlgKZ1Rjpna1T5ibUn0pT6fxgZ2U+FAD0cYNyaCjVmzMbiIkLUId 33 | jJEDGYXenyoIFI0rrJvF/0nl98BZjsT9+GNs0ZlejS0rfn2+DWlrLur3aqG2i3Yr 34 | L9nWZ51bSB7UhHALN2t2qd9etKWmz4YbuFHPvHOVRQKCAQEAgmdMoboXcYcdw5hE 35 | 3M8ixtu7kqHrPkGcWWEPY4+SzD3JdyrJ2ZgybE3xIpJtjaRCLCkCpLYXYVZFAuwK 36 | Y+8vfwZ6+PmpJIgayQq6G1j8YJud680gBraSjeal54ntoIhx/Nwc+NjXvvMwFJp2 37 | rcIWctXy+c6QVgfwd8tvfgfKlGefW++COGLdzlULO+xkFI1qMo3JDzKviddCwMIr 38 | sz93E0fZoH3Hz7fwCWxi5W7/y4aTu5OsGLiL8itCug5khTSF3N9ZlcWii6n1PEoB 39 | KVghLQMlvT2ykq50ls1mPdIMdYgTH+Et43eUMb71PLicb3yMG/ns8Bur71z2jinu 40 | dI9bBQKCAQBk30XjTuUFIbw9mX6oNA/zYbEni2rzXVQdB70DY8EG/1+li99hrhTn 41 | r3xWaNnXNtcPhY3HohnCjlAzMa7MaIOzqvHw8JaEcKog6WVK4tb25sjAWtiOLR9l 42 | Gu8V0LejKDNH1ihVjbvhHM6RnoGKlpuhUnskeyUI8GkIQsiZq7TXd4EGT/DDTFJw 43 | MZTmFwb+dImTPeKQsq1e48bbGku0QRSi3XiqxI01ro6tMhxWq2qmoFLmu52ymtPM 44 | lvtlxcuBDzATUAO1OU+2Daa/Yl6q1IHrIiEs8SGCfxvULT38knq1/vGUkq077+00 45 | CxojVjiiktu2DMglNswIdytyaVZM4/pVAoIBAQDmXzA7dptH9tOrp5/pYO4C8Fk0 46 | 4NB7OgKrhtE8YQtfghQn1O0FjvNT/o1+Gf9mf40Np7OtVulhZ56fQupmkk56TAJu 47 | P1YDVDRPLOvMkYeIsqqv6HnXGQaS6Bv91wrqwWPjLDD3pH+AdjP27SnObzARjRUq 48 | /s5LmI+u87rCldkGLG4qO4sjptWRlU8pIsLWQ3NckXINT7i8wwXzEfOcu7IzKWg6 49 | sdzMWxc7Vt/4WE5m+9DLkpk3sD9dqELHry5lGWbpLHhfoVXoRdoFNVmyi2ckdr0J 50 | FJ6hkWkgQ4ee+yHyL3FfYRbGIolqaVSCd8/CvvDZ/24eS3/7TpCzf8SUqtL6 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /spec/units/distinguished_name_spec.rb: -------------------------------------------------------------------------------- 1 | describe CertificateAuthority::DistinguishedName do 2 | before(:each) do 3 | @distinguished_name = CertificateAuthority::DistinguishedName.new 4 | end 5 | 6 | it "should provide the standard x.509 distinguished name common attributes" do 7 | expect(@distinguished_name.respond_to?(:cn)).to be_truthy 8 | expect(@distinguished_name.respond_to?(:l)).to be_truthy 9 | expect(@distinguished_name.respond_to?(:s)).to be_truthy 10 | expect(@distinguished_name.respond_to?(:o)).to be_truthy 11 | expect(@distinguished_name.respond_to?(:ou)).to be_truthy 12 | expect(@distinguished_name.respond_to?(:c)).to be_truthy 13 | expect(@distinguished_name.respond_to?(:emailAddress)).to be_truthy 14 | expect(@distinguished_name.respond_to?(:serialNumber)).to be_truthy 15 | end 16 | 17 | it "should provide human-readable equivalents to the distinguished name common attributes" do 18 | expect(@distinguished_name.respond_to?(:common_name)).to be_truthy 19 | expect(@distinguished_name.respond_to?(:locality)).to be_truthy 20 | expect(@distinguished_name.respond_to?(:state)).to be_truthy 21 | expect(@distinguished_name.respond_to?(:organization)).to be_truthy 22 | expect(@distinguished_name.respond_to?(:organizational_unit)).to be_truthy 23 | expect(@distinguished_name.respond_to?(:country)).to be_truthy 24 | expect(@distinguished_name.respond_to?(:email_address)).to be_truthy 25 | expect(@distinguished_name.respond_to?(:serial_number)).to be_truthy 26 | end 27 | 28 | it "should require a common name" do 29 | expect(@distinguished_name.valid?).to be_falsey 30 | expect(@distinguished_name.errors.size).to eq(1) 31 | @distinguished_name.common_name = "chrischandler.name" 32 | expect(@distinguished_name.valid?).to be_truthy 33 | end 34 | 35 | it "should be convertible to an OpenSSL::X509::Name" do 36 | @distinguished_name.common_name = "chrischandler.name" 37 | @distinguished_name.to_x509_name 38 | end 39 | 40 | describe "from_openssl" do 41 | before do 42 | subject = "/CN=justincummins.name/L=on my laptop/ST=relaxed/C=as/O=programmer/OU=using this code" 43 | @name = OpenSSL::X509::Name.parse subject 44 | @dn = CertificateAuthority::DistinguishedName.from_openssl @name 45 | end 46 | 47 | it "should reject non Name objects" do 48 | expect { CertificateAuthority::DistinguishedName.from_openssl "Not a OpenSSL::X509::Name" }.to raise_error(RuntimeError) 49 | end 50 | 51 | [:common_name, :locality, :state, :country, :organization, :organizational_unit].each do |field| 52 | it "should set the #{field} attribute" do 53 | expect(@dn.send(field)).not_to be_nil 54 | end 55 | end 56 | 57 | it "should create an equivalent object" do 58 | expect(@dn.to_x509_name.to_s.split('/')).to match_array(@name.to_s.split('/')) 59 | end 60 | 61 | end 62 | 63 | describe CertificateAuthority::WrappedDistinguishedName do 64 | it "should mark the DN as having custom OIDs if there's an unknown subject element" do 65 | OpenSSL::ASN1::ObjectId.register("2.3.4.5","testing","testingCustomOIDs") 66 | subject = "/testingCustomOIDs=custom/CN=justincummins.name/L=on my laptop/ST=relaxed/C=as/O=programmer/OU=using this code" 67 | @name = OpenSSL::X509::Name.parse subject 68 | @dn = CertificateAuthority::DistinguishedName.from_openssl @name 69 | expect(@dn.custom_oids?).to be_truthy 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/certificate_authority/signing_request.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | class SigningRequest 3 | attr_accessor :distinguished_name 4 | attr_accessor :key_material 5 | attr_accessor :raw_body 6 | attr_accessor :openssl_csr 7 | attr_accessor :digest 8 | attr_accessor :attributes 9 | 10 | def initialize() 11 | @attributes = [] 12 | end 13 | 14 | # Fake attribute for convenience because adding 15 | # alternative names on a CSR is remarkably non-trivial. 16 | def subject_alternative_names=(alt_names) 17 | raise "alt_names must be an Array" unless alt_names.is_a?(Array) 18 | 19 | factory = OpenSSL::X509::ExtensionFactory.new 20 | name_list = alt_names.map{|m| "DNS:#{m}"}.join(",") 21 | ext = factory.create_ext("subjectAltName",name_list,false) 22 | ext_set = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence([ext])]) 23 | attr = OpenSSL::X509::Attribute.new("extReq", ext_set) 24 | @attributes << attr 25 | end 26 | 27 | def read_attributes_by_oid(*oids) 28 | attributes.detect { |a| oids.include?(a.oid) } 29 | end 30 | protected :read_attributes_by_oid 31 | 32 | def to_cert 33 | cert = Certificate.new 34 | if !@distinguished_name.nil? 35 | cert.distinguished_name = @distinguished_name 36 | end 37 | cert.key_material = @key_material 38 | if attribute = read_attributes_by_oid('extReq', 'msExtReq') 39 | set = OpenSSL::ASN1.decode(attribute.value) 40 | seq = set.value.first 41 | seq.value.collect { |asn1ext| OpenSSL::X509::Extension.new(asn1ext).to_a }.each do |o, v, c| 42 | Certificate::EXTENSIONS.each do |klass| 43 | cert.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v && klass::OPENSSL_IDENTIFIER == o 44 | end 45 | end 46 | end 47 | cert 48 | end 49 | 50 | def to_pem 51 | to_x509_csr.to_pem 52 | end 53 | 54 | def to_x509_csr 55 | raise "Must specify a DN/subject on csr" if @distinguished_name.nil? 56 | raise "Invalid DN in request" unless @distinguished_name.valid? 57 | raise "CSR must have key material" if @key_material.nil? 58 | raise "CSR must include a public key on key material" if @key_material.public_key.nil? 59 | raise "Need a private key on key material for CSR generation" if @key_material.private_key.nil? 60 | 61 | opensslcsr = OpenSSL::X509::Request.new 62 | opensslcsr.subject = @distinguished_name.to_x509_name 63 | opensslcsr.public_key = @key_material.public_key 64 | opensslcsr.attributes = @attributes unless @attributes.nil? 65 | opensslcsr.sign @key_material.private_key, OpenSSL::Digest.new(@digest || "SHA512") 66 | opensslcsr 67 | end 68 | 69 | def self.from_x509_csr(raw_csr) 70 | csr = SigningRequest.new 71 | openssl_csr = OpenSSL::X509::Request.new(raw_csr) 72 | csr.distinguished_name = DistinguishedName.from_openssl openssl_csr.subject 73 | csr.raw_body = raw_csr 74 | csr.openssl_csr = openssl_csr 75 | csr.attributes = openssl_csr.attributes 76 | key_material = SigningRequestKeyMaterial.new 77 | key_material.public_key = openssl_csr.public_key 78 | csr.key_material = key_material 79 | csr 80 | end 81 | 82 | def self.from_netscape_spkac(raw_spkac) 83 | openssl_spkac = OpenSSL::Netscape::SPKI.new raw_spkac 84 | csr = SigningRequest.new 85 | csr.raw_body = raw_spkac 86 | key_material = SigningRequestKeyMaterial.new 87 | key_material.public_key = openssl_spkac.public_key 88 | csr 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/units/certificate_revocation_list_spec.rb: -------------------------------------------------------------------------------- 1 | describe CertificateAuthority::CertificateRevocationList do 2 | before(:each) do 3 | @crl = CertificateAuthority::CertificateRevocationList.new 4 | 5 | @root_certificate = CertificateAuthority::Certificate.new 6 | @root_certificate.signing_entity = true 7 | @root_certificate.subject.common_name = "CRL Root" 8 | @root_certificate.key_material.generate_key(768) 9 | @root_certificate.serial_number.number = 1 10 | @root_certificate.sign! 11 | 12 | @certificate = CertificateAuthority::Certificate.new 13 | @certificate.key_material.generate_key(768) 14 | @certificate.subject.common_name = "http://bogusSite.com" 15 | @certificate.parent = @root_certificate 16 | @certificate.serial_number.number = 2 17 | @certificate.sign! 18 | 19 | @serial_number = CertificateAuthority::SerialNumber.new 20 | @serial_number.revoked_at = Time.now 21 | @serial_number.number = 5 22 | 23 | @crl.parent = @root_certificate 24 | @certificate.revoked_at = Time.now 25 | end 26 | 27 | it "should accept a list of certificates" do 28 | @crl << @certificate 29 | end 30 | 31 | it "should complain if you add a certificate without a revocation time" do 32 | @certificate.revoked_at = nil 33 | expect{ @crl << @certificate}.to raise_error(RuntimeError) 34 | end 35 | 36 | it "should have a 'parent' that will be responsible for signing" do 37 | @crl.parent = @root_certificate 38 | expect(@crl.parent).not_to be_nil 39 | end 40 | 41 | it "should raise an error if you try and sign a CRL without attaching a parent" do 42 | @crl.parent = nil 43 | expect { @crl.sign! }.to raise_error(RuntimeError) 44 | end 45 | 46 | it "should be able to generate a proper CRL" do 47 | @crl << @certificate 48 | expect {@crl.to_pem}.to raise_error(RuntimeError) 49 | @crl.parent = @root_certificate 50 | @crl.sign! 51 | expect(@crl.to_pem).not_to be_nil 52 | expect(OpenSSL::X509::CRL.new(@crl.to_pem)).not_to be_nil 53 | end 54 | 55 | it "should be able to mix Certificates and SerialNumbers for convenience" do 56 | @crl << @certificate 57 | @crl << @serial_number 58 | @crl.parent = @root_certificate 59 | @crl.sign! 60 | openssl_csr = OpenSSL::X509::CRL.new(@crl.to_pem) 61 | expect(openssl_csr.revoked.size).to eq(2) 62 | end 63 | 64 | it "should have the correct number of entities" do 65 | @crl << @certificate 66 | @crl.parent = @root_certificate 67 | @crl.sign! 68 | openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem) 69 | expect(openssl_clr.revoked).to be_a(Array) 70 | expect(openssl_clr.revoked.size).to eq(1) 71 | end 72 | 73 | it "should have the serial numbers of revoked entities" do 74 | @crl << @certificate 75 | @crl << @serial_number 76 | @crl.parent = @root_certificate 77 | @crl.sign! 78 | openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem) 79 | expect(openssl_clr.revoked).to be_a(Array) 80 | expect(openssl_clr.revoked.first.serial).to eq(@certificate.serial_number.number) 81 | expect(openssl_clr.revoked.last.serial).to eq(@serial_number.number) 82 | end 83 | 84 | it "should be valid according to OpenSSL and signer" do 85 | @crl << @certificate 86 | @crl.parent = @root_certificate 87 | @crl.sign! 88 | openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem) 89 | openssl_root = OpenSSL::X509::Certificate.new(@root_certificate.to_pem) 90 | expect(openssl_clr.verify(openssl_root.public_key)).to be_truthy 91 | end 92 | 93 | describe "Digests" do 94 | it "should use SHA512 by default" do 95 | @crl << @certificate 96 | @crl.parent = @root_certificate 97 | @crl.sign! 98 | openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem) 99 | expect(openssl_clr.signature_algorithm).to eq("sha512WithRSAEncryption") 100 | end 101 | 102 | it "should support alternate digests supported by OpenSSL" do 103 | @crl << @certificate 104 | @crl.parent = @root_certificate 105 | @crl.sign!({"digest" => "SHA1"}) 106 | openssl_clr = OpenSSL::X509::CRL.new(@crl.to_pem) 107 | expect(openssl_clr.signature_algorithm).to eq("sha1WithRSAEncryption") 108 | end 109 | end 110 | 111 | describe "Next update" do 112 | it "should be able to set a 'next_update' value" do 113 | @crl.next_update = (60 * 60 * 10) # 10 Hours 114 | expect(@crl.next_update).not_to be_nil 115 | end 116 | 117 | it "should throw an error if we try and sign up with a negative next_update" do 118 | @crl.sign! 119 | @crl.next_update = - (60 * 60 * 10) 120 | expect{@crl.sign!}.to raise_error(RuntimeError) 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/samples/certs/client.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 1 (0x0) 4 | Serial Number: 2 (0x2) 5 | Signature Algorithm: sha1WithRSAEncryption 6 | Issuer: C=AU, ST=Some-State, L=locality, O=Internet Widgits Pty Ltd, OU=section, CN=example ca/emailAddress=emailaddr@foo.com 7 | Validity 8 | Not Before: Sep 14 18:43:15 2012 GMT 9 | Not After : Sep 14 18:43:15 2013 GMT 10 | Subject: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=client.example.com 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | RSA Public Key: (1024 bit) 14 | Modulus (1024 bit): 15 | 00:dd:81:af:cb:67:4f:9d:2b:37:9f:bb:52:6a:17: 16 | f3:25:ba:ca:6d:23:45:94:dc:08:9b:cd:82:56:79: 17 | da:0b:47:c9:d0:3f:a8:16:bb:56:77:3a:85:a9:ab: 18 | 20:f3:1e:2d:5c:fa:b2:23:90:86:81:bf:a9:f6:10: 19 | 85:55:3f:c6:aa:7e:fa:43:96:08:c0:fa:e8:2f:cb: 20 | e5:2d:01:d1:12:22:a2:3f:7a:c5:75:4e:6f:47:d3: 21 | 2e:7a:fe:17:6e:4f:a3:20:60:5b:46:57:51:69:ce: 22 | 8f:33:87:05:df:1e:cb:fb:32:43:2a:71:6c:44:d1: 23 | a6:2d:98:41:e4:88:36:9a:f1 24 | Exponent: 65537 (0x10001) 25 | Signature Algorithm: sha1WithRSAEncryption 26 | 9e:7f:e1:05:a7:9a:86:6c:0d:6e:37:76:ae:2e:5b:78:e5:22: 27 | 6f:52:40:36:e8:7e:cf:20:77:4f:3b:b4:7b:7c:72:46:68:df: 28 | fe:04:87:16:9f:cd:6e:0f:79:01:25:cf:ca:5e:b8:47:31:9e: 29 | f1:20:44:26:78:79:40:00:57:cc:a6:a8:39:67:ef:01:d7:65: 30 | 1c:dd:5d:8f:6e:48:43:f4:03:48:46:8f:08:95:cf:27:3f:30: 31 | da:82:a7:33:82:5f:82:cb:e6:2c:f0:25:e8:87:3b:e5:bf:82: 32 | 79:72:a9:10:45:69:3c:7f:f0:dd:c9:50:6e:02:c9:05:16:cf: 33 | c3:58:15:3f:a6:32:ec:80:4f:88:b4:72:d2:5f:70:62:24:98: 34 | e5:99:c8:a7:d9:dd:0c:b0:cb:9c:70:d1:6a:44:21:d9:d7:65: 35 | a6:71:6f:60:64:7e:28:de:5d:98:42:6d:aa:fc:32:f9:1c:d6: 36 | 5c:d7:b6:15:18:79:09:80:7e:d7:9e:74:16:a5:80:39:6d:93: 37 | 8e:8e:4e:c5:8c:f5:4c:ea:d3:fd:12:bc:fa:fc:b8:e2:2c:30: 38 | 52:f4:eb:ad:d9:56:e9:84:e9:a8:df:a3:16:fa:d2:1e:74:49: 39 | 5d:d1:24:10:f2:2e:c4:b9:4a:a9:2d:3d:a4:70:6d:24:00:26: 40 | 46:bf:e2:98:16:4d:c8:55:40:a7:ab:76:b3:c6:a7:72:46:2d: 41 | 9b:fd:a2:ca:b8:62:9c:59:53:cc:64:ef:60:76:10:c8:c9:e7: 42 | 51:11:82:d4:81:04:73:e9:af:df:2d:c4:c7:2d:e4:17:d4:e2: 43 | 10:82:68:56:ae:7a:f2:3c:60:b7:59:29:39:6a:56:86:94:fc: 44 | 93:2b:5b:f0:ac:80:1d:c7:c5:b7:27:36:94:1c:ad:e9:1c:6b: 45 | f3:8a:2a:6c:c8:ce:69:52:b2:42:d9:b9:e7:8e:a3:d4:18:07: 46 | a1:db:bf:54:3c:ec:2e:68:7f:cf:d6:71:8f:3e:99:88:e4:ea: 47 | 7f:98:22:3a:31:68:24:a5:47:23:e2:d6:21:8f:1f:5f:a7:9a: 48 | 12:10:ba:6d:ac:22:e7:97:95:93:a2:b5:1c:f8:c8:86:1a:ad: 49 | 32:ff:64:4f:25:8d:d5:25:29:46:85:30:bc:c4:86:41:1b:6b: 50 | 24:7e:04:b6:eb:46:39:55:9c:4d:84:86:2f:bf:11:26:a9:40: 51 | 3d:2d:f4:90:22:05:7f:27:3b:13:d1:86:17:70:05:e1:68:be: 52 | 12:ce:c5:30:7b:0a:1b:7b:8a:89:e6:e7:9a:9c:b0:8b:c1:f8: 53 | c3:b6:0a:4c:41:da:fe:cd:ea:ce:89:3b:a5:8d:30:90:93:93: 54 | 9b:85:b6:61:46:8d:69:f2 55 | -----BEGIN CERTIFICATE----- 56 | MIID9DCCAdwCAQIwDQYJKoZIhvcNAQEFBQAwgaExCzAJBgNVBAYTAkFVMRMwEQYD 57 | VQQIEwpTb21lLVN0YXRlMREwDwYDVQQHEwhsb2NhbGl0eTEhMB8GA1UEChMYSW50 58 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQLEwdzZWN0aW9uMRMwEQYDVQQD 59 | EwpleGFtcGxlIGNhMSAwHgYJKoZIhvcNAQkBFhFlbWFpbGFkZHJAZm9vLmNvbTAe 60 | Fw0xMjA5MTQxODQzMTVaFw0xMzA5MTQxODQzMTVaMGIxCzAJBgNVBAYTAkFVMRMw 61 | EQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0 62 | eSBMdGQxGzAZBgNVBAMTEmNsaWVudC5leGFtcGxlLmNvbTCBnzANBgkqhkiG9w0B 63 | AQEFAAOBjQAwgYkCgYEA3YGvy2dPnSs3n7tSahfzJbrKbSNFlNwIm82CVnnaC0fJ 64 | 0D+oFrtWdzqFqasg8x4tXPqyI5CGgb+p9hCFVT/Gqn76Q5YIwProL8vlLQHREiKi 65 | P3rFdU5vR9Muev4Xbk+jIGBbRldRac6PM4cF3x7L+zJDKnFsRNGmLZhB5Ig2mvEC 66 | AwEAATANBgkqhkiG9w0BAQUFAAOCAgEAnn/hBaeahmwNbjd2ri5beOUib1JANuh+ 67 | zyB3Tzu0e3xyRmjf/gSHFp/Nbg95ASXPyl64RzGe8SBEJnh5QABXzKaoOWfvAddl 68 | HN1dj25IQ/QDSEaPCJXPJz8w2oKnM4JfgsvmLPAl6Ic75b+CeXKpEEVpPH/w3clQ 69 | bgLJBRbPw1gVP6Yy7IBPiLRy0l9wYiSY5ZnIp9ndDLDLnHDRakQh2ddlpnFvYGR+ 70 | KN5dmEJtqvwy+RzWXNe2FRh5CYB+1550FqWAOW2Tjo5OxYz1TOrT/RK8+vy44iww 71 | UvTrrdlW6YTpqN+jFvrSHnRJXdEkEPIuxLlKqS09pHBtJAAmRr/imBZNyFVAp6t2 72 | s8anckYtm/2iyrhinFlTzGTvYHYQyMnnURGC1IEEc+mv3y3Exy3kF9TiEIJoVq56 73 | 8jxgt1kpOWpWhpT8kytb8KyAHcfFtyc2lByt6Rxr84oqbMjOaVKyQtm5546j1BgH 74 | odu/VDzsLmh/z9Zxjz6ZiOTqf5giOjFoJKVHI+LWIY8fX6eaEhC6bawi55eVk6K1 75 | HPjIhhqtMv9kTyWN1SUpRoUwvMSGQRtrJH4EtutGOVWcTYSGL78RJqlAPS30kCIF 76 | fyc7E9GGF3AF4Wi+Es7FMHsKG3uKiebnmpywi8H4w7YKTEHa/s3qzok7pY0wkJOT 77 | m4W2YUaNafI= 78 | -----END CERTIFICATE----- 79 | -------------------------------------------------------------------------------- /spec/samples/certs/server.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 1 (0x0) 4 | Serial Number: 1 (0x1) 5 | Signature Algorithm: sha1WithRSAEncryption 6 | Issuer: C=AU, ST=Some-State, L=locality, O=Internet Widgits Pty Ltd, OU=section, CN=example ca/emailAddress=emailaddr@foo.com 7 | Validity 8 | Not Before: Sep 14 18:43:03 2012 GMT 9 | Not After : Sep 14 18:43:03 2013 GMT 10 | Subject: C=AU, ST=Some-State, L=locality, O=Internet Widgits Pty Ltd, OU=ouname, CN=server.example.com/emailAddress=email@example.com 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | RSA Public Key: (1024 bit) 14 | Modulus (1024 bit): 15 | 00:c2:6b:ec:81:d7:3f:94:09:f0:a6:f1:78:65:79: 16 | a7:28:26:5c:b7:96:86:5b:77:9a:be:67:7a:16:e0: 17 | a2:c8:8d:9c:ec:5d:9a:56:0e:c7:f4:8b:11:01:df: 18 | d2:94:80:ba:87:2b:cf:d4:33:4a:06:21:3b:c6:31: 19 | b5:fc:b4:30:f6:fd:28:58:5f:18:11:46:08:fa:12: 20 | 75:82:c2:e9:e6:65:9a:e4:fe:8d:f0:36:63:91:06: 21 | bb:43:f6:72:50:5c:8b:00:e3:53:2a:09:34:06:e6: 22 | 05:16:30:d0:f8:96:3b:b0:7d:b6:8d:ef:5f:b0:c1: 23 | 31:bc:77:d4:a9:93:31:d7:f1 24 | Exponent: 65537 (0x10001) 25 | Signature Algorithm: sha1WithRSAEncryption 26 | aa:c7:e4:d5:c5:33:e8:75:43:01:23:9f:ae:91:c3:17:dc:54: 27 | d5:34:65:e3:76:ec:00:e4:71:06:70:84:10:2d:ae:ea:21:90: 28 | 70:27:ec:86:15:97:e5:e4:10:62:19:08:b5:56:86:4f:84:ef: 29 | 43:c7:86:06:33:8a:61:bb:71:ac:f9:e1:d2:f0:08:83:32:bb: 30 | 73:a0:fd:39:66:8f:a3:d9:bb:59:bb:c2:cc:5e:8f:56:fc:72: 31 | b2:42:da:d2:31:1b:98:be:0d:0c:8d:1a:8e:12:fe:7f:ef:5b: 32 | cf:93:b7:e1:ba:c8:a0:c4:de:60:4f:74:ea:12:9d:3a:ea:81: 33 | 28:b3:ed:14:6e:22:00:23:56:b0:ef:d4:7e:6b:7d:4a:fb:7e: 34 | 3b:c2:a8:9e:84:42:43:ad:6d:b1:41:78:75:a6:32:46:5c:98: 35 | c2:e5:3b:d0:dd:cc:17:35:7b:f9:54:25:ef:38:07:82:dc:a3: 36 | 32:69:bb:15:28:71:a9:c7:a3:8e:55:29:61:04:eb:ee:05:e9: 37 | 9f:4b:f6:c7:6e:9b:02:19:e9:0c:5e:66:a1:65:fe:ae:6e:25: 38 | a4:a4:31:3c:40:0d:b9:f4:c2:44:40:23:65:85:58:33:5a:0d: 39 | 84:3d:24:71:43:0f:65:69:28:75:de:ae:b3:b7:82:a4:09:f1: 40 | b7:21:8f:5d:76:66:4c:d7:08:19:80:68:a0:9f:33:df:46:a7: 41 | ab:7f:45:4d:1f:d1:45:54:8f:53:b9:da:77:86:b3:e2:b2:7e: 42 | 72:a2:6e:ad:08:01:2a:05:79:d7:ba:a2:17:c7:82:72:58:7c: 43 | 4d:fb:b9:0e:09:54:24:1e:34:e0:ae:32:d7:0a:00:1b:23:e4: 44 | 95:1b:8e:28:6c:7b:31:55:ad:6b:bb:e0:76:d3:2e:d2:14:0c: 45 | 02:9a:b5:65:ce:54:c7:28:08:7b:85:3c:43:00:09:c3:90:4e: 46 | b0:9c:57:f0:66:d2:18:95:ce:4e:18:f0:81:f1:16:a1:b0:ca: 47 | a8:85:33:c4:8c:b5:06:9c:eb:e6:5b:b9:13:31:14:53:83:3c: 48 | 8f:0e:02:56:f9:b7:07:d5:a0:66:8a:8a:06:ee:c7:46:a2:e6: 49 | b2:f5:ef:be:f5:ac:e0:a3:fa:9e:1d:03:b1:40:0f:b8:8c:bb: 50 | ab:f0:13:55:db:05:65:eb:8a:f7:03:86:e0:a5:bb:f2:ae:ab: 51 | f4:32:7b:2c:28:56:fb:7d:30:b3:c1:71:c7:52:3c:a9:ee:a0: 52 | 82:6f:b6:90:54:b7:5b:f2:74:11:4d:40:89:e4:cb:c3:50:b8: 53 | f7:e6:98:bd:95:7e:1c:8f:b9:e8:9f:ef:d6:ca:3b:77:40:6a: 54 | dc:1e:51:74:c0:32:53:61 55 | -----BEGIN CERTIFICATE----- 56 | MIIEOzCCAiMCAQEwDQYJKoZIhvcNAQEFBQAwgaExCzAJBgNVBAYTAkFVMRMwEQYD 57 | VQQIEwpTb21lLVN0YXRlMREwDwYDVQQHEwhsb2NhbGl0eTEhMB8GA1UEChMYSW50 58 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQLEwdzZWN0aW9uMRMwEQYDVQQD 59 | EwpleGFtcGxlIGNhMSAwHgYJKoZIhvcNAQkBFhFlbWFpbGFkZHJAZm9vLmNvbTAe 60 | Fw0xMjA5MTQxODQzMDNaFw0xMzA5MTQxODQzMDNaMIGoMQswCQYDVQQGEwJBVTET 61 | MBEGA1UECBMKU29tZS1TdGF0ZTERMA8GA1UEBxMIbG9jYWxpdHkxITAfBgNVBAoT 62 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UECxMGb3VuYW1lMRswGQYD 63 | VQQDExJzZXJ2ZXIuZXhhbXBsZS5jb20xIDAeBgkqhkiG9w0BCQEWEWVtYWlsQGV4 64 | YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCa+yB1z+UCfCm 65 | 8XhleacoJly3loZbd5q+Z3oW4KLIjZzsXZpWDsf0ixEB39KUgLqHK8/UM0oGITvG 66 | MbX8tDD2/ShYXxgRRgj6EnWCwunmZZrk/o3wNmORBrtD9nJQXIsA41MqCTQG5gUW 67 | MND4ljuwfbaN71+wwTG8d9SpkzHX8QIDAQABMA0GCSqGSIb3DQEBBQUAA4ICAQCq 68 | x+TVxTPodUMBI5+ukcMX3FTVNGXjduwA5HEGcIQQLa7qIZBwJ+yGFZfl5BBiGQi1 69 | VoZPhO9Dx4YGM4phu3Gs+eHS8AiDMrtzoP05Zo+j2btZu8LMXo9W/HKyQtrSMRuY 70 | vg0MjRqOEv5/71vPk7fhusigxN5gT3TqEp066oEos+0UbiIAI1aw79R+a31K+347 71 | wqiehEJDrW2xQXh1pjJGXJjC5TvQ3cwXNXv5VCXvOAeC3KMyabsVKHGpx6OOVSlh 72 | BOvuBemfS/bHbpsCGekMXmahZf6ubiWkpDE8QA259MJEQCNlhVgzWg2EPSRxQw9l 73 | aSh13q6zt4KkCfG3IY9ddmZM1wgZgGignzPfRqerf0VNH9FFVI9Tudp3hrPisn5y 74 | om6tCAEqBXnXuqIXx4JyWHxN+7kOCVQkHjTgrjLXCgAbI+SVG44obHsxVa1ru+B2 75 | 0y7SFAwCmrVlzlTHKAh7hTxDAAnDkE6wnFfwZtIYlc5OGPCB8RahsMqohTPEjLUG 76 | nOvmW7kTMRRTgzyPDgJW+bcH1aBmiooG7sdGouay9e++9azgo/qeHQOxQA+4jLur 77 | 8BNV2wVl64r3A4bgpbvyrqv0MnssKFb7fTCzwXHHUjyp7qCCb7aQVLdb8nQRTUCJ 78 | 5MvDULj35pi9lX4cj7non+/Wyjt3QGrcHlF0wDJTYQ== 79 | -----END CERTIFICATE----- 80 | -------------------------------------------------------------------------------- /spec/units/working_with_openssl_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Using OpenSSL" do 2 | shared_examples_for "an ossl issuer and its signed cert" do 3 | it "should issue a certificate verified by the issuer" do 4 | expect(@signed.verify(@issuer.public_key )).to be_truthy 5 | end 6 | 7 | it "should issue a certificate with a matching issuer subject string" do 8 | expect(@signed.issuer.to_s).to eq(@issuer.subject.to_s) 9 | end 10 | 11 | it "should issue a certificate with a matching issuer subject openssl name" do 12 | expect(@signed.issuer).to eq(@issuer.subject) 13 | end 14 | 15 | end 16 | 17 | context "Signing CSRs" do 18 | shared_examples_for "a csr operation" do 19 | before :all do 20 | @ca = sample_file("certs/ca.crt").read 21 | @ca_key = sample_file("certs/ca.key").read 22 | 23 | @issuer = OpenSSL::X509::Certificate.new(@ca) 24 | issuer_ca = CertificateAuthority::Certificate.from_openssl(@issuer) 25 | issuer_ca.key_material.private_key = OpenSSL::PKey::RSA.new(@ca_key) 26 | 27 | @our_csr = CertificateAuthority::SigningRequest.from_x509_csr(@csr_pem) 28 | signed = @our_csr.to_cert 29 | signed.parent = issuer_ca 30 | signed.serial_number.number = 2 31 | 32 | signed.sign! 33 | 34 | @cert = @signed = OpenSSL::X509::Certificate.new(signed.to_pem) 35 | end 36 | it_should_behave_like "an ossl issuer and its signed cert" 37 | end 38 | 39 | context "With a server CSR" do 40 | before :all do 41 | @csr_pem = sample_file("certs/server.csr").read 42 | end 43 | 44 | it_should_behave_like "a csr operation" 45 | end 46 | 47 | context "With a client CSR" do 48 | before :all do 49 | @csr_pem = sample_file("certs/client.csr").read 50 | end 51 | 52 | it_should_behave_like "a csr operation" 53 | end 54 | end 55 | 56 | context "Handling externally supplied CAs and certs" do 57 | shared_examples_for "comparing a pair of openssl certs" do 58 | context "using openssl" do 59 | before :all do 60 | @issuer = @ca 61 | @signed = @cert 62 | end 63 | it_should_behave_like "an ossl issuer and its signed cert" 64 | end 65 | 66 | context "using certificate_authority" do 67 | before :all do 68 | # from openssl 69 | @our_ca = CertificateAuthority::Certificate.from_openssl(@ca) 70 | @our_cert = CertificateAuthority::Certificate.from_openssl(@cert) 71 | 72 | # and back 73 | @issuer = OpenSSL::X509::Certificate.new(@our_ca.to_pem) 74 | @signed = OpenSSL::X509::Certificate.new(@our_cert.to_pem) 75 | end 76 | 77 | it "should match the original ca's distinguished name" do 78 | expect(@our_ca.distinguished_name.to_x509_name).to eq(@ca.subject) 79 | end 80 | 81 | it "should match the original openssl ca" do 82 | back = OpenSSL::X509::Certificate.new(@our_ca.to_pem) 83 | expect(back.subject).to eq(@ca.subject) 84 | end 85 | 86 | it "should match the original cert's distinguished name" do 87 | expect(@our_cert.distinguished_name.to_x509_name).to eq(@cert.subject) 88 | end 89 | 90 | it "should match the original openssl cert" do 91 | back = OpenSSL::X509::Certificate.new(@our_cert.to_pem) 92 | expect(back.subject).to eq(@cert.subject) 93 | end 94 | 95 | it_should_behave_like "an ossl issuer and its signed cert" 96 | end 97 | end 98 | 99 | context "A custom CA signing a client cert" do 100 | before :all do 101 | @ca = OpenSSL::X509::Certificate.new(sample_file("certs/ca.crt").read) 102 | @cert = OpenSSL::X509::Certificate.new(sample_file("certs/client.crt").read) 103 | end 104 | 105 | it_should_behave_like "comparing a pair of openssl certs" 106 | end 107 | 108 | context "A custom CA signing a server cert" do 109 | before :all do 110 | @ca = OpenSSL::X509::Certificate.new(sample_file("certs/ca.crt").read) 111 | @cert = OpenSSL::X509::Certificate.new(sample_file("certs/server.crt").read) 112 | end 113 | 114 | it_should_behave_like "comparing a pair of openssl certs" 115 | end 116 | 117 | context "Github's signer" do 118 | before :all do 119 | @ca = OpenSSL::X509::Certificate.new(sample_file("certs/DigiCertHighAssuranceEVCA-1.pem").read) 120 | @cert = OpenSSL::X509::Certificate.new(sample_file("certs/github.com.pem").read) 121 | end 122 | it_should_behave_like "comparing a pair of openssl certs" 123 | end 124 | 125 | context "Apple's WWDR signer" do 126 | before :all do 127 | @ca = OpenSSL::X509::Certificate.new(sample_file("certs/apple_wwdr_issuer.pem").read) 128 | @cert = OpenSSL::X509::Certificate.new(sample_file("certs/apple_wwdr_issued_cert.pem").read) 129 | end 130 | it_should_behave_like "comparing a pair of openssl certs" 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /spec/samples/certs/apple_wwdr_issuer.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 25 (0x19) 5 | Signature Algorithm: sha1WithRSAEncryption 6 | Issuer: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple Root CA 7 | Validity 8 | Not Before: Feb 14 18:56:35 2008 GMT 9 | Not After : Feb 14 18:56:35 2016 GMT 10 | Subject: C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Apple Worldwide Developer Relations Certification Authority 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | RSA Public Key: (2048 bit) 14 | Modulus (2048 bit): 15 | 00:ca:38:54:a6:cb:56:aa:c8:24:39:48:e9:8c:ee: 16 | ec:5f:b8:7f:26:91:bc:34:53:7a:ce:7c:63:80:61: 17 | 77:64:5e:a5:07:23:b6:39:fe:50:2d:15:56:58:70: 18 | 2d:7e:c4:6e:c1:4a:85:3e:2f:f0:de:84:1a:a1:57: 19 | c9:af:7b:18:ff:6a:fa:15:12:49:15:08:19:ac:aa: 20 | db:2a:32:ed:96:63:68:52:15:3d:8c:8a:ec:bf:6b: 21 | 18:95:e0:03:ac:01:7d:97:05:67:ce:0e:85:95:37: 22 | 6a:ed:09:b6:ae:67:cd:51:64:9f:c6:5c:d1:bc:57: 23 | 6e:67:35:80:76:36:a4:87:81:6e:38:8f:d8:2b:15: 24 | 4e:7b:25:d8:5a:bf:4e:83:c1:8d:d2:93:d5:1a:71: 25 | b5:60:9c:9d:33:4e:55:f9:12:58:0c:86:b8:16:0d: 26 | c1:e5:77:45:8d:50:48:ba:2b:2d:e4:94:85:e1:e8: 27 | c4:9d:c6:68:a5:b0:a3:fc:67:7e:70:ba:02:59:4b: 28 | 77:42:91:39:b9:f5:cd:e1:4c:ef:c0:3b:48:8c:a6: 29 | e5:21:5d:fd:6a:6a:bb:a7:16:35:60:d2:e6:ad:f3: 30 | 46:29:c9:e8:c3:8b:e9:79:c0:6a:61:67:15:b2:f0: 31 | fd:e5:68:bc:62:5f:6e:cf:99:dd:ef:1b:63:fe:92: 32 | 65:ab 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Key Usage: critical 36 | Digital Signature, Certificate Sign, CRL Sign 37 | X509v3 Basic Constraints: critical 38 | CA:TRUE 39 | X509v3 Subject Key Identifier: 40 | 88:27:17:09:A9:B6:18:60:8B:EC:EB:BA:F6:47:59:C5:52:54:A3:B7 41 | X509v3 Authority Key Identifier: 42 | keyid:2B:D0:69:47:94:76:09:FE:F4:6B:8D:2E:40:A6:F7:47:4D:7F:08:5E 43 | 44 | X509v3 CRL Distribution Points: 45 | URI:http://www.apple.com/appleca/root.crl 46 | 47 | 1.2.840.113635.100.6.2.1: 48 | .. 49 | Signature Algorithm: sha1WithRSAEncryption 50 | da:32:00:96:c5:54:94:d3:3b:82:37:66:7d:2e:68:d5:c3:c6: 51 | b8:cb:26:8c:48:90:cf:13:24:6a:46:8e:63:d4:f0:d0:13:06: 52 | dd:d8:c4:c1:37:15:f2:33:13:39:26:2d:ce:2e:55:40:e3:0b: 53 | 03:af:fa:12:c2:e7:0d:21:b8:d5:80:cf:ac:28:2f:ce:2d:b3: 54 | 4e:af:86:19:04:c6:e9:50:dd:4c:29:47:10:23:fc:6c:bb:1b: 55 | 98:6b:48:89:e1:5b:9d:de:46:db:35:85:35:ef:3e:d0:e2:58: 56 | 4b:38:f4:ed:75:5a:1f:5c:70:1d:56:39:12:e5:e1:0d:11:e4: 57 | 89:25:06:bd:d5:b4:15:8e:5e:d0:59:97:90:e9:4b:81:e2:df: 58 | 18:af:44:74:1e:19:a0:3a:47:cc:91:1d:3a:eb:23:5a:fe:a5: 59 | 2d:97:f7:7b:bb:d6:87:46:42:85:eb:52:3d:26:b2:63:a8:b4: 60 | b1:ca:8f:f4:cc:e2:b3:c8:47:e0:bf:9a:59:83:fa:da:98:53: 61 | 2a:82:f5:7c:65:2e:95:d9:33:5d:f5:ed:65:cc:31:37:c5:5a: 62 | 04:e8:6b:e1:e7:88:03:4a:75:9e:9b:28:cb:4a:40:88:65:43: 63 | 75:dd:cb:3a:25:23:c5:9e:57:f8:2e:ce:d2:a9:92:5e:73:2e: 64 | 2f:25:75:15 65 | -----BEGIN CERTIFICATE----- 66 | MIIEIzCCAwugAwIBAgIBGTANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET 67 | MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv 68 | biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDgwMjE0MTg1 69 | NjM1WhcNMTYwMjE0MTg1NjM1WjCBljELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFw 70 | cGxlIEluYy4xLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVs 71 | YXRpb25zMUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0 72 | aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD 73 | ggEPADCCAQoCggEBAMo4VKbLVqrIJDlI6Yzu7F+4fyaRvDRTes58Y4Bhd2RepQcj 74 | tjn+UC0VVlhwLX7EbsFKhT4v8N6EGqFXya97GP9q+hUSSRUIGayq2yoy7ZZjaFIV 75 | PYyK7L9rGJXgA6wBfZcFZ84OhZU3au0Jtq5nzVFkn8Zc0bxXbmc1gHY2pIeBbjiP 76 | 2CsVTnsl2Fq/ToPBjdKT1RpxtWCcnTNOVfkSWAyGuBYNweV3RY1QSLorLeSUheHo 77 | xJ3GaKWwo/xnfnC6AllLd0KRObn1zeFM78A7SIym5SFd/Wpqu6cWNWDS5q3zRinJ 78 | 6MOL6XnAamFnFbLw/eVovGJfbs+Z3e8bY/6SZasCAwEAAaOBrjCBqzAOBgNVHQ8B 79 | Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUiCcXCam2GGCL7Ou6 80 | 9kdZxVJUo7cwHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wNgYDVR0f 81 | BC8wLTAroCmgJ4YlaHR0cDovL3d3dy5hcHBsZS5jb20vYXBwbGVjYS9yb290LmNy 82 | bDAQBgoqhkiG92NkBgIBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEA2jIAlsVUlNM7 83 | gjdmfS5o1cPGuMsmjEiQzxMkakaOY9Tw0BMG3djEwTcV8jMTOSYtzi5VQOMLA6/6 84 | EsLnDSG41YDPrCgvzi2zTq+GGQTG6VDdTClHECP8bLsbmGtIieFbnd5G2zWFNe8+ 85 | 0OJYSzj07XVaH1xwHVY5EuXhDRHkiSUGvdW0FY5e0FmXkOlLgeLfGK9EdB4ZoDpH 86 | zJEdOusjWv6lLZf3e7vWh0ZChetSPSayY6i0scqP9Mzis8hH4L+aWYP62phTKoL1 87 | fGUuldkzXfXtZcwxN8VaBOhr4eeIA0p1npsoy0pAiGVDdd3LOiUjxZ5X+C7O0qmS 88 | XnMuLyV1FQ== 89 | -----END CERTIFICATE----- 90 | -------------------------------------------------------------------------------- /lib/certificate_authority/ocsp_handler.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | class OCSPResponseBuilder 3 | attr_accessor :ocsp_response 4 | attr_accessor :verification_mechanism 5 | attr_accessor :ocsp_request_reader 6 | attr_accessor :parent 7 | attr_accessor :next_update 8 | 9 | GOOD = OpenSSL::OCSP::V_CERTSTATUS_GOOD 10 | REVOKED = OpenSSL::OCSP::V_CERTSTATUS_REVOKED 11 | 12 | NO_REASON=0 13 | KEY_COMPROMISED=OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE 14 | UNSPECIFIED=OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED 15 | 16 | def build_response() 17 | raise "Requires a parent for signing" if @parent.nil? 18 | if @verification_mechanism.nil? 19 | ## If no verification callback is provided we're marking it GOOD 20 | @verification_mechanism = lambda {|cert_id| [GOOD,NO_REASON] } 21 | end 22 | 23 | @ocsp_request_reader.ocsp_request.certid.each do |cert_id| 24 | result,reason = verification_mechanism.call(cert_id.serial) 25 | 26 | ## cert_id, status, reason, rev_time, this update, next update, ext 27 | ## - unit of time is seconds 28 | ## - rev_time is currently set to "now" 29 | @ocsp_response.add_status(cert_id, 30 | result, reason, 31 | 0, 0, @next_update, nil) 32 | end 33 | 34 | @ocsp_response.sign(OpenSSL::X509::Certificate.new(@parent.to_pem), @parent.key_material.private_key, nil, nil) 35 | OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, @ocsp_response) 36 | end 37 | 38 | def self.from_request_reader(request_reader,verification_mechanism=nil) 39 | response_builder = OCSPResponseBuilder.new 40 | response_builder.ocsp_request_reader = request_reader 41 | 42 | ocsp_response = OpenSSL::OCSP::BasicResponse.new 43 | ocsp_response.copy_nonce(request_reader.ocsp_request) 44 | response_builder.ocsp_response = ocsp_response 45 | response_builder.next_update = 60*15 #Default of 15 minutes 46 | response_builder 47 | end 48 | end 49 | 50 | class OCSPRequestReader 51 | attr_accessor :raw_ocsp_request 52 | attr_accessor :ocsp_request 53 | 54 | def serial_numbers 55 | @ocsp_request.certid.collect do |cert_id| 56 | cert_id.serial 57 | end 58 | end 59 | 60 | def self.from_der(request_body) 61 | reader = OCSPRequestReader.new 62 | reader.raw_ocsp_request = request_body 63 | reader.ocsp_request = OpenSSL::OCSP::Request.new(request_body) 64 | 65 | reader 66 | end 67 | end 68 | 69 | ## DEPRECATED 70 | class OCSPHandler 71 | include Validations 72 | 73 | attr_accessor :ocsp_request 74 | attr_accessor :certificate_ids 75 | 76 | attr_accessor :certificates 77 | attr_accessor :parent 78 | 79 | attr_accessor :ocsp_response_body 80 | 81 | def validate 82 | errors.add :parent, "A parent entity must be set" if parent.nil? 83 | all_certificates_available 84 | end 85 | 86 | def initialize 87 | self.certificates = {} 88 | end 89 | 90 | def <<(cert) 91 | self.certificates[cert.serial_number.number.to_s] = cert 92 | end 93 | 94 | def extract_certificate_serials 95 | openssl_request = OpenSSL::OCSP::Request.new(@ocsp_request) 96 | 97 | if openssl_request.certid.nil? 98 | raise "Invalid openssl request" 99 | end 100 | self.certificate_ids = openssl_request.certid.collect do |cert_id| 101 | cert_id.serial 102 | end 103 | 104 | self.certificate_ids 105 | end 106 | 107 | 108 | def response 109 | raise "Invalid response" unless valid? 110 | 111 | openssl_ocsp_response = OpenSSL::OCSP::BasicResponse.new 112 | openssl_ocsp_request = OpenSSL::OCSP::Request.new(self.ocsp_request) 113 | openssl_ocsp_response.copy_nonce(openssl_ocsp_request) 114 | 115 | openssl_ocsp_request.certid.each do |cert_id| 116 | certificate = self.certificates[cert_id.serial.to_s] 117 | 118 | openssl_ocsp_response.add_status(cert_id, 119 | OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, 120 | 0, 0, 30, nil) 121 | end 122 | 123 | 124 | openssl_ocsp_response.sign(OpenSSL::X509::Certificate.new(self.parent.to_pem), self.parent.key_material.private_key, nil, nil) 125 | final_response = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, openssl_ocsp_response) 126 | self.ocsp_response_body = final_response 127 | self.ocsp_response_body 128 | end 129 | 130 | def to_der 131 | raise "No signed OCSP response body available" if self.ocsp_response_body.nil? 132 | self.ocsp_response_body.to_der 133 | end 134 | 135 | private 136 | 137 | def all_certificates_available 138 | openssl_ocsp_request = OpenSSL::OCSP::Request.new(self.ocsp_request) 139 | 140 | openssl_ocsp_request.certid.each do |cert_id| 141 | certificate = self.certificates[cert_id.serial.to_s] 142 | errors.add(:base, "Certificate #{cert_id.serial} has not been added yet") if certificate.nil? 143 | end 144 | end 145 | 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /spec/samples/certs/apple_wwdr_issued_cert.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 0f:50:11:d8:8f:07:09:bf 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Apple Worldwide Developer Relations Certification Authority 8 | Validity 9 | Not Before: Jun 26 21:18:40 2012 GMT 10 | Not After : Jun 26 21:18:40 2013 GMT 11 | Subject: UID=pass.void-star.com.meepedoo, CN=Pass Type ID: pass.void-star.com.meepedoo, OU=UL736KYQR9, O=Eric Monti, C=US 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (2048 bit) 15 | Modulus (2048 bit): 16 | 00:af:f9:a6:9f:c0:8b:a5:56:f2:b6:97:0a:86:42: 17 | d0:f1:54:01:98:95:9f:d9:69:2b:9c:be:b0:b5:f4: 18 | a4:ad:9e:e6:ef:8e:a5:dc:50:d0:ce:2a:89:a9:41: 19 | ce:44:36:af:90:33:e7:56:76:9e:68:91:df:c6:e7: 20 | b8:21:f2:d5:75:d2:2a:17:3a:9d:4a:e0:cc:d2:94: 21 | 90:e7:f2:36:2f:1c:41:00:02:76:45:fe:c2:6a:fc: 22 | 36:96:e7:7e:59:00:f2:85:9e:31:ff:a3:9b:a0:b8: 23 | 6d:95:9e:e4:f1:c4:d0:e9:7c:70:61:52:03:39:5c: 24 | b8:8a:34:69:22:82:c5:44:f9:cd:a1:25:57:26:86: 25 | e4:31:d5:08:c9:9d:5f:73:44:10:21:6d:99:90:74: 26 | f6:69:fb:20:de:a9:46:49:a3:a9:96:ab:66:44:e6: 27 | bd:56:65:8e:7d:dd:07:7e:71:bd:13:0f:b1:50:07: 28 | af:eb:71:78:af:46:d5:71:39:da:ed:f2:d1:db:8d: 29 | 81:64:7f:56:c6:87:f1:7e:a5:a3:f4:9f:02:01:b9: 30 | d0:36:7f:87:f2:0d:d6:93:4e:cb:d8:32:9c:5a:ea: 31 | 44:07:64:6a:9f:1c:14:8e:9a:3a:0e:a3:86:2c:e8: 32 | 20:2a:f1:32:e0:11:2a:13:8a:d3:4c:73:5e:ab:70: 33 | fa:4d 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | Authority Information Access: 37 | OCSP - URI:http://ocsp.apple.com/ocsp-wwdr03 38 | 39 | X509v3 Subject Key Identifier: 40 | 02:87:24:00:CA:53:38:8F:C3:4D:5C:80:98:E1:65:95:38:D5:2B:69 41 | X509v3 Basic Constraints: 42 | CA:FALSE 43 | X509v3 Authority Key Identifier: 44 | keyid:88:27:17:09:A9:B6:18:60:8B:EC:EB:BA:F6:47:59:C5:52:54:A3:B7 45 | 46 | X509v3 Certificate Policies: 47 | Policy: 1.2.840.113635.100.5.1 48 | CPS: http://www.apple.com/appleca/ 49 | User Notice: 50 | 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. 51 | 52 | X509v3 CRL Distribution Points: 53 | URI:http://crl.apple.com/wwdrca.crl 54 | 55 | X509v3 Key Usage: 56 | Digital Signature 57 | X509v3 Extended Key Usage: 58 | TLS Web Client Authentication, 1.2.840.113635.100.4.14 59 | 1.2.840.113635.100.6.3.2: 60 | .. 61 | 1.2.840.113635.100.6.1.16: 62 | ..pass.void-star.com.meepedoo 63 | Signature Algorithm: sha1WithRSAEncryption 64 | 5d:e8:2d:5a:d7:65:9e:92:dd:bf:66:1c:04:99:08:a9:40:b7: 65 | 92:dc:fa:d4:c8:8c:cf:ad:3a:99:22:34:0c:0f:72:c9:4f:7f: 66 | c5:90:dc:8a:5d:47:c0:dd:ee:47:f7:01:81:6a:06:61:66:c3: 67 | 44:a1:0b:96:e5:70:a2:2f:c3:bb:98:d0:bf:07:0a:3d:56:d1: 68 | 95:01:08:16:6e:9a:5e:6b:45:ce:d9:b5:78:09:0f:eb:ff:11: 69 | a6:9a:eb:65:f3:b3:b1:14:a5:6f:97:a1:53:31:65:a6:e0:ea: 70 | e6:70:2f:df:5a:f9:b5:e4:59:2b:33:d4:a0:a3:4c:c6:61:c8: 71 | 56:5a:ca:be:4e:ac:12:c7:d3:1e:e5:b6:e3:de:04:c4:63:e5: 72 | 0e:33:4d:b9:33:92:7e:11:a4:ee:85:2b:65:00:7f:a5:dc:f6: 73 | 19:5b:69:37:fe:61:a7:e6:45:27:c5:1c:3a:b6:46:76:fb:f3: 74 | 56:93:00:2a:4f:b4:1e:d3:ed:75:8c:32:e5:09:c8:28:84:46: 75 | 29:4d:db:8f:e9:6e:1c:9e:bb:76:74:6a:8f:63:d5:04:1a:b3: 76 | 16:42:cc:70:4c:b1:88:e2:6d:58:bd:52:d2:3c:dc:52:9d:de: 77 | 37:94:20:00:07:6e:06:48:e7:17:86:44:a2:3a:23:07:c1:74: 78 | ef:6f:2a:a5 79 | -----BEGIN CERTIFICATE----- 80 | MIIF8zCCBNugAwIBAgIID1AR2I8HCb8wDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV 81 | BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js 82 | ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3 83 | aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw 84 | HhcNMTIwNjI2MjExODQwWhcNMTMwNjI2MjExODQwWjCBmDErMCkGCgmSJomT8ixk 85 | AQEMG3Bhc3Mudm9pZC1zdGFyLmNvbS5tZWVwZWRvbzEyMDAGA1UEAwwpUGFzcyBU 86 | eXBlIElEOiBwYXNzLnZvaWQtc3Rhci5jb20ubWVlcGVkb28xEzARBgNVBAsMClVM 87 | NzM2S1lRUjkxEzARBgNVBAoMCkVyaWMgTW9udGkxCzAJBgNVBAYTAlVTMIIBIjAN 88 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/mmn8CLpVbytpcKhkLQ8VQBmJWf 89 | 2WkrnL6wtfSkrZ7m746l3FDQziqJqUHORDavkDPnVnaeaJHfxue4IfLVddIqFzqd 90 | SuDM0pSQ5/I2LxxBAAJ2Rf7Cavw2lud+WQDyhZ4x/6OboLhtlZ7k8cTQ6XxwYVID 91 | OVy4ijRpIoLFRPnNoSVXJobkMdUIyZ1fc0QQIW2ZkHT2afsg3qlGSaOplqtmROa9 92 | VmWOfd0HfnG9Ew+xUAev63F4r0bVcTna7fLR242BZH9WxofxfqWj9J8CAbnQNn+H 93 | 8g3Wk07L2DKcWupEB2RqnxwUjpo6DqOGLOggKvEy4BEqE4rTTHNeq3D6TQIDAQAB 94 | o4ICPzCCAjswPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vb2Nz 95 | cC5hcHBsZS5jb20vb2NzcC13d2RyMDMwHQYDVR0OBBYEFAKHJADKUziPw01cgJjh 96 | ZZU41StpMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUiCcXCam2GGCL7Ou69kdZxVJU 97 | o7cwggEPBgNVHSAEggEGMIIBAjCB/wYJKoZIhvdjZAUBMIHxMCkGCCsGAQUFBwIB 98 | Fh1odHRwOi8vd3d3LmFwcGxlLmNvbS9hcHBsZWNhLzCBwwYIKwYBBQUHAgIwgbYM 99 | gbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1 100 | bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0 101 | ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBh 102 | bmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjAwBgNVHR8EKTAn 103 | MCWgI6Ahhh9odHRwOi8vY3JsLmFwcGxlLmNvbS93d2RyY2EuY3JsMAsGA1UdDwQE 104 | AwIHgDAeBgNVHSUEFzAVBggrBgEFBQcDAgYJKoZIhvdjZAQOMBAGCiqGSIb3Y2QG 105 | AwIEAgUAMCsGCiqGSIb3Y2QGARAEHQwbcGFzcy52b2lkLXN0YXIuY29tLm1lZXBl 106 | ZG9vMA0GCSqGSIb3DQEBBQUAA4IBAQBd6C1a12Wekt2/ZhwEmQipQLeS3PrUyIzP 107 | rTqZIjQMD3LJT3/FkNyKXUfA3e5H9wGBagZhZsNEoQuW5XCiL8O7mNC/Bwo9VtGV 108 | AQgWbppea0XO2bV4CQ/r/xGmmutl87OxFKVvl6FTMWWm4OrmcC/fWvm15FkrM9Sg 109 | o0zGYchWWsq+TqwSx9Me5bbj3gTEY+UOM025M5J+EaTuhStlAH+l3PYZW2k3/mGn 110 | 5kUnxRw6tkZ2+/NWkwAqT7Qe0+11jDLlCcgohEYpTduP6W4cnrt2dGqPY9UEGrMW 111 | QsxwTLGI4m1YvVLSPNxSnd43lCAAB24GSOcXhkSiOiMHwXTvbyql 112 | -----END CERTIFICATE----- 113 | -------------------------------------------------------------------------------- /spec/samples/certs/DigiCertHighAssuranceEVCA-1.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 08:bb:b0:25:47:13:4b:c9:b1:10:d7:c1:a2:12:59:c5 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV Root CA 8 | Validity 9 | Not Before: Nov 10 00:00:00 2006 GMT 10 | Not After : Nov 10 00:00:00 2021 GMT 11 | Subject: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV CA-1 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (2048 bit) 15 | Modulus (2048 bit): 16 | 00:f3:96:62:d8:75:6e:19:ff:3f:34:7c:49:4f:31: 17 | 7e:0d:04:4e:99:81:e2:b3:85:55:91:30:b1:c0:af: 18 | 70:bb:2c:a8:e7:18:aa:3f:78:f7:90:68:52:86:01: 19 | 88:97:e2:3b:06:65:90:aa:bd:65:76:c2:ec:be:10: 20 | 5b:37:78:83:60:75:45:c6:bd:74:aa:b6:9f:a4:3a: 21 | 01:50:17:c4:39:69:b9:f1:4f:ef:82:c1:ca:f3:4a: 22 | db:cc:9e:50:4f:4d:40:a3:3a:90:e7:86:66:bc:f0: 23 | 3e:76:28:4c:d1:75:80:9e:6a:35:14:35:03:9e:db: 24 | 0c:8c:c2:28:ad:50:b2:ce:f6:91:a3:c3:a5:0a:58: 25 | 49:f6:75:44:6c:ba:f9:ce:e9:ab:3a:02:e0:4d:f3: 26 | ac:e2:7a:e0:60:22:05:3c:82:d3:52:e2:f3:9c:47: 27 | f8:3b:d8:b2:4b:93:56:4a:bf:70:ab:3e:e9:68:c8: 28 | 1d:8f:58:1d:2a:4d:5e:27:3d:ad:0a:59:2f:5a:11: 29 | 20:40:d9:68:04:68:2d:f4:c0:84:0b:0a:1b:78:df: 30 | ed:1a:58:dc:fb:41:5a:6d:6b:f2:ed:1c:ee:5c:32: 31 | b6:5c:ec:d7:a6:03:32:a6:e8:de:b7:28:27:59:88: 32 | 80:ff:7b:ad:89:58:d5:1e:14:a4:f2:b0:70:d4:a0: 33 | 3e:a7 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Key Usage: critical 37 | Digital Signature, Certificate Sign, CRL Sign 38 | X509v3 Extended Key Usage: 39 | TLS Web Server Authentication, TLS Web Client Authentication, Code Signing, E-mail Protection, Time Stamping 40 | X509v3 Certificate Policies: 41 | Policy: 2.16.840.1.114412.2.1 42 | CPS: http://www.digicert.com/ssl-cps-repository.htm 43 | User Notice: 44 | Explicit Text: 45 | 46 | X509v3 Basic Constraints: critical 47 | CA:TRUE 48 | Authority Information Access: 49 | OCSP - URI:http://ocsp.digicert.com 50 | CA Issuers - URI:http://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt 51 | 52 | X509v3 CRL Distribution Points: 53 | URI:http://crl3.digicert.com/DigiCertHighAssuranceEVRootCA.crl 54 | URI:http://crl4.digicert.com/DigiCertHighAssuranceEVRootCA.crl 55 | 56 | X509v3 Subject Key Identifier: 57 | 4C:58:CB:25:F0:41:4F:52:F4:28:C8:81:43:9B:A6:A8:A0:E6:92:E5 58 | X509v3 Authority Key Identifier: 59 | keyid:B1:3E:C3:69:03:F8:BF:47:01:D4:98:26:1A:08:02:EF:63:64:2B:C3 60 | 61 | Signature Algorithm: sha1WithRSAEncryption 62 | 50:1e:43:b0:f7:4d:29:96:5b:bb:a7:d3:0a:b5:b5:d5:d0:27: 63 | aa:f9:af:c7:25:d1:95:d5:2f:5a:53:bd:42:07:7e:78:49:ca: 64 | 0b:eb:4c:55:e2:ea:2f:7f:49:ad:c7:ff:d1:2d:3e:9c:a0:64: 65 | 2b:51:9e:91:26:28:bb:87:bb:75:7c:bc:a1:fd:66:68:2e:4c: 66 | 4a:16:cc:fe:06:cf:31:ea:80:6e:e4:bd:e8:03:72:f6:25:b5: 67 | 41:83:61:d0:97:0a:27:1d:b3:f7:2b:32:84:8f:5b:e7:cc:3f: 68 | e2:2c:67:86:94:f4:b2:2b:6c:52:3b:67:2a:8d:58:95:00:14: 69 | 46:24:ac:0b:fa:c9:8e:c7:26:80:df:d1:e1:97:e3:f8:bb:68: 70 | c6:9c:bd:be:08:54:3b:10:32:7c:81:1f:2b:28:95:a8:41:0a: 71 | c6:d0:30:66:b4:e9:f2:a2:00:69:20:07:ca:82:4c:1e:cf:a7: 72 | 98:b8:0c:ee:cd:16:1c:be:1a:63:d4:c0:99:f6:67:b2:f0:8e: 73 | 17:2d:58:c2:80:aa:5d:96:c7:b3:28:ed:f0:da:8e:b6:47:1b: 74 | 8f:4e:15:f1:97:4c:0b:4b:af:81:d4:46:94:62:2c:43:a7:3c: 75 | 25:48:19:63:f2:5c:aa:15:89:76:84:85:73:91:7d:28:3c:09: 76 | 83:82:bc:f7 77 | -----BEGIN CERTIFICATE----- 78 | MIIG4zCCBcugAwIBAgIQCLuwJUcTS8mxENfBohJZxTANBgkqhkiG9w0BAQUFADBs 79 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 80 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 81 | ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTIxMTExMDAwMDAwMFowaTEL 82 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 83 | LmRpZ2ljZXJ0LmNvbTEoMCYGA1UEAxMfRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug 84 | RVYgQ0EtMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPOWYth1bhn/ 85 | PzR8SU8xfg0ETpmB4rOFVZEwscCvcLssqOcYqj9495BoUoYBiJfiOwZlkKq9ZXbC 86 | 7L4QWzd4g2B1Rca9dKq2n6Q6AVAXxDlpufFP74LByvNK28yeUE9NQKM6kOeGZrzw 87 | PnYoTNF1gJ5qNRQ1A57bDIzCKK1Qss72kaPDpQpYSfZ1RGy6+c7pqzoC4E3zrOJ6 88 | 4GAiBTyC01Li85xH+DvYskuTVkq/cKs+6WjIHY9YHSpNXic9rQpZL1oRIEDZaARo 89 | LfTAhAsKG3jf7RpY3PtBWm1r8u0c7lwytlzs16YDMqbo3rcoJ1mIgP97rYlY1R4U 90 | pPKwcNSgPqcCAwEAAaOCA4IwggN+MA4GA1UdDwEB/wQEAwIBhjA7BgNVHSUENDAy 91 | BggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUH 92 | AwgwggHEBgNVHSAEggG7MIIBtzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUH 93 | AgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5o 94 | dG0wggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0 95 | AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1 96 | AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABp 97 | AGcAaQBDAGUAcgB0ACAARQBWACAAQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBl 98 | AGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBo 99 | AGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAg 100 | AGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAg 101 | AGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wDwYDVR0TAQH/BAUwAwEB/zCBgwYI 102 | KwYBBQUHAQEEdzB1MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j 103 | b20wTQYIKwYBBQUHMAKGQWh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NBQ2VydHMv 104 | RGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3J0MIGPBgNVHR8EgYcwgYQw 105 | QKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hBc3N1 106 | cmFuY2VFVlJvb3RDQS5jcmwwQKA+oDyGOmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNv 107 | bS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwHQYDVR0OBBYEFExY 108 | yyXwQU9S9CjIgUObpqig5pLlMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoIAu9j 109 | ZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQBQHkOw900pllu7p9MKtbXV0Ceq+a/HJdGV 110 | 1S9aU71CB354ScoL60xV4uovf0mtx//RLT6coGQrUZ6RJii7h7t1fLyh/WZoLkxK 111 | Fsz+Bs8x6oBu5L3oA3L2JbVBg2HQlwonHbP3KzKEj1vnzD/iLGeGlPSyK2xSO2cq 112 | jViVABRGJKwL+smOxyaA39Hhl+P4u2jGnL2+CFQ7EDJ8gR8rKJWoQQrG0DBmtOny 113 | ogBpIAfKgkwez6eYuAzuzRYcvhpj1MCZ9mey8I4XLVjCgKpdlsezKO3w2o62RxuP 114 | ThXxl0wLS6+B1EaUYixDpzwlSBlj8lyqFYl2hIVzkX0oPAmDgrz3 115 | -----END CERTIFICATE----- 116 | -------------------------------------------------------------------------------- /spec/samples/certs/github.com.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 0e:77:76:8a:5d:07:f0:e5:79:59:ca:2a:9d:50:82:b5 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV CA-1 8 | Validity 9 | Not Before: May 27 00:00:00 2011 GMT 10 | Not After : Jul 29 12:00:00 2013 GMT 11 | 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 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (2048 bit) 15 | Modulus (2048 bit): 16 | 00:ed:d3:89:c3:5d:70:72:09:f3:33:4f:1a:72:74: 17 | d9:b6:5a:95:50:bb:68:61:9f:f7:fb:1f:19:e1:da: 18 | 04:31:af:15:7c:1a:7f:f9:73:af:1d:e5:43:2b:56: 19 | 09:00:45:69:4a:e8:c4:5b:df:c2:77:52:51:19:5b: 20 | d1:2b:d9:39:65:36:a0:32:19:1c:41:73:fb:32:b2: 21 | 3d:9f:98:ec:82:5b:0b:37:64:39:2c:b7:10:83:72: 22 | cd:f0:ea:24:4b:fa:d9:94:2e:c3:85:15:39:a9:3a: 23 | f6:88:da:f4:27:89:a6:95:4f:84:a2:37:4e:7c:25: 24 | 78:3a:c9:83:6d:02:17:95:78:7d:47:a8:55:83:ee: 25 | 13:c8:19:1a:b3:3c:f1:5f:fe:3b:02:e1:85:fb:11: 26 | 66:ab:09:5d:9f:4c:43:f0:c7:24:5e:29:72:28:ce: 27 | d4:75:68:4f:24:72:29:ae:39:28:fc:df:8d:4f:4d: 28 | 83:73:74:0c:6f:11:9b:a7:dd:62:de:ff:e2:eb:17: 29 | e6:ff:0c:bf:c0:2d:31:3b:d6:59:a2:f2:dd:87:4a: 30 | 48:7b:6d:33:11:14:4d:34:9f:32:38:f6:c8:19:9d: 31 | f1:b6:3d:c5:46:ef:51:0b:8a:c6:33:ed:48:61:c4: 32 | 1d:17:1b:bd:7c:b6:67:e9:39:cf:a5:52:80:0a:f4: 33 | ea:cd 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Authority Key Identifier: 37 | keyid:4C:58:CB:25:F0:41:4F:52:F4:28:C8:81:43:9B:A6:A8:A0:E6:92:E5 38 | 39 | X509v3 Subject Key Identifier: 40 | 87:D1:8F:19:6E:E4:87:6F:53:8C:77:91:07:50:DF:A3:BF:55:47:20 41 | X509v3 Subject Alternative Name: 42 | DNS:github.com, DNS:www.github.com 43 | Authority Information Access: 44 | OCSP - URI:http://ocsp.digicert.com 45 | CA Issuers - URI:http://www.digicert.com/CACerts/DigiCertHighAssuranceEVCA-1.crt 46 | 47 | X509v3 Basic Constraints: critical 48 | CA:FALSE 49 | X509v3 CRL Distribution Points: 50 | URI:http://crl3.digicert.com/ev2009a.crl 51 | URI:http://crl4.digicert.com/ev2009a.crl 52 | 53 | X509v3 Certificate Policies: 54 | Policy: 2.16.840.1.114412.2.1 55 | CPS: http://www.digicert.com/ssl-cps-repository.htm 56 | User Notice: 57 | Explicit Text: 58 | 59 | X509v3 Extended Key Usage: 60 | TLS Web Server Authentication, TLS Web Client Authentication 61 | Netscape Cert Type: 62 | SSL Client, SSL Server 63 | X509v3 Key Usage: critical 64 | Digital Signature, Key Encipherment 65 | Signature Algorithm: sha1WithRSAEncryption 66 | 14:52:71:1f:86:9d:6d:35:3e:86:bb:66:1a:8b:85:98:b9:00: 67 | 4c:cb:42:b5:46:fc:06:e7:44:39:c8:e8:52:d8:11:14:23:b3: 68 | 72:96:e9:14:94:9e:2f:00:28:f7:d5:04:45:40:00:c6:f4:57: 69 | 42:42:de:09:89:97:11:0d:14:5c:6b:bd:0b:f7:18:a3:5f:67: 70 | 02:f3:09:38:63:bf:c1:12:9d:30:ba:8e:a5:54:74:59:53:67: 71 | a1:1b:50:5b:26:da:fd:13:7e:59:17:bf:49:ef:94:7e:45:a4: 72 | fd:3a:49:32:f0:6a:ff:89:8d:a9:61:a9:aa:9b:96:46:c8:1c: 73 | e0:18:1c:e6:fb:82:f4:0a:ab:52:a6:ca:e8:54:22:d9:db:2a: 74 | 3d:5a:22:7b:80:ea:07:05:d4:f9:c7:f0:53:59:5f:bb:77:7e: 75 | de:93:70:41:4e:23:cb:78:79:79:c4:2e:ea:d7:66:2a:18:f7: 76 | d1:c5:7c:e2:12:78:82:8d:1d:ec:82:9e:01:a2:e5:02:be:78: 77 | a1:b9:59:58:c5:4c:6f:4f:a5:31:b4:49:5b:5e:98:1e:2e:38: 78 | f6:19:c4:39:a2:4a:fb:79:05:b8:f2:59:e5:26:12:70:ad:c0: 79 | e8:75:23:1f:18:d1:0b:e0:9f:65:e4:c3:d7:49:87:5b:72:6c: 80 | b1:2f:ac:6f 81 | -----BEGIN CERTIFICATE----- 82 | MIIHKjCCBhKgAwIBAgIQDnd2il0H8OV5WcoqnVCCtTANBgkqhkiG9w0BAQUFADBp 83 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 84 | d3cuZGlnaWNlcnQuY29tMSgwJgYDVQQDEx9EaWdpQ2VydCBIaWdoIEFzc3VyYW5j 85 | ZSBFViBDQS0xMB4XDTExMDUyNzAwMDAwMFoXDTEzMDcyOTEyMDAwMFowgcoxHTAb 86 | BgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGCNzwCAQMTAlVT 87 | MRswGQYLKwYBBAGCNzwCAQITCkNhbGlmb3JuaWExETAPBgNVBAUTCEMzMjY4MTAy 88 | MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2Fu 89 | IEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYDVQQDEwpnaXRo 90 | dWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7dOJw11wcgnz 91 | M08acnTZtlqVULtoYZ/3+x8Z4doEMa8VfBp/+XOvHeVDK1YJAEVpSujEW9/Cd1JR 92 | GVvRK9k5ZTagMhkcQXP7MrI9n5jsglsLN2Q5LLcQg3LN8OokS/rZlC7DhRU5qTr2 93 | iNr0J4mmlU+EojdOfCV4OsmDbQIXlXh9R6hVg+4TyBkaszzxX/47AuGF+xFmqwld 94 | n0xD8MckXilyKM7UdWhPJHIprjko/N+NT02Dc3QMbxGbp91i3v/i6xfm/wy/wC0x 95 | O9ZZovLdh0pIe20zERRNNJ8yOPbIGZ3xtj3FRu9RC4rGM+1IYcQdFxu9fLZn6TnP 96 | pVKACvTqzQIDAQABo4IDajCCA2YwHwYDVR0jBBgwFoAUTFjLJfBBT1L0KMiBQ5um 97 | qKDmkuUwHQYDVR0OBBYEFIfRjxlu5IdvU4x3kQdQ36O/VUcgMCUGA1UdEQQeMByC 98 | CmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMIGBBggrBgEFBQcBAQR1MHMwJAYI 99 | KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBLBggrBgEFBQcwAoY/ 100 | aHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ0FDZXJ0cy9EaWdpQ2VydEhpZ2hBc3N1 101 | cmFuY2VFVkNBLTEuY3J0MAwGA1UdEwEB/wQCMAAwYQYDVR0fBFowWDAqoCigJoYk 102 | aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL2V2MjAwOWEuY3JsMCqgKKAmhiRodHRw 103 | Oi8vY3JsNC5kaWdpY2VydC5jb20vZXYyMDA5YS5jcmwwggHEBgNVHSAEggG7MIIB 104 | tzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cuZGln 105 | aWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFkBggrBgEFBQcCAjCC 106 | AVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABp 107 | AGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBw 108 | AHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQ 109 | AC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQBy 110 | AHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0 111 | ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwBy 112 | AHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBl 113 | AG4AYwBlAC4wHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBEGCWCGSAGG 114 | +EIBAQQEAwIGwDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQEFBQADggEBABRS 115 | cR+GnW01Poa7ZhqLhZi5AEzLQrVG/AbnRDnI6FLYERQjs3KW6RSUni8AKPfVBEVA 116 | AMb0V0JC3gmJlxENFFxrvQv3GKNfZwLzCThjv8ESnTC6jqVUdFlTZ6EbUFsm2v0T 117 | flkXv0nvlH5FpP06STLwav+JjalhqaqblkbIHOAYHOb7gvQKq1KmyuhUItnbKj1a 118 | InuA6gcF1PnH8FNZX7t3ft6TcEFOI8t4eXnELurXZioY99HFfOISeIKNHeyCngGi 119 | 5QK+eKG5WVjFTG9PpTG0SVtemB4uOPYZxDmiSvt5BbjyWeUmEnCtwOh1Ix8Y0Qvg 120 | n2Xkw9dJh1tybLEvrG8= 121 | -----END CERTIFICATE----- 122 | 123 | -------------------------------------------------------------------------------- /spec/units/key_material_spec.rb: -------------------------------------------------------------------------------- 1 | describe CertificateAuthority::KeyMaterial do 2 | [CertificateAuthority::MemoryKeyMaterial, CertificateAuthority::SigningRequestKeyMaterial].each do |key_material_class| 3 | before do 4 | @key_material = key_material_class.new 5 | end 6 | 7 | it "#{key_material_class} should know if a key is in memory or hardware" do 8 | expect(@key_material.is_in_hardware?).not_to be_nil 9 | expect(@key_material.is_in_memory?).not_to be_nil 10 | end 11 | 12 | it "should use memory by default" do 13 | expect(@key_material.is_in_memory?).to be_truthy 14 | end 15 | end 16 | 17 | describe "reading keys from PEM" do 18 | before(:each) do 19 | @key_pair=<= '2.5.0' 124 | expected =< {"keyUsage" => {"usage" => ["critical", "keyCertSign"] }} }) 43 | 44 | @certificate = CertificateAuthority::Certificate.new 45 | @certificate.key_material.generate_key(768) 46 | @certificate.subject.common_name = "http://questionablesite.com" 47 | @certificate.parent = @root_certificate 48 | @certificate.serial_number.number = 2 49 | @certificate.sign! 50 | 51 | @ocsp_request = OpenSSL::OCSP::Request.new 52 | @ocsp_request.add_nonce 53 | openssl_cert_issuer = OpenSSL::X509::Certificate.new(@root_certificate.to_pem) 54 | openssl_cert_subject = OpenSSL::X509::Certificate.new(@certificate.to_pem) 55 | 56 | cert_id = OpenSSL::OCSP::CertificateId.new(openssl_cert_subject, openssl_cert_issuer) 57 | @ocsp_request.add_certid(cert_id) 58 | @ocsp_request_reader = CertificateAuthority::OCSPRequestReader.from_der(@ocsp_request.to_der) 59 | 60 | @response_builder = CertificateAuthority::OCSPResponseBuilder.from_request_reader(@ocsp_request_reader) 61 | @response_builder.parent = @root_certificate 62 | end 63 | 64 | it "should build from a OCSPRequestReader" do 65 | expect(@response_builder).not_to be_nil 66 | expect(@response_builder).to be_a(CertificateAuthority::OCSPResponseBuilder) 67 | end 68 | 69 | it "should build a response" do 70 | response = @response_builder.build_response 71 | expect(response).to be_a(OpenSSL::OCSP::Response) 72 | end 73 | 74 | it "should verify against the root" do 75 | response = @response_builder.build_response 76 | root_cert = OpenSSL::X509::Certificate.new(@root_certificate.to_pem) 77 | store = OpenSSL::X509::Store.new 78 | store.add_cert(root_cert) 79 | expect(response.basic.verify([root_cert],store)).to be_truthy 80 | end 81 | 82 | it "should have a configurable nextUpdate" do 83 | time = 30 * 60 # 30 minutes 84 | @response_builder.next_update=time 85 | response = @response_builder.build_response 86 | response.basic.status.each do |status| 87 | ## 3 seconds of wabble is OK 88 | expect(status[5]).to be_within(3).of(status[4] + time) 89 | end 90 | end 91 | 92 | describe "verification mechanisms" do 93 | it "should support an everything's OK default (though somewhat useless)" do 94 | response = @response_builder.build_response 95 | response.basic.status.each do |status| 96 | expect(status[1]).to eq(OpenSSL::OCSP::V_CERTSTATUS_GOOD) 97 | end 98 | end 99 | 100 | it "should support an overridable verification mechanism callback" do 101 | verification = lambda {|serial_number| 102 | [CertificateAuthority::OCSPResponseBuilder::REVOKED,CertificateAuthority::OCSPResponseBuilder::UNSPECIFIED] 103 | } 104 | @response_builder.verification_mechanism = verification 105 | response = @response_builder.build_response 106 | 107 | response.basic.status.each do |status| 108 | expect(status[1]).to eq(OpenSSL::OCSP::V_CERTSTATUS_REVOKED) 109 | end 110 | end 111 | end 112 | end 113 | 114 | 115 | ## DEPRECATED 116 | describe CertificateAuthority::OCSPHandler do 117 | before(:each) do 118 | @ocsp_handler = CertificateAuthority::OCSPHandler.new 119 | @root_certificate = CertificateAuthority::Certificate.new 120 | @root_certificate.signing_entity = true 121 | @root_certificate.subject.common_name = "OCSP Root" 122 | @root_certificate.key_material.generate_key(768) 123 | @root_certificate.serial_number.number = 1 124 | @root_certificate.sign! 125 | 126 | @certificate = CertificateAuthority::Certificate.new 127 | @certificate.key_material.generate_key(768) 128 | @certificate.subject.common_name = "http://questionablesite.com" 129 | @certificate.parent = @root_certificate 130 | @certificate.serial_number.number = 2 131 | @certificate.sign! 132 | 133 | @ocsp_request = OpenSSL::OCSP::Request.new 134 | openssl_cert_issuer = OpenSSL::X509::Certificate.new(@root_certificate.to_pem) 135 | openssl_cert_subject = OpenSSL::X509::Certificate.new(@certificate.to_pem) 136 | 137 | cert_id = OpenSSL::OCSP::CertificateId.new(openssl_cert_subject, openssl_cert_issuer) 138 | @ocsp_request.add_certid(cert_id) 139 | 140 | @ocsp_handler.ocsp_request = @ocsp_request.to_der 141 | end 142 | 143 | it "should be able to accept an OCSP Request" do 144 | @ocsp_handler.ocsp_request = @ocsp_request 145 | expect(@ocsp_handler.ocsp_request).not_to be_nil 146 | end 147 | 148 | it "should raise an error if you try and extract certificates without a raw request" do 149 | @ocsp_handler.extract_certificate_serials 150 | @ocsp_handler.ocsp_request = nil 151 | expect {@ocsp_handler.extract_certificate_serials}.to raise_error(RuntimeError) 152 | end 153 | 154 | it "should return a hash of extracted certificates from OCSP requests" do 155 | result = @ocsp_handler.extract_certificate_serials 156 | expect(result.size).to eq(1) 157 | end 158 | 159 | it "should be able to generate an OCSP response" do 160 | @ocsp_handler.extract_certificate_serials 161 | @ocsp_handler << @certificate 162 | @ocsp_handler.parent = @root_certificate 163 | @ocsp_handler.response 164 | end 165 | 166 | it "should require a 'parent' entity for signing" do 167 | @ocsp_handler.parent = @root_certificate 168 | expect(@ocsp_handler.parent).not_to be_nil 169 | end 170 | 171 | it "should raise an error if you ask for the signed OCSP response without generating it" do 172 | @ocsp_handler.extract_certificate_serials 173 | @ocsp_handler << @certificate 174 | @ocsp_handler.parent = @root_certificate 175 | expect { @ocsp_handler.to_der }.to raise_error(RuntimeError) 176 | @ocsp_handler.response 177 | expect(@ocsp_handler.to_der).not_to be_nil 178 | end 179 | 180 | it "should raise an error if you generate a response without adding all certificates in request" do 181 | @ocsp_handler.extract_certificate_serials 182 | @ocsp_handler.parent = @root_certificate 183 | expect { @ocsp_handler.response }.to raise_error(RuntimeError) 184 | end 185 | 186 | it "should raise an error if you generate a response without adding a parent signing entity" do 187 | @ocsp_handler.extract_certificate_serials 188 | @ocsp_handler << @certificate 189 | expect { @ocsp_handler.response }.to raise_error(RuntimeError) 190 | end 191 | 192 | describe "Response" do 193 | before(:each) do 194 | @ocsp_handler.extract_certificate_serials 195 | @ocsp_handler << @certificate 196 | @ocsp_handler.parent = @root_certificate 197 | @ocsp_handler.response 198 | 199 | @openssl_ocsp_response = OpenSSL::OCSP::Response.new(@ocsp_handler.to_der) 200 | end 201 | 202 | it "should have a correct status/status string" do 203 | expect(@openssl_ocsp_response.status_string).to eq("successful") 204 | expect(@openssl_ocsp_response.status).to eq(0) 205 | end 206 | 207 | it "should have an embedded BasicResponse with certificate statuses" do 208 | # [#, 0, 1, nil, 2011-04-15 23:29:47 UTC, 2011-04-15 23:30:17 UTC, []] 209 | expect(@openssl_ocsp_response.basic.status.first[1]).to eq(0) # Everything is OK 210 | end 211 | 212 | it "should have a next_update time" do 213 | expect(@openssl_ocsp_response.basic.status.first[5]).not_to be_nil 214 | expect(@openssl_ocsp_response.basic.status.first[5].class).to eq(Time) 215 | end 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /spec/units/extensions_spec.rb: -------------------------------------------------------------------------------- 1 | describe CertificateAuthority::Extensions do 2 | describe CertificateAuthority::Extensions::BasicConstraints do 3 | it "should only allow true/false" do 4 | basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new 5 | expect(basic_constraints.valid?).to be_truthy 6 | basic_constraints.ca = "moo" 7 | expect(basic_constraints.valid?).to be_falsey 8 | end 9 | 10 | it "should respond to :path_len" do 11 | basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new 12 | expect(basic_constraints.respond_to?(:path_len)).to be_truthy 13 | end 14 | 15 | it "should raise an error if :path_len isn't a non-negative integer" do 16 | basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new 17 | expect {basic_constraints.path_len = "moo"}.to raise_error(ArgumentError) 18 | expect {basic_constraints.path_len = -1}.to raise_error(ArgumentError) 19 | expect {basic_constraints.path_len = 1.5}.to raise_error(ArgumentError) 20 | end 21 | 22 | it "should generate a proper OpenSSL extension string" do 23 | basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new 24 | basic_constraints.ca = true 25 | basic_constraints.path_len = 2 26 | expect(basic_constraints.to_s).to eq("CA:true,pathlen:2") 27 | end 28 | 29 | it "should parse values from a proper OpenSSL extension string" do 30 | basic_constraints = CertificateAuthority::Extensions::BasicConstraints.parse("CA:true,pathlen:2", true) 31 | expect(basic_constraints.critical).to be_truthy 32 | expect(basic_constraints.ca).to be_truthy 33 | expect(basic_constraints.path_len).to eq(2) 34 | end 35 | end 36 | 37 | describe CertificateAuthority::Extensions::SubjectAlternativeName do 38 | it "should respond to :uris" do 39 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 40 | expect(subjectAltName.respond_to?(:uris)).to be_truthy 41 | end 42 | 43 | it "should require 'uris' to be an Array" do 44 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 45 | expect {subjectAltName.uris = "not an array"}.to raise_error(RuntimeError) 46 | end 47 | 48 | it "should generate a proper OpenSSL extension string for URIs" do 49 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 50 | subjectAltName.uris = ["http://localhost.altname.example.com"] 51 | expect(subjectAltName.to_s).to eq("URI:http://localhost.altname.example.com") 52 | 53 | subjectAltName.uris = ["http://localhost.altname.example.com", "http://other.example.com"] 54 | expect(subjectAltName.to_s).to eq("URI:http://localhost.altname.example.com,URI:http://other.example.com") 55 | end 56 | 57 | it "should parse URIs from a proper OpenSSL extension string" do 58 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("URI:http://localhost.altname.example.com", false) 59 | expect(subjectAltName.uris).to eq(["http://localhost.altname.example.com"]) 60 | 61 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("URI:http://localhost.altname.example.com,URI:http://other.example.com", false) 62 | expect(subjectAltName.uris).to eq(["http://localhost.altname.example.com", "http://other.example.com"]) 63 | end 64 | 65 | it "should respond to :dns_names" do 66 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 67 | expect(subjectAltName.respond_to?(:dns_names)).to be_truthy 68 | end 69 | 70 | it "should require 'dns_names' to be an Array" do 71 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 72 | expect {subjectAltName.dns_names = "not an array"}.to raise_error(RuntimeError) 73 | end 74 | 75 | it "should generate a proper OpenSSL extension string for DNS names" do 76 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 77 | subjectAltName.dns_names = ["localhost.altname.example.com"] 78 | expect(subjectAltName.to_s).to eq("DNS:localhost.altname.example.com") 79 | 80 | subjectAltName.dns_names = ["localhost.altname.example.com", "other.example.com"] 81 | expect(subjectAltName.to_s).to eq("DNS:localhost.altname.example.com,DNS:other.example.com") 82 | end 83 | 84 | it "should parse DNS names from a proper OpenSSL extension string" do 85 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com", false) 86 | expect(subjectAltName.dns_names).to eq(["localhost.altname.example.com"]) 87 | 88 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com,DNS:other.example.com", false) 89 | expect(subjectAltName.dns_names).to eq(["localhost.altname.example.com", "other.example.com"]) 90 | end 91 | 92 | it "should respond to :ips" do 93 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 94 | expect(subjectAltName.respond_to?(:ips)).to be_truthy 95 | end 96 | 97 | it "should require 'ips' to be an Array" do 98 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 99 | expect {subjectAltName.ips = "not an array"}.to raise_error(RuntimeError) 100 | end 101 | 102 | it "should generate a proper OpenSSL extension string for IPs" do 103 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 104 | subjectAltName.ips = ["1.2.3.4"] 105 | expect(subjectAltName.to_s).to eq("IP:1.2.3.4") 106 | 107 | subjectAltName.ips = ["1.2.3.4", "5.6.7.8"] 108 | expect(subjectAltName.to_s).to eq("IP:1.2.3.4,IP:5.6.7.8") 109 | end 110 | 111 | it "should parse IPs from a proper OpenSSL extension string" do 112 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("IP:1.2.3.4", false) 113 | expect(subjectAltName.ips).to eq(["1.2.3.4"]) 114 | 115 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("IP:1.2.3.4,IP:5.6.7.8", false) 116 | expect(subjectAltName.ips).to eq(["1.2.3.4", "5.6.7.8"]) 117 | end 118 | 119 | describe 'emails' do 120 | let(:subject) { CertificateAuthority::Extensions::SubjectAlternativeName.new } 121 | 122 | it "should require 'emails' to be an Array" do 123 | expect { 124 | subject.emails = "not an array" 125 | }.to raise_error "Emails must be an array" 126 | end 127 | 128 | it "should generate a proper OpenSSL extension string for emails" do 129 | subject.emails = ["copy"] 130 | expect(subject.to_s).to eq("email:copy") 131 | 132 | subject.emails = ["copy", "foo@bar.com"] 133 | expect(subject.to_s).to eq("email:copy,email:foo@bar.com") 134 | end 135 | end 136 | 137 | it "should generate a proper OpenSSL extension string for URIs IPs and DNS names together" do 138 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.new 139 | subjectAltName.ips = ["1.2.3.4"] 140 | expect(subjectAltName.to_s).to eq("IP:1.2.3.4") 141 | 142 | subjectAltName.dns_names = ["localhost.altname.example.com"] 143 | expect(subjectAltName.to_s).to eq("DNS:localhost.altname.example.com,IP:1.2.3.4") 144 | 145 | subjectAltName.dns_names = ["localhost.altname.example.com", "other.example.com"] 146 | expect(subjectAltName.to_s).to eq("DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4") 147 | 148 | subjectAltName.ips = ["1.2.3.4", "5.6.7.8"] 149 | expect(subjectAltName.to_s).to eq("DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4,IP:5.6.7.8") 150 | 151 | subjectAltName.uris = ["http://localhost.altname.example.com"] 152 | expect(subjectAltName.to_s).to eq("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") 153 | 154 | subjectAltName.uris = ["http://localhost.altname.example.com", "http://other.altname.example.com"] 155 | expect(subjectAltName.to_s).to eq("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") 156 | end 157 | 158 | it "should parse URIs IPs and DNS names together from a proper OpenSSL extension string" do 159 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("IP:1.2.3.4", false) 160 | expect(subjectAltName.ips).to eq(["1.2.3.4"]) 161 | 162 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com,IP:1.2.3.4", false) 163 | expect(subjectAltName.dns_names).to eq(["localhost.altname.example.com"]) 164 | 165 | subjectAltName = CertificateAuthority::Extensions::SubjectAlternativeName.parse("DNS:localhost.altname.example.com,DNS:other.example.com,IP:1.2.3.4", false) 166 | expect(subjectAltName.dns_names).to eq(["localhost.altname.example.com", "other.example.com"]) 167 | 168 | 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) 169 | expect(subjectAltName.ips).to eq(["1.2.3.4", "5.6.7.8"]) 170 | 171 | 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) 172 | expect(subjectAltName.uris).to eq(["http://localhost.altname.example.com"]) 173 | 174 | 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) 175 | expect(subjectAltName.uris).to eq(["http://localhost.altname.example.com", "http://other.altname.example.com"]) 176 | 177 | subjectAltName.emails= ["copy", "foo@bar.com"] 178 | expect(subjectAltName.to_s).to eq("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") 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /lib/certificate_authority/certificate.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | class Certificate 3 | include Validations 4 | include Revocable 5 | 6 | attr_accessor :distinguished_name 7 | attr_accessor :serial_number 8 | attr_accessor :key_material 9 | attr_accessor :not_before 10 | attr_accessor :not_after 11 | attr_accessor :extensions 12 | attr_accessor :openssl_body 13 | 14 | alias :subject :distinguished_name #Same thing as the DN 15 | 16 | attr_accessor :parent 17 | 18 | def validate 19 | errors.add :base, "Distinguished name must be valid" unless distinguished_name.valid? 20 | errors.add :base, "Key material must be valid" unless key_material.valid? 21 | errors.add :base, "Serial number must be valid" unless serial_number.valid? 22 | errors.add :base, "Extensions must be valid" unless extensions.each do |item| 23 | unless item.respond_to?(:valid?) 24 | true 25 | else 26 | item.valid? 27 | end 28 | end 29 | end 30 | 31 | def initialize 32 | self.distinguished_name = DistinguishedName.new 33 | self.serial_number = SerialNumber.new 34 | self.key_material = MemoryKeyMaterial.new 35 | self.not_before = Date.today.utc 36 | self.not_after = Date.today.advance(:years => 1).utc 37 | self.parent = self 38 | self.extensions = load_extensions() 39 | 40 | self.signing_entity = false 41 | 42 | end 43 | 44 | =begin 45 | def self.from_openssl openssl_cert 46 | unless openssl_cert.is_a? OpenSSL::X509::Certificate 47 | raise "Can only construct from an OpenSSL::X509::Certificate" 48 | end 49 | 50 | certificate = Certificate.new 51 | # Only subject, key_material, and body are used for signing 52 | certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject 53 | certificate.key_material.public_key = openssl_cert.public_key 54 | certificate.openssl_body = openssl_cert 55 | certificate.serial_number.number = openssl_cert.serial.to_i 56 | certificate.not_before = openssl_cert.not_before 57 | certificate.not_after = openssl_cert.not_after 58 | # TODO extensions 59 | certificate 60 | end 61 | =end 62 | 63 | def sign!(signing_profile={}) 64 | raise "Invalid certificate #{self.errors.full_messages}" unless valid? 65 | merge_profile_with_extensions(signing_profile) 66 | 67 | openssl_cert = OpenSSL::X509::Certificate.new 68 | openssl_cert.version = 2 69 | openssl_cert.not_before = self.not_before 70 | openssl_cert.not_after = self.not_after 71 | openssl_cert.public_key = self.key_material.public_key 72 | 73 | openssl_cert.serial = self.serial_number.number 74 | 75 | openssl_cert.subject = self.distinguished_name.to_x509_name 76 | openssl_cert.issuer = parent.distinguished_name.to_x509_name 77 | 78 | factory = OpenSSL::X509::ExtensionFactory.new 79 | factory.subject_certificate = openssl_cert 80 | 81 | #NB: If the parent doesn't have an SSL body we're making this a self-signed cert 82 | if parent.openssl_body.nil? 83 | factory.issuer_certificate = openssl_cert 84 | else 85 | factory.issuer_certificate = parent.openssl_body 86 | end 87 | 88 | factory.config = build_openssl_config 89 | 90 | # Order matters: e.g. for self-signed, subjectKeyIdentifier must come before authorityKeyIdentifier 91 | self.extensions.keys.sort{|a,b| b<=>a}.each do |k| 92 | e = extensions[k] 93 | next if e.to_s.nil? or e.to_s == "" ## If the extension returns an empty string we won't include it 94 | ext = factory.create_ext(e.openssl_identifier, e.to_s, e.critical) 95 | openssl_cert.add_extension(ext) 96 | end 97 | 98 | if signing_profile["digest"].nil? 99 | digest = OpenSSL::Digest.new("SHA512") 100 | else 101 | digest = OpenSSL::Digest.new(signing_profile["digest"]) 102 | end 103 | 104 | self.openssl_body = openssl_cert.sign(parent.key_material.private_key, digest) 105 | end 106 | 107 | def is_signing_entity? 108 | self.extensions["basicConstraints"].ca 109 | end 110 | 111 | def signing_entity=(signing) 112 | self.extensions["basicConstraints"].ca = signing 113 | end 114 | 115 | def revoked? 116 | !self.revoked_at.nil? 117 | end 118 | 119 | def to_pem 120 | raise "Certificate has no signed body" if self.openssl_body.nil? 121 | self.openssl_body.to_pem 122 | end 123 | 124 | def to_csr 125 | csr = SigningRequest.new 126 | csr.distinguished_name = self.distinguished_name 127 | csr.key_material = self.key_material 128 | factory = OpenSSL::X509::ExtensionFactory.new 129 | exts = [] 130 | self.extensions.keys.each do |k| 131 | ## Don't copy over key identifiers for CSRs 132 | next if k == "subjectKeyIdentifier" || k == "authorityKeyIdentifier" 133 | e = extensions[k] 134 | ## If the extension returns an empty string we won't include it 135 | next if e.to_s.nil? or e.to_s == "" 136 | exts << factory.create_ext(e.openssl_identifier, e.to_s, e.critical) 137 | end 138 | attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)]) 139 | attrs = [ 140 | OpenSSL::X509::Attribute.new("extReq", attrval), 141 | OpenSSL::X509::Attribute.new("msExtReq", attrval) 142 | ] 143 | csr.attributes = attrs 144 | csr 145 | end 146 | 147 | def self.from_x509_cert(raw_cert) 148 | openssl_cert = OpenSSL::X509::Certificate.new(raw_cert) 149 | Certificate.from_openssl(openssl_cert) 150 | end 151 | 152 | def is_root_entity? 153 | self.parent == self && is_signing_entity? 154 | end 155 | 156 | def is_intermediate_entity? 157 | (self.parent != self) && is_signing_entity? 158 | end 159 | 160 | private 161 | 162 | def merge_profile_with_extensions(signing_profile={}) 163 | return self.extensions if signing_profile["extensions"].nil? 164 | signing_config = signing_profile["extensions"] 165 | signing_config.keys.each do |k| 166 | extension = self.extensions[k] 167 | items = signing_config[k] 168 | items.keys.each do |profile_item_key| 169 | if extension.respond_to?("#{profile_item_key}=".to_sym) 170 | if k == 'subjectAltName' && profile_item_key == 'emails' 171 | items[profile_item_key].map do |email| 172 | if email == 'email:copy' 173 | fail "no email address provided for subject: #{subject.to_x509_name}" unless subject.email_address 174 | "email:#{subject.email_address}" 175 | else 176 | email 177 | end 178 | end 179 | end 180 | extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] ) 181 | else 182 | p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!" 183 | end 184 | end 185 | end 186 | end 187 | 188 | # Enumeration of the extensions. Not the worst option since 189 | # the likelihood of these needing to be updated is low at best. 190 | EXTENSIONS = [ 191 | CertificateAuthority::Extensions::BasicConstraints, 192 | CertificateAuthority::Extensions::CrlDistributionPoints, 193 | CertificateAuthority::Extensions::SubjectKeyIdentifier, 194 | CertificateAuthority::Extensions::AuthorityKeyIdentifier, 195 | CertificateAuthority::Extensions::AuthorityInfoAccess, 196 | CertificateAuthority::Extensions::KeyUsage, 197 | CertificateAuthority::Extensions::ExtendedKeyUsage, 198 | CertificateAuthority::Extensions::SubjectAlternativeName, 199 | CertificateAuthority::Extensions::CertificatePolicies 200 | ] 201 | 202 | def load_extensions 203 | extension_hash = {} 204 | 205 | EXTENSIONS.each do |klass| 206 | extension = klass.new 207 | extension_hash[extension.openssl_identifier] = extension 208 | end 209 | 210 | extension_hash 211 | end 212 | 213 | def build_openssl_config 214 | OpenSSL::Config.parse(openssl_config_string) 215 | end 216 | 217 | def openssl_config_string 218 | lines = openssl_config_without_multi_value + openssl_config_with_multi_value 219 | return '' if lines.empty? 220 | (["[extensions]" ]+ lines).join("\n") 221 | end 222 | 223 | def openssl_config_without_multi_value 224 | no_multi_value_keys = self.extensions.keys.select { |k| extensions[k].config_extensions.empty? } 225 | 226 | lines = no_multi_value_keys.map do |k| 227 | value = extensions[k].to_s 228 | value.empty? ? '' : "#{k} = #{value}" 229 | end.reject(&:empty?) 230 | lines 231 | end 232 | 233 | def openssl_config_with_multi_value 234 | multi_value_keys = self.extensions.keys.reject { |k| extensions[k].config_extensions.empty? } 235 | sections = {} 236 | 237 | entries = multi_value_keys.map do |k| 238 | sections.merge!(extensions[k].config_extensions) 239 | value = comma_terminate(extensions[k]) + section_ref_str(extensions[k].config_extensions.keys) 240 | "#{k} = #{value}" 241 | end.reject(&:empty?) 242 | 243 | section_lines = sections.keys.flat_map do |k| 244 | section_lines(k, sections[k]) 245 | end 246 | entries + [''] + section_lines 247 | end 248 | 249 | def comma_terminate(val) 250 | s = val.to_s 251 | s.empty? ? s : "#{s}," 252 | end 253 | 254 | def section_ref_str(section_names) 255 | section_names.map { |n| "@#{n}"}.join(',') 256 | end 257 | 258 | def section_lines(section_name, value_hash) 259 | ["[#{section_name}]"] + value_hash.keys.map { |k| "#{k} = #{value_hash[k]}"} + [''] 260 | end 261 | 262 | def merge_options(config,hash) 263 | hash.keys.each do |k| 264 | config[k] = hash[k] 265 | end 266 | config 267 | end 268 | 269 | def self.from_openssl openssl_cert 270 | unless openssl_cert.is_a? OpenSSL::X509::Certificate 271 | raise "Can only construct from an OpenSSL::X509::Certificate" 272 | end 273 | 274 | certificate = Certificate.new 275 | # Only subject, key_material, and body are used for signing 276 | certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject 277 | certificate.key_material.public_key = openssl_cert.public_key 278 | certificate.openssl_body = openssl_cert 279 | certificate.serial_number.number = openssl_cert.serial.to_i 280 | certificate.not_before = openssl_cert.not_before 281 | certificate.not_after = openssl_cert.not_after 282 | EXTENSIONS.each do |klass| 283 | _,v,c = (openssl_cert.extensions.detect { |e| e.to_a.first == klass::OPENSSL_IDENTIFIER } || []).to_a 284 | certificate.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v 285 | end 286 | 287 | certificate 288 | end 289 | 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /lib/certificate_authority/extensions.rb: -------------------------------------------------------------------------------- 1 | module CertificateAuthority 2 | module Extensions 3 | module ExtensionAPI 4 | def to_s 5 | raise "Implementation required" 6 | end 7 | 8 | def self.parse(value, critical) 9 | raise "Implementation required" 10 | end 11 | 12 | def config_extensions 13 | {} 14 | end 15 | 16 | def openssl_identifier 17 | raise "Implementation required" 18 | end 19 | 20 | def ==(value) 21 | raise "Implementation required" 22 | end 23 | end 24 | 25 | # Specifies whether an X.509v3 certificate can act as a CA, signing other 26 | # certificates to be verified. If set, a path length constraint can also be 27 | # specified. 28 | # Reference: Section 4.2.1.10 of RFC3280 29 | # http://tools.ietf.org/html/rfc3280#section-4.2.1.10 30 | class BasicConstraints 31 | OPENSSL_IDENTIFIER = "basicConstraints" 32 | 33 | include ExtensionAPI 34 | include Validations 35 | 36 | attr_accessor :critical 37 | attr_accessor :ca 38 | attr_accessor :path_len 39 | 40 | def validate 41 | unless [true, false].include? self.critical 42 | errors.add :critical, 'must be true or false' 43 | end 44 | unless [true, false].include? self.ca 45 | errors.add :ca, 'must be true or false' 46 | end 47 | end 48 | 49 | def initialize 50 | @critical = false 51 | @ca = false 52 | end 53 | 54 | def openssl_identifier 55 | OPENSSL_IDENTIFIER 56 | end 57 | 58 | def is_ca? 59 | @ca 60 | end 61 | 62 | def path_len=(value) 63 | fail(ArgumentError, "path_len must be a non-negative integer") if !value.is_a?(Integer) || value < 0 64 | @path_len = value 65 | end 66 | 67 | def to_s 68 | res = [] 69 | res << "CA:#{@ca}" 70 | res << "pathlen:#{@path_len}" unless @path_len.nil? 71 | res.join(',') 72 | end 73 | 74 | def ==(o) 75 | o.class == self.class && o.state == state 76 | end 77 | 78 | def self.parse(value, critical) 79 | obj = self.new 80 | return obj if value.nil? 81 | obj.critical = critical 82 | value.split(/,\s*/).each do |v| 83 | c = v.split(':', 2) 84 | obj.ca = (c.last.upcase == "TRUE") if c.first == "CA" 85 | obj.path_len = c.last.to_i if c.first == "pathlen" 86 | end 87 | obj 88 | end 89 | 90 | protected 91 | def state 92 | [@critical,@ca,@path_len] 93 | end 94 | end 95 | 96 | # Specifies where CRL information be be retrieved. This extension isn't 97 | # critical, but is recommended for proper CAs. 98 | # Reference: Section 4.2.1.14 of RFC3280 99 | # http://tools.ietf.org/html/rfc3280#section-4.2.1.14 100 | class CrlDistributionPoints 101 | OPENSSL_IDENTIFIER = "crlDistributionPoints" 102 | 103 | include ExtensionAPI 104 | 105 | attr_accessor :critical 106 | attr_accessor :uris 107 | 108 | def initialize 109 | @critical = false 110 | @uris = [] 111 | end 112 | 113 | def openssl_identifier 114 | OPENSSL_IDENTIFIER 115 | end 116 | 117 | ## NB: At this time it seems OpenSSL's extension handlers don't support 118 | ## any of the config options the docs claim to support... everything comes back 119 | ## "missing value" on GENERAL NAME. Even if copied verbatim 120 | def config_extensions 121 | { 122 | # "custom_crl_fields" => {"fullname" => "URI:#{fullname}"}, 123 | # "issuer_sect" => {"CN" => "crlissuer.com", "C" => "US", "O" => "shudder"} 124 | } 125 | end 126 | 127 | # This is for legacy support. Technically it can (and probably should) 128 | # be an array. But if someone is calling the old accessor we shouldn't 129 | # necessarily break it. 130 | def uri=(value) 131 | @uris << value 132 | end 133 | 134 | def to_s 135 | res = [] 136 | @uris.each do |uri| 137 | res << "URI:#{uri}" 138 | end 139 | res.join(',') 140 | end 141 | 142 | def ==(o) 143 | o.class == self.class && o.state == state 144 | end 145 | 146 | def self.parse(value, critical) 147 | obj = self.new 148 | return obj if value.nil? 149 | obj.critical = critical 150 | value.split(/,\s*/).each do |v| 151 | c = v.split(':', 2) 152 | obj.uris << c.last if c.first == "URI" 153 | end 154 | obj 155 | end 156 | 157 | protected 158 | def state 159 | [@critical,@uri] 160 | end 161 | end 162 | 163 | # Identifies the public key associated with a given certificate. 164 | # Should be required for "CA" certificates. 165 | # Reference: Section 4.2.1.2 of RFC3280 166 | # http://tools.ietf.org/html/rfc3280#section-4.2.1.2 167 | class SubjectKeyIdentifier 168 | OPENSSL_IDENTIFIER = "subjectKeyIdentifier" 169 | 170 | include ExtensionAPI 171 | 172 | attr_accessor :critical 173 | attr_accessor :identifier 174 | 175 | def initialize 176 | @critical = false 177 | @identifier = "hash" 178 | end 179 | 180 | def openssl_identifier 181 | OPENSSL_IDENTIFIER 182 | end 183 | 184 | def to_s 185 | res = [] 186 | res << @identifier 187 | res.join(',') 188 | end 189 | 190 | def ==(o) 191 | o.class == self.class && o.state == state 192 | end 193 | 194 | def self.parse(value, critical) 195 | obj = self.new 196 | return obj if value.nil? 197 | obj.critical = critical 198 | obj.identifier = value 199 | obj 200 | end 201 | 202 | protected 203 | def state 204 | [@critical,@identifier] 205 | end 206 | end 207 | 208 | # Identifies the public key associated with a given private key. 209 | # Reference: Section 4.2.1.1 of RFC3280 210 | # http://tools.ietf.org/html/rfc3280#section-4.2.1.1 211 | class AuthorityKeyIdentifier 212 | OPENSSL_IDENTIFIER = "authorityKeyIdentifier" 213 | 214 | include ExtensionAPI 215 | 216 | attr_accessor :critical 217 | attr_accessor :identifier 218 | 219 | def initialize 220 | @critical = false 221 | @identifier = ["keyid", "issuer"] 222 | end 223 | 224 | def openssl_identifier 225 | OPENSSL_IDENTIFIER 226 | end 227 | 228 | def to_s 229 | res = [] 230 | res += @identifier 231 | res.join(',') 232 | end 233 | 234 | def ==(o) 235 | o.class == self.class && o.state == state 236 | end 237 | 238 | def self.parse(value, critical) 239 | obj = self.new 240 | return obj if value.nil? 241 | obj.critical = critical 242 | obj.identifier = value.split(/,\s*/).last.chomp 243 | obj 244 | end 245 | 246 | protected 247 | def state 248 | [@critical,@identifier] 249 | end 250 | end 251 | 252 | # Specifies how to access CA information and services for the CA that 253 | # issued this certificate. 254 | # Generally used to specify OCSP servers. 255 | # Reference: Section 4.2.2.1 of RFC3280 256 | # http://tools.ietf.org/html/rfc3280#section-4.2.2.1 257 | class AuthorityInfoAccess 258 | OPENSSL_IDENTIFIER = "authorityInfoAccess" 259 | 260 | include ExtensionAPI 261 | 262 | attr_accessor :critical 263 | attr_accessor :ocsp 264 | attr_accessor :ca_issuers 265 | 266 | def initialize 267 | @critical = false 268 | @ocsp = [] 269 | @ca_issuers = [] 270 | end 271 | 272 | def openssl_identifier 273 | OPENSSL_IDENTIFIER 274 | end 275 | 276 | def to_s 277 | res = [] 278 | res += @ocsp.map {|o| "OCSP;URI:#{o}" } 279 | res += @ca_issuers.map {|c| "caIssuers;URI:#{c}" } 280 | res.join(',') 281 | end 282 | 283 | def ==(o) 284 | o.class == self.class && o.state == state 285 | end 286 | 287 | def self.parse(value, critical) 288 | obj = self.new 289 | return obj if value.nil? 290 | obj.critical = critical 291 | value.split("\n").each do |v| 292 | if v =~ /^OCSP/ 293 | obj.ocsp << v.split.last 294 | end 295 | 296 | if v =~ /^CA Issuers/ 297 | obj.ca_issuers << v.split.last 298 | end 299 | end 300 | obj 301 | end 302 | 303 | protected 304 | def state 305 | [@critical,@ocsp,@ca_issuers] 306 | end 307 | end 308 | 309 | # Specifies the allowed usage purposes of the keypair specified in this certificate. 310 | # Reference: Section 4.2.1.3 of RFC3280 311 | # http://tools.ietf.org/html/rfc3280#section-4.2.1.3 312 | # 313 | # Note: OpenSSL when parsing an extension will return results in the form 314 | # 'Digital Signature', but on signing you have to set it to 'digitalSignature'. 315 | # So copying an extension from an imported cert isn't going to work yet. 316 | class KeyUsage 317 | OPENSSL_IDENTIFIER = "keyUsage" 318 | 319 | include ExtensionAPI 320 | 321 | attr_accessor :critical 322 | attr_accessor :usage 323 | 324 | def initialize 325 | @critical = false 326 | @usage = ["digitalSignature", "nonRepudiation"] 327 | end 328 | 329 | def openssl_identifier 330 | OPENSSL_IDENTIFIER 331 | end 332 | 333 | def to_s 334 | res = [] 335 | res += @usage 336 | res.join(',') 337 | end 338 | 339 | def ==(o) 340 | o.class == self.class && o.state == state 341 | end 342 | 343 | def self.parse(value, critical) 344 | obj = self.new 345 | return obj if value.nil? 346 | obj.critical = critical 347 | obj.usage = value.split(/,\s*/) 348 | obj 349 | end 350 | 351 | protected 352 | def state 353 | [@critical,@usage] 354 | end 355 | end 356 | 357 | # Specifies even more allowed usages in addition to what is specified in 358 | # the Key Usage extension. 359 | # Reference: Section 4.2.1.13 of RFC3280 360 | # http://tools.ietf.org/html/rfc3280#section-4.2.1.13 361 | class ExtendedKeyUsage 362 | OPENSSL_IDENTIFIER = "extendedKeyUsage" 363 | 364 | include ExtensionAPI 365 | 366 | attr_accessor :critical 367 | attr_accessor :usage 368 | 369 | def initialize 370 | @critical = false 371 | @usage = ["serverAuth"] 372 | end 373 | 374 | def openssl_identifier 375 | OPENSSL_IDENTIFIER 376 | end 377 | 378 | def to_s 379 | res = [] 380 | res += @usage 381 | res.join(',') 382 | end 383 | 384 | def ==(o) 385 | o.class == self.class && o.state == state 386 | end 387 | 388 | def self.parse(value, critical) 389 | obj = self.new 390 | return obj if value.nil? 391 | obj.critical = critical 392 | obj.usage = value.split(/,\s*/) 393 | obj 394 | end 395 | 396 | protected 397 | def state 398 | [@critical,@usage] 399 | end 400 | end 401 | 402 | # Specifies additional "names" for which this certificate is valid. 403 | # Reference: Section 4.2.1.7 of RFC3280 404 | # http://tools.ietf.org/html/rfc3280#section-4.2.1.7 405 | class SubjectAlternativeName 406 | OPENSSL_IDENTIFIER = "subjectAltName" 407 | 408 | include ExtensionAPI 409 | 410 | attr_accessor :critical 411 | attr_accessor :uris, :dns_names, :ips, :emails 412 | 413 | def initialize 414 | @critical = false 415 | @uris = [] 416 | @dns_names = [] 417 | @ips = [] 418 | @emails = [] 419 | end 420 | 421 | def openssl_identifier 422 | OPENSSL_IDENTIFIER 423 | end 424 | 425 | def uris=(value) 426 | raise "URIs must be an array" unless value.is_a?(Array) 427 | @uris = value 428 | end 429 | 430 | def dns_names=(value) 431 | raise "DNS names must be an array" unless value.is_a?(Array) 432 | @dns_names = value 433 | end 434 | 435 | def ips=(value) 436 | raise "IPs must be an array" unless value.is_a?(Array) 437 | @ips = value 438 | end 439 | 440 | def emails=(value) 441 | raise "Emails must be an array" unless value.is_a?(Array) 442 | @emails = value 443 | end 444 | 445 | def to_s 446 | res = [] 447 | res += @uris.map {|u| "URI:#{u}" } 448 | res += @dns_names.map {|d| "DNS:#{d}" } 449 | res += @ips.map {|i| "IP:#{i}" } 450 | res += @emails.map {|i| "email:#{i}" } 451 | res.join(',') 452 | end 453 | 454 | def ==(o) 455 | o.class == self.class && o.state == state 456 | end 457 | 458 | def self.parse(value, critical) 459 | obj = self.new 460 | return obj if value.nil? 461 | obj.critical = critical 462 | value.split(/,\s*/).each do |v| 463 | c = v.split(':', 2) 464 | obj.uris << c.last if c.first == "URI" 465 | obj.dns_names << c.last if c.first == "DNS" 466 | obj.ips << c.last if c.first == "IP" 467 | obj.emails << c.last if c.first == "EMAIL" 468 | end 469 | obj 470 | end 471 | 472 | protected 473 | def state 474 | [@critical,@uris,@dns_names,@ips,@emails] 475 | end 476 | end 477 | 478 | class CertificatePolicies 479 | OPENSSL_IDENTIFIER = "certificatePolicies" 480 | 481 | include ExtensionAPI 482 | 483 | attr_accessor :critical 484 | attr_accessor :policy_identifier 485 | attr_accessor :cps_uris 486 | ##User notice 487 | attr_accessor :explicit_text 488 | attr_accessor :organization 489 | attr_accessor :notice_numbers 490 | 491 | def initialize 492 | self.critical = false 493 | @contains_data = false 494 | end 495 | 496 | def openssl_identifier 497 | OPENSSL_IDENTIFIER 498 | end 499 | 500 | def user_notice=(value={}) 501 | value.keys.each do |key| 502 | self.send("#{key}=".to_sym, value[key]) 503 | end 504 | end 505 | 506 | def config_extensions 507 | config_extension = {} 508 | custom_policies = {} 509 | notice = {} 510 | unless self.policy_identifier.nil? 511 | custom_policies["policyIdentifier"] = self.policy_identifier 512 | end 513 | 514 | if !self.cps_uris.nil? and self.cps_uris.is_a?(Array) 515 | self.cps_uris.each_with_index do |cps_uri,i| 516 | custom_policies["CPS.#{i}"] = cps_uri 517 | end 518 | end 519 | 520 | unless self.explicit_text.nil? 521 | notice["explicitText"] = self.explicit_text 522 | end 523 | 524 | unless self.organization.nil? 525 | notice["organization"] = self.organization 526 | end 527 | 528 | unless self.notice_numbers.nil? 529 | notice["noticeNumbers"] = self.notice_numbers 530 | end 531 | 532 | if notice.keys.size > 0 533 | custom_policies["userNotice.1"] = "@notice" 534 | config_extension["notice"] = notice 535 | end 536 | 537 | if custom_policies.keys.size > 0 538 | config_extension["custom_policies"] = custom_policies 539 | @contains_data = true 540 | end 541 | 542 | config_extension 543 | end 544 | 545 | def to_s 546 | return "" unless @contains_data 547 | res = [] 548 | res << "ia5org" 549 | res += @config_extensions["custom_policies"] unless @config_extensions.nil? 550 | res.join(',') 551 | end 552 | 553 | def self.parse(value, critical) 554 | obj = self.new 555 | return obj if value.nil? 556 | obj.critical = critical 557 | value.split(/,\s*/).each do |v| 558 | c = v.split(':', 2) 559 | obj.policy_identifier = c.last if c.first == "policyIdentifier" 560 | obj.cps_uris << c.last if c.first =~ %r{CPS.\d+} 561 | # TODO: explicit_text, organization, notice_numbers 562 | end 563 | obj 564 | end 565 | end 566 | 567 | # DEPRECATED 568 | # Specifics the purposes for which a certificate can be used. 569 | # The basicConstraints, keyUsage, and extendedKeyUsage extensions are now used instead. 570 | # https://www.openssl.org/docs/apps/x509v3_config.html#Netscape_Certificate_Type 571 | class NetscapeCertificateType 572 | OPENSSL_IDENTIFIER = "nsCertType" 573 | 574 | include ExtensionAPI 575 | 576 | attr_accessor :critical 577 | attr_accessor :flags 578 | 579 | def initialize 580 | self.critical = false 581 | self.flags = [] 582 | end 583 | 584 | def openssl_identifier 585 | OPENSSL_IDENTIFIER 586 | end 587 | 588 | def to_s 589 | res = [] 590 | res += self.flags 591 | res.join(',') 592 | end 593 | 594 | def self.parse(value, critical) 595 | obj = self.new 596 | return obj if value.nil? 597 | obj.critical = critical 598 | obj.flags = value.split(/,\s*/) 599 | obj 600 | end 601 | end 602 | 603 | # DEPRECATED 604 | # Contains a comment which will be displayed when the certificate is viewed in some browsers. 605 | # https://www.openssl.org/docs/apps/x509v3_config.html#Netscape_String_extensions_ 606 | class NetscapeComment 607 | OPENSSL_IDENTIFIER = "nsComment" 608 | 609 | include ExtensionAPI 610 | 611 | attr_accessor :critical 612 | attr_accessor :comment 613 | 614 | def initialize 615 | self.critical = false 616 | end 617 | 618 | def openssl_identifier 619 | OPENSSL_IDENTIFIER 620 | end 621 | 622 | def to_s 623 | res = [] 624 | res << self.comment if self.comment 625 | res.join(',') 626 | end 627 | 628 | def self.parse(value, critical) 629 | obj = self.new 630 | return obj if value.nil? 631 | obj.critical = critical 632 | obj.comment = value 633 | obj 634 | end 635 | end 636 | 637 | end 638 | end 639 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = CertificateAuthority - Because it shouldn't be this damned complicated 2 | 3 | {Build Status}[https://travis-ci.org/cchandler/certificate_authority] 4 | {Code Climate}[https://codeclimate.com/github/cchandler/certificate_authority] 5 | {Code Coverage}[https://coveralls.io/r/cchandler/certificate_authority] 6 | 7 | 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. 8 | 9 | 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. 10 | 11 | Because this library is built using the native Ruby bindings for OpenSSL it also supports PKCS#11 cryptographic hardware for secure maintenance of private key materials. 12 | 13 | = So you want to maintain a certificate authority root 14 | 15 | Let's suppose hypothetically you want to be able to issue and manage your own certificates. This section is meant to outline the basic functions you'll need(optionally want) to support. Not everyone is out to be in total compliance with WebTrust[link:http://www.webtrust.org/] or {Mozilla's rules for CA inclusion}[link:https://wiki.mozilla.org/CA:How_to_apply]. 16 | 17 | The three primary elements to be aware of are: 18 | 19 | [Certificate Authority] These are the functions primarily related to the signing, issuance, and revocation of certificates. 20 | 21 | [Registration Authority] These are the functions primarily related to registering and requesting certificates and vetting of the entities requesting certification. 22 | 23 | [Validation Authority] These are the functions related to verifying the status of certificates out in the wild. Mostly CRLs and OCSP related functions. 24 | 25 | = Establishing a new root in software 26 | 27 | Let's look at a complete example for generating a new root certificate. Assuming that you don't have a PKCS#11 hardware token available (lists coming...) we'll have to store this safe. 28 | 29 | Generating a self-signed root certificate is fairly easy: 30 | 31 | require 'certificate_authority' 32 | root = CertificateAuthority::Certificate.new 33 | root.subject.common_name= "http://mydomain.com" 34 | root.serial_number.number=1 35 | root.key_material.generate_key 36 | root.signing_entity = true 37 | signing_profile = {"extensions" => {"keyUsage" => {"usage" => ["critical", "keyCertSign"] }} } 38 | root.sign!(signing_profile) 39 | 40 | The required elements for the gem at this time are a common name for the subject and a serial number for the certificate. Since this is our self-signed root we're going to give it the first serial available of 1. Because certificate_authority is not designed to manage the issuance lifecycle you'll be expected to store serial numbers yourself. 41 | 42 | Next, after taking care of required fields, we will require key material for the new certificate. There's a convenience method made available on the key_material object for generating new keys. The private key will be available in: 43 | 44 | root.key_material.private_key 45 | 46 | and the public key: 47 | 48 | root.key_material.public_key 49 | 50 | Make sure to save the private key somewhere safe! 51 | 52 | Lastly, we declare that the certificate we're about to sign is itself a signing entity so we can continue on and sign other certificates. 53 | 54 | == Creating a new intermediate 55 | 56 | Maybe you don't want to actually sign certificates with your super-secret root certificate. This is actually how a good number of most public certificate authorities do it. Rather than sign with the primary root, they generate an intermediate root that is then responsible for signing the final certificates. If you wanted to create an intermediate root certificate you would do something like the following: 57 | 58 | intermediate = CertificateAuthority::Certificate.new 59 | intermediate.subject.common_name= "My snazzy intermediate!" 60 | intermediate.serial_number.number=2 61 | intermediate.key_material.generate_key 62 | intermediate.signing_entity = true 63 | intermediate.parent = root 64 | signing_profile = {"extensions" => {"keyUsage" => {"usage" => ["critical", "keyCertSign"] }} } 65 | intermediate.sign!(signing_profile) 66 | 67 | 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, our root will be responsible for signing this intermediate when we call +sign!+. 68 | 69 | = Creating new certificates (in general) 70 | 71 | Now that we have a root certificate (and possibly an intermediate) we can sign end-user certificates. It is, perhaps unsurprisingly, similar to all the others: 72 | 73 | plain_cert = CertificateAuthority::Certificate.new 74 | plain_cert.subject.common_name= "http://mydomain.com" 75 | plain_cert.serial_number.number=4 76 | plain_cert.key_material.generate_key 77 | plain_cert.parent = root # or intermediate 78 | plain_cert.sign! 79 | 80 | That's all there is to it! In this example we generate the key material ourselves, but it's possible for the end-user to generate certificate signing request (CSR) that we can then parse and consume automatically (coming soon). To get the PEM formatted certificate for the user you would need to call: 81 | 82 | plain_cert.to_pem 83 | 84 | to get the certificate body. 85 | 86 | = Signing Profiles 87 | 88 | Creating basic certificates is all well and good, but maybe you want _more_ signing control. +certificate_authority+ supports the idea of signing profiles. These are hashes containing values that +sign!+ will use to merge in additional control options for setting extensions on the certificate. 89 | 90 | Here's an example of a full signing profile for most of the common V3 extensions: 91 | 92 | signing_profile = { 93 | "extensions" => { 94 | "basicConstraints" => {"ca" => false}, 95 | "crlDistributionPoints" => {"uri" => "http://notme.com/other.crl" }, 96 | "subjectKeyIdentifier" => {}, 97 | "authorityKeyIdentifier" => {}, 98 | "authorityInfoAccess" => {"ocsp" => ["http://youFillThisOut/ocsp/"] }, 99 | "keyUsage" => {"usage" => ["digitalSignature","nonRepudiation"] }, 100 | "extendedKeyUsage" => {"usage" => [ "serverAuth","clientAuth"]}, 101 | "subjectAltName" => {"uris" => ["http://subdomains.youFillThisOut/"]}, 102 | "certificatePolicies" => { 103 | "policy_identifier" => "1.3.5.8", "cps_uris" => ["http://my.host.name/", "http://my.your.name/"], 104 | "user_notice" => { 105 | "explicit_text" => "Explicit Text Here", 106 | "organization" => "Organization name", 107 | "notice_numbers" => "1,2,3,4" 108 | } 109 | } 110 | } 111 | } 112 | 113 | Using a signing profile is done this way: 114 | 115 | certificate.sign!(signing_profile) 116 | 117 | At that point all the configuration options will be merged into the extensions. 118 | 119 | == Basic Constraints 120 | 121 | The basic constraints extension allows you to control whether or not a certificate can sign other certificates. 122 | 123 | [CA] If this value is true then this certificate has the authority to sign additional certificates. 124 | 125 | [pathlen] This is the maximum length of the chain-of-trust. For instance, if an intermediate certificate has a pathlen of 1 then it can sign additional certificates, but it cannot create another signing entity because the total chain-of-trust would have a length greater than 1. 126 | 127 | == CRL Distribution Points 128 | 129 | This extension controls where a conformant client can go to obtain a list of certificate revocation information. At this point +certificate_authority+ only supports a list of URIs. The formal RFC however provides for the ability to provide a URI and an issuer identifier that allows a different signing entity to generate/sign the CRL. 130 | 131 | [uri] The URI in subject alternative name format of the URI endpoint. Example: "http://ca.chrischandler.name/some_identifier.crl" 132 | 133 | == Subject Key Identifier 134 | 135 | This extension is required to be present, but doesn't offer any configurable parameters. Directly from the RFC: 136 | 137 | The subject key identifier extension provides a means of identifying 138 | certificates that contain a particular public key. 139 | 140 | To facilitate certification path construction, this extension MUST 141 | appear in all conforming CA certificates, that is, all certificates 142 | including the basic constraints extension (section 4.2.1.10) where 143 | the value of cA is TRUE. The value of the subject key identifier 144 | MUST be the value placed in the key identifier field of the Authority 145 | Key Identifier extension (section 4.2.1.1) of certificates issued by 146 | the subject of this certificate. 147 | 148 | == Authority Key Identifier 149 | 150 | Just like the subject key identifier, this is required under most circumstances and doesn't contain any meaningful configuration options. From the RFC: 151 | 152 | The keyIdentifier field of the authorityKeyIdentifier extension MUST 153 | be included in all certificates generated by conforming CAs to 154 | facilitate certification path construction. There is one exception; 155 | where a CA distributes its public key in the form of a "self-signed" 156 | certificate, the authority key identifier MAY be omitted. The 157 | signature on a self-signed certificate is generated with the private 158 | key associated with the certificate's subject public key. (This 159 | proves that the issuer possesses both the public and private keys.) 160 | In this case, the subject and authority key identifiers would be 161 | identical, but only the subject key identifier is needed for 162 | certification path building. 163 | 164 | == Authority Info Access 165 | 166 | The authority info access extension allows a CA to sign a certificate with information a client can use to get up-to-the-minute status information on a signed certificate. This takes the form of an OCSP[link:http://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol] (Online Certificate Status Protocol) endpoints. 167 | 168 | [ocsp] This is an array of URIs specifying possible endpoints that will be able to provide a signed response. +certificate_authority+ has an OCSP message handler for parsing OCSP requests and generating OCSP signed responses. 169 | 170 | == Key Usage 171 | 172 | This extension contains a list of the functions this certificate is allowed to participate in. 173 | 174 | [usage] An array of OIDs in string format. The acceptable values are specified by OpenSSL and are: +digitalSignature+, +nonRepudiation+, +keyEncipherment+, +dataEncipherment+, +keyAgreement+, +keyCertSign+, +cRLSign+, +encipherOnly+ and +decipherOnly+. 175 | 176 | == Extended Key Usage 177 | 178 | This one is like key usage, but allows for certain application specific purposes. It's generally only present in end-user certificates. 179 | 180 | [usage] An array of OIDs in string format. The only ones with practical significance at this point are: +serverAuth+, +clientAuth+, and +codeSigning+. 181 | 182 | == Subject Alternative Name 183 | 184 | If the certificate needs to work for multiple domains then you can specify the others for which it is valid in the subject alternative name field. 185 | 186 | [uris] An array of full URIs for other common names this certificate should be valid for. For instance, if you want http://ca.chrischandler.name and http://www.ca.chrischandler.name to share the same cert you would place both in the +uris+ attribute of the subject alternative name. 187 | 188 | == Certificate Policies 189 | 190 | This is one of the most esoteric of the extensions. This allows a conformant certificate authority to embed signing policy information into the certificate body. Public certificate authorities are required to maintain a Certificate Practice Statement in accordance with {RFC 2527}[link:http://www.ietf.org/rfc/rfc2527.txt]. 191 | 192 | These CPSs define what vetting criteria and maintenance practices are required to issue, maintain, and revoke a certificate. While it might be overkill for private certificates, if you wanted to make an actual public CA you would need to put together a practice statement and embed it in certificates you issue. 193 | 194 | [policy_identifier] This is an arbitrary OID (that you make up!) that uniquely represents the policy you are enforcing for whatever kind of certificate this is meant to be. 195 | 196 | [cps_uris] This is an array of URIs where a client or human can go to get information related to your certification practice. 197 | 198 | [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. 199 | 200 | = Certificate Signing Requests (CSRs) 201 | 202 | 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. 203 | 204 | Here's an example of using +certificate_authority+ to generate a CSR. 205 | 206 | csr = CertificateAuthority::SigningRequest.new 207 | dn = CertificateAuthority::DistinguishedName.new 208 | dn.common_name = "localhost" 209 | csr.distinguished_name = dn 210 | k = CertificateAuthority::MemoryKeyMaterial.new 211 | k.generate_key(2048) 212 | csr.key_material = k 213 | csr.digest = "SHA256" 214 | csr.to_x509_csr.to_pem 215 | 216 | Similarly, reading a CSR in is as simple as providing the PEM formatted version to +SigningRequest.from_x509_csr+. 217 | 218 | csr = CertificateAuthority::SigningRequest.from_x509_csr(@pem_csr) 219 | 220 | 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. 221 | 222 | = Certificate Revocation Lists (CRLs) 223 | 224 | Revocation lists let clients know when a certificate in the wild should no longer be trusted. 225 | 226 | 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. 227 | 228 | Ideally you would place the result CRL somewhere generally accessible on the Internet and reference the URI in the +crlDistributionPoints+ extension on issued certificates. 229 | 230 | crl = CertificateAuthority::CertificateRevocationList.new 231 | crl << certificate # Some CertificateAuthority::Certificate 232 | crl << serial_number # Also works with plain CertificateAuthority::SerialNumber 233 | crl.parent = root_certificate # A valid root 234 | crl.next_update = (60 * 60 * 10) # 10 Hours 235 | crl.sign! 236 | crl.to_pem 237 | 238 | = OCSP Support 239 | 240 | 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. 241 | If a client wishes to check the validity of a certificate they can query this endpoint. 242 | This request will only contain serial numbers, so you'll need to uniquely identify your authority in the AIA path. 243 | 244 | If a client sends you a DER encoded OCSP request you can read it out via +OCSPRequestReader+ 245 | 246 | ocsp_request_reader = CertificateAuthority::OCSPRequestReader.from_der(@ocsp_request.to_der) 247 | ocsp_request_reader.serial_numbers 248 | 249 | Then, you can construct a response like this 250 | 251 | response_builder = CertificateAuthority::OCSPResponseBuilder.from_request_reader(ocsp_request_reader) 252 | response_builder.parent = root 253 | response = response_builder.build_response # Returns OpenSSL::OCSP::Response 254 | response.to_der 255 | 256 | The response builder will copy a (possible) nonce from the request. By default, the +OCSPResponseBuilder+ will say that every certificate is GOOD. 257 | You should definitely override this if you plan on revoking certificates. 258 | 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. 259 | 260 | response_builder = CertificateAuthority::OCSPResponseBuilder.from_request_reader(ocsp_request_reader) 261 | response_builder.verification_mechanism = lambda {|certid| 262 | [CertificateAuthority::OCSPResponseBuilder::REVOKED,CertificateAuthority::OCSPResponseBuilder::UNSPECIFIED] 263 | } 264 | response_builder.parent = root 265 | response = response_builder.build_response # Response will say everything is revoked for unspecified reasons 266 | 267 | 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. 268 | The default is 15 minutes. 269 | 270 | response_builder.next_update = 30 * 60 # 30 minutes 271 | 272 | = PKCS#11 Support 273 | 274 | 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. 275 | 276 | To configure a certificate to utilize PKCS#11 instead of in memory keys all you need to do is: 277 | 278 | root = CertificateAuthority::Certificate.new 279 | root.subject.common_name= "http://mydomain.com" 280 | root.serial_number.number=1 281 | root.signing_entity = true 282 | 283 | key_material_in_hardware = CertificateAuthority::Pkcs11KeyMaterial.new 284 | key_material_in_hardware.token_id = "46" 285 | key_material_in_hardware.pkcs11_lib = "/usr/lib/libeTPkcs11.so" 286 | key_material_in_hardware.openssl_pkcs11_engine_lib = "/usr/lib/engines/engine_pkcs11.so" 287 | key_material_in_hardware.pin = "11111111" 288 | 289 | root.key_material = key_material_in_hardware 290 | root.sign! 291 | 292 | Your current version of OpenSSL _must_ include dynamic engine support and you will need to have OpenSSL PKCS#11 engine support. You will also require the actual PKCS#11 driver from the hardware manufacturer. As of today the only tokens I've gotten to work are: 293 | 294 | [eTokenPro] Released by Aladdin (now SafeNet Inc.). I have only had success with the version 4 and 5 (32 bit only) copy of the driver. The newer authentication client released by SafeNet appears to be completely broken for interacting with the tokens outside of SafeNet's own tools. If anyone has a different experience I'd like to hear from you. 295 | 296 | [ACS CryptoMate] Also a 32-bit only driver. You'll have to jump through some hoops to get the Linux PKCS#11 driver but it works surprisingly well. It also appears to support symmetric key operations in hardware. 297 | 298 | [Your company] Do you make a PKCS#11 device? I'd love to get it working but I probably can't afford your device. Get in touch with me and if you're willing to loan me one for a week I can get it listed. 299 | 300 | 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. 301 | 302 | = Hopefully in the future 303 | 304 | * More PKCS#11 hardware (I need driver support from the manufacturers) 305 | 306 | = Todone 307 | 308 | * Support for working with CSRs to request & issue certificates 309 | * OCSP support 310 | 311 | = Misc notes 312 | 313 | * Firefox will complain about root/intermediate certificates unless both digitalSignature and keyEncipherment are specified as keyUsage attributes. Thanks diogomonica 314 | 315 | = Special thanks and Contributions 316 | 317 | * Diogo Monica @diogo 318 | * Justin Cummins @sul3n3t 319 | * @databus23 320 | * Colin Jones @trptcolin 321 | * Eric Monti @emonti 322 | * TJ Vanderpoel @bougyman 323 | 324 | == Meta 325 | 326 | Written by Chris Chandler(http://chrischandler.name) 327 | 328 | Released under the MIT License: http://www.opensource.org/licenses/mit-license.php 329 | 330 | Main page: http://github.com/cchandler/certificate_authority 331 | 332 | Issue tracking: https://github.com/cchandler/certificate_authority/issues 333 | -------------------------------------------------------------------------------- /spec/units/certificate_spec.rb: -------------------------------------------------------------------------------- 1 | describe CertificateAuthority::Certificate do 2 | before(:each) do 3 | @certificate = CertificateAuthority::Certificate.new 4 | end 5 | 6 | describe CertificateAuthority::SigningEntity do 7 | it "should behave as a signing entity" do 8 | expect(@certificate.respond_to?(:is_signing_entity?)).to be_truthy 9 | end 10 | 11 | it "should only be a signing entity if it's identified as a CA", :rfc3280 => true do 12 | expect(@certificate.is_signing_entity?).to be_falsey 13 | @certificate.signing_entity = true 14 | expect(@certificate.is_signing_entity?).to be_truthy 15 | end 16 | 17 | describe "Root certificates" do 18 | before(:each) do 19 | @certificate.signing_entity = true 20 | end 21 | 22 | it "should be able to be identified as a root certificate" do 23 | expect(@certificate.is_root_entity?).to be_truthy 24 | end 25 | 26 | it "should only be a root certificate if the parent entity is itself", :rfc3280 => true do 27 | expect(@certificate.parent).to eq(@certificate) 28 | end 29 | 30 | it "should be a root certificate by default" do 31 | expect(@certificate.is_root_entity?).to be_truthy 32 | end 33 | 34 | it "should be able to self-sign" do 35 | @certificate.serial_number.number = 1 36 | @certificate.subject.common_name = "chrischandler.name" 37 | @certificate.key_material.generate_key(768) 38 | @certificate.sign! 39 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 40 | expect(cert.subject.to_s).to eq(cert.issuer.to_s) 41 | end 42 | 43 | it "should have the basicContraint CA:TRUE" do 44 | @certificate.serial_number.number = 1 45 | @certificate.subject.common_name = "chrischandler.name" 46 | @certificate.key_material.generate_key(768) 47 | @certificate.sign! 48 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 49 | expect(cert.extensions.map{|i| [i.oid,i.value] }.select{|i| i.first == "basicConstraints"}.first[1]).to eq("CA:TRUE") 50 | end 51 | end 52 | 53 | describe "Intermediate certificates" do 54 | before(:each) do 55 | @different_cert = CertificateAuthority::Certificate.new 56 | @different_cert.signing_entity = true 57 | @different_cert.subject.common_name = "chrischandler.name root" 58 | @different_cert.key_material.generate_key(768) 59 | @different_cert.serial_number.number = 2 60 | @different_cert.sign! #self-signed 61 | @certificate.parent = @different_cert 62 | @certificate.signing_entity = true 63 | end 64 | 65 | it "should be able to be identified as an intermediate certificate" do 66 | expect(@certificate.is_intermediate_entity?).to be_truthy 67 | end 68 | 69 | it "should not be identified as a root" do 70 | expect(@certificate.is_root_entity?).to be_falsey 71 | end 72 | 73 | it "should only be an intermediate certificate if the parent is a different entity" do 74 | expect(@certificate.parent).not_to eq(@certificate) 75 | expect(@certificate.parent).not_to be_nil 76 | end 77 | 78 | it "should correctly be signed by a parent certificate" do 79 | @certificate.subject.common_name = "chrischandler.name" 80 | @certificate.key_material.generate_key(768) 81 | @certificate.signing_entity = true 82 | @certificate.serial_number.number = 1 83 | @certificate.sign! 84 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 85 | expect(cert.subject.to_s).not_to eq(cert.issuer.to_s) 86 | end 87 | 88 | it "should have the basicContraint CA:TRUE" do 89 | @certificate.subject.common_name = "chrischandler.name" 90 | @certificate.key_material.generate_key(768) 91 | @certificate.signing_entity = true 92 | @certificate.serial_number.number = 3 93 | @certificate.sign! 94 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 95 | expect(cert.extensions.map{|i| [i.oid,i.value] }.select{|i| i.first == "basicConstraints"}.first[1]).to eq("CA:TRUE") 96 | end 97 | 98 | end 99 | 100 | describe "Terminal certificates" do 101 | before(:each) do 102 | @different_cert = CertificateAuthority::Certificate.new 103 | @different_cert.signing_entity = true 104 | @different_cert.subject.common_name = "chrischandler.name root" 105 | @different_cert.key_material.generate_key(768) 106 | @different_cert.serial_number.number = 1 107 | @different_cert.sign! #self-signed 108 | @certificate.parent = @different_cert 109 | end 110 | 111 | it "should not be identified as an intermediate certificate" do 112 | expect(@certificate.is_intermediate_entity?).to be_falsey 113 | end 114 | 115 | it "should not be identified as a root" do 116 | expect(@certificate.is_root_entity?).to be_falsey 117 | end 118 | 119 | it "should have the basicContraint CA:FALSE" do 120 | @certificate.subject.common_name = "chrischandler.name" 121 | @certificate.key_material.generate_key(768) 122 | @certificate.signing_entity = false 123 | @certificate.serial_number.number = 1 124 | @certificate.sign! 125 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 126 | expect(cert.extensions.map{|i| [i.oid,i.value] }.select{|i| i.first == "basicConstraints"}.first[1]).to eq("CA:FALSE") 127 | end 128 | end 129 | 130 | 131 | it "should be able to be identified as a root certificate" do 132 | expect(@certificate.respond_to?(:is_root_entity?)).to be_truthy 133 | end 134 | end #End of SigningEntity 135 | 136 | describe "Signed certificates" do 137 | before(:each) do 138 | @certificate = CertificateAuthority::Certificate.new 139 | @certificate.subject.common_name = "chrischandler.name" 140 | @certificate.key_material.generate_key(768) 141 | @certificate.serial_number.number = 1 142 | @certificate.sign! 143 | end 144 | 145 | it "should have a PEM encoded certificate body available" do 146 | expect(@certificate.to_pem).not_to be_nil 147 | expect(OpenSSL::X509::Certificate.new(@certificate.to_pem)).not_to be_nil 148 | end 149 | end 150 | 151 | describe "X.509 V3 Extensions on Signed Certificates" do 152 | before(:each) do 153 | @certificate = CertificateAuthority::Certificate.new 154 | @certificate.subject.common_name = "chrischandler.name" 155 | @certificate.key_material.generate_key(768) 156 | @certificate.serial_number.number = 1 157 | @signing_profile = { 158 | "extensions" => { 159 | "subjectAltName" => {"uris" => ["www.chrischandler.name"]}, 160 | "certificatePolicies" => { 161 | "policy_identifier" => "1.3.5.7", 162 | "cps_uris" => ["http://my.host.name/", "http://my.your.name/"], 163 | "user_notice" => { 164 | "explicit_text" => "Testing!", "organization" => "RSpec Test organization name", "notice_numbers" => "1,2,3,4" 165 | } 166 | } 167 | } 168 | } 169 | @certificate.sign!(@signing_profile) 170 | end 171 | 172 | describe "SubjectAltName" do 173 | before(:each) do 174 | @certificate = CertificateAuthority::Certificate.new 175 | @certificate.subject.common_name = "chrischandler.name" 176 | @certificate.key_material.generate_key(768) 177 | @certificate.serial_number.number = 1 178 | end 179 | 180 | it "should have a subjectAltName if specified" do 181 | @certificate.sign!({"extensions" => {"subjectAltName" => {"uris" => ["www.chrischandler.name"]}}}) 182 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 183 | expect(cert.extensions.map(&:oid).include?("subjectAltName")).to be_truthy 184 | end 185 | 186 | it "should NOT have a subjectAltName if one was not specified" do 187 | @certificate.sign! 188 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 189 | expect(cert.extensions.map(&:oid).include?("subjectAltName")).to be_falsey 190 | end 191 | 192 | it 'should replace email:copy with email address' do 193 | @certificate.subject.email_address = 'foo@bar.com' 194 | @certificate.sign!( 195 | { "extensions" => { "subjectAltName" => { 'emails' => %w[copy fubar@bar.com] } } } 196 | ) 197 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 198 | alt = cert.extensions.select { |e| e.oid == 'subjectAltName' }.first 199 | expect(alt.value).to eq('email:foo@bar.com, email:fubar@bar.com') 200 | end 201 | end 202 | 203 | describe "AuthorityInfoAccess" do 204 | before(:each) do 205 | @certificate = CertificateAuthority::Certificate.new 206 | @certificate.subject.common_name = "chrischandler.name" 207 | @certificate.key_material.generate_key(768) 208 | @certificate.serial_number.number = 1 209 | end 210 | 211 | it "should have an authority info access if specified" do 212 | @certificate.sign!({"extensions" => {"authorityInfoAccess" => {"ocsp" => ["www.chrischandler.name"]}}}) 213 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 214 | expect(cert.extensions.map(&:oid).include?("authorityInfoAccess")).to be_truthy 215 | end 216 | end 217 | 218 | describe "CrlDistributionPoints" do 219 | before(:each) do 220 | @certificate = CertificateAuthority::Certificate.new 221 | @certificate.subject.common_name = "chrischandler.name" 222 | @certificate.key_material.generate_key(768) 223 | @certificate.serial_number.number = 1 224 | end 225 | 226 | it "should have a crlDistributionPoint if specified" do 227 | @certificate.sign!({"extensions" => {"crlDistributionPoints" => {"uris" => ["http://crlThingy.com"]}}}) 228 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 229 | expect(cert.extensions.map(&:oid).include?("crlDistributionPoints")).to be_truthy 230 | end 231 | 232 | it "should NOT have a crlDistributionPoint if one was not specified" do 233 | @certificate.sign! 234 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 235 | expect(cert.extensions.map(&:oid).include?("crlDistributionPoints")).to be_falsey 236 | end 237 | end 238 | 239 | 240 | describe "CertificatePolicies" do 241 | before(:each) do 242 | @certificate = CertificateAuthority::Certificate.new 243 | @certificate.subject.common_name = "chrischandler.name" 244 | @certificate.key_material.generate_key(768) 245 | @certificate.serial_number.number = 1 246 | end 247 | 248 | it "should have a certificatePolicy if specified" do 249 | @certificate.sign!({ 250 | "extensions" => { 251 | "certificatePolicies" => { 252 | "policy_identifier" => "1.3.5.7", 253 | "cps_uris" => ["http://my.host.name/", "http://my.your.name/"] 254 | } 255 | } 256 | }) 257 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 258 | expect(cert.extensions.map(&:oid).include?("certificatePolicies")).to be_truthy 259 | end 260 | 261 | pending "should contain a nested userNotice if specified" do 262 | #pending 263 | @certificate.sign!({ 264 | "extensions" => { 265 | "certificatePolicies" => { 266 | "policy_identifier" => "1.3.5.7", 267 | "cps_uris" => ["http://my.host.name/", "http://my.your.name/"], 268 | "user_notice" => { 269 | "explicit_text" => "Testing explicit text!", "organization" => "RSpec Test organization name", "notice_numbers" => "1,2,3,4" 270 | } 271 | } 272 | } 273 | }) 274 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 275 | expect(cert.extensions.map(&:oid).include?("certificatePolicies")).to be_truthy 276 | ## Checking OIDs after they've run through OpenSSL is a pain... 277 | ## The nicely structured data will be flattened to a single String 278 | cert.extensions.each do |ext| 279 | if ext.oid == "certificatePolicies" 280 | expect(ext.to_a[1]).to include("Testing explicit text!") 281 | end 282 | end 283 | end 284 | 285 | it "should NOT include a certificatePolicy if not specified" do 286 | @certificate.sign! 287 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 288 | expect(cert.extensions.map(&:oid).include?("certificatePolicies")).to be_falsey 289 | end 290 | end 291 | 292 | 293 | it "should support BasicConstraints" do 294 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 295 | expect(cert.extensions.map(&:oid).include?("basicConstraints")).to be_truthy 296 | end 297 | 298 | it "should support subjectKeyIdentifier" do 299 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 300 | expect(cert.extensions.map(&:oid).include?("subjectKeyIdentifier")).to be_truthy 301 | end 302 | 303 | it "should support authorityKeyIdentifier" do 304 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 305 | expect(cert.extensions.map(&:oid).include?("authorityKeyIdentifier")).to be_truthy 306 | end 307 | 308 | it "should order subjectKeyIdentifier before authorityKeyIdentifier" do 309 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 310 | expect(cert.extensions.map(&:oid).select do |oid| 311 | ["subjectKeyIdentifier", "authorityKeyIdentifier"].include?(oid) 312 | end).to eq(["subjectKeyIdentifier", "authorityKeyIdentifier"]) 313 | end 314 | 315 | it "should support keyUsage" do 316 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 317 | expect(cert.extensions.map(&:oid).include?("keyUsage")).to be_truthy 318 | end 319 | 320 | it "should support extendedKeyUsage" do 321 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 322 | expect(cert.extensions.map(&:oid).include?("extendedKeyUsage")).to be_truthy 323 | end 324 | end 325 | 326 | describe "Signing profile" do 327 | before(:each) do 328 | @certificate = CertificateAuthority::Certificate.new 329 | @certificate.subject.common_name = "chrischandler.name" 330 | @certificate.key_material.generate_key(768) 331 | @certificate.serial_number.number = 1 332 | 333 | @signing_profile = { 334 | "extensions" => { 335 | "basicConstraints" => {"ca" => false}, 336 | "crlDistributionPoints" => {"uri" => "http://notme.com/other.crl" }, 337 | "subjectKeyIdentifier" => {}, 338 | "authorityKeyIdentifier" => {}, 339 | "authorityInfoAccess" => {"ocsp" => ["http://youFillThisOut/ocsp/"], "ca_issuers" => ["http://me.com/other.crt"] }, 340 | "keyUsage" => {"usage" => ["digitalSignature","nonRepudiation"] }, 341 | "extendedKeyUsage" => {"usage" => [ "serverAuth","clientAuth"]}, 342 | "subjectAltName" => {"uris" => ["http://subdomains.youFillThisOut/"]}, 343 | "certificatePolicies" => { 344 | "policy_identifier" => "1.3.5.8", "cps_uris" => ["http://my.host.name/", "http://my.your.name/"], "user_notice" => { 345 | "explicit_text" => "Explicit Text Here", "organization" => "Organization name", "notice_numbers" => "1,2,3,4" 346 | } 347 | } 348 | } 349 | } 350 | end 351 | 352 | it "should be able to sign with an optional policy hash" do 353 | @certificate.sign!(@signing_profile) 354 | end 355 | 356 | it "should support a default signing digest of SHA512" do 357 | @certificate.sign!(@signing_profile) 358 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 359 | expect(cert.signature_algorithm).to eq("sha512WithRSAEncryption") 360 | end 361 | 362 | it "should support a configurable digest algorithm" do 363 | @signing_profile.merge!({"digest" => "SHA1"}) 364 | @certificate.sign!(@signing_profile) 365 | cert = OpenSSL::X509::Certificate.new(@certificate.to_pem) 366 | expect(cert.signature_algorithm).to eq("sha1WithRSAEncryption") 367 | end 368 | 369 | end 370 | 371 | describe "from_openssl" do 372 | before(:each) do 373 | @pem_cert=< Time.now + year - day 482 | end 483 | 484 | it "should be able to have a revoked at time" do 485 | expect(@certificate.revoked?).to be_falsey 486 | @certificate.revoked_at = Time.now.utc 487 | expect(@certificate.revoked?).to be_truthy 488 | end 489 | 490 | end 491 | --------------------------------------------------------------------------------