Browse Source

Move TLS engine outside server

new-scoring
aeris 2 years ago
parent
commit
2105242e0a
3 changed files with 455 additions and 432 deletions
  1. 1
    0
      lib/cryptcheck.rb
  2. 453
    0
      lib/cryptcheck/tls/engine.rb
  3. 1
    432
      lib/cryptcheck/tls/server.rb

+ 1
- 0
lib/cryptcheck.rb View File

@@ -39,6 +39,7 @@ module CryptCheck
39 39
 		autoload :Cipher, 'cryptcheck/tls/cipher'
40 40
 		autoload :Curve, 'cryptcheck/tls/curve'
41 41
 		autoload :Cert, 'cryptcheck/tls/cert'
42
+		autoload :Engine, 'cryptcheck/tls/engine'
42 43
 		autoload :Server, 'cryptcheck/tls/server'
43 44
 		autoload :TcpServer, 'cryptcheck/tls/server'
44 45
 		autoload :UdpServer, 'cryptcheck/tls/server'

+ 453
- 0
lib/cryptcheck/tls/engine.rb View File

@@ -0,0 +1,453 @@
1
+require 'socket'
2
+require 'openssl'
3
+
4
+module CryptCheck
5
+	module Tls
6
+		module Engine
7
+			TCP_TIMEOUT = 10
8
+			SSL_TIMEOUT = 2*TCP_TIMEOUT
9
+
10
+			class TLSException < ::StandardError
11
+			end
12
+			class TLSNotAvailableException < TLSException
13
+				def to_s
14
+					'TLS seems not supported on this server'
15
+				end
16
+			end
17
+			class MethodNotAvailable < TLSException
18
+			end
19
+			class CipherNotAvailable < TLSException
20
+			end
21
+			class InappropriateFallback < TLSException
22
+			end
23
+			class Timeout < ::StandardError
24
+			end
25
+			class TLSTimeout < Timeout
26
+			end
27
+			class ConnectionError < ::StandardError
28
+			end
29
+
30
+			attr_reader :certs, :keys, :dh, :supported_methods, :supported_ciphers, :supported_curves, :curves_preference
31
+
32
+			def initialize(hostname, family, ip, port)
33
+				@hostname, @family, @ip, @port = hostname, family, ip, port
34
+				@dh                            = []
35
+
36
+				@name = "#@ip:#@port"
37
+				@name += " [#@hostname]" if @hostname
38
+
39
+				Logger.info { @name.colorize :blue }
40
+
41
+				fetch_supported_methods
42
+				fetch_supported_ciphers
43
+				fetch_dh
44
+				fetch_ciphers_preferences
45
+				fetch_ecdsa_certs
46
+				fetch_supported_curves
47
+				fetch_curves_preference
48
+
49
+				check_fallback_scsv
50
+
51
+				verify_certs
52
+			end
53
+
54
+			def supported_method?(method)
55
+				ssl_client method
56
+				Logger.info { "  Method #{method}" }
57
+				true
58
+			rescue TLSException
59
+				Logger.debug { "  Method #{method} : not supported" }
60
+				false
61
+			end
62
+
63
+			def fetch_supported_methods
64
+				Logger.info { '' }
65
+				Logger.info { 'Supported methods' }
66
+				@supported_methods = Method.select { |m| supported_method? m }
67
+			end
68
+
69
+			def supported_cipher?(method, cipher)
70
+				connection = ssl_client method, cipher
71
+				Logger.info { "  Cipher #{cipher}" }
72
+				dh = connection.tmp_key
73
+				if dh
74
+					Logger.info { "    PFS : #{dh}" }
75
+				end
76
+				connection
77
+			rescue TLSException
78
+				Logger.debug { "  Cipher #{cipher} : not supported" }
79
+				nil
80
+			end
81
+
82
+			def fetch_supported_ciphers
83
+				Logger.info { '' }
84
+				Logger.info { 'Supported ciphers' }
85
+				@supported_ciphers = @supported_methods.collect do |method|
86
+					ciphers = Cipher[method].collect do |cipher|
87
+						connection = supported_cipher? method, cipher
88
+						next nil unless connection
89
+						[cipher, connection]
90
+					end.compact.to_h
91
+					[method, ciphers]
92
+				end.to_h
93
+			end
94
+
95
+			def fetch_ciphers_preferences
96
+				Logger.info { '' }
97
+				Logger.info { 'Cipher suite preferences' }
98
+
99
+				@preferences = @supported_ciphers.collect do |method, ciphers|
100
+					ciphers     = ciphers.keys
101
+					preferences = if ciphers.size < 2
102
+									  Logger.info { "  #{method}  : " + 'not applicable'.colorize(:unknown) }
103
+									  nil
104
+								  else
105
+									  a, b, _ = ciphers
106
+									  ab      = ssl_client(method, [a, b]).cipher.first
107
+									  ba      = ssl_client(method, [b, a]).cipher.first
108
+									  if ab != ba
109
+										  Logger.info { "  #{method} : " + 'client preference'.colorize(:warning) }
110
+										  :client
111
+									  else
112
+										  sort        = -> (a, b) do
113
+											  connection = ssl_client method, [a, b]
114
+											  cipher     = connection.cipher.first
115
+											  cipher == a.name ? -1 : 1
116
+										  end
117
+										  preferences = ciphers.sort &sort
118
+										  Logger.info { "  #{method}  : " + preferences.collect { |c| c.to_s :short }.join(', ') }
119
+										  preferences
120
+									  end
121
+								  end
122
+					[method, preferences]
123
+				end.to_h
124
+			end
125
+
126
+			def fetch_dh
127
+				@dh = @supported_ciphers.collect do |_, ciphers|
128
+					ciphers.values.collect(&:tmp_key).select { |d| d.is_a? OpenSSL::PKey::DH }
129
+				end.flatten
130
+			end
131
+
132
+			def fetch_ecdsa_certs
133
+				@ecdsa_certs = {}
134
+
135
+				@supported_ciphers.each do |method, ciphers|
136
+					ecdsa = ciphers.keys.detect &:ecdsa?
137
+					next unless ecdsa
138
+
139
+					@ecdsa_certs = Curve.collect do |curve|
140
+						begin
141
+							connection = ssl_client method, ecdsa, curves: curve
142
+							[curve, connection]
143
+						rescue TLSException
144
+							nil
145
+						end
146
+					end.compact.to_h
147
+
148
+					break
149
+				end
150
+			end
151
+
152
+			def fetch_supported_curves
153
+				Logger.info { '' }
154
+				Logger.info { 'Supported elliptic curves' }
155
+				@supported_curves = []
156
+
157
+				ecdsa_curve = @ecdsa_certs.keys.first
158
+				if ecdsa_curve
159
+					# If we have an ECDSA cipher, we need at least the certificate curve to do handshake,
160
+					# but with lowest priority to check for ECHDE and not just ECDSA
161
+
162
+					@supported_ciphers.each do |method, ciphers|
163
+						ecdsa = ciphers.keys.detect &:ecdsa?
164
+						next unless ecdsa
165
+						@supported_curves = Curve.select do |curve|
166
+							next true if curve == ecdsa_curve # ECDSA curve is always supported
167
+							begin
168
+								connection       = ssl_client method, ecdsa, curves: [curve, ecdsa_curve]
169
+								# Not too fast !!!
170
+								# Handshake will **always** succeed, because ECDSA
171
+								# curve is always supported.
172
+								# So, we need to test for the real curve!
173
+								# Treaky case : if server preference is enforced,
174
+								# ECDSA curve can be prefered over ECDHE one and so
175
+								# really supported curve can be detected as not supported :(
176
+
177
+								dh               = connection.tmp_key
178
+								negociated_curve = dh.curve
179
+								supported        = ecdsa_curve != negociated_curve
180
+								if supported
181
+									Logger.info { "  ECC curve #{curve.name}" }
182
+								else
183
+									Logger.debug { "  ECC curve #{curve.name} : not supported" }
184
+								end
185
+								supported
186
+							rescue TLSException
187
+								false
188
+							end
189
+						end
190
+						break
191
+					end
192
+				else
193
+					# If we have no ECDSA ciphers, ECC supported are only ECDH ones
194
+					# So peak an ECDH cipher and test all curves
195
+					@supported_ciphers.each do |method, ciphers|
196
+						ecdh = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? }
197
+						next unless ecdh
198
+						@supported_curves = Curve.select do |curve|
199
+							begin
200
+								ssl_client method, ecdh, curves: curve
201
+								Logger.info { "  ECC curve #{curve.name}" }
202
+								true
203
+							rescue TLSException
204
+								Logger.debug { "  ECC curve #{curve.name} : not supported" }
205
+								false
206
+							end
207
+						end
208
+						break
209
+					end
210
+				end
211
+			end
212
+
213
+			def fetch_curves_preference
214
+				@curves_preference = if @supported_curves.size < 2
215
+										 Logger.info { 'Curves preference : ' + 'not applicable'.colorize(:unknown) }
216
+										 nil
217
+									 else
218
+										 method, cipher = @supported_ciphers.collect do |method, ciphers|
219
+											 cipher = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? }
220
+											 [method, cipher]
221
+										 end.detect { |n| !n.nil? }
222
+
223
+										 a, b, _ = @supported_curves
224
+										 ab, ba  = [a, b], [b, a]
225
+										 if cipher.ecdsa?
226
+											 # In case of ECDSA, add the cert key at the end
227
+											 # Or no negociation possible
228
+											 ecdsa_curve = @ecdsa_certs.keys.first
229
+											 ab << ecdsa_curve
230
+											 ba << ecdsa_curve
231
+										 end
232
+										 ab = ssl_client(method, cipher, curves: ab).tmp_key.curve
233
+										 ba = ssl_client(method, cipher, curves: ba).tmp_key.curve
234
+										 if ab != ba
235
+											 Logger.info { 'Curves preference : ' + 'client preference'.colorize(:warning) }
236
+											 :client
237
+										 else
238
+											 sort        = -> (a, b) do
239
+												 curves = [a, b]
240
+												 if cipher.ecdsa?
241
+													 # In case of ECDSA, add the cert key at the end
242
+													 # Or no negociation possible
243
+													 curves << ecdsa_curve
244
+												 end
245
+												 connection = ssl_client method, cipher, curves: curves
246
+												 curve      = connection.tmp_key.curve
247
+												 a == curve ? -1 : 1
248
+											 end
249
+											 preferences = @supported_curves.sort &sort
250
+											 Logger.info { 'Curves preference : ' + preferences.collect { |c| c.name }.join(', ') }
251
+											 preferences
252
+										 end
253
+									 end
254
+			end
255
+
256
+			def check_fallback_scsv
257
+				Logger.info { '' }
258
+
259
+				@fallback_scsv = false
260
+				if @supported_methods.size > 1
261
+					# We will try to connect to the not better supported method
262
+					method = @supported_methods[1]
263
+
264
+					begin
265
+						ssl_client method, fallback: true
266
+					rescue InappropriateFallback
267
+						@fallback_scsv = true
268
+					end
269
+				else
270
+					@fallback_scsv = nil
271
+				end
272
+
273
+				text, color = case @fallback_scsv
274
+								  when true
275
+									  ['supported', :good]
276
+								  when false
277
+									  ['not supported', :error]
278
+								  when nil
279
+									  ['not applicable', :unknown]
280
+							  end
281
+				Logger.info { 'Fallback SCSV : ' + text.colorize(color) }
282
+			end
283
+
284
+			Method.each do |method|
285
+				class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
286
+					def #{method.to_sym.downcase}?
287
+						@supported_methods.detect { |m| m == :#{method.to_sym} }
288
+					end
289
+				RUBY_EVAL
290
+			end
291
+
292
+			Cipher::TYPES.each do |type, _|
293
+				class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
294
+					def #{type}?
295
+						uniq_supported_ciphers.any? { |c| c.#{type}? }
296
+					end
297
+				RUBY_EVAL
298
+			end
299
+
300
+			private
301
+			def connect(&block)
302
+				socket   = ::Socket.new @family, sock_type
303
+				sockaddr = ::Socket.sockaddr_in @port, @ip
304
+				#Logger.trace { "Connecting to #{@ip}:#{@port}" }
305
+				begin
306
+					status = socket.connect_nonblock sockaddr
307
+					#Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" }
308
+					raise ConnectionError, status unless status == 0
309
+					#Logger.trace { "Connected to #{@ip}:#{@port}" }
310
+					block_given? ? block.call(socket) : nil
311
+				rescue ::IO::WaitReadable
312
+					#Logger.trace { "Waiting for read to #{@ip}:#{@port}" }
313
+					raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT
314
+					retry
315
+				rescue ::IO::WaitWritable
316
+					#Logger.trace { "Waiting for write to #{@ip}:#{@port}" }
317
+					raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select nil, [socket], nil, TCP_TIMEOUT
318
+					retry
319
+				ensure
320
+					socket.close
321
+				end
322
+			end
323
+
324
+			def ssl_connect(socket, context, method, &block)
325
+				ssl_socket          = ::OpenSSL::SSL::SSLSocket.new socket, context
326
+				ssl_socket.hostname = @hostname if @hostname and method != :SSLv2
327
+				#Logger.trace { "SSL connecting to #{name}" }
328
+				begin
329
+					ssl_socket.connect_nonblock
330
+					#Logger.trace { "SSL connected to #{name}" }
331
+					return block_given? ? block.call(ssl_socket) : nil
332
+				rescue ::OpenSSL::SSL::SSLErrorWaitReadable
333
+					#Logger.trace { "Waiting for SSL read to #{name}" }
334
+					raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT
335
+					retry
336
+				rescue ::OpenSSL::SSL::SSLErrorWaitWritable
337
+					#Logger.trace { "Waiting for SSL write to #{name}" }
338
+					raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT
339
+					retry
340
+				rescue ::OpenSSL::SSL::SSLError => e
341
+					case e.message
342
+						when /state=SSLv2 read server hello A$/,
343
+								/state=SSLv3 read server hello A$/,
344
+								/state=SSLv3 read server hello A: wrong version number$/,
345
+								/state=SSLv3 read server hello A: tlsv1 alert protocol version$/,
346
+								/state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/
347
+							raise MethodNotAvailable, e
348
+						when /state=SSLv2 read server hello A: peer error no cipher$/,
349
+								/state=error: no ciphers available$/,
350
+								/state=SSLv3 read server hello A: sslv3 alert handshake failure$/,
351
+								/state=error: missing export tmp dh key$/,
352
+								/state=error: wrong curve$/
353
+							raise CipherNotAvailable, e
354
+						when /state=SSLv3 read server hello A: tlsv1 alert inappropriate fallback$/
355
+							raise InappropriateFallback, e
356
+					end
357
+					raise
358
+				rescue ::SystemCallError => e
359
+					case e.message
360
+						when /^Connection reset by peer - SSL_connect$/
361
+							raise TLSNotAvailableException, e
362
+					end
363
+					raise
364
+				ensure
365
+					ssl_socket.close
366
+				end
367
+			end
368
+
369
+			def ssl_client(method, ciphers = nil, curves: nil, fallback: false, &block)
370
+				ssl_context = ::OpenSSL::SSL::SSLContext.new method.to_sym
371
+				ssl_context.enable_fallback_scsv if fallback
372
+
373
+				if ciphers
374
+					ciphers = [ciphers] unless ciphers.is_a? Enumerable
375
+					ciphers = ciphers.collect(&:name).join ':'
376
+				else
377
+					ciphers = Cipher::ALL
378
+				end
379
+				ssl_context.ciphers = ciphers
380
+
381
+				if curves
382
+					curves                  = [curves] unless curves.is_a? Enumerable
383
+					# OpenSSL fails if the same curve is selected multiple times
384
+					# So because Array#uniq preserves order, remove the less prefered ones
385
+					curves                  = curves.collect(&:name).uniq.join ':'
386
+					ssl_context.ecdh_curves = curves
387
+				end
388
+
389
+				Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" }
390
+				connect do |socket|
391
+					ssl_connect socket, ssl_context, method do |ssl_socket|
392
+						return block_given? ? block.call(ssl_socket) : ssl_socket
393
+					end
394
+				end
395
+			end
396
+
397
+			def verify_certs
398
+				Logger.info { '' }
399
+				Logger.info { 'Certificates' }
400
+
401
+				# Let's begin the fun
402
+				# First, collect "standard" connections
403
+				# { method => { cipher => connection, ... }, ... }
404
+				certs  = @supported_ciphers.values.collect(&:values).flatten 1
405
+				# Then, collect "ecdsa" connections
406
+				# { curve => connection, ... }
407
+				certs  += @ecdsa_certs.values
408
+				# For anonymous cipher, there is no certificate at all
409
+				certs  = certs.reject { |c| c.peer_cert.nil? }
410
+				# Then, fetch cert
411
+				certs  = certs.collect { |c| Cert.new c }
412
+				# Then, filter cert to keep uniq fingerprint
413
+				@certs = certs.uniq { |c| c.fingerprint }
414
+
415
+				@certs.each do |cert|
416
+					key      = cert.key
417
+					identity = cert.valid?(@hostname || @ip)
418
+					trust    = cert.trusted?
419
+					Logger.info { "  Certificate #{cert.subject} [#{cert.serial}] issued by #{cert.issuer}" }
420
+					Logger.info { '    Key : ' + Tls.key_to_s(key) }
421
+					if identity
422
+						Logger.info { '    Identity : ' + 'valid'.colorize(:good) }
423
+					else
424
+						Logger.info { '    Identity : ' + 'invalid'.colorize(:error) }
425
+					end
426
+					if trust == :trusted
427
+						Logger.info { '    Trust : ' + 'trusted'.colorize(:good) }
428
+					else
429
+						Logger.info { '    Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust }
430
+					end
431
+				end
432
+				@keys = @certs.collect &:key
433
+			end
434
+
435
+			def uniq_dh
436
+				dh, find = [], []
437
+				@dh.each do |k|
438
+					f = [k.type, k.size]
439
+					unless find.include? f
440
+						dh << k
441
+						find << f
442
+					end
443
+				end
444
+				@dh = dh
445
+			end
446
+
447
+			private
448
+			def uniq_supported_ciphers
449
+				@supported_ciphers.values.collect(&:keys).flatten.uniq
450
+			end
451
+		end
452
+	end
453
+end

+ 1
- 432
lib/cryptcheck/tls/server.rb View File

@@ -1,287 +1,6 @@
1
-require 'socket'
2
-require 'openssl'
3
-require 'httparty'
4
-
5 1
 module CryptCheck
6 2
 	module Tls
7 3
 		class Server
8
-			TCP_TIMEOUT = 10
9
-			SSL_TIMEOUT = 2*TCP_TIMEOUT
10
-
11
-			class TLSException < ::StandardError
12
-			end
13
-			class TLSNotAvailableException < TLSException
14
-				def to_s
15
-					'TLS seems not supported on this server'
16
-				end
17
-			end
18
-			class MethodNotAvailable < TLSException
19
-			end
20
-			class CipherNotAvailable < TLSException
21
-			end
22
-			class InappropriateFallback < TLSException
23
-			end
24
-			class Timeout < ::StandardError
25
-			end
26
-			class TLSTimeout < Timeout
27
-			end
28
-			class ConnectionError < ::StandardError
29
-			end
30
-
31
-			attr_reader :certs, :keys, :dh, :supported_methods, :supported_ciphers, :supported_curves, :curves_preference
32
-
33
-			def initialize(hostname, family, ip, port)
34
-				@hostname, @family, @ip, @port = hostname, family, ip, port
35
-				@dh                            = []
36
-
37
-				@name = "#@ip:#@port"
38
-				@name += " [#@hostname]" if @hostname
39
-
40
-				Logger.info { @name.colorize :blue }
41
-
42
-				fetch_supported_methods
43
-				fetch_supported_ciphers
44
-				fetch_dh
45
-				fetch_ciphers_preferences
46
-				fetch_ecdsa_certs
47
-				fetch_supported_curves
48
-				fetch_curves_preference
49
-
50
-				check_fallback_scsv
51
-
52
-				verify_certs
53
-			end
54
-
55
-			def supported_method?(method)
56
-				ssl_client method
57
-				Logger.info { "  Method #{method}" }
58
-				true
59
-			rescue TLSException
60
-				Logger.debug { "  Method #{method} : not supported" }
61
-				false
62
-			end
63
-
64
-			def fetch_supported_methods
65
-				Logger.info { '' }
66
-				Logger.info { 'Supported methods' }
67
-				@supported_methods = Method.select { |m| supported_method? m }
68
-			end
69
-
70
-			def supported_cipher?(method, cipher)
71
-				connection = ssl_client method, cipher
72
-				Logger.info { "  Cipher #{cipher}" }
73
-				dh = connection.tmp_key
74
-				if dh
75
-					Logger.info { "    PFS : #{dh}" }
76
-				end
77
-				connection
78
-			rescue TLSException
79
-				Logger.debug { "  Cipher #{cipher} : not supported" }
80
-				nil
81
-			end
82
-
83
-			def fetch_supported_ciphers
84
-				Logger.info { '' }
85
-				Logger.info { 'Supported ciphers' }
86
-				@supported_ciphers = @supported_methods.collect do |method|
87
-					ciphers = Cipher[method].collect do |cipher|
88
-						connection = supported_cipher? method, cipher
89
-						next nil unless connection
90
-						[cipher, connection]
91
-					end.compact.to_h
92
-					[method, ciphers]
93
-				end.to_h
94
-			end
95
-
96
-			def fetch_ciphers_preferences
97
-				Logger.info { '' }
98
-				Logger.info { 'Cipher suite preferences' }
99
-
100
-				@preferences = @supported_ciphers.collect do |method, ciphers|
101
-					ciphers     = ciphers.keys
102
-					preferences = if ciphers.size < 2
103
-									  Logger.info { "  #{method}  : " + 'not applicable'.colorize(:unknown) }
104
-									  nil
105
-								  else
106
-									  a, b, _ = ciphers
107
-									  ab      = ssl_client(method, [a, b]).cipher.first
108
-									  ba      = ssl_client(method, [b, a]).cipher.first
109
-									  if ab != ba
110
-										  Logger.info { "  #{method} : " + 'client preference'.colorize(:warning) }
111
-										  :client
112
-									  else
113
-										  sort        = -> (a, b) do
114
-											  connection = ssl_client method, [a, b]
115
-											  cipher     = connection.cipher.first
116
-											  cipher == a.name ? -1 : 1
117
-										  end
118
-										  preferences = ciphers.sort &sort
119
-										  Logger.info { "  #{method}  : " + preferences.collect { |c| c.to_s :short }.join(', ') }
120
-										  preferences
121
-									  end
122
-								  end
123
-					[method, preferences]
124
-				end.to_h
125
-			end
126
-
127
-			def fetch_dh
128
-				@dh = @supported_ciphers.collect do |_, ciphers|
129
-					ciphers.values.collect(&:tmp_key).select { |d| d.is_a? OpenSSL::PKey::DH }
130
-				end.flatten
131
-			end
132
-
133
-			def fetch_ecdsa_certs
134
-				@ecdsa_certs = {}
135
-
136
-				@supported_ciphers.each do |method, ciphers|
137
-					ecdsa = ciphers.keys.detect &:ecdsa?
138
-					next unless ecdsa
139
-
140
-					@ecdsa_certs = Curve.collect do |curve|
141
-						begin
142
-							connection = ssl_client method, ecdsa, curves: curve
143
-							[curve, connection]
144
-						rescue TLSException
145
-							nil
146
-						end
147
-					end.compact.to_h
148
-
149
-					break
150
-				end
151
-			end
152
-
153
-			def fetch_supported_curves
154
-				Logger.info { '' }
155
-				Logger.info { 'Supported elliptic curves' }
156
-				@supported_curves = []
157
-
158
-				ecdsa_curve = @ecdsa_certs.keys.first
159
-				if ecdsa_curve
160
-					# If we have an ECDSA cipher, we need at least the certificate curve to do handshake,
161
-					# but with lowest priority to check for ECHDE and not just ECDSA
162
-
163
-					@supported_ciphers.each do |method, ciphers|
164
-						ecdsa = ciphers.keys.detect &:ecdsa?
165
-						next unless ecdsa
166
-						@supported_curves = Curve.select do |curve|
167
-							next true if curve == ecdsa_curve # ECDSA curve is always supported
168
-							begin
169
-								connection       = ssl_client method, ecdsa, curves: [curve, ecdsa_curve]
170
-								# Not too fast !!!
171
-								# Handshake will **always** succeed, because ECDSA
172
-								# curve is always supported.
173
-								# So, we need to test for the real curve!
174
-								# Treaky case : if server preference is enforced,
175
-								# ECDSA curve can be prefered over ECDHE one and so
176
-								# really supported curve can be detected as not supported :(
177
-
178
-								dh               = connection.tmp_key
179
-								negociated_curve = dh.curve
180
-								supported        = ecdsa_curve != negociated_curve
181
-								if supported
182
-									Logger.info { "  ECC curve #{curve.name}" }
183
-								else
184
-									Logger.debug { "  ECC curve #{curve.name} : not supported" }
185
-								end
186
-								supported
187
-							rescue TLSException
188
-								false
189
-							end
190
-						end
191
-						break
192
-					end
193
-				else
194
-					# If we have no ECDSA ciphers, ECC supported are only ECDH ones
195
-					# So peak an ECDH cipher and test all curves
196
-					@supported_ciphers.each do |method, ciphers|
197
-						ecdh = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? }
198
-						next unless ecdh
199
-						@supported_curves = Curve.select do |curve|
200
-							begin
201
-								ssl_client method, ecdh, curves: curve
202
-								Logger.info { "  ECC curve #{curve.name}" }
203
-								true
204
-							rescue TLSException
205
-								Logger.debug { "  ECC curve #{curve.name} : not supported" }
206
-								false
207
-							end
208
-						end
209
-						break
210
-					end
211
-				end
212
-			end
213
-
214
-			def fetch_curves_preference
215
-				@curves_preference = if @supported_curves.size < 2
216
-										 Logger.info { 'Curves preference : ' + 'not applicable'.colorize(:unknown) }
217
-										 nil
218
-									 else
219
-										 method, cipher = @supported_ciphers.collect do |method, ciphers|
220
-											 cipher = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? }
221
-											 [method, cipher]
222
-										 end.detect { |n| !n.nil? }
223
-
224
-										 a, b, _ = @supported_curves
225
-										 ab, ba  = [a, b], [b, a]
226
-										 if cipher.ecdsa?
227
-											 # In case of ECDSA, add the cert key at the end
228
-											 # Or no negociation possible
229
-											 ecdsa_curve = @ecdsa_certs.keys.first
230
-											 ab << ecdsa_curve
231
-											 ba << ecdsa_curve
232
-										 end
233
-										 ab = ssl_client(method, cipher, curves: ab).tmp_key.curve
234
-										 ba = ssl_client(method, cipher, curves: ba).tmp_key.curve
235
-										 if ab != ba
236
-											 Logger.info { 'Curves preference : ' + 'client preference'.colorize(:warning) }
237
-											 :client
238
-										 else
239
-											 sort        = -> (a, b) do
240
-												 curves = [a, b]
241
-												 if cipher.ecdsa?
242
-													 # In case of ECDSA, add the cert key at the end
243
-													 # Or no negociation possible
244
-													 curves << ecdsa_curve
245
-												 end
246
-												 connection = ssl_client method, cipher, curves: curves
247
-												 curve      = connection.tmp_key.curve
248
-												 a == curve ? -1 : 1
249
-											 end
250
-											 preferences = @supported_curves.sort &sort
251
-											 Logger.info { 'Curves preference : ' + preferences.collect { |c| c.name }.join(', ') }
252
-											 preferences
253
-										 end
254
-									 end
255
-			end
256
-
257
-			def check_fallback_scsv
258
-				Logger.info { '' }
259
-
260
-				@fallback_scsv = false
261
-				if @supported_methods.size > 1
262
-					# We will try to connect to the not better supported method
263
-					method = @supported_methods[1]
264
-
265
-					begin
266
-						ssl_client method, fallback: true
267
-					rescue InappropriateFallback
268
-						@fallback_scsv = true
269
-					end
270
-				else
271
-					@fallback_scsv = nil
272
-				end
273
-
274
-				text, color = case @fallback_scsv
275
-								  when true
276
-									  ['supported', :good]
277
-								  when false
278
-									  ['not supported', :error]
279
-								  when nil
280
-									  ['not applicable', :unknown]
281
-							  end
282
-				Logger.info { 'Fallback SCSV : ' + text.colorize(color) }
283
-			end
284
-
285 4
 			Method.each do |method|
286 5
 				class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
287 6
 					def #{method.to_sym.downcase}?
@@ -396,157 +115,7 @@ module CryptCheck
396 115
 				@certs + @dh
397 116
 			end
398 117
 
399
-			private
400
-			def connect(&block)
401
-				socket   = ::Socket.new @family, sock_type
402
-				sockaddr = ::Socket.sockaddr_in @port, @ip
403
-				#Logger.trace { "Connecting to #{@ip}:#{@port}" }
404
-				begin
405
-					status = socket.connect_nonblock sockaddr
406
-					#Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" }
407
-					raise ConnectionError, status unless status == 0
408
-					#Logger.trace { "Connected to #{@ip}:#{@port}" }
409
-					block_given? ? block.call(socket) : nil
410
-				rescue ::IO::WaitReadable
411
-					#Logger.trace { "Waiting for read to #{@ip}:#{@port}" }
412
-					raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT
413
-					retry
414
-				rescue ::IO::WaitWritable
415
-					#Logger.trace { "Waiting for write to #{@ip}:#{@port}" }
416
-					raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select nil, [socket], nil, TCP_TIMEOUT
417
-					retry
418
-				ensure
419
-					socket.close
420
-				end
421
-			end
422
-
423
-			def ssl_connect(socket, context, method, &block)
424
-				ssl_socket          = ::OpenSSL::SSL::SSLSocket.new socket, context
425
-				ssl_socket.hostname = @hostname if @hostname and method != :SSLv2
426
-				#Logger.trace { "SSL connecting to #{name}" }
427
-				begin
428
-					ssl_socket.connect_nonblock
429
-					#Logger.trace { "SSL connected to #{name}" }
430
-					return block_given? ? block.call(ssl_socket) : nil
431
-				rescue ::OpenSSL::SSL::SSLErrorWaitReadable
432
-					#Logger.trace { "Waiting for SSL read to #{name}" }
433
-					raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT
434
-					retry
435
-				rescue ::OpenSSL::SSL::SSLErrorWaitWritable
436
-					#Logger.trace { "Waiting for SSL write to #{name}" }
437
-					raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT
438
-					retry
439
-				rescue ::OpenSSL::SSL::SSLError => e
440
-					case e.message
441
-						when /state=SSLv2 read server hello A$/,
442
-								/state=SSLv3 read server hello A$/,
443
-								/state=SSLv3 read server hello A: wrong version number$/,
444
-								/state=SSLv3 read server hello A: tlsv1 alert protocol version$/,
445
-								/state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/
446
-							raise MethodNotAvailable, e
447
-						when /state=SSLv2 read server hello A: peer error no cipher$/,
448
-								/state=error: no ciphers available$/,
449
-								/state=SSLv3 read server hello A: sslv3 alert handshake failure$/,
450
-								/state=error: missing export tmp dh key$/,
451
-								/state=error: wrong curve$/
452
-							raise CipherNotAvailable, e
453
-						when /state=SSLv3 read server hello A: tlsv1 alert inappropriate fallback$/
454
-							raise InappropriateFallback, e
455
-					end
456
-					raise
457
-				rescue ::SystemCallError => e
458
-					case e.message
459
-						when /^Connection reset by peer - SSL_connect$/
460
-							raise TLSNotAvailableException, e
461
-					end
462
-					raise
463
-				ensure
464
-					ssl_socket.close
465
-				end
466
-			end
467
-
468
-			def ssl_client(method, ciphers = nil, curves: nil, fallback: false, &block)
469
-				ssl_context = ::OpenSSL::SSL::SSLContext.new method.to_sym
470
-				ssl_context.enable_fallback_scsv if fallback
471
-
472
-				if ciphers
473
-					ciphers = [ciphers] unless ciphers.is_a? Enumerable
474
-					ciphers = ciphers.collect(&:name).join ':'
475
-				else
476
-					ciphers = Cipher::ALL
477
-				end
478
-				ssl_context.ciphers = ciphers
479
-
480
-				if curves
481
-					curves                  = [curves] unless curves.is_a? Enumerable
482
-					# OpenSSL fails if the same curve is selected multiple times
483
-					# So because Array#uniq preserves order, remove the less prefered ones
484
-					curves                  = curves.collect(&:name).uniq.join ':'
485
-					ssl_context.ecdh_curves = curves
486
-				end
487
-
488
-				Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" }
489
-				connect do |socket|
490
-					ssl_connect socket, ssl_context, method do |ssl_socket|
491
-						return block_given? ? block.call(ssl_socket) : ssl_socket
492
-					end
493
-				end
494
-			end
495
-
496
-			def verify_certs
497
-				Logger.info { '' }
498
-				Logger.info { 'Certificates' }
499
-
500
-				# Let's begin the fun
501
-				# First, collect "standard" connections
502
-				# { method => { cipher => connection, ... }, ... }
503
-				certs  = @supported_ciphers.values.collect(&:values).flatten 1
504
-				# Then, collect "ecdsa" connections
505
-				# { curve => connection, ... }
506
-				certs  += @ecdsa_certs.values
507
-				# For anonymous cipher, there is no certificate at all
508
-				certs  = certs.reject { |c| c.peer_cert.nil? }
509
-				# Then, fetch cert
510
-				certs  = certs.collect { |c| Cert.new c }
511
-				# Then, filter cert to keep uniq fingerprint
512
-				@certs = certs.uniq { |c| c.fingerprint }
513
-
514
-				@certs.each do |cert|
515
-					key      = cert.key
516
-					identity = cert.valid?(@hostname || @ip)
517
-					trust    = cert.trusted?
518
-					Logger.info { "  Certificate #{cert.subject} [#{cert.serial}] issued by #{cert.issuer}" }
519
-					Logger.info { '    Key : ' + Tls.key_to_s(key) }
520
-					if identity
521
-						Logger.info { '    Identity : ' + 'valid'.colorize(:good) }
522
-					else
523
-						Logger.info { '    Identity : ' + 'invalid'.colorize(:error) }
524
-					end
525
-					if trust == :trusted
526
-						Logger.info { '    Trust : ' + 'trusted'.colorize(:good) }
527
-					else
528
-						Logger.info { '    Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust }
529
-					end
530
-				end
531
-				@keys = @certs.collect &:key
532
-			end
533
-
534
-			def uniq_dh
535
-				dh, find = [], []
536
-				@dh.each do |k|
537
-					f = [k.type, k.size]
538
-					unless find.include? f
539
-						dh << k
540
-						find << f
541
-					end
542
-				end
543
-				@dh = dh
544
-			end
545
-
546
-			private
547
-			def uniq_supported_ciphers
548
-				@supported_ciphers.values.collect(&:keys).flatten.uniq
549
-			end
118
+			include Engine
550 119
 		end
551 120
 
552 121
 		class TcpServer < Server

Loading…
Cancel
Save