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.

567 lines
16KB

  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 }
  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.to_sym} }
  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. uniq_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. uniq_supported_ciphers.any? { |c| c.pfs? }
  282. end
  283. def pfs_only?
  284. uniq_supported_ciphers.all? { |c| c.pfs? }
  285. end
  286. def ecdhe?
  287. uniq_supported_ciphers.any? { |c| c.ecdhe? }
  288. end
  289. def ecdhe_only?
  290. uniq_supported_ciphers.all? { |c| c.ecdhe? }
  291. end
  292. def aead?
  293. uniq_supported_ciphers.any? { |c| c.aead? }
  294. end
  295. def aead_only?
  296. uniq_supported_ciphers.all? { |c| c.aead? }
  297. end
  298. def sweet32?
  299. uniq_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. include Statused
  308. CHECKS = [
  309. # Protocols
  310. [:ssl, -> (s) { s.ssl? }, :critical],
  311. [:tls12, -> (s) { s.tlsv1_2? }, :good],
  312. [:tls12_only, -> (s) { s.tlsv1_2_only? }, :perfect],
  313. # Ciphers
  314. [:dss, -> (s) { s.dss? }, :critical],
  315. [:anonymous, -> (s) { s.anonymous? }, :critical],
  316. [:null, -> (s) { s.null? }, :critical],
  317. [:export, -> (s) { s.export? }, :critical],
  318. [:des, -> (s) { s.des? }, :critical],
  319. [:md5, -> (s) { s.md5? }, :critical],
  320. [:rc4, -> (s) { s.rc4? }, :error],
  321. [:sweet32, -> (s) { s.sweet32? }, :error],
  322. [:no_pfs, -> (s) { not s.pfs_only? }, :warning],
  323. [:pfs, -> (s) { s.pfs? }, :good],
  324. [:pfs_only, -> (s) { s.pfs_only? }, :perfect],
  325. [:no_ecdhe, -> (s) { not s.ecdhe? }, :warning],
  326. [:ecdhe, -> (s) { s.ecdhe? }, :good],
  327. [:ecdhe_only, -> (s) { s.ecdhe_only? }, :perfect],
  328. [:aead, -> (s) { s.aead? }, :good],
  329. #[:aead_only, -> (s) { s.aead_only? }, :best],
  330. ].freeze
  331. def checks
  332. checks = CHECKS
  333. unless self.fallback_scsv? == nil
  334. checks += [
  335. [:no_fallback_scsv, -> (s) { not s.fallback_scsv? }, :error],
  336. [:fallback_scsv, -> (s) { s.fallback_scsv? }, :good]
  337. ]
  338. end
  339. checks
  340. end
  341. def children
  342. @certs + @dh
  343. end
  344. private
  345. def connect(&block)
  346. socket = ::Socket.new @family, sock_type
  347. sockaddr = ::Socket.sockaddr_in @port, @ip
  348. #Logger.trace { "Connecting to #{@ip}:#{@port}" }
  349. begin
  350. status = socket.connect_nonblock sockaddr
  351. #Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" }
  352. raise ConnectionError, status unless status == 0
  353. #Logger.trace { "Connected to #{@ip}:#{@port}" }
  354. block_given? ? block.call(socket) : nil
  355. rescue ::IO::WaitReadable
  356. #Logger.trace { "Waiting for read to #{@ip}:#{@port}" }
  357. raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT
  358. retry
  359. rescue ::IO::WaitWritable
  360. #Logger.trace { "Waiting for write to #{@ip}:#{@port}" }
  361. raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select nil, [socket], nil, TCP_TIMEOUT
  362. retry
  363. ensure
  364. socket.close
  365. end
  366. end
  367. def ssl_connect(socket, context, method, &block)
  368. ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context
  369. ssl_socket.hostname = @hostname if @hostname and method != :SSLv2
  370. #Logger.trace { "SSL connecting to #{name}" }
  371. begin
  372. ssl_socket.connect_nonblock
  373. #Logger.trace { "SSL connected to #{name}" }
  374. return block_given? ? block.call(ssl_socket) : nil
  375. rescue ::OpenSSL::SSL::SSLErrorWaitReadable
  376. #Logger.trace { "Waiting for SSL read to #{name}" }
  377. raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT
  378. retry
  379. rescue ::OpenSSL::SSL::SSLErrorWaitWritable
  380. #Logger.trace { "Waiting for SSL write to #{name}" }
  381. raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT
  382. retry
  383. rescue ::OpenSSL::SSL::SSLError => e
  384. case e.message
  385. when /state=SSLv2 read server hello A$/,
  386. /state=SSLv3 read server hello A$/,
  387. /state=SSLv3 read server hello A: wrong version number$/,
  388. /state=SSLv3 read server hello A: tlsv1 alert protocol version$/,
  389. /state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/
  390. raise MethodNotAvailable, e
  391. when /state=SSLv2 read server hello A: peer error no cipher$/,
  392. /state=error: no ciphers available$/,
  393. /state=SSLv3 read server hello A: sslv3 alert handshake failure$/,
  394. /state=error: missing export tmp dh key$/,
  395. /state=error: wrong curve$/
  396. raise CipherNotAvailable, e
  397. when /state=SSLv3 read server hello A: tlsv1 alert inappropriate fallback$/
  398. raise InappropriateFallback, e
  399. end
  400. raise
  401. rescue ::SystemCallError => e
  402. case e.message
  403. when /^Connection reset by peer - SSL_connect$/
  404. raise TLSNotAvailableException, e
  405. end
  406. raise
  407. ensure
  408. ssl_socket.close
  409. end
  410. end
  411. def ssl_client(method, ciphers = nil, curves: nil, fallback: false, &block)
  412. ssl_context = ::OpenSSL::SSL::SSLContext.new method.to_sym
  413. ssl_context.enable_fallback_scsv if fallback
  414. if ciphers
  415. ciphers = [ciphers] unless ciphers.is_a? Enumerable
  416. ciphers = ciphers.collect(&:name).join ':'
  417. else
  418. ciphers = Cipher::ALL
  419. end
  420. ssl_context.ciphers = ciphers
  421. if curves
  422. curves = [curves] unless curves.is_a? Enumerable
  423. # OpenSSL fails if the same curve is selected multiple times
  424. # So because Array#uniq preserves order, remove the less prefered ones
  425. curves = curves.collect(&:name).uniq.join ':'
  426. ssl_context.ecdh_curves = curves
  427. end
  428. Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" }
  429. connect do |socket|
  430. ssl_connect socket, ssl_context, method do |ssl_socket|
  431. return block_given? ? block.call(ssl_socket) : ssl_socket
  432. end
  433. end
  434. end
  435. def verify_certs
  436. Logger.info { '' }
  437. Logger.info { 'Certificates' }
  438. # Let's begin the fun
  439. # First, collect "standard" connections
  440. # { method => { cipher => connection, ... }, ... }
  441. certs = @supported_ciphers.values.collect(&:values).flatten 1
  442. # Then, collect "ecdsa" connections
  443. # { curve => connection, ... }
  444. certs += @ecdsa_certs.values
  445. # For anonymous cipher, there is no certificate at all
  446. certs = certs.reject { |c| c.peer_cert.nil? }
  447. # Then, fetch cert
  448. certs = certs.collect { |c| Cert.new c }
  449. # Then, filter cert to keep uniq fingerprint
  450. @certs = certs.uniq { |c| c.fingerprint }
  451. @certs.each do |cert|
  452. key = cert.key
  453. identity = cert.valid?(@hostname || @ip)
  454. trust = cert.trusted?
  455. Logger.info { " Certificate #{cert.subject} [#{cert.serial}] issued by #{cert.issuer}" }
  456. Logger.info { ' Key : ' + Tls.key_to_s(key) }
  457. if identity
  458. Logger.info { ' Identity : ' + 'valid'.colorize(:good) }
  459. else
  460. Logger.info { ' Identity : ' + 'invalid'.colorize(:error) }
  461. end
  462. if trust == :trusted
  463. Logger.info { ' Trust : ' + 'trusted'.colorize(:good) }
  464. else
  465. Logger.info { ' Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust }
  466. end
  467. end
  468. @keys = @certs.collect &:key
  469. end
  470. def uniq_dh
  471. dh, find = [], []
  472. @dh.each do |k|
  473. f = [k.type, k.size]
  474. unless find.include? f
  475. dh << k
  476. find << f
  477. end
  478. end
  479. @dh = dh
  480. end
  481. private
  482. def uniq_supported_ciphers
  483. @supported_ciphers.values.collect(&:keys).flatten.uniq
  484. end
  485. end
  486. class TcpServer < Server
  487. private
  488. def sock_type
  489. ::Socket::SOCK_STREAM
  490. end
  491. end
  492. class UdpServer < Server
  493. private
  494. def sock_type
  495. ::Socket::SOCK_DGRAM
  496. end
  497. end
  498. end
  499. end