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.

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