소스 검색

Refactor TLS server

new-scoring
aeris 2 년 전
부모
커밋
f1860ab3ed
8개의 변경된 파일417개의 추가작업 그리고 325개의 파일을 삭제
  1. 2
    0
      lib/cryptcheck.rb
  2. 0
    10
      lib/cryptcheck/tls.rb
  3. 53
    0
      lib/cryptcheck/tls/cert.rb
  4. 135
    115
      lib/cryptcheck/tls/cipher.rb
  5. 33
    0
      lib/cryptcheck/tls/curve.rb
  6. 4
    0
      lib/cryptcheck/tls/fixture.rb
  7. 30
    0
      lib/cryptcheck/tls/method.rb
  8. 160
    200
      lib/cryptcheck/tls/server.rb

+ 2
- 0
lib/cryptcheck.rb 파일 보기

@@ -35,7 +35,9 @@ module CryptCheck
35 35
 	autoload :Logger, 'cryptcheck/logger'
36 36
 	autoload :Tls, 'cryptcheck/tls'
37 37
 	module Tls
38
+		autoload :Method, 'cryptcheck/tls/method'
38 39
 		autoload :Cipher, 'cryptcheck/tls/cipher'
40
+		autoload :Curve, 'cryptcheck/tls/curve'
39 41
 		autoload :Server, 'cryptcheck/tls/server'
40 42
 		autoload :TcpServer, 'cryptcheck/tls/server'
41 43
 		autoload :UdpServer, 'cryptcheck/tls/server'

+ 0
- 10
lib/cryptcheck/tls.rb 파일 보기

@@ -7,16 +7,6 @@ module CryptCheck
7 7
 			::CryptCheck.analyze host, port, TcpServer, Grade
8 8
 		end
9 9
 
10
-		def self.colorize(cipher)
11
-			colors = case
12
-						 when /^SSL/ =~ cipher
13
-							 :critical
14
-						 when :TLSv1_2 == cipher
15
-							 :good
16
-					 end
17
-			cipher.to_s.colorize colors
18
-		end
19
-
20 10
 		def self.key_to_s(key)
21 11
 			size, color = case key.type
22 12
 							  when :ecc

+ 53
- 0
lib/cryptcheck/tls/cert.rb 파일 보기

@@ -0,0 +1,53 @@
1
+module CryptCheck
2
+	module Tls
3
+		class Cert
4
+			SIGNATURE_ALGORITHMS = {
5
+					'dsaWithSHA'                             => %i(sha1 dss),
6
+					'dsaWithSHA1'                            => %i(sha1 dss),
7
+					'dsaWithSHA1_2'                          => %i(sha1 dss),
8
+					'dsa_with_SHA224'                        => %i(sha2 dss),
9
+					'dsa_with_SHA256'                        => %i(sha2 dss),
10
+
11
+					'mdc2WithRSA'                            => %i(mdc2 rsa),
12
+
13
+					'md2WithRSAEncryption'                   => %i(md2 rsa),
14
+
15
+					'md4WithRSAEncryption'                   => %i(md4, rsa),
16
+
17
+					'md5WithRSA'                             => %i(md5 rsa),
18
+					'md5WithRSAEncryption'                   => %i(md5 rsa),
19
+
20
+					'shaWithRSAEncryption'                   => %i(sha rsa),
21
+					'sha1WithRSA'                            => %i(sha1 rsa),
22
+					'sha1WithRSAEncryption'                  => %i(sha1 rsa),
23
+					'sha224WithRSAEncryption'                => %i(sha2 rsa),
24
+					'sha256WithRSAEncryption'                => %i(sha2 rsa),
25
+					'sha384WithRSAEncryption'                => %i(sha2 rsa),
26
+					'sha512WithRSAEncryption'                => %i(sha2 rsa),
27
+
28
+					'ripemd160WithRSA'                       => %i(ripemd160 rsa),
29
+
30
+					'ecdsa-with-SHA1'                        => %i(sha1 ecc),
31
+					'ecdsa-with-SHA224'                      => %i(sha2 ecc),
32
+					'ecdsa-with-SHA256'                      => %i(sha2 ecc),
33
+					'ecdsa-with-SHA384'                      => %i(sha2 ecc),
34
+					'ecdsa-with-SHA512'                      => %i(sha2 ecc),
35
+
36
+					'id_GostR3411_94_with_GostR3410_2001'    => %i(ghost),
37
+					'id_GostR3411_94_with_GostR3410_94'      => %i(ghost),
38
+					'id_GostR3411_94_with_GostR3410_94_cc'   => %i(ghost),
39
+					'id_GostR3411_94_with_GostR3410_2001_cc' => %i(ghost)
40
+			}
41
+
42
+			%i(md2 mdc2 md4 md5 ripemd160 sha sha1 sha2 rsa dss ecc ghost).each do |name|
43
+				class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
44
+					def #{name}_sig?
45
+						@chains.any? do |chain|
46
+							SIGNATURE_ALGORITHMS[chain[:cert].signature_algorithm].include? :#{name}
47
+						end
48
+					end
49
+				RUBY_EVAL
50
+			end
51
+		end
52
+	end
53
+end

