sslackey 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +46 -0
- data/Rakefile +9 -0
- data/examples/README.md +11 -0
- data/examples/cacert.pem +1570 -0
- data/examples/simple.rb +49 -0
- data/lib/sslackey/authority_checker.rb +107 -0
- data/lib/sslackey/cache/redis_revocation_cache.rb +30 -0
- data/lib/sslackey/revocation_checker.rb +90 -0
- data/lib/sslackey/version.rb +3 -0
- data/lib/sslackey.rb +3 -0
- data/spec/authority_checker_spec.rb +148 -0
- data/spec/cache/redis_revocation_cache_spec.rb +61 -0
- data/spec/fixtures/AkamaiSub3.crl +0 -0
- data/spec/fixtures/cacert.pem +696 -0
- data/spec/fixtures/crl_only_cert.pem +18 -0
- data/spec/fixtures/ocsp_enabled_cert.pem +35 -0
- data/spec/fixtures/sample_certificate_revocation_list.crl +0 -0
- data/spec/fixtures/sample_ocsp_response.der +0 -0
- data/spec/fixtures/ssl.rb +29 -0
- data/spec/revocation_checker_spec.rb +113 -0
- data/spec/spec_helper.rb +22 -0
- data/sslackey.gemspec +35 -0
- metadata +227 -0
    
        data/examples/simple.rb
    ADDED
    
    | @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            require 'net/https'
         | 
| 2 | 
            +
            require 'uri'
         | 
| 3 | 
            +
            require 'logger'
         | 
| 4 | 
            +
            require 'openssl'
         | 
| 5 | 
            +
            require 'sslackey'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
             | 
| 8 | 
            +
            module OpenSSL
         | 
| 9 | 
            +
              module SSL
         | 
| 10 | 
            +
                class SSLSocket
         | 
| 11 | 
            +
                  def post_connection_check(hostname)
         | 
| 12 | 
            +
                    unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname)
         | 
| 13 | 
            +
                      raise SSLError, "hostname was not match with the server certificate"
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    checker = RevocationChecker.new()
         | 
| 17 | 
            +
                    status = checker.check_revocation_status(peer_cert)
         | 
| 18 | 
            +
                    raise SSLError, "Bad revocation status: #{status}" unless status == :successful
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    return true
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            RevocationChecker.setup File.join(File.dirname(__FILE__), 'cacert.pem')
         | 
| 27 | 
            +
            RevocationChecker.cache = RedisRevocationCache.new("localhost", "6379")
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            #Test the connection
         | 
| 30 | 
            +
            LOGGER = Logger.new(STDERR)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            # tdameritrade.com is broken on ocsp parsing
         | 
| 33 | 
            +
            # americanexpress.com : requires CRL check
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            url = URI.parse('https://www.google.com ')
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            http = Net::HTTP.new(url.host, url.port)
         | 
| 38 | 
            +
            http.set_debug_output $stderr
         | 
| 39 | 
            +
            http.use_ssl=true
         | 
| 40 | 
            +
            store = OpenSSL::X509::Store.new
         | 
| 41 | 
            +
            store.add_file File.join(File.dirname(__FILE__), 'cacert.pem')
         | 
| 42 | 
            +
            http.cert_store = store
         | 
| 43 | 
            +
            http.verify_mode = OpenSSL::SSL::VERIFY_PEER
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            http.start() do |http|
         | 
| 46 | 
            +
              request = Net::HTTP::Get.new url.request_uri
         | 
| 47 | 
            +
              response = http.request request # Net::HTTPResponse object
         | 
| 48 | 
            +
              puts response
         | 
| 49 | 
            +
            end
         | 
| @@ -0,0 +1,107 @@ | |
| 1 | 
            +
            class AuthorityChecker
         | 
| 2 | 
            +
              REVOCATION_RESPONSES = [:successful, :unknown, :revoked]
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              attr_accessor :trusted_certs_file_path
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def initialize(trusted_certs_path)
         | 
| 7 | 
            +
                @trusted_certs_file_path = trusted_certs_path
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              def validate(certificate, issuer_certificate)
         | 
| 12 | 
            +
                ocsp_url = nil
         | 
| 13 | 
            +
                crl_url = nil
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                certificate.extensions.each do |extension|
         | 
| 16 | 
            +
                  props = extension.to_h
         | 
