Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

engine.rb 15KB

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