+ 135
- 115
lib/cryptcheck/tls/cipher.rb 파일 보기

@@ -36,11 +36,21 @@ module CryptCheck
36 36
 					ccm:       %w(CCM)
37 37
 			}
38 38
 
39
-			attr_reader :protocol, :name, :size, :key, :dh
39
+			attr_reader :method, :name, :states, :status
40 40
 
41
-			def initialize(protocol, cipher, dh=nil, key=nil)
42
-				@protocol, @dh, @key    = protocol, dh, key
43
-				@name, _, @size = cipher
41
+			def initialize(method, name)
42
+				@method, @name = method, name
43
+				fetch_states
44
+			end
45
+
46
+			extend Enumerable
47
+
48
+			def self.each(&block)
49
+				SUPPORTED.each &block
50
+			end
51
+
52
+			def self.[](method)
53
+				SUPPORTED[method]
44 54
 			end
45 55
 
46 56
 			TYPES.each do |name, ciphers|
@@ -57,12 +67,15 @@ module CryptCheck
57 67
 			def self.cbc?(cipher)
58 68
 				!aead? cipher
59 69
 			end
70
+
60 71
 			def cbc?
61 72
 				!aead?
62 73
 			end
63
-			def aead?(cipher)
74
+
75
+			def self.aead?(cipher)
64 76
 				gcm?(cipher) or ccm?(cipher)
65 77
 			end
78
+
66 79
 			def aead?
67 80
 				gcm? or ccm?
68 81
 			end
@@ -79,24 +92,14 @@ module CryptCheck
79 92
 				dhe? or ecdhe?
80 93
 			end
81 94
 
82
-			def sweet32?
83
-				enc = params[:enc]
84
-				return false unless enc # No encryption
85
-				block = enc[2]
86
-				return false unless block # No block encryption
87
-				block <= 64
95
+			def ecc?
96
+				ecdsa? or ecdhe?
88 97
 			end
89 98
 
90 99
 			def sweet32?
91
-				enc = params[:enc]
92
-				return false unless enc # No encryption
93
-				block = enc[2]
94
-				return false unless block # No block encryption
95
-				block <= 64
96
-			end
97
-
98
-			def colorize
99
-				@name.colorize self.score
100
+				size = self.block_size
101
+				return false unless size # Not block encryption
102
+				size <= 64
100 103
 			end
101 104
 
102 105
 			CHECKS = [
@@ -114,13 +117,13 @@ module CryptCheck
114 117
 
115 118
 					#[:cbc, Proc.new { |s| s.cbc? }, :warning],
116 119
 					#[:dhe, Proc.new { |s| s.dhe? }, :warning],
117
-					[:weak_dh, Proc.new do |s|
118
-						dh = s.dh
119
-						next nil unless dh
120
-						status = dh.status
121
-						next status if %i(critical error warning).include? status
122
-						nil
123
-					end ],
120
+					# [:weak_dh, Proc.new do |s|
121
+					# 	dh = s.dh
122
+					# 	next nil unless dh
123
+					# 	status = dh.status
124
+					# 	next status if %i(critical error warning).include? status
125
+					# 	nil
126
+					# end],
124 127
 					[:no_pfs, Proc.new { |s| not s.pfs? }, :warning],
125 128
 
126 129
 					[:pfs, Proc.new { |s| s.pfs? }, :good],
@@ -128,25 +131,22 @@ module CryptCheck
128 131
 					[:aead, Proc.new { |s| s.aead? }, :good],
129 132
 			]
130 133
 
131
-			def states
132
-				return @states if @states
133
-				@states = { critical: [], error: [], warning: [], good: [], perfect: [], best: [] }
134
+			def fetch_states
135
+				@states = Status.collect { |s| [s, []] }.to_h
134 136
 				CHECKS.each do |name, check, status|
135 137
 					result = check.call self
136 138
 					states[status ? status : result] << name if result
137 139
 				end
138
-				@states
140
+
141
+				@status = Status[@states.reject { |_, v| v.empty? }.keys]
139 142
 			end
140 143
 
