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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0f3fd5982c4c87671c828d1e0a53597b82150ab9
|
4
|
+
data.tar.gz: 5b26c0a0cf2ddc39944a865d74eabec1c1a7cb0f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b1cfb84bbc4dc74d63495ee1259c72f71c2e71c602296157d9695c4f4651e5db0a55970b08d0854d2f94ae675818b31b6da39bc598a5c3fb56c6d0d70e733b1
|
7
|
+
data.tar.gz: b278b942d7e86e9c52c479f083d535a2a3024210f9c5eef9ea117742bfd44f635e25314464d164d4234a9a715bc623e3be814f7ba559aa0dbde52c14cbde6dab
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Bryce Kerley
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# CertValidator
|
2
|
+
|
3
|
+
Validate an X509 certificate against its CRL or OCSP endpoint. Raise exceptions
|
4
|
+
if OCSP isn't available.
|
5
|
+
|
6
|
+
## Compatibility
|
7
|
+
|
8
|
+
This project aims for compatibility with:
|
9
|
+
|
10
|
+
* Ruby 1.9.3
|
11
|
+
* Ruby 2.0
|
12
|
+
* Ruby 2.1
|
13
|
+
* JRuby 1.7 in Ruby 1.9 and 2.0 modes
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
gem 'cert_validator'
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install cert_validator
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
some_cert # an OpenSSL::X509::Certificate
|
33
|
+
|
34
|
+
validator = CertValidator.new some_cert
|
35
|
+
|
36
|
+
validator.crl_available? # return true if certificate has a CRL endpoint
|
37
|
+
|
38
|
+
validator.crl_valid? # validate against the certificate's CRL endpoint
|
39
|
+
|
40
|
+
validator.crl_file = some_path # allow overriding the CRL
|
41
|
+
|
42
|
+
# return true if certificate has an OCSP endpoint and the Ruby OpenSSL module
|
43
|
+
# supports OCSP
|
44
|
+
validator.ocsp_available?
|
45
|
+
|
46
|
+
validator.ocsp_valid? # validate against the certificate's OCSP endpoint
|
47
|
+
```
|
48
|
+
|
49
|
+
## Contributing
|
50
|
+
|
51
|
+
1. Fork it ( https://github.com/[my-github-username]/cert_validator/fork )
|
52
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
53
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
54
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
55
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cert_validator/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "cert_validator"
|
8
|
+
spec.version = CertValidator::VERSION
|
9
|
+
spec.authors = ["Bryce Kerley"]
|
10
|
+
spec.email = ["bkerley@brycekerley.net"]
|
11
|
+
spec.summary = %q{Validate X509 certificates against CRL and OCSP.}
|
12
|
+
spec.description = %q{Validate an X509 certificate against its listed OCSP endpoint and/or a CRL.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec", '~> 3.0.0'
|
24
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
%w{version errors asn1 crl_validator ocsp}.each { |f| require "cert_validator/#{f}" }
|
2
|
+
|
3
|
+
class CertValidator
|
4
|
+
attr_reader :certificate
|
5
|
+
attr_reader :ca
|
6
|
+
|
7
|
+
def initialize(cert, ca)
|
8
|
+
@certificate = cert
|
9
|
+
@ca = ca
|
10
|
+
end
|
11
|
+
|
12
|
+
def crl=(crl)
|
13
|
+
crl_validator.crl = crl
|
14
|
+
end
|
15
|
+
|
16
|
+
def crl_available?
|
17
|
+
crl_validator.available?
|
18
|
+
end
|
19
|
+
|
20
|
+
def crl_valid?
|
21
|
+
crl_validator.valid?
|
22
|
+
end
|
23
|
+
|
24
|
+
def ocsp_available?
|
25
|
+
ocsp_validator.available?
|
26
|
+
end
|
27
|
+
|
28
|
+
def ocsp_valid?
|
29
|
+
ocsp_validator.valid?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def crl_validator
|
34
|
+
@crl_validator ||= CrlValidator.new certificate, ca
|
35
|
+
end
|
36
|
+
|
37
|
+
def ocsp_validator
|
38
|
+
@ocsp_validator ||= OcspValidator.new certificate, ca
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class CertValidator
|
2
|
+
class CrlValidator
|
3
|
+
class Extractor
|
4
|
+
attr_reader :certificate
|
5
|
+
|
6
|
+
def initialize(cert)
|
7
|
+
@certificate = cert
|
8
|
+
end
|
9
|
+
|
10
|
+
def distribution_points
|
11
|
+
return [] unless has_crl_extension?
|
12
|
+
decoded_payload.value.map{ |v| descend_to_string v.value }
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_distribution_points?
|
16
|
+
! distribution_points.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def has_crl_extension?
|
20
|
+
!! crl_extension
|
21
|
+
end
|
22
|
+
|
23
|
+
def crl_extension
|
24
|
+
@crl_extension ||= certificate.extensions.detect{ |e| e.oid == 'crlDistributionPoints' }
|
25
|
+
end
|
26
|
+
|
27
|
+
def crl_extension_payload
|
28
|
+
@crl_extension_payload ||= Asn1.new(crl_extension.to_der).extension_payload
|
29
|
+
end
|
30
|
+
|
31
|
+
def decoded_payload
|
32
|
+
@decoded_payload ||= Asn1.new(crl_extension_payload).decode
|
33
|
+
end
|
34
|
+
|
35
|
+
def descend_to_string(asn_data)
|
36
|
+
seen = Set.new
|
37
|
+
current = asn_data
|
38
|
+
loop do
|
39
|
+
raise RecursiveExtractError.new if seen.include? current
|
40
|
+
seen.add current
|
41
|
+
current = current.first.value
|
42
|
+
|
43
|
+
return current if current.is_a? String
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'cert_validator/crl/extractor'
|
2
|
+
class CertValidator
|
3
|
+
class CrlValidator
|
4
|
+
attr_reader :certificate
|
5
|
+
attr_reader :ca
|
6
|
+
attr_writer :crl
|
7
|
+
|
8
|
+
attr_reader :revoked_time
|
9
|
+
|
10
|
+
def initialize(cert, ca)
|
11
|
+
@certificate = cert
|
12
|
+
@ca = ca
|
13
|
+
end
|
14
|
+
|
15
|
+
def available?
|
16
|
+
return true if has_crl_data?
|
17
|
+
return false unless extractor.has_distribution_points?
|
18
|
+
|
19
|
+
begin
|
20
|
+
return false unless vivified_crl
|
21
|
+
rescue OpenSSL::X509::CRLError
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
return true
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid?
|
29
|
+
return false unless available?
|
30
|
+
|
31
|
+
begin
|
32
|
+
return false unless vivified_crl
|
33
|
+
rescue OpenSSL::X509::CRLError
|
34
|
+
return false
|
35
|
+
end
|
36
|
+
|
37
|
+
return false unless matches_ca?
|
38
|
+
|
39
|
+
return false if revoked?
|
40
|
+
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
|
44
|
+
def crl
|
45
|
+
return @crl if defined? @crl
|
46
|
+
|
47
|
+
distribution_points = extractor.distribution_points
|
48
|
+
distribution_points.first do |dp|
|
49
|
+
@crl = fetch dp
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def has_crl_data?
|
55
|
+
!! crl
|
56
|
+
end
|
57
|
+
|
58
|
+
def extractor
|
59
|
+
@extractor ||= Extractor.new certificate
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch(uri)
|
63
|
+
resp = Net::HTTP.get_response URI(uri)
|
64
|
+
return resp.body if resp.code == 200
|
65
|
+
|
66
|
+
return nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def vivified_crl
|
70
|
+
return @vivified_crl if defined? @vivified_crl
|
71
|
+
|
72
|
+
if crl.is_a? OpenSSL::X509::CRL
|
73
|
+
return @vivified_crl = crl
|
74
|
+
else
|
75
|
+
return @vivified_crl = OpenSSL::X509::CRL.new(crl)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def revoked?
|
80
|
+
vivified_crl.revoked.find do |entry|
|
81
|
+
entry.serial == certificate.serial
|
82
|
+
end.tap do |entry|
|
83
|
+
next if entry.nil?
|
84
|
+
@revoked_time = entry.time
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def matches_ca?
|
89
|
+
vivified_crl.verify ca.public_key
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class CertValidator
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class OcspNotAvailableError < Error
|
6
|
+
def initialize
|
7
|
+
super "OCSP functionality isn't available in this version of Ruby."
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class RecursiveExtractError < Error
|
12
|
+
def initialize
|
13
|
+
super "Tried to extract a value from a recursive structure. Please file a bug!"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class CrlFetchError < Error
|
18
|
+
def initialize
|
19
|
+
super "Couldn't fetch CRL."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module OcspFailures
|
24
|
+
class OcspFailure < Error
|
25
|
+
end
|
26
|
+
|
27
|
+
class FetchError < OcspFailure
|
28
|
+
def initialize
|
29
|
+
super "Couldn't fetch OCSP."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class NonzeroStatus < OcspFailure
|
34
|
+
def initialize(status)
|
35
|
+
super "OCSP status was #{status}, expected 0"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ResponseMismatch < OcspFailure
|
40
|
+
def initialize
|
41
|
+
super "OCSP response did not match certificate issuer"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class MissingStatus < OcspFailure
|
46
|
+
def initialize
|
47
|
+
super "OCSP response was missing status section"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class UnacceptableNonce < OcspFailure
|
52
|
+
def initialize
|
53
|
+
super "OCSP response had unacceptable result from nonce check"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class SerialMismatch < OcspFailure
|
58
|
+
def initialize(got, expected)
|
59
|
+
super "OCSP response serial was #{got.inspect}, expected #{expected.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class NotValidNow < OcspFailure
|
64
|
+
def initialize(validity_range)
|
65
|
+
super "OCSP response only valid from #{validity_range.begin} to #{validity_range.end}, currently #{Time.now}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Revoked < OcspFailure
|
70
|
+
def initialize
|
71
|
+
super "OCSP response indicates cert was revoked"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class UnexpectedStatus < OcspFailure
|
76
|
+
def initialize(got)
|
77
|
+
super "OCSP response was #{got.inspect}, expected 0"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|