Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

engine.rb 14KB

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