141
-			def score
142
-				%i(critical error warning good perfect best).each do |s|
143
-					return s unless self.states[s].empty?
144
-				end
145
-				:none
144
+			def to_s
145
+				states = @states.collect { |k, vs| vs.collect { |v| v.to_s.colorize k }}.flatten.join ' '
146
+				"#{@method} #{@name.colorize @status} [#{states}]"
146 147
 			end
147 148
 
148 149
 			PRIORITY = { good: 1, none: 2, warning: 3, error: 4, critical: 5 }
149
-
150 150
 			def self.sort(ciphers)
151 151
 				ciphers.sort do |a, b|
152 152
 					error_a, error_b = PRIORITY[a.score], PRIORITY[b.score]
@@ -169,89 +169,109 @@ module CryptCheck
169 169
 				end
170 170
 			end
171 171
 
172
-			def self.list(cipher_suite = 'ALL:COMPLEMENTOFALL', protocol: :TLSv1_2)
173
-				context         = OpenSSL::SSL::SSLContext.new protocol
172
+			def self.list(cipher_suite = 'ALL:COMPLEMENTOFALL', method: :TLSv1_2)
173
+				context         = OpenSSL::SSL::SSLContext.new method
174 174
 				context.ciphers = cipher_suite
175
-				ciphers         = context.ciphers.collect { |c| self.new protocol, c }
175
+				ciphers         = context.ciphers.collect { |c| self.new method, c }
176 176
 				self.sort ciphers
177 177
 			end
178 178
 
179
-			def params
180
-				key_exchange   = case
181
-									 when ecdhe? || ecdh?
182
-										 [:ecdh, dh]
183
-									 when dhe? || dh?
184
-										 [:dh, dh]
185
-									 when dss?
186
-										 [:dss, key]
187
-									 else
188
-										 [:rsa, key]
189
-								 end
190
-				authentication = case
191
-									 when ecdsa?
192
-										 [:ecdsa, key]
193
-									 when rsa?
194
-										 [:rsa, key]
195
-									 when dss?
196
-										 [:dss, key]
197
-									 when anonymous?
198
-										 nil
199
-									 else
200
-										 [:rsa, key]
201
-								 end
202
-				encryption     = case
203
-									 when chacha20?
204
-										 :chacha20
205
-									 when aes?
206
-										 :aes
207
-									 when camellia?
208
-										 :camellia
209
-									 when seed?
210
-										 :seed
211
-									 when idea?
212
-										 :idea
213
-									 when des3?
214
-										 :'3des'
215
-									 when des?
216
-										 :des
217
-									 when rc4?
218
-										 :rc4
219
-									 when rc2?
220
-										 :rc2
221
-								 end
222
-				mode           = case
223
-									 when gcm?
224
-										 :gcm
225
-									 when ccm?
226
-										 :ccm
227
-									 when rc4? || chacha20?
228
-										 nil
229
-									 else
230
-										 :cbc
231
-								 end
232
-				b              = case encryption
233
-									 when :rc4
234
-										 nil
235
-									 when :'3des', :idea, :rc2
236
-										 64
237
-									 when :aes, :camellia, :seed
238
-										 128
239
-								 end
240
-				encryption     = [encryption, size, b, mode] if encryption
241
-				mac            = case
242
-									 when poly1305?
243
-										 [:poly1305, 128]
244
-									 when sha384?
245
-										 [:sha384, 384]
246
-									 when sha256?
247
-										 [:sha256, 256]
248
-									 when sha1?
249
-										 [:sha1, 160]
250
-									 when md5?
251
-										 [:md5, 128]
252
-								 end
253
-				{ kex: key_exchange, auth: authentication, enc: encryption, mac: mac, pfs: pfs? }
179
+			def kex
180
+				case
181
+					when ecdhe? || ecdh?
182
+						:ecdh
183
+					when dhe? || dh?
184
+						:dh
185
+					when dss?
186
+						:dss
187
+					else
188
+						:rsa
189
+				end
254 190
 			end