| 17 | 
            +
                  if props["oid"] == "authorityInfoAccess"
         | 
| 18 | 
            +
                    ocsp_url = AuthorityChecker.parse_authority_info_access(props["value"])
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  if props["oid"] == "crlDistributionPoints"
         | 
| 22 | 
            +
                    crl_url = AuthorityChecker.parse_crl_distribution_points(props["value"])
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
                if ocsp_url
         | 
| 26 | 
            +
                  response = perform_ocsp_check(certificate, issuer_certificate, ocsp_url)
         | 
| 27 | 
            +
                elsif crl_url
         | 
| 28 | 
            +
                  response = perform_crl_check(certificate, crl_url)
         | 
| 29 | 
            +
                else
         | 
| 30 | 
            +
                  raise "Could not find valid oscp or crl extension to check against in certificate #{certificate.subject}"
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                raise "Unknown revocation response #{response}" unless AuthorityChecker::REVOCATION_RESPONSES.include?(response)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                response
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              def self.parse_authority_info_access(ocsp_string)
         | 
| 39 | 
            +
                ocsp_string.each_line do |line|
         | 
| 40 | 
            +
                  if line.index(/OCSP/)
         | 
| 41 | 
            +
                    urls = line.scan(/URI:.*/)
         | 
| 42 | 
            +
                    url = urls[0]
         | 
| 43 | 
            +
                    url.slice!(/URI:/)
         | 
| 44 | 
            +
                    return url
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              def self.parse_crl_distribution_points(crl_string)
         | 
| 50 | 
            +
                crl_string.each_line do |line|
         | 
| 51 | 
            +
                  urls = line.scan(/URI:.*/)
         | 
| 52 | 
            +
                  crl_url = urls[0]
         | 
| 53 | 
            +
                  crl_url.slice!(/URI:/)
         | 
| 54 | 
            +
                  return crl_url
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
             | 
| 59 | 
            +
              def perform_ocsp_check(certificate, issuer_certificate, ocsp_url)
         | 
| 60 | 
            +
                certificate_file = write_certificate_file(certificate, "provider")
         | 
| 61 | 
            +
                issuer_file = write_certificate_file(issuer_certificate, "issuer")
         | 
| 62 | 
            +
                output_file = create_response_file("ocsp_output")
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                generate_ocsp_response(issuer_file.path, certificate_file.path, output_file.path, ocsp_url)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                read_ocsp_response(output_file).to_sym
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
              def generate_ocsp_response(issuer_file_path, certificate_file_path, output_file_path, ocsp_url)
         | 
| 70 | 
            +
                `openssl ocsp -no_nonce -CAfile #{trusted_certs_file_path} -issuer #{issuer_file_path} -cert #{certificate_file_path} -respout #{output_file_path} -url #{ocsp_url}`
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              def read_ocsp_response(output_file)
         | 
| 74 | 
            +
                output_file.rewind
         | 
| 75 | 
            +
                response = OpenSSL::OCSP::Response.new(output_file.read)
         | 
| 76 | 
            +
                output_file.close
         | 
| 77 | 
            +
                response.status_string
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
              def write_certificate_file(certificate, file_name)
         | 
| 81 | 
            +
                file = Tempfile.new(file_name)
         | 
| 82 | 
            +
                file.write(certificate)
         | 
| 83 | 
            +
                file.rewind
         | 
| 84 | 
            +
                file.close
         | 
| 85 | 
            +
                file
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
              def create_response_file(file_name)
         | 
| 89 | 
            +
                Tempfile.new(file_name)
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
              def perform_crl_check(certificate, crl_url)
         | 
| 93 | 
            +
                content = fetch_crl_content(crl_url)
         | 
| 94 | 
            +
                crl = OpenSSL::X509::CRL.new(content)
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                revoked_matches = crl.revoked.select { |elem| elem.serial == certificate.serial }
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                return :revoked unless revoked_matches.empty?
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                :successful
         | 
| 101 | 
            +
              end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
              def fetch_crl_content(crl_url)
         | 
| 104 | 
            +
                `curl -s '#{crl_url}'`
         | 
| 105 | 
            +
              end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
            end
         | 
| @@ -0,0 +1,30 @@ | |
| 1 | 
            +
            require 'redis'
         | 
