Browse Source

Verify certificates during checks

new-scoring
aeris 2 years ago
parent
commit
d1efc0ec07
1 changed files with 87 additions and 62 deletions
  1. 87
    62
      lib/cryptcheck/tls/server.rb

+ 87
- 62
lib/cryptcheck/tls/server.rb View File

@@ -31,7 +31,6 @@ module CryptCheck
31 31
 			def initialize(hostname, family, ip, port)
32 32
 				@hostname, @family, @ip, @port = hostname, family, ip, port
33 33
 				@dh                            = []
34
-				@chains                        = []
35 34
 
36 35
 				@name = "#@ip:#@port"
37 36
 				@name += " [#@hostname]" if @hostname
@@ -40,24 +39,23 @@ module CryptCheck
40 39
 
41 40
 				fetch_supported_methods
42 41
 				fetch_supported_ciphers
42
+				fetch_dh
43 43
 				fetch_ciphers_preferences
44
-
45 44
 				fetch_ecdsa_certs
46 45
 				fetch_supported_curves
47 46
 				fetch_curves_preference
48 47
 
49
-				# verify_certs
50
-
51 48
 				check_fallback_scsv
52
-				exit
49
+
50
+				verify_certs
53 51
 			end
54 52
 
55 53
 			def supported_method?(method)
56 54
 				ssl_client method
57
-				Logger.info { "Method #{method} : supported" }
55
+				Logger.info { "  Method #{method}" }
58 56
 				true
59 57
 			rescue TLSException
60
-				Logger.debug { "Method #{method} : not supported" }
58
+				Logger.debug { "  Method #{method} : not supported" }
61 59
 				false
62 60
 			end
63 61
 
@@ -69,10 +67,14 @@ module CryptCheck
69 67
 
70 68
 			def supported_cipher?(method, cipher)
71 69
 				connection = ssl_client method, cipher
72
-				Logger.info { "Cipher #{cipher} : supported" }
70
+				Logger.info { "  Cipher #{cipher}" }
71
+				dh = connection.tmp_key
72
+				if dh
73
+					Logger.info { "    PFS : #{dh}" }
74
+				end
73 75
 				connection
74 76
 			rescue TLSException
75
-				Logger.debug { "Cipher #{cipher} : not supported" }
77
+				Logger.debug { "  Cipher #{cipher} : not supported" }
76 78
 				nil
77 79
 			end
78 80
 
@@ -96,14 +98,14 @@ module CryptCheck
96 98
 				@preferences = @supported_ciphers.collect do |method, ciphers|
97 99
 					ciphers     = ciphers.keys
98 100
 					preferences = if ciphers.size < 2
99
-									  Logger.info { method.to_s + ' : ' + 'not applicable'.colorize(:unknown) }
101
+									  Logger.info { "  #{method}  : " + 'not applicable'.colorize(:unknown) }
100 102
 									  nil
101 103
 								  else
102 104
 									  a, b, _ = ciphers
103 105
 									  ab      = ssl_client(method, [a, b]).cipher.first
104 106
 									  ba      = ssl_client(method, [b, a]).cipher.first
105 107
 									  if ab != ba
106
-										  Logger.info { method.to_s + ' : ' + 'client preference'.colorize(:warning) }
108
+										  Logger.info { "  #{method}  : " + 'client preference'.colorize(:warning) }
107 109
 										  :client
108 110
 									  else
109 111
 										  sort        = -> (a, b) do
@@ -112,7 +114,7 @@ module CryptCheck
112 114
 											  cipher == a.name ? -1 : 1
113 115
 										  end
114 116
 										  preferences = ciphers.sort &sort
115
-										  Logger.info { method.to_s + ' : ' + preferences.collect { |c| c.to_s :short }.join(', ') }
117
+										  Logger.info { "  #{method}  : " + preferences.collect { |c| c.to_s :short }.join(', ') }
116 118
 										  preferences
117 119
 									  end
118 120
 								  end
@@ -120,6 +122,12 @@ module CryptCheck
120 122
 				end.to_h
121 123
 			end
122 124
 
125
+			def fetch_dh
126
+				@dh = @supported_ciphers.collect do |_, ciphers|
127
+					ciphers.values.collect(&:tmp_key).select { |d| d.is_a? OpenSSL::PKey::DH }.collect &:size
128
+				end.flatten
129
+			end
130
+
123 131
 			def fetch_ecdsa_certs
124 132
 				@ecdsa_certs = {}
125 133
 
@@ -129,9 +137,8 @@ module CryptCheck
129 137
 
130 138
 					@ecdsa_certs = Curve.collect do |curve|
131 139
 						begin
