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.

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