You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

525 lines
15KB

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