132
-							connection  = ssl_client method, ecdsa, curves: curve
133
-							cert, chain = connection.peer_cert, connection.peer_cert_chain
134
-							[curve, { cert: cert, chain: chain }]
140
+							connection = ssl_client method, ecdsa, curves: curve
141
+							[curve, connection]
135 142
 						rescue TLSException
136 143
 							nil
137 144
 						end
@@ -156,17 +163,17 @@ module CryptCheck
156 163
 						@supported_curves = Curve.select do |curve|
157 164
 							next true if curve == ecdsa_curve # ECDSA curve is always supported
158 165
 							begin
159
-								connection = ssl_client method, ecdsa, curves: [curve, ecdsa_curve]
166
+								connection       = ssl_client method, ecdsa, curves: [curve, ecdsa_curve]
160 167
 								# Not too fast !!!
161 168
 								# Handshake will **always** succeed, because ECDSA curve is always supported
162 169
 								# So, need to test for the real curve
163
-								dh         = connection.tmp_key
164
-								curve      = dh.curve
165
-								supported  = curve != ecdsa_curve
170
+								dh               = connection.tmp_key
171
+								negociated_curve = dh.curve
172
+								supported        = negociated_curve != ecdsa_curve
166 173
 								if supported
167
-									Logger.info { "ECC curve #{curve} : supported" }
174
+									Logger.info { "  ECC curve #{curve}" }
168 175
 								else
169
-									Logger.debug { "ECC curve #{curve} : not supported" }
176
+									Logger.debug { "  ECC curve #{curve} : not supported" }
170 177
 								end
171 178
 								supported
172 179
 							rescue TLSException
@@ -184,10 +191,10 @@ module CryptCheck
184 191
 						@supported_curves = Curve.select do |curve|
185 192
 							begin
186 193
 								ssl_client method, ecdh, curves: curve
187
-								Logger.info { "ECC curve #{curve} : supported" }
194
+								Logger.info { "  ECC curve #{curve}" }
188 195
 								true
189 196
 							rescue TLSException
190
-								Logger.debug { "ECC curve #{curve} : not supported" }
197
+								Logger.debug { "  ECC curve #{curve} : not supported" }
191 198
 								false
192 199
 							end
193 200
 						end
@@ -207,14 +214,28 @@ module CryptCheck
207 214
 										 end.detect { |n| !n.nil? }
208 215
 
209 216
 										 a, b, _ = @supported_curves
210
-										 ab      = ssl_client(method, cipher, curves: [a, b]).tmp_key.curve
211
-										 ba      = ssl_client(method, cipher, curves: [b, a]).tmp_key.curve
217
+										 ab, ba  = [a, b], [b, a]
218
+										 if cipher.ecdsa?
219
+											 # In case of ECDSA, add the cert key at the end
220
+											 # Or no negociation possible
221
+											 ecdsa_curve = @ecdsa_certs.keys.first
222
+											 ab << ecdsa_curve
223
+											 ba << ecdsa_curve
224
+										 end
225
+										 ab = ssl_client(method, cipher, curves: ab).tmp_key.curve
226
+										 ba = ssl_client(method, cipher, curves: ba).tmp_key.curve
212 227
 										 if ab != ba
213 228
 											 Logger.info { 'Curves preference : ' + 'client preference'.colorize(:warning) }
214 229
 											 :client
215 230
 										 else
216 231
 											 sort        = -> (a, b) do
217
-												 connection = ssl_client method, cipher, curves: [a, b]
232
+												 curves = [a, b]
233
+												 if cipher.ecdsa?
234
+													 # In case of ECDSA, add the cert key at the end
235
+													 # Or no negociation possible
236
+													 curves << ecdsa_curve
237
+												 end
238
+												 connection = ssl_client method, cipher, curves: curves
218 239
 												 curve      = connection.tmp_key.curve
219 240
 												 curve == a.name ? -1 : 1
220 241
 											 end
@@ -364,14 +385,16 @@ module CryptCheck
364 385
 					retry
365 386
 				rescue ::OpenSSL::SSL::SSLError => e
366 387
 					case e.message
367
-						when /state=SSLv3 read server hello A$/,
388
+						when /state=SSLv2 read server hello A$/,
389
+								/state=SSLv3 read server hello A$/,
368 390
 								/state=SSLv3 read server hello A: wrong version number$/,
369
-								/state=SSLv3 read server hello A: tlsv1 alert protocol version$/
391
+								/state=SSLv3 read server hello A: tlsv1 alert protocol version$/,
392
+								/state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/
370 393
 							raise MethodNotAvailable, e
371
-						when /state=SSLv2 read server hello A: peer error no cipher/,
394
+						when /state=SSLv2 read server hello A: peer error no cipher$/,
372 395
 								/state=error: no ciphers available$/,
