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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +4 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +6 -0
- data/cert_validator.gemspec +24 -0
- data/lib/cert_validator.rb +40 -0
- data/lib/cert_validator/asn1.rb +15 -0
- data/lib/cert_validator/crl/extractor.rb +48 -0
- data/lib/cert_validator/crl_validator.rb +93 -0
- data/lib/cert_validator/errors.rb +81 -0
- data/lib/cert_validator/ocsp.rb +13 -0
- data/lib/cert_validator/ocsp/extractor.rb +52 -0
- data/lib/cert_validator/ocsp/null_validator.rb +17 -0
- data/lib/cert_validator/ocsp/real_validator.rb +117 -0
- data/lib/cert_validator/version.rb +3 -0
- data/lib/tasks/ca.rb +112 -0
- data/lib/tasks/helper.rb +36 -0
- data/spec/cert_validator_spec.rb +73 -0
- data/spec/crl_extractor_spec.rb +42 -0
- data/spec/crl_validator_spec.rb +59 -0
- data/spec/null_ocsp_validator_spec.rb +19 -0
- data/spec/ocsp_extractor_spec.rb +31 -0
- data/spec/ocsp_validator_spec.rb +34 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/ca/crl_only.crt +15 -0
- data/spec/support/ca/digicert.crl +0 -0
- data/spec/support/ca/empty.crt +13 -0
- data/spec/support/ca/github.crt +34 -0
- data/spec/support/ca/good.crt +16 -0
- data/spec/support/ca/mismatched.crl +13 -0
- data/spec/support/ca/ocsp_only.crt +15 -0
- data/spec/support/ca/revoked.crl +9 -0
- data/spec/support/ca/revoked.crt +16 -0
- data/spec/support/ca/root.crt +14 -0
- data/spec/support/ca/root.key +9 -0
- data/spec/support/certs.rb +17 -0
- data/spec/support/ocsp_guard.rb +2 -0
- data/spec/support/validator_expectations.rb +13 -0
- 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,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
|
data/lib/tasks/ca.rb
ADDED
@@ -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
|
data/lib/tasks/helper.rb
ADDED
@@ -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
|