191
+
192
+			def auth
193
+				case
194
+					when ecdsa?
195
+						:ecdsa
196
+					when rsa?
197
+						:rsa
198
+					when dss?
199
+						:dss
200
+					when anonymous?
201
+						nil
202
+					else
203
+						:rsa
204
+				end
205
+			end
206
+
207
+			def encryption
208
+				case
209
+					when chacha20?
210
+						:chacha20
211
+					when aes?
212
+						:aes
213
+					when camellia?
214
+						:camellia
215
+					when seed?
216
+						:seed
217
+					when idea?
218
+						:idea
219
+					when des3?
220
+						:'3des'
221
+					when des?
222
+						:des
223
+					when rc4?
224
+						:rc4
225
+					when rc2?
226
+						:rc2
227
+				end
228
+			end
229
+
230
+			def mode
231
+				case
232
+					when gcm?
233
+						:gcm
234
+					when ccm?
235
+						:ccm
236
+					when rc4? || chacha20?
237
+						nil
238
+					else
239
+						:cbc
240
+				end
241
+			end
242
+
243
+			def block_size
244
+				case self.encryption
245
+					when :'3des', :idea, :rc2
246
+						64
247
+					when :aes, :camellia, :seed
248
+						128
249
+					else
250
+						nil
251
+				end
252
+			end
253
+
254
+			def hmac
255
+				case
256
+					when poly1305?
257
+						[:poly1305, 128]
258
+					when sha384?
259
+						[:sha384, 384]
260
+					when sha256?
261
+						[:sha256, 256]
262
+					when sha1?
263
+						[:sha1, 160]
264
+					when md5?
265
+						[:md5, 128]
266
+				end
267
+			end
268
+
269
+			ALL = 'ALL:COMPLEMENTOFALL'
270
+			SUPPORTED = Method.collect do |m|
271
+				context         = ::OpenSSL::SSL::SSLContext.new m.name
272
+				context.ciphers = ALL
273
+				[m, context.ciphers.collect { |c| Cipher.new m, c.first }]
274
+			end.to_h
255 275
 		end
256 276
 	end
257 277
 end

+ 33
- 0
lib/cryptcheck/tls/curve.rb 파일 보기

@@ -0,0 +1,33 @@
1
+module CryptCheck
2
+	module Tls
3
+		class Curve
4
+			attr_reader :name
5
+
6
+			def initialize(name)
7
+				@name = name
8
+			end
9
+
10
+			# SUPPORTED = %w(sect163k1 sect163r1 sect163r2 sect193r1
11
+			# 	sect193r2 sect233k1 sect233r1 sect239k1 sect283k1 sect283r1
12
+			# 	sect409k1 sect409r1 sect571k1 sect571r1 secp160k1 secp160r1
13
+			# 	secp160r2 secp192k1 secp192r1 secp224k1 secp224r1 secp256k1
14
+			# 	secp256r1 secp384r1 secp521r1
15
+			# 	prime256v1
16
+			# 	brainpoolP256r1 brainpoolP384r1 brainpoolP512r1)
17
+			SUPPORTED = %w(secp256k1 sect283k1 sect283r1 secp384r1
18
+				sect409k1 sect409r1 secp521r1 sect571k1 sect571r1
19
+				prime192v1 prime256v1
20
+				brainpoolP256r1 brainpoolP384r1 brainpoolP512r1).collect { |c| self.new c }
21
+
22
+			extend Enumerable
23
+
24
+			def self.each(&block)
25
+				SUPPORTED.each &block
26
+			end
27
+
28
+			def to_s
29
+				@name
30
+			end
31
+		end
32
+	end
33
+end

+ 4
- 0
lib/cryptcheck/tls/fixture.rb 파일 보기

@@ -62,6 +62,10 @@ class ::OpenSSL::PKey::EC
62 62
 		self.group.degree
63 63
 	end
64 64
 
65
+	def curve
66
+		self.group.curve_name
67
+	end
68
+
65 69
 	def to_s
66 70
 		"ECC #{self.size} bits"
67 71
 	end

+ 30
- 0
lib/cryptcheck/tls/method.rb 파일 보기

@@ -0,0 +1,30 @@
1
+module CryptCheck
2
+	module Tls
3
+		class Method
4
+			extend Enumerable
5
+
6
+			def self.each(&block)
7
+				SUPPORTED.each &block
8
+			end
9
+
10
+			attr_reader :name
11
+
12
+			def initialize(name)
13
+				@name = name
14
+			end
15
+
16
+			EXISTING  = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2)
17
+			SUPPORTED = (EXISTING & ::OpenSSL::SSL::SSLContext::METHODS).collect { |m| self.new m }
18
+
19
+			def to_s
20
+				colors = case @name
21
+							 when *%i(SSLv3 SSLv2)
22
+								 :critical
23
+							 when :TLSv1_2
24
+								 :good
25
+						 end
26
+				@name.to_s.colorize colors
27
+			end
28
+		end
29
+	end
30
+end

+ 160
- 200
lib/cryptcheck/tls/server.rb 파일 보기

@@ -5,10 +5,8 @@ require 'httparty'
5 5
 module CryptCheck
6 6
 	module Tls
7 7
 		class Server
8
-			TCP_TIMEOUT       = 10
9
-			SSL_TIMEOUT       = 2*TCP_TIMEOUT
10
-			EXISTING_METHODS  = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2)
11
-			SUPPORTED_METHODS = ::OpenSSL::SSL::SSLContext::METHODS
8
+			TCP_TIMEOUT = 10
9
+			SSL_TIMEOUT = 2*TCP_TIMEOUT
12 10
 