| 2 | 
            +
            require 'redis/namespace'
         | 
| 3 | 
            +
            require 'active_support/all'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class RedisRevocationCache
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              attr_accessor :redis, :expiration_seconds
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              def initialize(redis_host, redis_port)
         | 
| 10 | 
            +
                @redis = Redis::Namespace.new(:revocation, :redis => Redis.new(:host => redis_host, :port => redis_port, :threadsafe => true))
         | 
| 11 | 
            +
                @expiration_seconds = 3600
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def cached_response(certificate)
         | 
| 15 | 
            +
                response = redis.get(get_key(certificate))
         | 
| 16 | 
            +
                LOGGER.info("got a cached response: #{response}") if response && defined?(LOGGER)
         | 
| 17 | 
            +
                response.try(:to_sym)
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              def cache_response(certificate, response)
         | 
| 21 | 
            +
                key = get_key(certificate)
         | 
| 22 | 
            +
                LOGGER.info "caching revocation response for certificate: #{certificate.subject}" if defined?(LOGGER)
         | 
| 23 | 
            +
                redis.set(key, response)
         | 
| 24 | 
            +
                redis.expire(key, expiration_seconds)
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              def get_key(certificate)
         | 
| 28 | 
            +
                certificate.subject.hash
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
| @@ -0,0 +1,90 @@ | |
| 1 | 
            +
            # Load all known issuer certs into memory. Key by cert name
         | 
| 2 | 
            +
            require 'logger'
         | 
| 3 | 
            +
            require 'openssl'
         | 
| 4 | 
            +
            require 'tempfile'
         | 
| 5 | 
            +
            require 'redis'
         | 
| 6 | 
            +
            require 'redis/namespace'
         | 
| 7 | 
            +
            require 'active_support/all'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            class RevocationChecker
         | 
| 10 | 
            +
              @issuers = {}
         | 
| 11 | 
            +
              @issuers_by_name = {}
         | 
| 12 | 
            +
              @trusted_certs_file_path = nil
         | 
| 13 | 
            +
              @cache = nil
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              class << self
         | 
| 16 | 
            +
                attr_accessor :issuers, :issuers_by_name, :trusted_certs_file_path, :cache
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              def self.setup(trusted_certs_file_path)
         | 
| 20 | 
            +
                RevocationChecker.issuers = {}
         | 
| 21 | 
            +
                RevocationChecker.issuers_by_name = {}
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                RevocationChecker.trusted_certs_file_path = trusted_certs_file_path
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                certs_file = File.read(RevocationChecker.trusted_certs_file_path)
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                certs = certs_file.scan(/-----BEGIN CERTIFICATE-----[^-]*-----END CERTIFICATE-----/)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                certs.each do |cert|
         | 
| 30 | 
            +
                  certificate = OpenSSL::X509::Certificate.new(cert)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  certificate.extensions.each do |extension|
         | 
| 33 | 
            +
                    props = extension.to_h
         | 
| 34 | 
            +
                    if props["oid"] == "subjectKeyIdentifier"
         | 
| 35 | 
            +
                      issuer_key = props["value"]
         | 
| 36 | 
            +
                      RevocationChecker.issuers[issuer_key] = certificate
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                  RevocationChecker.issuers_by_name[certificate.subject.hash] = certificate
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def check_revocation_status(certificate)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                unless RevocationChecker.cache
         | 
| 46 | 
            +
                  LOGGER.info("skipping revocation caching") if defined? LOGGER
         | 
| 47 | 
            +
                  return get_latest_revocation_status(certificate)
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                if  cached_response = RevocationChecker.cache.cached_response(certificate)
         | 
| 51 | 
            +
                  return cached_response
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                response = get_latest_revocation_status(certificate)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                RevocationChecker.cache.cache_response(certificate, response)
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                response
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
             | 
| 62 | 
            +
              def get_latest_revocation_status(certificate)
         | 
| 63 | 
            +
                issuer_certificate = nil
         | 
| 64 | 
            +
                certificate.extensions.each do |extension|
         | 
| 65 | 
            +
                  props = extension.to_h
         | 
| 66 | 
            +
                  if props["oid"] == "authorityKeyIdentifier"
         | 
| 67 | 
            +
                    issuer_key = RevocationChecker.parse_authority_key_identifier(props["value"])
         | 