373 396
 								/state=SSLv3 read server hello A: sslv3 alert handshake failure$/,
374
-								/state=error: missing export tmp dh key/
397
+								/state=error: missing export tmp dh key$/
375 398
 							raise CipherNotAvailable, e
376 399
 						when /state=SSLv3 read server hello A: tlsv1 alert inappropriate fallback$/
377 400
 							raise InappropriateFallback, e
@@ -403,7 +426,9 @@ module CryptCheck
403 426
 
404 427
 				if curves
405 428
 					curves                  = [curves] unless curves.is_a? Enumerable
406
-					curves                  = curves.collect(&:name).join ':'
429
+					# OpenSSL fails if the same curve is selected multiple times
430
+					# So because Array#uniq preserves order, remove the less prefered ones
431
+					curves                  = curves.collect(&:name).uniq.join ':'
407 432
 					ssl_context.ecdh_curves = curves
408 433
 				end
409 434
 
@@ -417,47 +442,47 @@ module CryptCheck
417 442
 
418 443
 			def verify_certs
419 444
 				Logger.info { '' }
445
+				Logger.info { 'Certificates' }
446
+
447
+				# Let's begin the fun
448
+				# First, collect "standard" connections
449
+				# { method => { cipher => connection, ... }, ... }
450
+				certs = @supported_ciphers.values.collect(&:values).flatten 1
451
+				# Then, collect "ecdsa" connections
452
+				# { curve => connection, ... }
453
+				certs += @ecdsa_certs.values
454
+				# Then, fetch cert and chain
455
+				certs = certs.collect { |c| [c.peer_cert, c.peer_cert_chain] }
456
+				# Then, filter cert to keep uniq subject + issuer + serial
457
+				#certs = certs.uniq { |c, _| [c.subject, c.serial, c.issuer] }
458
+				# Then, filter cert to keep uniq fingerprint
459
+				certs = certs.uniq { |c, _| OpenSSL::Digest::SHA256.hexdigest c.to_der }
420 460
 
421 461
 				view = {}
422
-				@chains.each do |cert, chain|
462
+				certs.each do |cert, chain|
423 463
 					id = cert.subject, cert.serial, cert.issuer
424 464
 					next if view.include? id
425 465
 					subject, serial, issuer = id
426 466
 					key                     = cert.public_key
427 467
 
428
-					Logger.info { "Certificate #{subject} [#{serial}] issued by #{issuer}" }
429
-					Logger.info { "Key : #{Tls.key_to_s key }" }
430
-					valid    = ::OpenSSL::SSL.verify_certificate_identity cert, (@hostname || @ip)
431
-					trusted  = verify_trust chain, cert
432
-					view[id] = { cert: cert, chain: chain, key: key, valid: valid, trusted: trusted }
433
-				end
434
-				@chains = view.values
435
-				@keys   = @chains.collect { |c| c[:key] }
436
-			end
437
-
438
-			def verify_trust(chain, cert)
439
-				store         = ::OpenSSL::X509::Store.new
440
-				store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
441
-				store.set_default_paths
442
-
443
-				%w(/etc/ssl/certs).each do |directory|
444
-					::Dir.glob(::File.join directory, '*.pem').each do |file|
445
-						cert = ::OpenSSL::X509::Certificate.new ::File.read file
446
-						begin
447
-							store.add_cert cert
448
-						rescue ::OpenSSL::X509::StoreError
449
-						end
468
+					identity = ::OpenSSL::SSL.verify_certificate_identity cert, (@hostname || @ip)
469
+					trust    = Cert.trusted? cert, chain
470
+					view[id] = { cert: cert, chain: chain, key: key, identity: identity, trust: trust }
471
+					Logger.info { "  Certificate #{subject} [#{serial}] issued by #{issuer}" }
472
+					Logger.info { '    Key : ' +  Tls.key_to_s(key) }
473
+					if identity
474
+						Logger.info { '    Identity : ' + 'valid'.colorize(:good) }
475
+					else
476
+						Logger.info { '    Identity : ' + 'invalid'.colorize(:error) }
450 477
 					end
451
-				end
452
-				chain.each do |cert|
453
-					begin
454
-						store.add_cert cert
455
-					rescue ::OpenSSL::X509::StoreError
478
+					if trust == :trusted
479
+						Logger.info { '    Trust : ' + 'trusted'.colorize(:good) }
480
+					else
481
+						Logger.info { '    Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust }
456 482
 					end
457 483
 				end
458
-				trusted = store.verify cert
459
-				p store.error_string unless trusted
460
-				trusted
484
+				@chains = view.values
485
+				@keys   = @chains.collect { |c| c[:key] }
461 486
 			end
462 487
 
463 488
 			def uniq_dh

Loading…
Cancel
Save