13 11
 			class TLSException < ::StandardError
14 12
 			end
@@ -30,96 +28,169 @@ module CryptCheck
30 28
 			class ConnectionError < ::StandardError
31 29
 			end
32 30
 
33
-			attr_reader :family, :ip, :port, :hostname, :prefered_ciphers, :cert, :cert_valid, :cert_trusted, :dh
34
-
35 31
 			def initialize(hostname, family, ip, port)
36 32
 				@hostname, @family, @ip, @port = hostname, family, ip, port
37 33
 				@dh                            = []
38 34
 				@chains                        = []
39
-				Logger.info { name.colorize :blue }
40
-				fetch_prefered_ciphers
41
-				check_supported_cipher
42
-				verify_certs
43
-				check_fallback_scsv
44
-				uniq_dh
45
-			end
46 35
 
47
-			def cipher_size
48
-				supported_ciphers.collect { |c| c.size }.min
49
-			end
36
+				@name = "#@ip:#@port"
37
+				@name += " [#@hostname]" if @hostname
50 38
 
51
-			def supported_protocols
52
-				@supported_ciphers.keys
53
-			end
39
+				Logger.info { @name.colorize :blue }
40
+
41
+				fetch_supported_methods
42
+				fetch_supported_ciphers
43
+				fetch_ecdsa_certs
44
+				fetch_supported_curves
45
+
46
+				# verify_certs
54 47
 
55
-			def supported_ciphers
56
-				@supported_ciphers.values.flatten 1
48
+				check_fallback_scsv
49
+
50
+				exit
57 51
 			end
58 52
 
59
-			def supported_ciphers_by_protocol(protocol)
60
-				@supported_ciphers[protocol]
53
+			def supported_method?(method)
54
+				ssl_client method
55
+				Logger.info { "Supported method : #{method}" }
56
+				true
57
+			rescue TLSException
58
+				Logger.debug { "Method not supported : #{method}" }
59
+				false
61 60
 			end
62 61
 