| 68 | 
            +
                    issuer_certificate = RevocationChecker.issuers[issuer_key]
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                unless issuer_certificate
         | 
| 73 | 
            +
                  issuer_certificate = RevocationChecker.issuers_by_name[certificate.issuer.hash]
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                raise "No issuer certificate #{certificate.issuer} found for certificate #{certificate.subject}" unless issuer_certificate
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                real_time_checker = AuthorityChecker.new(RevocationChecker.trusted_certs_file_path)
         | 
| 79 | 
            +
                response = real_time_checker.validate(certificate, issuer_certificate)
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                response
         | 
| 82 | 
            +
              end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
              def self.parse_authority_key_identifier(authority_key_identifier_string)
         | 
| 85 | 
            +
                authority_key_identifier_string.slice!(/keyid:/)
         | 
| 86 | 
            +
                authority_key_identifier_string.slice!(/\n/)
         | 
| 87 | 
            +
                authority_key_identifier_string
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
            end
         | 
    
        data/lib/sslackey.rb
    ADDED
    
    
| @@ -0,0 +1,148 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'openssl'
         | 
| 3 | 
            +
            require 'tempfile'
         | 
| 4 | 
            +
            require 'uri'
         | 
| 5 | 
            +
            require 'lib/sslackey/authority_checker'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            def load_ocsp_enabled_cert
         | 
| 8 | 
            +
              ocsp_enabled_cert = File.read(File.expand_path "../fixtures/ocsp_enabled_cert.pem", __FILE__)
         | 
| 9 | 
            +
              OpenSSL::X509::Certificate.new(ocsp_enabled_cert)
         | 
| 10 | 
            +
            end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            def load_non_ocsp_cert
         | 
| 13 | 
            +
              crl_only_cert = File.read(File.expand_path "../fixtures/crl_only_cert.pem", __FILE__)
         | 
| 14 | 
            +
              OpenSSL::X509::Certificate.new(crl_only_cert)
         | 
| 15 | 
            +
            end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            def load_sample_ocsp_response
         | 
| 18 | 
            +
              File.open(File.expand_path "../fixtures/sample_ocsp_response.der", __FILE__)
         | 
| 19 | 
            +
            end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            def load_crl_without_cert_revoked
         | 
| 22 | 
            +
              File.read(File.expand_path "../fixtures/AkamaiSub3.crl", __FILE__)
         | 
| 23 | 
            +
            end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            def load_crl_with_cert_revoked
         | 
| 26 | 
            +
              File.read(File.expand_path "../fixtures/sample_certificate_revocation_list.crl", __FILE__)
         | 
| 27 | 
            +
            end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            describe AuthorityChecker do
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              describe ".parse_authority_info_access" do
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                context "with a multi line authority info string" do
         | 
| 34 | 
            +
                  it "only finds the value that matches OCSP" do
         | 
| 35 | 
            +
                    ocsp_string = "CA Issuers - URI:http://crt.usertrust.com/USERTrustLegacySecureServerCA.crt\nOCSP - URI:http://ocsp.usertrust.com\n"
         | 
| 36 | 
            +
                    AuthorityChecker.parse_authority_info_access(ocsp_string).should == "http://ocsp.usertrust.com"
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    ocsp_string = "OCSP - URI:http://ocsp.verisign.com\nCA Issuers - URI:http://SVRIntl-G3-aia.verisign.com/SVRIntlG3.cer\n"
         | 
| 39 | 
            +
                    AuthorityChecker.parse_authority_info_access(ocsp_string).should == "http://ocsp.verisign.com"
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                context "with a single authority info line" do
         | 
| 44 | 
            +
                  it "finds the right value" do
         | 
| 45 | 
            +
                    ocsp_string = "OCSP - URI:http ://ocsp.verisign.com"
         | 
| 46 | 
            +
                    AuthorityChecker.parse_authority_info_access(ocsp_string).should == "http ://ocsp.verisign.com"
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              describe ".parse_crl_distribution_points" do
         | 
| 52 | 
            +
                context 'with a valid crl info string' do
         | 
| 53 | 
            +
                  it "finds a matching crl url" do
         | 
| 54 | 
            +
                    crl_string = "URI:http://crl.globalsign.net/AkamaiSub3.crl\n"
         | 
