check_certificate_chain 1.1.2 → 2.1.2
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 +4 -4
- data/bin/check_certificate_chain +207 -102
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6478165736e8bd911791335f79397d3c95cfab4
|
4
|
+
data.tar.gz: cf224a9090678d4bbdfecc4d20f7aa2190475d57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22612d4dbde4962e02052eae88bc653a5ad3253ebb635fe152b0753a390892e71d54a5cdde3405d47a020822cc7eb735d47fb403a2a9a4a7915d20687fb3020f
|
7
|
+
data.tar.gz: 00f6402d27ad02b78b82336ca648465acfd88cc7263a2f2ff062ee37d8a722e6c200495f1b80df79c50d3a82de39ab867f812dfd0fa57b6085c4abb74f044a82
|
data/bin/check_certificate_chain
CHANGED
@@ -1,134 +1,239 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require 'uri'
|
4
3
|
require 'openssl'
|
4
|
+
require 'uri'
|
5
5
|
require 'socket'
|
6
|
+
require 'net/http'
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
hostname = URI(ARGV[0])
|
9
|
+
hostname = hostname.host.nil? ? hostname.to_s : hostname.host
|
9
10
|
|
10
|
-
class String
|
11
|
-
def red
|
12
|
-
"\e[0;31;49m#{self}\e[0m"
|
13
|
-
end
|
14
11
|
|
15
|
-
|
16
|
-
"\e[0;32;49m#{self}\e[0m"
|
17
|
-
end
|
12
|
+
openssl_context = OpenSSL::SSL::SSLContext.new
|
18
13
|
|
19
|
-
|
20
|
-
"\e[1;39;49m#{self}\e[0m"
|
21
|
-
end
|
22
|
-
end
|
14
|
+
tcp_socket = TCPSocket.new(hostname, 443)
|
23
15
|
|
24
|
-
|
25
|
-
module X509
|
26
|
-
class Certificate
|
27
|
-
def self_signed?
|
28
|
-
self.verify self.public_key
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
16
|
+
chain = nil
|
33
17
|
|
34
|
-
|
35
|
-
|
18
|
+
OpenSSL::SSL::SSLSocket.new(tcp_socket, openssl_context).tap do |ssl_socket|
|
19
|
+
ssl_socket.hostname = hostname
|
20
|
+
ssl_socket.sync_close = true
|
21
|
+
ssl_socket.connect
|
36
22
|
|
37
|
-
|
23
|
+
chain = ssl_socket.peer_cert_chain
|
38
24
|
|
39
|
-
|
25
|
+
ssl_socket.sysclose
|
26
|
+
end
|
40
27
|
|
41
|
-
|
42
|
-
|
28
|
+
certificate_store = OpenSSL::X509::Store.new
|
29
|
+
certificate_store.set_default_paths
|
43
30
|
|
44
|
-
|
31
|
+
output = {}
|
32
|
+
output[:header] = "---"
|
33
|
+
output[:hostname_check] = ""
|
34
|
+
output[:date_check] = ""
|
35
|
+
output[:short] = []
|
36
|
+
output[:long] = []
|
37
|
+
output[:issues] = []
|
38
|
+
output[:ocsp_check] = []
|
39
|
+
|
40
|
+
def is_root?(certificate)
|
41
|
+
self_signed = certificate.verify certificate.public_key
|
42
|
+
|
43
|
+
basic_constraints = certificate.extensions.find do |extension|
|
44
|
+
extension.oid.eql?("basicConstraints")
|
45
|
+
end
|
46
|
+
ca, value = basic_constraints.value.split(":")
|
47
|
+
is_ca = ca.eql?("CA") && value.eql?("TRUE")
|
48
|
+
|
49
|
+
self_signed && is_ca
|
50
|
+
end
|
45
51
|
|
46
|
-
chain = ssl.peer_cert_chain
|
47
52
|
certificate = chain.first
|
48
53
|
|
49
|
-
|
50
|
-
|
51
|
-
output[:
|
52
|
-
|
53
|
-
|
54
|
-
output[:
|
55
|
-
|
56
|
-
|
57
|
-
|
54
|
+
# Check certificate hostname
|
55
|
+
if OpenSSL::SSL.verify_certificate_identity certificate, hostname
|
56
|
+
output[:hostname_check] << "The hostname (#{hostname}) is correctly listed in the certificate."
|
57
|
+
check_certificate_hostname = true
|
58
|
+
else
|
59
|
+
output[:hostname_check] << "None of the common names in the certificate match the name that was entered (#{hostname})."
|
60
|
+
check_certificate_hostname = false
|
61
|
+
end
|
62
|
+
|
63
|
+
# Check certificate expiration date
|
64
|
+
NOW = Time.now
|
65
|
+
BEFORE = certificate.not_before
|
58
66
|
AFTER = certificate.not_after
|
59
67
|
|
60
68
|
def days
|
61
|
-
|
69
|
+
((AFTER - NOW).to_i.abs / 86400).to_s
|
62
70
|
end
|
63
|
-
|
71
|
+
|
64
72
|
if AFTER > NOW
|
65
|
-
|
66
|
-
|
73
|
+
output[:date_check] << "Certificate is up to date. (#{days}) days remaining."
|
74
|
+
not_expired = true
|
67
75
|
else
|
68
|
-
|
69
|
-
|
76
|
+
output[:date_check] << "Certificate is outdated. This certificate has expired (#{days}) days ago."
|
77
|
+
not_expired = false
|
70
78
|
end
|
71
79
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
80
|
+
### OCSP Check
|
81
|
+
authority_info_access = certificate.extensions.find{|ext| ext.oid.eql?("authorityInfoAccess")}
|
82
|
+
if check_certificate_hostname && not_expired && authority_info_access
|
83
|
+
if issuer = chain.find{|chain_certificate| certificate.verify(chain_certificate.public_key)}
|
84
|
+
digest = OpenSSL::Digest::SHA1.new
|
85
|
+
certificate_id = OpenSSL::OCSP::CertificateId.new certificate, issuer, digest
|
86
|
+
|
87
|
+
ocsp_request = OpenSSL::OCSP::Request.new
|
88
|
+
ocsp_request.add_certid certificate_id
|
89
|
+
ocsp_request.add_nonce
|
90
|
+
|
91
|
+
###
|
92
|
+
uri_string = authority_info_access.value.split("\n").find{|str| str.start_with?("OCSP")}
|
93
|
+
ocsp_uri = URI uri_string[/URI:(.+)/, 1]
|
94
|
+
ocsp_uri.path = "/" if ocsp_uri.path.empty?
|
95
|
+
|
96
|
+
def ocsp_post(ocsp_uri, ocsp_request)
|
97
|
+
Net::HTTP.start(ocsp_uri.host, ocsp_uri.port) do |http|
|
98
|
+
http.post ocsp_uri.path, ocsp_request.to_der, {"Content-Type" => "application/ocsp-request"}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
http_response = ocsp_post(ocsp_uri, ocsp_request)
|
103
|
+
case http_response
|
104
|
+
when Net::HTTPSuccess then http_response
|
105
|
+
when Net::HTTPRedirection
|
106
|
+
ocsp_uri = URI(http_response['location'])
|
107
|
+
http_response = ocsp_post(ocsp_uri, ocsp_request)
|
108
|
+
end
|
109
|
+
|
110
|
+
ocsp_response = OpenSSL::OCSP::Response.new http_response.body
|
111
|
+
# Response status can be from 0 to 6. If response status is other then 0 it is error and If the value of responseStatus is one of the error conditions, the responseBytes (basic response) field is not set.
|
112
|
+
# RFC6960 4.2.1
|
113
|
+
if ocsp_response.status.zero?
|
114
|
+
basic = ocsp_response.basic
|
115
|
+
if basic.verify [issuer], certificate_store
|
116
|
+
# Check nonce
|
117
|
+
nonce_status = ocsp_request.check_nonce basic
|
118
|
+
case nonce_status
|
119
|
+
when 1
|
120
|
+
output[:ocsp_check] << "OCSP: nonce is ok."
|
121
|
+
nonce_check = true
|
122
|
+
when -1, 2, 3
|
123
|
+
output[:ocsp_check] << "OCSP: nonce is no supported."
|
124
|
+
nonce_check = true
|
125
|
+
when 0
|
126
|
+
output[:ocsp_check] << "OCSP: nonce check failed."
|
127
|
+
nonce_check = false
|
128
|
+
end
|
129
|
+
|
130
|
+
if nonce_check
|
131
|
+
ocsp_single_response = basic.find_response(certificate_id)
|
132
|
+
unless ocsp_single_response
|
133
|
+
output[:ocsp_check] << "OCSP: no response for this certificate"
|
134
|
+
end
|
135
|
+
|
136
|
+
unless ocsp_single_response.check_validity
|
137
|
+
output[:ocsp_check] << "OCSP: time validity failed."
|
138
|
+
end
|
139
|
+
|
140
|
+
case ocsp_single_response.cert_status
|
141
|
+
when 0
|
142
|
+
output[:ocsp_check] << "OCSP: certificate is ok."
|
143
|
+
when 1
|
144
|
+
output[:ocsp_check] << "OCSP: certificate is revoked."
|
145
|
+
when 2
|
146
|
+
output[:ocsp_check] << "OCSP: certificate status is unknown."
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
else
|
151
|
+
output[:ocsp_check] << "OCSP: response returned an error. Status of the response is #{ocsp_response.status_string}"
|
152
|
+
end
|
153
|
+
end
|
78
154
|
end
|
155
|
+
###
|
79
156
|
|
80
|
-
|
81
|
-
|
82
|
-
chain.
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
157
|
+
|
158
|
+
# Check if certificate chain contains root anchor
|
159
|
+
root_anchor = chain.find do |certificate|
|
160
|
+
is_root?(certificate)
|
161
|
+
end
|
162
|
+
|
163
|
+
if root_anchor
|
164
|
+
output[:issues] << " Certificate chain contains root_anchor. Extra certificate (Which serves no purpose) is increasing the handshake latency."
|
165
|
+
end
|
166
|
+
|
167
|
+
def long_output(chain_certificate, output)
|
168
|
+
|
169
|
+
output[:long] << "Common name: #{chain_certificate.subject.to_s[/CN=(.+)/, 1]}"
|
170
|
+
sans = chain_certificate.extensions.find{|extension| extension.oid.eql?("subjectAltName")}
|
171
|
+
unless sans.nil?
|
172
|
+
sans = sans.value.delete("DSN:")
|
173
|
+
output[:long] << "SANs: #{sans}"
|
174
|
+
end
|
175
|
+
output[:long] << chain_certificate.not_before.strftime("Valid from %B %-d, %Y ") +
|
176
|
+
chain_certificate.not_after.strftime("to %B %-d, %Y")
|
177
|
+
output[:long] << "Serial Number: #{chain_certificate.serial.to_s(16).downcase}"
|
178
|
+
output[:long] << "Signature Algorithm: #{chain_certificate.signature_algorithm}"
|
179
|
+
output[:long] << "Issuer: #{chain_certificate.issuer.to_s[/CN=(.+)/, 1]}"
|
180
|
+
# output[:long] << "---\n"
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
|
185
|
+
chain_check_status = true
|
186
|
+
chain_order_status = true
|
187
|
+
|
188
|
+
chain.each_with_index do |chain_certificate, index|
|
189
|
+
output[:short] << "#{index} s:#{chain_certificate.subject.to_s}"
|
190
|
+
output[:short] << " i:#{chain_certificate.issuer.to_s}"
|
191
|
+
|
192
|
+
long_output(chain_certificate, output)
|
193
|
+
output[:long] << "---"
|
194
|
+
|
195
|
+
if chain_check_status
|
196
|
+
check_status = chain.any? do |possible_issuer|
|
197
|
+
unless possible_issuer.eql? chain_certificate &&
|
198
|
+
is_root?(chain_certificate)
|
199
|
+
if chain_certificate.verify possible_issuer.public_key
|
200
|
+
if chain.index(possible_issuer) - chain.index(chain_certificate) > 1
|
201
|
+
chain_order_status = false
|
202
|
+
end
|
203
|
+
true
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# If check failed check against the root store
|
209
|
+
unless check_status
|
210
|
+
if certificate_store.verify chain_certificate
|
211
|
+
long_output(certificate_store.chain.last, output)
|
212
|
+
output[:long] << "---"
|
213
|
+
else
|
214
|
+
chain_check_status = false
|
215
|
+
output[:issues] << " Root certificate is not trusted."
|
216
|
+
end
|
217
|
+
end
|
218
|
+
else
|
219
|
+
chain_check_status = false
|
220
|
+
output[:issues] << " Chain is broken."
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
unless chain_order_status
|
225
|
+
output[:issues] << " Incorrect chain order."
|
128
226
|
end
|
129
227
|
|
130
228
|
puts output[:header]
|
131
229
|
puts output[:short]
|
132
|
-
puts
|
133
|
-
puts output[:
|
230
|
+
puts "---\n"
|
231
|
+
puts output[:hostname_check]
|
232
|
+
puts output[:date_check]
|
233
|
+
puts output[:ocsp_check]
|
234
|
+
unless output[:issues].empty?
|
235
|
+
puts "Issues:"
|
236
|
+
puts output[:issues]
|
237
|
+
end
|
238
|
+
puts "---\n"
|
134
239
|
puts output[:long]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: check_certificate_chain
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jora Porcu
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: openssl
|
@@ -16,16 +16,16 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '2'
|
20
|
-
type: :
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '2'
|
27
|
-
description:
|
28
|
-
email:
|
26
|
+
version: '2.0'
|
27
|
+
description: CLI tool to displayh certificate chain and OCSP status
|
28
|
+
email: jora@gmail.com
|
29
29
|
executables:
|
30
30
|
- check_certificate_chain
|
31
31
|
extensions: []
|
@@ -55,5 +55,5 @@ rubyforge_project:
|
|
55
55
|
rubygems_version: 2.6.12
|
56
56
|
signing_key:
|
57
57
|
specification_version: 4
|
58
|
-
summary:
|
58
|
+
summary: CLI tool to check certificate chain
|
59
59
|
test_files: []
|