63
-			EXISTING_METHODS.each do |method|
64
-				class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
65
-					def #{method.to_s.downcase}?
66
-						!prefered_ciphers[:#{method}].nil?
67
-					end
68
-				RUBY_EVAL
62
+			def fetch_supported_methods
63
+				Logger.info { '' }
64
+				@supported_methods = Method.select { |m| supported_method? m }
69 65
 			end
70 66
 
71
-			def key_status
72
-				Status[@keys]
67
+			def supported_cipher?(method, cipher)
68
+				connection = ssl_client method, cipher
69
+				Logger.info { "Supported cipher #{cipher}" }
70
+				connection
71
+			rescue TLSException
72
+				Logger.debug { "Not supported cipher #{cipher}" }
73
+				nil
73 74
 			end
74 75
 
75
-			def dh_status
76
-				Status[@dh]
76
+			def fetch_supported_ciphers
77
+				Logger.info { '' }
78
+				@supported_ciphers = @supported_methods.collect do |method|
79
+					ciphers = Cipher[method].collect do |cipher|
80
+						connection = supported_cipher? method, cipher
81
+						next nil unless connection
82
+						[cipher, connection]
83
+					end.compact.to_h
84
+					[method, ciphers]
85
+				end.to_h
77 86
 			end
78 87
 
79
-			SIGNATURE_ALGORITHMS = {
80
-					'dsaWithSHA'                             => %i(sha1 dss),
81
-					'dsaWithSHA1'                            => %i(sha1 dss),
82
-					'dsaWithSHA1_2'                          => %i(sha1 dss),
83
-					'dsa_with_SHA224'                        => %i(sha2 dss),
84
-					'dsa_with_SHA256'                        => %i(sha2 dss),
88
+			def fetch_ecdsa_certs
89
+				@ecdsa_certs = {}
85 90
 
86
-					'mdc2WithRSA'                            => %i(mdc2 rsa),
91
+				@supported_ciphers.each do |method, ciphers|
92
+					ecdsa = ciphers.keys.detect &:ecdsa?
93
+					next unless ecdsa
87 94
 
88
-					'md2WithRSAEncryption'                   => %i(md2 rsa),
95
+					@ecdsa_certs = Curve.collect do |curve|
96
+						begin
97
+							connection  = ssl_client method, ecdsa, curves: curve
98
+							cert, chain = connection.peer_cert, connection.peer_cert_chain
99
+							[curve, { cert: cert, chain: chain }]
100
+						rescue TLSException
101
+							nil
102
+						end
103
+					end.compact.to_h
89 104
 
90
-					'md4WithRSAEncryption'                   => %i(md4, rsa),
105
+					break
106
+				end
107
+			end
91 108
 
92
-					'md5WithRSA'                             => %i(md5 rsa),
93
-					'md5WithRSAEncryption'                   => %i(md5 rsa),
109
+			def fetch_supported_curves
110
+				Logger.info { '' }
94 111
 
95
-					'shaWithRSAEncryption'                   => %i(sha rsa),
96
-					'sha1WithRSA'                            => %i(sha1 rsa),
97
-					'sha1WithRSAEncryption'                  => %i(sha1 rsa),
98
-					'sha224WithRSAEncryption'                => %i(sha2 rsa),
99
-					'sha256WithRSAEncryption'                => %i(sha2 rsa),
100
-					'sha384WithRSAEncryption'                => %i(sha2 rsa),
101
-					'sha512WithRSAEncryption'                => %i(sha2 rsa),
112
+				ecdsa_curve = @ecdsa_certs.keys.first
113
+				if ecdsa_curve
114
+					# If we have an ECDSA cipher, we need at least the certificate curve to do handshake,
115
+					# but with lowest priority to check for ECHDE and not just ECDSA
116
+
117
+					@supported_ciphers.each do |method, ciphers|
118
+						ecdsa = ciphers.keys.detect &:ecdsa?
119
+						next unless ecdsa
120
+						@supported_curves = Curve.select do |curve|
121
+							next true if curve == ecdsa_curve # ECDSA curve is always supported
122
+							begin
123
+								connection = ssl_client method, ecdsa, curves: [curve, ecdsa_curve]
124
+								# Not too fast !!!
125
+								# Handshake will **always** succeed, because ECDSA curve is always supported
126
+								# So, need to test for the real curve
127
+								dh         = connection.tmp_key
128
+								curve      = dh.curve
129
+								supported  = curve != ecdsa_curve
130
+								if supported
131
+									Logger.info { "Supported ECC curve #{curve}" }
132
+								else
133
+									Logger.debug { "Not supported ECC curve #{curve}" }
134
+								end
135
+								supported
136
+							rescue TLSException
137
+								false
138
+							end
139
+						end
140
+						break
141
+					end
142
+				else
143
+					# If we have no ECDSA ciphers, ECC supported are only ECDH ones
144
+					# So peak an ECDH cipher and test all curves
145
+					@supported_ciphers.each do |method, ciphers|
146
+						ecdh = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? }
147
+						next unless ecdh
148
+						@supported_curves = Curve.select do |curve|
149
+							begin
150
+								ssl_client method, ecdh, curves: curve
151
+								Logger.info { "Supported ECC curve #{curve}" }
152
+							rescue TLSException
153
+								Logger.debug { "Not supported ECC curve #{curve}" }
154
+								false
155
+							end
156
+						end
157
+						break
158
+					end
159
+				end
160
+			end
102 161
 
103
-					'ripemd160WithRSA'                       => %i(ripemd160 rsa),
162
+			def check_fallback_scsv
163
+				@fallback_scsv = false
164
+				if @supported_methods.size > 1
165
+					# We will try to connect to the not better supported method
166
+					method = @supported_methods[1]
104 167
 
105
-					'ecdsa-with-SHA1'                        => %i(sha1 ecc),
106
-					'ecdsa-with-SHA224'                      => %i(sha2 ecc),
107
-					'ecdsa-with-SHA256'                      => %i(sha2 ecc),
108
-					'ecdsa-with-SHA384'                      => %i(sha2 ecc),
109
-					'ecdsa-with-SHA512'                      => %i(sha2 ecc),
168
+					begin
169
+						ssl_client method, fallback: true
170
+					rescue InappropriateFallback
171
+						@fallback_scsv = true
172
+					end
173
+				else
174
+					@fallback_scsv = nil
175
+				end
110 176
 
111
-					'id_GostR3411_94_with_GostR3410_2001'    => %i(ghost),
112
-					'id_GostR3411_94_with_GostR3410_94'      => %i(ghost),
113
-					'id_GostR3411_94_with_GostR3410_94_cc'   => %i(ghost),
114
-					'id_GostR3411_94_with_GostR3410_2001_cc' => %i(ghost)
115
-			}
177
+				Logger.info { '' }
178
+				text, color = case @fallback_scsv
179
+								  when true
180
+									  ['Supported', :good]
181
+								  when false
182
+									  ['Not supported', :error]
183
+								  when nil
184
+									  ['Not applicable', :unknown]
185
+							  end
186
+				Logger.info { "Fallback SCSV : #{text.colorize color}" }
187
+			end
116 188
 
