sslackey 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Sslackey
2
+ VERSION = "0.6.0"
3
+ end
data/lib/sslackey.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "sslackey/authority_checker"
2
+ require "sslackey/revocation_checker"
3
+ require "sslackey/cache/redis_revocation_cache"
@@ -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