cert_validator 0.0.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +55 -0
  8. data/Rakefile +6 -0
  9. data/cert_validator.gemspec +24 -0
  10. data/lib/cert_validator.rb +40 -0
  11. data/lib/cert_validator/asn1.rb +15 -0
  12. data/lib/cert_validator/crl/extractor.rb +48 -0
  13. data/lib/cert_validator/crl_validator.rb +93 -0
  14. data/lib/cert_validator/errors.rb +81 -0
  15. data/lib/cert_validator/ocsp.rb +13 -0
  16. data/lib/cert_validator/ocsp/extractor.rb +52 -0
  17. data/lib/cert_validator/ocsp/null_validator.rb +17 -0
  18. data/lib/cert_validator/ocsp/real_validator.rb +117 -0
  19. data/lib/cert_validator/version.rb +3 -0
  20. data/lib/tasks/ca.rb +112 -0
  21. data/lib/tasks/helper.rb +36 -0
  22. data/spec/cert_validator_spec.rb +73 -0
  23. data/spec/crl_extractor_spec.rb +42 -0
  24. data/spec/crl_validator_spec.rb +59 -0
  25. data/spec/null_ocsp_validator_spec.rb +19 -0
  26. data/spec/ocsp_extractor_spec.rb +31 -0
  27. data/spec/ocsp_validator_spec.rb +34 -0
  28. data/spec/spec_helper.rb +15 -0
  29. data/spec/support/ca/crl_only.crt +15 -0
  30. data/spec/support/ca/digicert.crl +0 -0
  31. data/spec/support/ca/empty.crt +13 -0
  32. data/spec/support/ca/github.crt +34 -0
  33. data/spec/support/ca/good.crt +16 -0
  34. data/spec/support/ca/mismatched.crl +13 -0
  35. data/spec/support/ca/ocsp_only.crt +15 -0
  36. data/spec/support/ca/revoked.crl +9 -0
  37. data/spec/support/ca/revoked.crt +16 -0
  38. data/spec/support/ca/root.crt +14 -0
  39. data/spec/support/ca/root.key +9 -0
  40. data/spec/support/certs.rb +17 -0
  41. data/spec/support/ocsp_guard.rb +2 -0
  42. data/spec/support/validator_expectations.rb +13 -0
  43. metadata +150 -0