| 55 | 
            +
                    AuthorityChecker.parse_crl_distribution_points(crl_string).should == "http://crl.globalsign.net/AkamaiSub3.crl"
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              describe "#validate" do
         | 
| 61 | 
            +
                before do
         | 
| 62 | 
            +
                  @authority_checker = AuthorityChecker.new(nil)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
                context "when ocsp info present" do
         | 
| 65 | 
            +
                  it "uses ocsp strategy to verify certificate" do
         | 
| 66 | 
            +
                    AuthorityChecker.expects(:parse_authority_info_access).returns "ocsp.verisign.com"
         | 
| 67 | 
            +
                    AuthorityChecker.stubs(:parse_crl_distribution_points)
         | 
| 68 | 
            +
                    cert = load_ocsp_enabled_cert
         | 
| 69 | 
            +
                    AuthorityChecker.any_instance.expects(:perform_ocsp_check).with(cert, "stub issuer cert", "ocsp.verisign.com").returns :successful
         | 
| 70 | 
            +
                    @authority_checker.validate(cert, "stub issuer cert").should == :successful
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                context "when only crl info present" do
         | 
| 75 | 
            +
                  it "falls back to the crl strategy to verify the certificate" do
         | 
| 76 | 
            +
                    AuthorityChecker.stubs(:parse_authority_info_access)
         | 
| 77 | 
            +
                    AuthorityChecker.expects(:parse_crl_distribution_points).returns "crl.verisign.com"
         | 
| 78 | 
            +
                    cert = load_non_ocsp_cert
         | 
| 79 | 
            +
                    AuthorityChecker.any_instance.expects(:perform_crl_check).with(cert, "crl.verisign.com").returns :successful
         | 
| 80 | 
            +
                    @authority_checker.validate(cert, "stub issuer certificate").should == :successful
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                context "when neither crl or ocsp info is in the certificate" do
         | 
| 85 | 
            +
                  it "blows up" do
         | 
| 86 | 
            +
                    AuthorityChecker.stubs(:parse_crl_distribution_points)
         | 
| 87 | 
            +
                    AuthorityChecker.any_instance.stubs(:perform_crl_check)
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    cert = load_non_ocsp_cert
         | 
| 90 | 
            +
                    expect { @authority_checker.validate(cert, "stub issuer cert") }.to raise_error(/Could not find valid oscp or crl extension/)
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
              end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
              describe "#perform_crl_check" do
         | 
| 96 | 
            +
                it "returns a status of revoked when the certificate is on the crl" do
         | 
| 97 | 
            +
                  crl_response = load_crl_with_cert_revoked
         | 
| 98 | 
            +
                  AuthorityChecker.any_instance.expects(:fetch_crl_content).returns(crl_response)
         | 
| 99 | 
            +
                  checker = AuthorityChecker.new(nil)
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  cert = load_ocsp_enabled_cert
         | 
| 102 | 
            +
                  checker.perform_crl_check(cert, nil).should == :revoked
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                it "returns a status of successful when the certificate is not on the crl" do
         | 
| 106 | 
            +
                  crl_response = load_crl_without_cert_revoked
         | 
| 107 | 
            +
                  AuthorityChecker.any_instance.expects(:fetch_crl_content).returns(crl_response)
         | 
| 108 | 
            +
                  checker = AuthorityChecker.new(nil)
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  cert = load_ocsp_enabled_cert
         | 
| 111 | 
            +
                  checker.perform_crl_check(cert, "crl url").should == :successful
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
              end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
              describe "#perform_ocsp_check" do
         | 
| 116 | 
            +
                it "writes certificates to files and invokes open ssl verifier" do
         | 
| 117 | 
            +
                  cert = mock()
         | 
| 118 | 
            +
                  cert.expects(:path).returns "cert path"
         | 
| 119 | 
            +
                  issuer_cert = mock()
         | 
| 120 | 
            +
                  issuer_cert.expects(:path).returns "issuer path"
         | 
| 121 | 
            +
                  response = mock()
         | 
| 122 | 
            +
                  response.expects(:path).returns "response path"
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  AuthorityChecker.any_instance.expects(:write_certificate_file).with(cert, "provider").returns cert
         | 
| 125 | 
            +
                  AuthorityChecker.any_instance.expects(:write_certificate_file).with(issuer_cert, "issuer").returns issuer_cert
         | 
