sslcheck 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +177 -0
- data/Rakefile +14 -0
- data/acceptance/acceptance_helper.rb +1 -0
- data/acceptance/checking_certificates_spec.rb +23 -0
- data/acceptance/client_spec.rb +67 -0
- data/blinky_tests +3 -0
- data/lib/sslcheck.rb +20 -0
- data/lib/sslcheck/certificate.rb +121 -0
- data/lib/sslcheck/certificate_client.rb +10 -0
- data/lib/sslcheck/check.rb +65 -0
- data/lib/sslcheck/client.rb +68 -0
- data/lib/sslcheck/generic_error.rb +15 -0
- data/lib/sslcheck/parser.rb +96 -0
- data/lib/sslcheck/validator.rb +111 -0
- data/lib/sslcheck/validators/ca_bundle.rb +23 -0
- data/lib/sslcheck/validators/common_name.rb +27 -0
- data/lib/sslcheck/validators/errors.rb +15 -0
- data/lib/sslcheck/validators/expiration_date.rb +10 -0
- data/lib/sslcheck/validators/generic_validator.rb +15 -0
- data/lib/sslcheck/validators/issue_date.rb +10 -0
- data/lib/sslcheck/version.rb +3 -0
- data/run_acceptance_on_ci +7 -0
- data/sentinal +10 -0
- data/spec/ca_bundle_validator_spec.rb +24 -0
- data/spec/cert_fixtures.rb +814 -0
- data/spec/certificate_spec.rb +134 -0
- data/spec/check_spec.rb +172 -0
- data/spec/common_name_validator_spec.rb +40 -0
- data/spec/expiration_date_validator_spec.rb +36 -0
- data/spec/issue_date_validator_spec.rb +36 -0
- data/spec/parser_spec.rb +0 -0
- data/spec/response_spec.rb +13 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/validator_spec.rb +84 -0
- data/sslcheck.gemspec +26 -0
- metadata +165 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 99dd5040fd8190da53d30ee4fedff05c25705c33
|
4
|
+
data.tar.gz: d1f0f5e802311ae580f6a1501c240e3f535f683d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2759629f5a99a2c23fba38453b56bda4287d64dc225a1d4584048dbf1b8a8eaf1fc8c62ab0ad6e9fb8368ab778e61006018be91c4062ddc0333f297e2ac6c6fe
|
7
|
+
data.tar.gz: eebf070b57b40f597fe535325a267f7caa917205c34fbb86ed9ff88caf5ee8c0080e325c44566abd110b29f7b3b3a08f966697f0d078580bb96af80f9b512f34
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Clayton Lengel-Zigich
|
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,177 @@
|
|
1
|
+
## SSL Check
|
2
|
+
|
3
|
+
An easy way to verify the installation of SSL certificates.
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/clayton/sslcheck.svg?branch=master)](https://travis-ci.org/clayton/sslcheck)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Install the gem on your system
|
10
|
+
|
11
|
+
```
|
12
|
+
gem install sslcheck
|
13
|
+
```
|
14
|
+
|
15
|
+
Use the gem with bundler by adding the following to your `Gemfile`
|
16
|
+
|
17
|
+
```
|
18
|
+
gem "sslcheck"
|
19
|
+
```
|
20
|
+
|
21
|
+
## Using SSLCheck
|
22
|
+
|
23
|
+
The SSLCheck gem has a simple API.
|
24
|
+
|
25
|
+
First create a check:
|
26
|
+
|
27
|
+
```
|
28
|
+
check = SSLCheck::Check.new
|
29
|
+
```
|
30
|
+
|
31
|
+
Then, check a url:
|
32
|
+
|
33
|
+
```
|
34
|
+
check.check("github.com")
|
35
|
+
```
|
36
|
+
|
37
|
+
Is the certificate valid?
|
38
|
+
|
39
|
+
```
|
40
|
+
check.valid?
|
41
|
+
=> true
|
42
|
+
```
|
43
|
+
|
44
|
+
Are there any errors?
|
45
|
+
|
46
|
+
```
|
47
|
+
check.errors
|
48
|
+
=> []
|
49
|
+
```
|
50
|
+
|
51
|
+
What are the details of the certificate?
|
52
|
+
|
53
|
+
The peer certificate found during the check is available with a rich
|
54
|
+
(undocumented) API. See `SSLCheck::Certificate` for more details. A helper
|
55
|
+
method is provided on the `Certificate` to get at most of the important details.
|
56
|
+
|
57
|
+
```
|
58
|
+
checker.peer_cert.to_h
|
59
|
+
{:common_name=>"www.sslinsight.com",
|
60
|
+
:organization_unit=>"Domain Control Validated",
|
61
|
+
:not_before=>
|
62
|
+
#<DateTime: 2014-07-25T00:00:00+00:00 ((2456864j,0s,0n),+0s,2299161j)>,
|
63
|
+
:not_after=>
|
64
|
+
#<DateTime: 2015-07-25T23:59:59+00:00 ((2457229j,86399s,0n),+0s,2299161j)>,
|
65
|
+
:issued=>true,
|
66
|
+
:expired=>false,
|
67
|
+
:issuer=>
|
68
|
+
{:common_name=>"COMODO RSA Domain Validation Secure Server CA",
|
69
|
+
:country=>"GB",
|
70
|
+
:state=>"Greater Manchester",
|
71
|
+
:locality=>"Salford",
|
72
|
+
:organization=>"COMODO CA Limited"}}
|
73
|
+
=> {:common_name=>"www.sslinsight.com", :organization_unit=>"Domain Control Validated", :not_before=>#<DateTime: 2014-07-25T00:00:00+00:00 ((2456864j,0s,0n),+0s,2299161j)>, :not_after=>#<DateTime: 2015-07-25T23:59:59+00:00 ((2457229j,86399s,0n),+0s,2299161j)>, :issued=>true, :expired=>false, :issuer=>{:common_name=>"COMODO RSA Domain Validation Secure Server CA", :country=>"GB", :state=>"Greater Manchester", :locality=>"Salford", :organization=>"COMODO CA Limited"}}
|
74
|
+
```
|
75
|
+
|
76
|
+
What are the details of the CA Bundle?
|
77
|
+
|
78
|
+
Each certificate in the CA Bundle is available as an `SSLCheck::Certificate`
|
79
|
+
instance if needed.
|
80
|
+
|
81
|
+
|
82
|
+
### Potential Validation Errors
|
83
|
+
|
84
|
+
**SSLCheck::Errors::Validation::CommonNameMismatch**
|
85
|
+
|
86
|
+
Occurs when the common name supplied in the check does not match the common name
|
87
|
+
on the certificate, any alternate subject names or match a regex based on the
|
88
|
+
wildcard domain if the certificate was issued for a wildcard domain.
|
89
|
+
|
90
|
+
**SSLCheck::Errors::Validation::NotYetIssued**
|
91
|
+
|
92
|
+
Occurs when the certificates `not_before` date is in the future.
|
93
|
+
|
94
|
+
**SSLCheck::Errors::Validation::CertificateExpired**
|
95
|
+
|
96
|
+
Occurs when the certificates `not_after` date is in the past.
|
97
|
+
|
98
|
+
**SSLCheck::Errors::Validation::CABundleVerification**
|
99
|
+
|
100
|
+
Occurs when the CA Bundle (peer certificate chain) that is gathered during the
|
101
|
+
connection to the server cannot verify the certificate. This is a very common
|
102
|
+
error when setting up and install SSL Certificates and occurs when the web
|
103
|
+
server is not configured correctly or the CA Bundle certificates from the
|
104
|
+
certificate issuer were not configured or installed correctly.
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
### Validating Certificates
|
109
|
+
|
110
|
+
A certificate is considered valid if the certificate is present, the CA
|
111
|
+
bundle is present and all of the default validations pass.
|
112
|
+
|
113
|
+
By default, SSLCheck validates the following:
|
114
|
+
|
115
|
+
* Common Name matches (accounting for alternate names and wildcard certificates)
|
116
|
+
* CA Bundle Verification (can the CA bundle verify the certificate?)
|
117
|
+
* Issue Date (is in the past)
|
118
|
+
* Expiration Date (is in the future)
|
119
|
+
|
120
|
+
## Custom Client
|
121
|
+
|
122
|
+
By default, all certificates are fetched by opening an SSL Socket Connection and
|
123
|
+
grabbing the peer certificate and peer certificate chain (CA Bundle.)
|
124
|
+
|
125
|
+
A custom client should return a response that exposes the peer certificate,
|
126
|
+
peer certificate chain and url that was fetched. See `SSLCheck::Client::Response`
|
127
|
+
for more information.
|
128
|
+
|
129
|
+
### Using a custom client
|
130
|
+
|
131
|
+
```
|
132
|
+
check = SSLCheck::Check.new(MyClient.new)
|
133
|
+
```
|
134
|
+
|
135
|
+
## Custom Validators and Validations
|
136
|
+
|
137
|
+
A custom validator can be used to allow for additional validations or override
|
138
|
+
the default validations. For more information see `SSLCheck::Validator`
|
139
|
+
|
140
|
+
### Using a custom validator
|
141
|
+
|
142
|
+
```
|
143
|
+
# passing nil as the first argument to use the default client
|
144
|
+
check = SSLCheck::Check.new(nil, MyValidator.new)
|
145
|
+
```
|
146
|
+
|
147
|
+
## Contributing
|
148
|
+
|
149
|
+
* Fork
|
150
|
+
* Run the tests (`rake`)
|
151
|
+
* Commit & Push
|
152
|
+
* Submit a pull request
|
153
|
+
|
154
|
+
|
155
|
+
## License
|
156
|
+
|
157
|
+
The MIT License (MIT)
|
158
|
+
|
159
|
+
Copyright (c) 2014 Clayton Lengel-Zigich
|
160
|
+
|
161
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
162
|
+
of this software and associated documentation files (the "Software"), to deal
|
163
|
+
in the Software without restriction, including without limitation the rights
|
164
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
165
|
+
copies of the Software, and to permit persons to whom the Software is
|
166
|
+
furnished to do so, subject to the following conditions:
|
167
|
+
|
168
|
+
The above copyright notice and this permission notice shall be included in all
|
169
|
+
copies or substantial portions of the Software.
|
170
|
+
|
171
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
172
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
173
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
174
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
175
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
176
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
177
|
+
SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
5
|
+
t.pattern = Dir.glob('spec/**/*_spec.rb')
|
6
|
+
t.rspec_opts = '--format documentation'
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:acceptance) do |t|
|
10
|
+
t.pattern = Dir.glob('acceptance/**/*_spec.rb')
|
11
|
+
t.rspec_opts = '--format documentation'
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => [:spec, :acceptance]
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative '../spec/spec_helper'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module SSLCheck
|
4
|
+
describe 'Checking Certificates' do
|
5
|
+
context "when the certificate is valid" do
|
6
|
+
before do
|
7
|
+
@check = Check.new.check("http://www.sslinsight.com")
|
8
|
+
end
|
9
|
+
it 'should be valid' do
|
10
|
+
expect(@check.valid?).to be
|
11
|
+
end
|
12
|
+
it 'should not have any errors' do
|
13
|
+
expect(@check.errors).to be_empty
|
14
|
+
end
|
15
|
+
it 'should know the peer certificate' do
|
16
|
+
expect(@check.peer_cert).to be
|
17
|
+
end
|
18
|
+
it 'should know the ca bundle' do
|
19
|
+
expect(@check.ca_bundle).to be
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module SSLCheck
|
4
|
+
describe 'Client' do
|
5
|
+
context "Getting Certificates" do
|
6
|
+
context "When Things Go Well" do
|
7
|
+
it 'should have the host name' do
|
8
|
+
sut = Client.new
|
9
|
+
response = sut.get('https://www.sslinsight.com')
|
10
|
+
expect(response.host_name).to eq("www.sslinsight.com")
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should have the peer certificate' do
|
14
|
+
sut = Client.new
|
15
|
+
response = sut.get('https://www.sslinsight.com')
|
16
|
+
expect(response.peer_cert).to be_a(SSLCheck::Certificate)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should have the peer cert chain' do
|
20
|
+
sut = Client.new
|
21
|
+
response = sut.get('https://www.sslinsight.com')
|
22
|
+
expect(response.ca_bundle.first).to be_a(SSLCheck::Certificate)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when the URL is missing the protocol" do
|
27
|
+
it 'should still provide a hostname for the response' do
|
28
|
+
sut = Client.new
|
29
|
+
response = sut.get('www.sslinsight.com')
|
30
|
+
expect(response.host_name).to eq("www.sslinsight.com")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when the URL has an http protocol" do
|
35
|
+
it 'should still provide a hostname for the response' do
|
36
|
+
sut = Client.new
|
37
|
+
response = sut.get('http://www.sslinsight.com')
|
38
|
+
expect(response.host_name).to eq("www.sslinsight.com")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "When the URL is malformed" do
|
43
|
+
it 'should raise an invalid URI error' do
|
44
|
+
sut = Client.new
|
45
|
+
response = sut.get('this is not even close to a valid url.com')
|
46
|
+
expect(response.errors.first).to be_a(SSLCheck::Errors::Connection::InvalidURI)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "When there is no SSL Certificate present" do
|
51
|
+
it 'should raise a verification error' do
|
52
|
+
sut = Client.new
|
53
|
+
response = sut.get('http://www.claytonlz.com')
|
54
|
+
expect(response.errors.first).to be_a(SSLCheck::Errors::Connection::SSLVerify)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "When the certificate is self-signed" do
|
59
|
+
it 'should raise a verification error' do
|
60
|
+
sut = Client.new
|
61
|
+
response = sut.get('https://www.pcwebshop.co.uk')
|
62
|
+
expect(response.errors.first).to be_a(SSLCheck::Errors::Connection::SSLVerify)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/blinky_tests
ADDED
data/lib/sslcheck.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "sslcheck/version"
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
module SSLCheck
|
5
|
+
# Your code goes here...
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'sslcheck/certificate'
|
9
|
+
require 'sslcheck/validator'
|
10
|
+
require 'sslcheck/certificate_client'
|
11
|
+
require 'sslcheck/parser'
|
12
|
+
require 'sslcheck/generic_error'
|
13
|
+
require 'sslcheck/check'
|
14
|
+
require 'sslcheck/client'
|
15
|
+
require 'sslcheck/validators/generic_validator'
|
16
|
+
require 'sslcheck/validators/errors'
|
17
|
+
require 'sslcheck/validators/common_name'
|
18
|
+
require 'sslcheck/validators/issue_date'
|
19
|
+
require 'sslcheck/validators/expiration_date'
|
20
|
+
require 'sslcheck/validators/ca_bundle'
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module SSLCheck
|
4
|
+
class Certificate
|
5
|
+
def initialize(cert, clock=nil)
|
6
|
+
@cert = bootstrap_certificate(cert)
|
7
|
+
@clock = clock || DateTime
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_x509
|
11
|
+
OpenSSL::X509::Certificate.new @cert.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
{
|
16
|
+
:common_name => common_name,
|
17
|
+
:organization_unit => organizational_unit,
|
18
|
+
:not_before => not_before,
|
19
|
+
:not_after => not_after,
|
20
|
+
:issued => true,
|
21
|
+
:expired => false,
|
22
|
+
:issuer => {
|
23
|
+
:common_name => issuer_common_name,
|
24
|
+
:country => issuer_country,
|
25
|
+
:state => issuer_state,
|
26
|
+
:locality => issuer_locality,
|
27
|
+
:organization => issuer_organization
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
@cert.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def subject
|
37
|
+
@cert.subject.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def organizational_unit
|
41
|
+
match = subject.match(/OU=([\w\s]+)/)
|
42
|
+
match.captures.first if match
|
43
|
+
end
|
44
|
+
|
45
|
+
def common_name
|
46
|
+
subject.scan(/CN=(.*)/)[0][0]
|
47
|
+
end
|
48
|
+
|
49
|
+
def alternate_common_names
|
50
|
+
ext = @cert.extensions.find{|ext| ext.oid == "subjectAltName" }
|
51
|
+
return [] unless ext
|
52
|
+
alternates = ext.value.split(",")
|
53
|
+
names = alternates.map{|a| a.scan(/DNS:(.*)/)[0][0]}
|
54
|
+
names
|
55
|
+
end
|
56
|
+
|
57
|
+
def issuer
|
58
|
+
@cert.issuer.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
def issuer_country
|
62
|
+
match = issuer.match(/C=([\w\s]+)/)
|
63
|
+
match.captures.first if match
|
64
|
+
end
|
65
|
+
|
66
|
+
def issuer_state
|
67
|
+
match = issuer.match(/ST=([\w\s]+)/)
|
68
|
+
match.captures.first if match
|
69
|
+
end
|
70
|
+
|
71
|
+
def issuer_locality
|
72
|
+
match = issuer.match(/L=([\w\s]+)/)
|
73
|
+
match.captures.first if match
|
74
|
+
end
|
75
|
+
|
76
|
+
def issuer_organization
|
77
|
+
match = issuer.match(/O=([^\/]+)/)
|
78
|
+
match.captures.first if match
|
79
|
+
end
|
80
|
+
|
81
|
+
def issuer_common_name
|
82
|
+
issued_by
|
83
|
+
end
|
84
|
+
|
85
|
+
def issued_by
|
86
|
+
match = issuer.match("CN=(.*)")
|
87
|
+
match.captures.first if match
|
88
|
+
end
|
89
|
+
|
90
|
+
def public_key
|
91
|
+
@cert.public_key
|
92
|
+
end
|
93
|
+
|
94
|
+
def verify(ca)
|
95
|
+
@cert.verify(ca.public_key)
|
96
|
+
end
|
97
|
+
|
98
|
+
def not_before
|
99
|
+
DateTime.parse(@cert.not_before.to_s)
|
100
|
+
end
|
101
|
+
|
102
|
+
def not_after
|
103
|
+
DateTime.parse(@cert.not_after.to_s)
|
104
|
+
end
|
105
|
+
|
106
|
+
def expired?
|
107
|
+
@clock.now > not_after
|
108
|
+
end
|
109
|
+
|
110
|
+
def issued?
|
111
|
+
@clock.now > not_before
|
112
|
+
end
|
113
|
+
|
114
|
+
def bootstrap_certificate(cert)
|
115
|
+
return cert if cert.is_a?(OpenSSL::X509::Certificate)
|
116
|
+
return cert if cert.is_a?(SSLCheck::Certificate)
|
117
|
+
OpenSSL::X509::Certificate.new cert
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|