@@ -0,0 +1,13 @@
1
+ require 'openssl'
2
+
3
+ if defined? OpenSSL::OCSP
4
+ require 'cert_validator/ocsp/real_validator.rb'
5
+
6
+ CertValidator::OcspValidator = CertValidator::RealOcspValidator
7
+ else
8
+ require 'cert_validator/ocsp/null_validator.rb'
9
+
10
+ # use the null validator as a fallback
11
+ CertValidator::OcspValidator = CertValidator::NullOcspValidator
12
+ end
13
+ 2
@@ -0,0 +1,52 @@
1
+ class CertValidator
2
+ class RealOcspValidator
3
+ class Extractor
4
+ attr_reader :certificate
5
+
6
+ def initialize(cert)
7
+ @certificate = cert
8
+ end
9
+
10
+ def endpoint
11
+ return nil unless has_ocsp_extension?
12
+
13
+ ocsp_extension_payload
14
+ end
15
+
16
+ def has_ocsp_extension?
17
+ !! (ocsp_extension && ocsp_extension_payload)
18
+ end
19
+
20
+ def ocsp_extension
21
+ @ocsp_extension ||= certificate.extensions.detect{ |e| e.oid == 'authorityInfoAccess' }
22
+ end
23
+
24
+ def decoded_extension
25
+ @decoded_extension ||= Asn1.new(Asn1.new(ocsp_extension).extension_payload).decode
26
+ end
27
+
28
+ def ocsp_extension_payload
29
+ return @ocsp_extension_payload if defined? @ocsp_extension_payload
30
+
31
+ intermediate = decoded_extension.value.detect do |v|
32
+ v.first.value == 'OCSP'
33
+ end.value[1].value
34
+
35
+ @ocsp_extension_payload = descend_to_string(intermediate)
36
+ end
37
+
38
+ def descend_to_string(asn_data)
39
+ return asn_data if asn_data.is_a? String
40
+ seen = Set.new
41
+ current = asn_data
42
+ loop do
43
+ raise RecursiveExtractError.new if seen.include? current
44
+ seen.add current
45
+ current = current.first.value
46
+
47
+ return current if current.is_a? String
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ class CertValidator
2
+ class NullOcspValidator
3
+ attr_reader :certificate
4
+ attr_reader :ca
5
+
6
+ def initialize(_cert, _ca)
7
+ end
8
+
9
+ def available?
10
+ false
11
+ end
12
+
13
+ def valid?
14
+ raise OcspNotAvailableError.new
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,117 @@
1
+ require 'uri'
2
+ require 'base64'
3
+ require 'net/http'
4
+ require 'cert_validator/ocsp/extractor'
5
+
6
+ class CertValidator
7
+ class RealOcspValidator
8
+ attr_reader :certificate
9
+ attr_reader :ca
10
+ attr_accessor :logger
11
+
12
+ include OcspFailures
13
+
14
+ def initialize(cert, ca)
15
+ @certificate = cert
16
+ @ca = ca
17
+
18
+ @extractor = Extractor.new @certificate
19
+ end
20
+
21
+ def available?
22
+ @extractor.has_ocsp_extension?
23
+ end
24
+
25
+ def valid?
26
+ return false unless available?
27
+
28
+ begin
29
+ validate!
30
+ rescue => e
31
+ log e
32
+ return false
33
+ end
34
+
35
+ return true
36
+ end
37
+
38
+ def validate!
39
+ raise FetchError.new unless http_body = fetch(request_uri)
40
+
41
+ body = OpenSSL::OCSP::Response.new http_body
42
+
43
+ check_ocsp_response body
44
+ check_ocsp_payload body.basic.status.first
45
+ end
46
+
47
+ private
48
+ def log(msg)
49
+ return unless logger
50
+
51
+ logger.info msg
52
+ end
53
+
54
+ def check_ocsp_response(body)
55
+ raise NonzeroStatus.new(body.status) unless body.status == 0
56
+ raise ResponseMismatch.new unless body.basic.verify *verify_args
57
+ raise MissingStatus.new unless body.basic.status.first
58
+
59
+ # http://rdoc.info/stdlib/openssl/OpenSSL/OCSP/Request:check_nonce
60
+ # greater than zero is acceptable
61
+ nonce_result = req.check_nonce body.basic
62
+ raise UnacceptableNonce.new(nonce_result) unless nonce_result > 0
63
+
64
+ return true
65
+ end
66
+
67
+ def check_ocsp_payload(status)
68
+ unless status[0].serial == certificate.serial
69
+ raise SerialMisatch(got, expected)
70
+ end
71
+
72
+ validity_range = (status[4]..status[5])
73
+ unless validity_range.cover? Time.now
74
+ raise NotValidNow.new(validity_range)
75
+ end
76
+
77
+ raise Revoked if status[1] == 1
78
+ raise UnexpectedStatus(status[1]) if status[1] != 0
79
+
80
+ return true
81
+ end
82
+
83
+ def verify_args
84
+ store = OpenSSL::X509::Store.new
85
+ store.add_cert ca
86
+
87
+ [[ca], store]
88
+ end
89
+
90
+ def req
91
+ return @req if defined? @req
92
+
93
+ @req = OpenSSL::OCSP::Request.new
94
+ @req.add_nonce
95
+ @req.add_certid cert_id
96
+
97
+ return @req
98
+ end
99
+
100
+ def request_uri
101
+ return @request_uri if defined? @request_uri
102
+ pem = Base64.encode64(req.to_der).strip
103
+ return @request_uri = URI(@extractor.endpoint + '/' + URI.encode_www_form_component(pem))
104
+ end
105
+
106
+ def fetch(uri)
107
+ resp = Net::HTTP.get_response URI(uri)
108
+ return resp.body if resp.code == '200'
109
+
110
+ return nil
111
+ end
112
+
113
+ def cert_id
114
+ @cert_id ||= OpenSSL::OCSP::CertificateId.new certificate, ca
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ class CertValidator
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,112 @@
1
+ require 'r509/certificate_authority/signer'
2
+ require 'r509/crl/administrator'
3
+ require 'erb'
4
+ require_relative 'helper'
5
+
6
+ namespace :ca do
7
+ desc 'Generate all the certificates for testing'
8
+ task :all => %w{ good ocsp_only crl_only empty revoked }
9
+
10
+ task :clean do
11
+ Dir.chdir 'spec/support/ca' do
12
+ sh 'rm -f *.crt *.crl *.key *.txt *.yaml'
13
+ end
14
+ end
15
+
16
+ desc 'Generate a signing CA for testing certificates'
17
+ task :root => 'spec/support/ca/root.key'
18
+ file 'spec/support/ca/root.key' do |t|
19
+ subject = OpenSSL::X509::Name.new
20
+ 'C=US/ST=Florida/L=Miami/O=r509-cert-validator/CN='.split('/').each do |s|
21
+ key, value = s.split '=', 2
22
+ subject.add_entry key, value
23
+ end
24
+ csr = CaHelper.csr
25
+ cert = R509::CertificateAuthority::Signer.selfsign(
26
+ csr: csr,
27
+ not_after: (Time.now.to_i + (86400 * 3650)),
28
+ message_digest: 'sha1'
29
+ )
30
+
31
+ csr.key.write_pem 'spec/support/ca/root.key'
32
+ cert.write_pem 'spec/support/ca/root.crt'
33
+
34
+ sh "touch spec/support/ca/rcv_spec_list.txt"
35
+ sh "touch spec/support/ca/rcv_spec_crlnumber.txt"
36
+ end
37
+ file 'spec/support/ca/root.crt' => 'spec/support/ca/root.key'
38
+ file 'spec/support/ca/rcv_spec_list.txt' => 'spec/support/ca/root.key'
39
+ file 'spec/support/ca/rcv_spec_crlnumber.txt' => 'spec/support/ca/root.key'
40
+
41
+ file 'spec/support/ca/config.yaml' => 'spec/support/ca/config.yaml.erb' do |s|
42
+ erb = ERB.new File.read s.prerequisites.first
43
+ b = binding
44
+ cert_path = File.expand_path 'spec/support/ca/'
45
+ File.open s.name, 'w' do |f|
46
+ f.write erb.result b
47
+ end
48
+ end
49
+
50
+ desc 'Generate a valid certificate with CRL and OCSP data'
51
+ task :good => 'spec/support/ca/good.crt'
52
+ file 'spec/support/ca/good.crt' => [:root, 'spec/support/ca/config.yaml'] do
53
+ ca = CaHelper.ca
54
+ csr = CaHelper.options_builder.build_and_enforce(
55
+ csr: CaHelper.csr,
56
+ profile_name: 'good'
57
+ )
58
+
59
+ cert = ca.sign csr
60
+ cert.write_pem 'spec/support/ca/good.crt'
61
+ end
62
+
63
+ desc 'Generate a valid certificate with only CRL data'
64
+ task :crl_only => 'spec/support/ca/crl_only.crt'
65
+ file 'spec/support/ca/crl_only.crt' => [:root, 'spec/support/ca/config.yaml'] do |t|
66
+ ca = CaHelper.ca
67
+ csr = CaHelper.options_builder.build_and_enforce(
68
+ csr: CaHelper.csr,
69
+ profile_name: 'crl_only'
70
+ )
71
+ cert = ca.sign csr
72
+ cert.write_pem 'spec/support/ca/crl_only.crt'
73
+ end
74
+
75
+ desc 'Generate a valid certificate with only OCSP data'
76
+ task :ocsp_only => 'spec/support/ca/ocsp_only.crt'
77
+ file 'spec/support/ca/ocsp_only.crt' => [:root, 'spec/support/ca/config.yaml'] do |t|
78
+ ca = CaHelper.ca
79
+ csr = CaHelper.options_builder.build_and_enforce(
80
+ csr: CaHelper.csr,
81
+ profile_name: 'ocsp_only'
82
+ )
83
+ cert = ca.sign csr
84
+ cert.write_pem 'spec/support/ca/ocsp_only.crt'
85
+ end
86
+
87
+ desc 'Generate a certificate and revoke it in both CRL and OCSP'
88
+ task :revoked => 'spec/support/ca/revoked.crt'
89
+ file 'spec/support/ca/revoked.crt' => [:root, 'spec/support/ca/config.yaml'] do |t|
90
+ ca = CaHelper.ca
91
+ csr = CaHelper.options_builder.build_and_enforce(
92
+ csr: CaHelper.csr,
93
+ profile_name: 'good'
94
+ )
95
+
96
+ cert = ca.sign csr
97
+ cert.write_pem 'spec/support/ca/revoked.crt'
98
+
99
+ admin = R509::CRL::Administrator.new CaHelper.pool['rcv_spec_ca']
100
+ admin.revoke_cert cert.serial
101
+ crl = admin.generate_crl
102
+ crl.write_pem 'spec/support/ca/rcv_spec.crl'
103
+ end
104
+
105
+ desc 'Generate a valid certificate with no CRL or OCSP data'
106
+ task :empty => 'spec/support/ca/empty.crt'
107
+ file 'spec/support/ca/empty.crt' => [:root, 'spec/support/ca/config.yaml'] do
108
+ ca = CaHelper.ca
109
+ cert = ca.sign csr: CaHelper.csr
110
+ cert.write_pem 'spec/support/ca/empty.crt'
111
+ end
112
+ end
@@ -0,0 +1,36 @@
1
+ require 'r509/csr'
2
+ require 'r509/certificate_authority/signer'
3
+ require 'r509/certificate_authority/options_builder'
4
+ require 'r509/config/ca_config'
5
+
6
+ module CaHelper
7
+ def self.csr
8
+ R509::CSR.new(
9
+ subject: {
10
+ C: 'US',
11
+ ST: 'Florida',
12
+ L: 'Miami',
13
+ O: 'r509-cert-validator',
14
+ CN: 'localhost'
15
+ },
16
+ bit_length: 512,
17
+ type: 'RSA',
18
+ message_digest: 'sha1'
19
+ )
20
+ end
21
+
22
+ def self.ca
23
+ @ca ||= R509::CertificateAuthority::Signer.new pool['rcv_spec_ca']
24
+ end
25
+
26
+ def self.options_builder
27
+ @builder ||= R509::CertificateAuthority::OptionsBuilder.new pool['rcv_spec_ca']
28
+ end
29
+
30
+ def self.pool
31
+ @pool ||= R509::Config::CAConfigPool.from_yaml(
32
+ 'certificate_authorities',
33
+ File.read('spec/support/ca/config.yaml')
34
+ )
35
+ end
36
+ end
@@ -0,0 +1,73 @@
1
+ describe CertValidator do
2
+ subject{ described_class.new good_cert, ca }
3
+ let(:good_cert){ cert 'good' }
4
+ let(:ca){ cert 'root' }
5
+
6
+ it 'accepts a certificate on construction' do
7
+ expect{ described_class.new good_cert, ca }.to_not raise_error
8
+ end
9
+ it 'provides read-only access to the certificate' do
10
+ expect(subject.certificate).to eq good_cert
11
+ end
12
+
13
+ describe 'CRL functionality' do
14
+ let(:matched_crl_validator) do
15
+ described_class.new(good_cert, ca).tap do |validator|
16
+ validator.crl = crl 'revoked'
17
+ end
18
+ end
19
+
20
+ let(:mismatched_crl_validator) do
21
+ described_class.new(good_cert, ca).tap do |validator|
22
+ validator.crl = crl 'mismatched'
23
+ end
24
+ end
25
+
26
+ let(:revoked_crl_validator) do
27
+ described_class.new(cert('revoked'), ca).tap do |validator|
28
+ validator.crl = crl 'revoked'
29
+ end
30
+ end
31
+
32
+ it 'returns if CRL validation is available or not' do
33
+ expect(subject.crl_available?).to be
34
+ end
35
+
36
+ it 'positively valdiates a correct CRL' do
37
+ expect(matched_crl_validator.crl_valid?).to be
38
+ end
39
+
40
+ it 'negatively validates a mismatched CRL' do
41
+ expect(mismatched_crl_validator.crl_valid?).to_not be
42
+ end
43
+
44
+ it 'negatively validates a revoked certificate' do
45
+ expect(revoked_crl_validator.crl_valid?).to_not be
46
+ end
47
+ end
48
+
49
+ describe 'OCSP functionality' do
50
+ it 'returns if OCSP validation is available or not' do
51
+ expect(subject.ocsp_available?).to eq(true).or eq(false)
52
+ end
53
+
54
+ describe 'when available', real_ocsp: true do
55
+ it 'positively validates a non-revoked OCSP response' do
56
+ v = described_class.new cert('good'), ca
57
+ expect(v.ocsp_valid?).to be
58
+ end
59
+ pending 'negatively validates a mismatched OCSP response'
60
+
61
+ it 'negatively validates a revoked certificate' do
62
+ v = described_class.new cert('revoked'), ca
63
+ expect(v.ocsp_valid?).to_not be
64
+ end
65
+ end
66
+
67
+ describe 'when not available', null_ocsp: true do
68
+ it 'raises when asked to validate OCSP' do
69
+ expect{ subject.ocsp_valid? }.to raise_error CertValidator::OcspNotAvailableError
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,42 @@
1
+ describe CertValidator::CrlValidator::Extractor do
2
+ let(:good_cert){ cert 'good' }
3
+
4
+ it 'accepts a certificate on construction' do
5
+ expect{ described_class.new good_cert }.to_not raise_error
6
+ end
7
+
8
+ describe 'with multiple distribution points' do
9
+ subject{ described_class.new cert 'github' }
10
+
11
+ it 'extracts the CRL distribution points' do
12
+ points = nil
13
+ expect{ points = subject.distribution_points }.to_not raise_error
14
+
15
+ expect(points).to be_an Enumerable
16
+ expect(points.length).to eq 2
17
+
18
+ expect(points).to include 'http://crl3.digicert.com/sha2-ev-server-g1.crl'
19
+ expect(points).to include 'http://crl4.digicert.com/sha2-ev-server-g1.crl'
20
+ end
21
+ end
22
+
23
+ describe 'with one distribution point' do
24
+ subject{ described_class.new good_cert }
25
+
26
+ it 'extracts the CRL distribution point' do
27
+ points = nil
28
+ expect{ points = subject.distribution_points }.to_not raise_error
29
+ expect(points).to eq ['http://cert-validator-test.herokuapp.com/revoked.crl']
30
+ end
31
+ end
32
+
33
+ describe 'with no distribution points' do
34
+ subject{ described_class.new cert 'ocsp_only' }
35
+
36
+ it 'extracts no CRL distribution points' do
37
+ points = nil
38
+ expect{ points = subject.distribution_points }.to_not raise_error
39
+ expect(points).to be_empty
40
+ end
41
+ end
42
+ end