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.

server.rb 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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