check_certificate_chain 1.1.2 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/bin/check_certificate_chain +207 -102
  3. metadata +8 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eb156e21479cd1b22d66643549361b29421382ed
4
- data.tar.gz: 8fef1079f1cb3520d0d6efa016f3bfaf443418f5
3
+ metadata.gz: e6478165736e8bd911791335f79397d3c95cfab4
4
+ data.tar.gz: cf224a9090678d4bbdfecc4d20f7aa2190475d57
5
5
  SHA512:
6
- metadata.gz: bc46742aaabe807fd67cc8a1add4a1e953abea2c55f859629e4017da63c53f0930b06bf3a79d354eed62a6296f9262a53a0908f31490569d1e166da76766afd0
7
- data.tar.gz: '04635799b793319f72cc9960fbc74216952abfcfaaf1efc0311e1d5c050a532d00d6832ad1d61b1b3ae320e0594c34ca7ec61d8923650cb1cdc047fd8e257db0'
6
+ metadata.gz: 22612d4dbde4962e02052eae88bc653a5ad3253ebb635fe152b0753a390892e71d54a5cdde3405d47a020822cc7eb735d47fb403a2a9a4a7915d20687fb3020f
7
+ data.tar.gz: 00f6402d27ad02b78b82336ca648465acfd88cc7263a2f2ff062ee37d8a722e6c200495f1b80df79c50d3a82de39ab867f812dfd0fa57b6085c4abb74f044a82
@@ -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
- uri = URI(ARGV[0])
8
- uri = uri.host.nil? ? ARGV[0] : uri.host
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
- def green
16
- "\e[0;32;49m#{self}\e[0m"
17
- end
12
+ openssl_context = OpenSSL::SSL::SSLContext.new
18
13
 
19
- def bold
20
- "\e[1;39;49m#{self}\e[0m"
21
- end
22
- end
14
+ tcp_socket = TCPSocket.new(hostname, 443)
23
15
 
24
- module OpenSSL
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
- cert_store = OpenSSL::X509::Store.new
35
- cert_store.set_default_paths
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
- ctx = OpenSSL::SSL::SSLContext.new
23
+ chain = ssl_socket.peer_cert_chain
38
24
 
39
- socket = TCPSocket.new(uri, 443)
25
+ ssl_socket.sysclose
26
+ end
40
27
 
41
- ssl = OpenSSL::SSL::SSLSocket.new(socket, ctx)
42
- ssl.hostname = uri
28
+ certificate_store = OpenSSL::X509::Store.new
29
+ certificate_store.set_default_paths
43
30
 
44
- ssl.connect
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
- output = {}
50
- output[:header] = "--- " + "Certificate chain".bold
51
- output[:date] = ""
52
- output[:hostname] = ""
53
- output[:short] = ""
54
- output[:long] = ""
55
-
56
- NOW = Time.new
57
- BEFORE = certificate.not_before
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
- ((AFTER - NOW).to_i.abs / 86400).to_s
69
+ ((AFTER - NOW).to_i.abs / 86400).to_s
62
70
  end
63
-
71
+
64
72
  if AFTER > NOW
65
- output[:date] = "Certificate is up to date. (".green + days.bold +
66
- ") days remaining.".green + "\n---\n"
73
+ output[:date_check] << "Certificate is up to date. (#{days}) days remaining."
74
+ not_expired = true
67
75
  else
68
- output[:date] = "Certificate is outdated. This certificate has expired (".red +
69
- days.bold + ") days ago".red + "\n---\n"
76
+ output[:date_check] << "Certificate is outdated. This certificate has expired (#{days}) days ago."
77
+ not_expired = false
70
78
  end
71
79
 
72
- if OpenSSL::SSL.verify_certificate_identity(certificate, uri)
73
- output[:hostname] << "The hostname (".green + uri.bold +
74
- ") is correctly listed in the certificate.".green
75
- else
76
- output[:hostname] << "None of the common names in the certificate match the name that was entered (".red +
77
- uri.bold + ")".red
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
- check_chain_status = true
81
-
82
- chain.each_with_index do |cert, i|
83
- output[:short] << "#{i} s:#{chain[i].subject.to_s}\n" +
84
- " i:#{chain[i].issuer.to_s}\n"
85
-
86
- output[:short] << "---\n" if i.eql?(chain.size - 1)
87
-
88
- subject = cert.subject.to_s.split("CN=").last
89
- output[:long] << "Common name:".bold + " #{subject}\n"
90
-
91
- sans = cert.extensions.find {|ext| ext.oid.eql?("subjectAltName")}
92
- unless sans.nil?
93
- sans = sans.value.delete("DNS:")
94
- output[:long] << "SANs:".bold + " #{sans}\n"
95
- end
96
-
97
- output[:long] << "Valid".bold + " #{cert.not_before.strftime('from %B %d, %Y')} " +
98
- "#{cert.not_after.strftime('to %B %d, %Y')}\n"
99
- output[:long] << "Serial Number:".bold + " #{cert.serial.to_s(16)}\n"
100
- output[:long] << "Signature Algorithm:".bold + " #{cert.signature_algorithm}\n"
101
- output[:long] << "Issuer:".bold + " #{cert.issuer.to_s.split("CN=").last}\n"
102
-
103
- output[:long] << "--- "
104
-
105
- if check_chain_status
106
- unless chain[i+1].nil?
107
- if cert.verify chain[i+1].public_key
108
- output[:long] << "chain ok\n".green
109
- else
110
- output[:long] << "chain broken\n".red
111
- check_chain_status = false
112
- end
113
- else
114
- unless cert.self_signed?
115
- if cert_store.verify cert
116
- output[:long] << "checked against os store; chain ok\n".green
117
- else
118
- output[:long] << "checked agains os store; chain broken\n".red
119
- check_chain_status = false
120
- end
121
- else
122
- output[:long] << "\n"
123
- end
124
- end
125
- else
126
- output[:long] << "\n"
127
- end
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 output[:hostname]
133
- puts output[:date]
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: 1.1.2
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-07-21 00:00:00.000000000 Z
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: :development
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: Cli tool to check http connection certificates.
28
- email: jitlogan@gmail.com
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: Check HTTPS certificates
58
+ summary: CLI tool to check certificate chain
59
59
  test_files: []