117
-			%i(md2 mdc2 md4 md5 ripemd160 sha sha1 sha2 rsa dss ecc ghost).each do |name|
189
+			Method.each do |method|
190
+				method = method.name
118 191
 				class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
119
-					def #{name}_sig?
120
-						@chains.any? do |chain|
121
-							SIGNATURE_ALGORITHMS[chain[:cert].signature_algorithm].include? :#{name}
122
-						end
192
+					def #{method.to_s.downcase}?
193
+						@supported_methods.detect { |m| m.name == method } 
123 194
 					end
124 195
 				RUBY_EVAL
125 196
 			end
@@ -185,12 +256,6 @@ module CryptCheck
185 256
 			end
186 257
 
187 258
 			private
188
-			def name
189
-				name = "#@ip:#@port"
190
-				name += " [#@hostname]" if @hostname
191
-				name
192
-			end
193
-
194 259
 			def connect(&block)
195 260
 				socket   = ::Socket.new @family, sock_type
196 261
 				sockaddr = ::Socket.sockaddr_in @port, @ip
@@ -232,14 +297,16 @@ module CryptCheck
232 297
 					retry
233 298
 				rescue ::OpenSSL::SSL::SSLError => e
234 299
 					case e.message
235
-						when /state=SSLv.* read server hello A$/
300
+						when /state=SSLv3 read server hello A$/
236 301
 							raise TLSNotAvailableException, e
237
-						when /state=SSLv.* read server hello A: wrong version number$/
302
+						when /state=SSLv3 read server hello A: wrong version number$/
303
+							raise MethodNotAvailable, e
304
+						when /state=SSLv3 read server hello A: tlsv1 alert protocol version$/
238 305
 							raise MethodNotAvailable, e
239 306
 						when /state=error: no ciphers available$/,
240
-								/state=SSLv.* read server hello A: sslv.* alert handshake failure$/
307
+								/state=SSLv3 read server hello A: sslv3 alert handshake failure$/
241 308
 							raise CipherNotAvailable, e
242
-						when /state=SSLv.* read server hello A: tlsv.* alert inappropriate fallback$/
309
+						when /state=SSLv3 read server hello A: tlsv3 alert inappropriate fallback$/
243 310
 							raise InappropriateFallback, e
244 311
 					end
245 312
 					raise
@@ -254,32 +321,29 @@ module CryptCheck
254 321
 				end
255 322
 			end
256 323
 
257
-			# SUPPORTED_CURVES = %w(sect163k1 sect163r1 sect163r2 sect193r1
258
-			# 	sect193r2 sect233k1 sect233r1 sect239k1 sect283k1 sect283r1
259
-			# 	sect409k1 sect409r1 sect571k1 sect571r1 secp160k1 secp160r1
260
-			# 	secp160r2 secp192k1 secp192r1 secp224k1 secp224r1 secp256k1
261
-			# 	secp256r1 secp384r1 secp521r1
262
-			# 	prime256v1
263
-			# 	brainpoolP256r1 brainpoolP384r1 brainpoolP512r1)
264
-			SUPPORTED_CURVES = %w(secp256k1 sect283k1 sect283r1 secp384r1
265
-				sect409k1 sect409r1 secp521r1 sect571k1 sect571r1
266
-				prime192v1 prime256v1
267
-				brainpoolP256r1 brainpoolP384r1 brainpoolP512r1)
268
-
269
-			def ssl_client(method, ciphers = %w(ALL COMPLEMENTOFALL), curves = nil, fallback: false, &block)
324
+			def ssl_client(method, ciphers = nil, curves: nil, fallback: false, &block)
325
+				method      = method.name
270 326
 				ssl_context = ::OpenSSL::SSL::SSLContext.new method
271 327
 				ssl_context.enable_fallback_scsv if fallback
272
-				ssl_context.ciphers     = ciphers.join ':'
273 328
 
274
-				ssl_context.ecdh_curves = curves.join ':' if curves
275
-				#ssl_context.ecdh_auto = false
276
-				#ecdh = OpenSSL::PKey::EC.new('sect163r1').generate_key
277
-				#ssl_context.tmp_ecdh_callback = proc { ecdh }
329
+				if ciphers
330
+					ciphers = [ciphers] unless ciphers.is_a? Enumerable
331
+					ciphers = ciphers.collect(&:name).join ':'
332
+				else
333
+					ciphers = Cipher::ALL
334
+				end
335
+				ssl_context.ciphers = ciphers
336
+
337
+				if curves
338
+					curves                  = [curves] unless curves.is_a? Enumerable
339
+					curves                  = curves.collect(&:name).join ':'
340
+					ssl_context.ecdh_curves = curves
341
+				end
278 342
 
279 343
 				Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" }
280 344
 				connect do |socket|