| 126 | 
            +
                  AuthorityChecker.any_instance.expects(:create_response_file).returns(response).returns response
         | 
| 127 | 
            +
                  AuthorityChecker.any_instance.expects(:read_ocsp_response).returns 'successful'
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                  AuthorityChecker.any_instance.expects(:generate_ocsp_response).with("issuer path", "cert path", "response path", "ocsp.verisign.com")
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  checker = AuthorityChecker.new(nil)
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  checker.perform_ocsp_check(cert, issuer_cert, "ocsp.verisign.com").should == :successful
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
              end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
              describe "#read_ocsp_response" do
         | 
| 138 | 
            +
                context "when a valid response is written to a file" do
         | 
| 139 | 
            +
                  it "parses and reads a successful ocsp response correctly" do
         | 
| 140 | 
            +
                    checker = AuthorityChecker.new(nil)
         | 
| 141 | 
            +
                    checker.read_ocsp_response(load_sample_ocsp_response).should == 'successful'
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
              end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
             | 
| 147 | 
            +
            end
         | 
| 148 | 
            +
             | 
| @@ -0,0 +1,61 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'redis'
         | 
| 3 | 
            +
            require 'redis/namespace'
         | 
| 4 | 
            +
            require 'active_support/all'
         | 
| 5 | 
            +
            require 'lib/sslackey/cache/redis_revocation_cache'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            describe RedisRevocationCache do
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              describe "#initialize" do
         | 
| 10 | 
            +
                it "creates a new Redis with the correct host and port" do
         | 
| 11 | 
            +
                  Redis::Namespace.expects(:new).returns "a redis namespace"
         | 
| 12 | 
            +
                  Redis.expects(:new).with(:host => "redis.test.com", :port => 80, :threadsafe => true).returns "a redis"
         | 
| 13 | 
            +
                  RedisRevocationCache.new("redis.test.com", 80)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              context "caching methods" do
         | 
| 18 | 
            +
                before do
         | 
| 19 | 
            +
                  Redis::Namespace.stubs(:new).returns nil
         | 
| 20 | 
            +
                  @redis = mock()
         | 
| 21 | 
            +
                  @cache_service = RedisRevocationCache.new("some host", 90)
         | 
| 22 | 
            +
                  @cache_service.redis = @redis
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
                describe "#cached_response" do
         | 
| 25 | 
            +
                  it "gets the symbolized response from redis" do
         | 
| 26 | 
            +
                    @redis.expects(:get).with("12345").returns "successful"
         | 
| 27 | 
            +
                    @cache_service.expects(:get_key).returns "12345"
         | 
| 28 | 
            +
                    @cache_service.cached_response("some cert").should == :successful
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  it "returns nil response when no response cached" do
         | 
| 32 | 
            +
                    @redis.expects(:get).with("12345").returns nil
         | 
| 33 | 
            +
                    @cache_service.expects(:get_key).returns "12345"
         | 
| 34 | 
            +
                    @cache_service.cached_response("some cert").should  be_nil
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                describe "#cache_response" do
         | 
| 39 | 
            +
                  it "sets the key in redis along with an expiration time" do
         | 
| 40 | 
            +
                    cert = mock()
         | 
| 41 | 
            +
                    cert.stubs(:subject)
         | 
| 42 | 
            +
                    @redis.expects(:set).with("12345","successful").returns nil
         | 
| 43 | 
            +
                    @redis.expects(:expire).with("12345", @cache_service.expiration_seconds)
         | 
| 44 | 
            +
                    @cache_service.expects(:get_key).returns "12345"
         | 
| 45 | 
            +
                    @cache_service.cache_response(cert,"successful")
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                describe "#get_key" do
         | 
| 50 | 
            +
                  it "uses the hash of the certificate subject name as the caching key" do
         | 
| 51 | 
            +
                    cert = mock()
         | 
| 52 | 
            +
                    cert.stubs(:subject)
         | 
| 53 | 
            +
                    subject = mock()
         | 
| 54 | 
            +
                    cert.expects(:subject).returns subject
         | 
| 55 | 
            +
                    subject.expects(:hash).returns "12345"
         | 
| 56 | 
            +
                    @cache_service.get_key(cert).should == "12345"
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
            end
         | 
| Binary file |