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.
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: []