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.

524 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_methods, :supported_ciphers, :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. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  256. def #{method.to_sym.downcase}?
  257. @supported_methods.detect { |m| m == method }
  258. end
  259. RUBY_EVAL
  260. end
  261. Cipher::TYPES.each do |type, _|
  262. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  263. def #{type}?
  264. @supported_ciphers.any? { |c| c.#{type}? }
  265. end
  266. RUBY_EVAL
  267. end
  268. def ssl?
  269. sslv2? or sslv3?
  270. end
  271. def tls?
  272. tlsv1? or tlsv1_1? or tlsv1_2?
  273. end
  274. def tls_only?
  275. tls? and !ssl?
  276. end
  277. def tlsv1_2_only?
  278. tlsv1_2? and not ssl? and not tlsv1? and not tlsv1_1?
  279. end
  280. def pfs?
  281. supported_ciphers.any? { |c| c.pfs? }
  282. end
  283. def pfs_only?
  284. supported_ciphers.all? { |c| c.pfs? }
  285. end
  286. def ecdhe?
  287. supported_ciphers.any? { |c| c.ecdhe? }
  288. end
  289. def ecdhe_only?
  290. supported_ciphers.all? { |c| c.ecdhe? }
  291. end
  292. def aead?
  293. supported_ciphers.any? { |c| c.aead? }
  294. end
  295. def aead_only?
  296. supported_ciphers.all? { |c| c.aead? }
  297. end
  298. def sweet32?
  299. supported_ciphers.any? { |c| c.sweet32? }
  300. end
  301. def fallback_scsv?
  302. @fallback_scsv
  303. end
  304. def must_staple?
  305. @cert.extensions.any? { |e| e.oid == '1.3.6.1.5.5.7.1.24' }
  306. end
  307. private
  308. def connect(&block)
  309. socket = ::Socket.new @family, sock_type
  310. sockaddr = ::Socket.sockaddr_in @port, @ip
  311. #Logger.trace { "Connecting to #{@ip}:#{@port}" }
  312. begin
  313. status = socket.connect_nonblock sockaddr
  314. #Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" }
  315. raise ConnectionError, status unless status == 0
  316. #Logger.trace { "Connected to #{@ip}:#{@port}" }
  317. block_given? ? block.call(socket) : nil
  318. rescue ::IO::WaitReadable
  319. #Logger.trace { "Waiting for read to #{@ip}:#{@port}" }
  320. raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT
  321. retry
  322. rescue ::IO::WaitWritable
  323. #Logger.trace { "Waiting for write to #{@ip}:#{@port}" }
  324. raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select nil, [socket], nil, TCP_TIMEOUT
  325. retry
  326. ensure
  327. socket.close
  328. end
  329. end
  330. def ssl_connect(socket, context, method, &block)
  331. ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context
  332. ssl_socket.hostname = @hostname if @hostname and method != :SSLv2
  333. #Logger.trace { "SSL connecting to #{name}" }
  334. begin
  335. ssl_socket.connect_nonblock
  336. #Logger.trace { "SSL connected to #{name}" }
  337. return block_given? ? block.call(ssl_socket) : nil
  338. rescue ::OpenSSL::SSL::SSLErrorWaitReadable
  339. #Logger.trace { "Waiting for SSL read to #{name}" }
  340. raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT
  341. retry
  342. rescue ::OpenSSL::SSL::SSLErrorWaitWritable
  343. #Logger.trace { "Waiting for SSL write to #{name}" }
  344. raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT
  345. retry
  346. rescue ::OpenSSL::SSL::SSLError => e
  347. case e.message
  348. when /state=SSLv2 read server hello A$/,
  349. /state=SSLv3 read server hello A$/,
  350. /state=SSLv3 read server hello A: wrong version number$/,
  351. /state=SSLv3 read server hello A: tlsv1 alert protocol version$/,
  352. /state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/
  353. raise MethodNotAvailable, e
  354. when /state=SSLv2 read server hello A: peer error no cipher$/,
  355. /state=error: no ciphers available$/,
  356. /state=SSLv3 read server hello A: sslv3 alert handshake failure$/,
  357. /state=error: missing export tmp dh key$/,
  358. /state=error: wrong curve$/
  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. ssl_context = ::OpenSSL::SSL::SSLContext.new method.to_sym
  376. ssl_context.enable_fallback_scsv if fallback
  377. if ciphers
  378. ciphers = [ciphers] unless ciphers.is_a? Enumerable
  379. ciphers = ciphers.collect(&:name).join ':'
  380. else
  381. ciphers = Cipher::ALL
  382. end
  383. ssl_context.ciphers = ciphers
  384. if curves
  385. curves = [curves] unless curves.is_a? Enumerable
  386. # OpenSSL fails if the same curve is selected multiple times
  387. # So because Array#uniq preserves order, remove the less prefered ones
  388. curves = curves.collect(&:name).uniq.join ':'
  389. ssl_context.ecdh_curves = curves
  390. end
  391. Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" }
  392. connect do |socket|
  393. ssl_connect socket, ssl_context, method do |ssl_socket|
  394. return block_given? ? block.call(ssl_socket) : ssl_socket
  395. end
  396. end
  397. end
  398. def verify_certs
  399. Logger.info { '' }
  400. Logger.info { 'Certificates' }
  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. @certs.each do |cert|
  415. key = cert.key
  416. identity = cert.valid?(@hostname || @ip)
  417. trust = cert.trusted?
  418. Logger.info { " Certificate #{cert.subject} [#{cert.serial}] issued by #{cert.issuer}" }
  419. Logger.info { ' Key : ' + Tls.key_to_s(key) }
  420. if identity
  421. Logger.info { ' Identity : ' + 'valid'.colorize(:good) }
  422. else
  423. Logger.info { ' Identity : ' + 'invalid'.colorize(:error) }
  424. end
  425. if trust == :trusted
  426. Logger.info { ' Trust : ' + 'trusted'.colorize(:good) }
  427. else
  428. Logger.info { ' Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust }
  429. end
  430. end
  431. @keys = @certs.collect &:key
  432. end
  433. def uniq_dh
  434. dh, find = [], []
  435. @dh.each do |k|
  436. f = [k.type, k.size]
  437. unless find.include? f
  438. dh << k
  439. find << f
  440. end
  441. end
  442. @dh = dh
  443. end
  444. Cert::SIGNATURE_ALGORITHMS.each do |s|
  445. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  446. def #{s}_sign?
  447. @certs.any? &:#{s}?
  448. end
  449. RUBY_EVAL
  450. end
  451. end
  452. class TcpServer < Server
  453. private
  454. def sock_type
  455. ::Socket::SOCK_STREAM
  456. end
  457. end
  458. class UdpServer < Server
  459. private
  460. def sock_type
  461. ::Socket::SOCK_DGRAM
  462. end
  463. end
  464. end
  465. end