281 345
 					ssl_connect socket, ssl_context, method do |ssl_socket|
282
-						return block_given? ? block.call(ssl_socket) : nil
346
+						return block_given? ? block.call(ssl_socket) : ssl_socket
283 347
 					end
284 348
 				end
285 349
 			end
@@ -304,110 +368,6 @@ module CryptCheck
304 368
 				@keys   = @chains.collect { |c| c[:key] }
305 369
 			end
306 370
 
307
-			def prefered_cipher(method)
308
-				cipher = ssl_client(method) { |s| Cipher.new method, s.cipher, s.tmp_key }
309
-				Logger.info { "Prefered cipher for #{Tls.colorize method} : #{cipher.colorize}" }
310
-				cipher
311
-			rescue => e
312
-				Logger.debug { "Method #{Tls.colorize method} not supported : #{e}" }
313
-				nil
314
-			end
315
-
316
-			def fetch_prefered_ciphers
317
-				@prefered_ciphers = {}
318
-				EXISTING_METHODS.each do |method|
319
-					next unless SUPPORTED_METHODS.include? method
320
-					@prefered_ciphers[method] = prefered_cipher method
321
-				end
322
-				raise TLSNotAvailableException unless @prefered_ciphers.any? { |_, c| !c.nil? }
323
-			end
324
-
325
-			def available_ciphers(method)
326
-				context         = ::OpenSSL::SSL::SSLContext.new method
327
-				context.ciphers = %w(ALL COMPLEMENTOFALL)
328
-				context.ciphers
329
-			end
330
-
331
-			def supported_cipher?(method, cipher, curves = nil)
332
-				cert, chain, dh = ssl_client(method, [cipher], curves) do |s|
333
-					[s.peer_cert, s.peer_cert_chain, s.tmp_key]
334
-				end
335
-				@chains << [cert, chain]
336
-				@dh << dh if dh
337
-				p dh.group.curve_name
338
-				cipher = Cipher.new method, cipher, dh
339
-				dh     = dh ? " (#{'PFS'.colorize :good} : #{Tls.key_to_s dh})" : ''
340
-
341
-				states = cipher.states
342
-				text   = %i(critical error warning good perfect best).collect do |s|
343
-					states[s].collect { |t| t.to_s.colorize s }.join ' '
344
-				end.reject &:empty?
345
-				text   = text.join ' '
346
-
347
-				Logger.info { "#{Tls.colorize method} / #{cipher.colorize}#{dh} [#{text}]" }
348
-
349
-				cipher
350
-			rescue => e
351
-				cipher = Cipher.new method, cipher
352
-				Logger.debug { "#{Tls.colorize method} / #{cipher.colorize} : Not supported (#{e})" }
353
-				nil
354
-			end
355
-
356
-			def check_supported_cipher
357
-				Logger.info { '' }
358
-				@supported_ciphers = {}
359
-				EXISTING_METHODS.each do |method|
360
-					next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method]
361
-					supported_ciphers = []
362
-
363
-					available_ciphers = available_ciphers method
364
-					available_ciphers.each do |c|
365
-						cipher    = Cipher.new method, c
366
-						supported = supported_cipher? method, c.first
367
-						if supported
368
-							if cipher.ecdhe?
369
-								SUPPORTED_CURVES.each do |curve|
370
-									supported = supported_cipher? method, c.first, [curve]
371
-									supported_ciphers << supported if supported
372
-								end
373
-							else
374
-								supported_ciphers << supported
375
-							end
376
-						end
377
-					end
378
-					@supported_ciphers[method] = supported_ciphers
379
-				end
380
-			end
381
-
382
-			def check_fallback_scsv
383
-				Logger.info { '' }
384
-				@fallback_scsv = false
385
-
386
-				methods = @prefered_ciphers.reject { |_, v| v.nil? }.keys
387
-				if methods.size > 1
388
-					# We will try to connect to the not better supported method
389
-					method = methods[1]
390
-
391
-					begin
392
-						ssl_client method, fallback: true
393
-					rescue InappropriateFallback
394
-						@fallback_scsv = true
395
-					end
396
-				else
397
-					@fallback_scsv = nil
398
-				end
399
-
400
-				text, color = case @fallback_scsv
401
-								  when true
402
-									  ['Supported', :good]
403
-								  when false
404
-									  ['Not supported', :error]
405
-								  when nil
406
-									  ['Not applicable', :unknown]
407
-							  end
408
-				Logger.info { "Fallback SCSV : #{text.colorize color}" }
409
-			end
410
-
411 371
 			def verify_trust(chain, cert)
412 372
 				store         = ::OpenSSL::X509::Store.new
413 373
 				store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT

Loading…
취소
저장