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.
@@ -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