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.

464 lines
13KB

  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. EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2)
  10. SUPPORTED_METHODS = ::OpenSSL::SSL::SSLContext::METHODS
  11. class TLSException < ::StandardError
  12. end
  13. class TLSNotAvailableException < TLSException
  14. def to_s
  15. 'TLS seems not supported on this server'
  16. end
  17. end
  18. class MethodNotAvailable < TLSException
  19. end
  20. class CipherNotAvailable < TLSException
  21. end
  22. class InappropriateFallback < TLSException
  23. end
  24. class Timeout < ::StandardError
  25. end
  26. class TLSTimeout < Timeout
  27. end
  28. class ConnectionError < ::StandardError
  29. end
  30. attr_reader :family, :ip, :port, :hostname, :prefered_ciphers, :cert, :cert_valid, :cert_trusted, :dh
  31. def initialize(hostname, family, ip, port)
  32. @hostname, @family, @ip, @port = hostname, family, ip, port
  33. @dh = []
  34. @chains = []
  35. Logger.info { name.colorize :blue }
  36. fetch_prefered_ciphers
  37. check_supported_cipher
  38. verify_certs
  39. check_fallback_scsv
  40. uniq_dh
  41. end
  42. def cipher_size
  43. supported_ciphers.collect { |c| c.size }.min
  44. end
  45. def supported_protocols
  46. @supported_ciphers.keys
  47. end
  48. def supported_ciphers
  49. @supported_ciphers.values.flatten 1
  50. end
  51. def supported_ciphers_by_protocol(protocol)
  52. @supported_ciphers[protocol]
  53. end
  54. EXISTING_METHODS.each do |method|
  55. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  56. def #{method.to_s.downcase}?
  57. !prefered_ciphers[:#{method}].nil?
  58. end
  59. RUBY_EVAL
  60. end
  61. def key_status
  62. Status[@keys]
  63. end
  64. def dh_status
  65. Status[@dh]
  66. end
  67. SIGNATURE_ALGORITHMS = {
  68. 'dsaWithSHA' => %i(sha1 dss),
  69. 'dsaWithSHA1' => %i(sha1 dss),
  70. 'dsaWithSHA1_2' => %i(sha1 dss),
  71. 'dsa_with_SHA224' => %i(sha2 dss),
  72. 'dsa_with_SHA256' => %i(sha2 dss),
  73. 'mdc2WithRSA' => %i(mdc2 rsa),
  74. 'md2WithRSAEncryption' => %i(md2 rsa),
  75. 'md4WithRSAEncryption' => %i(md4, rsa),
  76. 'md5WithRSA' => %i(md5 rsa),
  77. 'md5WithRSAEncryption' => %i(md5 rsa),
  78. 'shaWithRSAEncryption' => %i(sha rsa),
  79. 'sha1WithRSA' => %i(sha1 rsa),
  80. 'sha1WithRSAEncryption' => %i(sha1 rsa),
  81. 'sha224WithRSAEncryption' => %i(sha2 rsa),
  82. 'sha256WithRSAEncryption' => %i(sha2 rsa),
  83. 'sha384WithRSAEncryption' => %i(sha2 rsa),
  84. 'sha512WithRSAEncryption' => %i(sha2 rsa),
  85. 'ripemd160WithRSA' => %i(ripemd160 rsa),
  86. 'ecdsa-with-SHA1' => %i(sha1 ecc),
  87. 'ecdsa-with-SHA224' => %i(sha2 ecc),
  88. 'ecdsa-with-SHA256' => %i(sha2 ecc),
  89. 'ecdsa-with-SHA384' => %i(sha2 ecc),
  90. 'ecdsa-with-SHA512' => %i(sha2 ecc),
  91. 'id_GostR3411_94_with_GostR3410_2001' => %i(ghost),
  92. 'id_GostR3411_94_with_GostR3410_94' => %i(ghost),
  93. 'id_GostR3411_94_with_GostR3410_94_cc' => %i(ghost),
  94. 'id_GostR3411_94_with_GostR3410_2001_cc' => %i(ghost)
  95. }
  96. %i(md2 mdc2 md4 md5 ripemd160 sha sha1 sha2 rsa dss ecc ghost).each do |name|
  97. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  98. def #{name}_sig?
  99. @chains.any? do |chain|
  100. SIGNATURE_ALGORITHMS[chain[:cert].signature_algorithm].include? :#{name}
  101. end
  102. end
  103. RUBY_EVAL
  104. end
  105. Cipher::TYPES.each do |type, _|
  106. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  107. def #{type}?
  108. supported_ciphers.any? { |c| c.#{type}? }
  109. end
  110. RUBY_EVAL
  111. end
  112. def ssl?
  113. sslv2? or sslv3?
  114. end
  115. def tls?
  116. tlsv1? or tlsv1_1? or tlsv1_2?
  117. end
  118. def tls_only?
  119. tls? and !ssl?
  120. end
  121. def tlsv1_2_only?
  122. tlsv1_2? and not ssl? and not tlsv1? and not tlsv1_1?
  123. end
  124. def pfs?
  125. supported_ciphers.any? { |c| c.pfs? }
  126. end
  127. def pfs_only?
  128. supported_ciphers.all? { |c| c.pfs? }
  129. end
  130. def ecdhe?
  131. supported_ciphers.any? { |c| c.ecdhe? }
  132. end
  133. def ecdhe_only?
  134. supported_ciphers.all? { |c| c.ecdhe? }
  135. end
  136. def aead?
  137. supported_ciphers.any? { |c| c.aead? }
  138. end
  139. def aead_only?
  140. supported_ciphers.all? { |c| c.aead? }
  141. end
  142. def sweet32?
  143. supported_ciphers.any? { |c| c.sweet32? }
  144. end
  145. def fallback_scsv?
  146. @fallback_scsv
  147. end
  148. def must_staple?
  149. @cert.extensions.any? { |e| e.oid == '1.3.6.1.5.5.7.1.24' }
  150. end
  151. private
  152. def name
  153. name = "#@ip:#@port"
  154. name += " [#@hostname]" if @hostname
  155. name
  156. end
  157. def connect(&block)
  158. socket = ::Socket.new @family, sock_type
  159. sockaddr = ::Socket.sockaddr_in @port, @ip
  160. #Logger.trace { "Connecting to #{@ip}:#{@port}" }
  161. begin
  162. status = socket.connect_nonblock sockaddr
  163. #Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" }
  164. raise ConnectionError, status unless status == 0
  165. #Logger.trace { "Connected to #{@ip}:#{@port}" }
  166. block_given? ? block.call(socket) : nil
  167. rescue ::IO::WaitReadable
  168. #Logger.trace { "Waiting for read to #{@ip}:#{@port}" }
  169. raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT
  170. retry
  171. rescue ::IO::WaitWritable
  172. #Logger.trace { "Waiting for write to #{@ip}:#{@port}" }
  173. raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select nil, [socket], nil, TCP_TIMEOUT
  174. retry
  175. ensure
  176. socket.close
  177. end
  178. end
  179. def ssl_connect(socket, context, method, &block)
  180. ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context
  181. ssl_socket.hostname = @hostname if @hostname and method != :SSLv2
  182. #Logger.trace { "SSL connecting to #{name}" }
  183. begin
  184. ssl_socket.connect_nonblock
  185. #Logger.trace { "SSL connected to #{name}" }
  186. return block_given? ? block.call(ssl_socket) : nil
  187. rescue ::OpenSSL::SSL::SSLErrorWaitReadable
  188. #Logger.trace { "Waiting for SSL read to #{name}" }
  189. raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT
  190. retry
  191. rescue ::OpenSSL::SSL::SSLErrorWaitWritable
  192. #Logger.trace { "Waiting for SSL write to #{name}" }
  193. raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT
  194. retry
  195. rescue ::OpenSSL::SSL::SSLError => e
  196. case e.message
  197. when /state=SSLv.* read server hello A$/
  198. raise TLSNotAvailableException, e
  199. when /state=SSLv.* read server hello A: wrong version number$/
  200. raise MethodNotAvailable, e
  201. when /state=error: no ciphers available$/,
  202. /state=SSLv.* read server hello A: sslv.* alert handshake failure$/
  203. raise CipherNotAvailable, e
  204. when /state=SSLv.* read server hello A: tlsv.* alert inappropriate fallback$/
  205. raise InappropriateFallback, e
  206. end
  207. raise
  208. rescue ::SystemCallError => e
  209. case e.message
  210. when /^Connection reset by peer - SSL_connect$/
  211. raise TLSNotAvailableException, e
  212. end
  213. raise
  214. ensure
  215. ssl_socket.close
  216. end
  217. end
  218. # SUPPORTED_CURVES = %w(sect163k1 sect163r1 sect163r2 sect193r1
  219. # sect193r2 sect233k1 sect233r1 sect239k1 sect283k1 sect283r1
  220. # sect409k1 sect409r1 sect571k1 sect571r1 secp160k1 secp160r1
  221. # secp160r2 secp192k1 secp192r1 secp224k1 secp224r1 secp256k1
  222. # secp256r1 secp384r1 secp521r1
  223. # prime256v1
  224. # brainpoolP256r1 brainpoolP384r1 brainpoolP512r1)
  225. SUPPORTED_CURVES = %w(secp256k1 sect283k1 sect283r1 secp384r1
  226. sect409k1 sect409r1 secp521r1 sect571k1 sect571r1
  227. prime192v1 prime256v1
  228. brainpoolP256r1 brainpoolP384r1 brainpoolP512r1)
  229. def ssl_client(method, ciphers = %w(ALL COMPLEMENTOFALL), curves = nil, fallback: false, &block)
  230. ssl_context = ::OpenSSL::SSL::SSLContext.new method
  231. ssl_context.enable_fallback_scsv if fallback
  232. ssl_context.ciphers = ciphers.join ':'
  233. ssl_context.ecdh_curves = curves.join ':' if curves
  234. #ssl_context.ecdh_auto = false
  235. #ecdh = OpenSSL::PKey::EC.new('sect163r1').generate_key
  236. #ssl_context.tmp_ecdh_callback = proc { ecdh }
  237. Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" }
  238. connect do |socket|
  239. ssl_connect socket, ssl_context, method do |ssl_socket|
  240. return block_given? ? block.call(ssl_socket) : nil
  241. end
  242. end
  243. end
  244. def verify_certs
  245. Logger.info { '' }
  246. view = {}
  247. @chains.each do |cert, chain|
  248. id = cert.subject, cert.serial, cert.issuer
  249. next if view.include? id
  250. subject, serial, issuer = id
  251. key = cert.public_key
  252. Logger.info { "Certificate #{subject} [#{serial}] issued by #{issuer}" }
  253. Logger.info { "Key : #{Tls.key_to_s key }" }
  254. valid = ::OpenSSL::SSL.verify_certificate_identity cert, (@hostname || @ip)
  255. trusted = verify_trust chain, cert
  256. view[id] = { cert: cert, chain: chain, key: key, valid: valid, trusted: trusted }
  257. end
  258. @chains = view.values
  259. @keys = @chains.collect { |c| c[:key] }
  260. end
  261. def prefered_cipher(method)
  262. cipher = ssl_client(method) { |s| Cipher.new method, s.cipher, s.tmp_key }
  263. Logger.info { "Prefered cipher for #{Tls.colorize method} : #{cipher.colorize}" }
  264. cipher
  265. rescue => e
  266. Logger.debug { "Method #{Tls.colorize method} not supported : #{e}" }
  267. nil
  268. end
  269. def fetch_prefered_ciphers
  270. @prefered_ciphers = {}
  271. EXISTING_METHODS.each do |method|
  272. next unless SUPPORTED_METHODS.include? method
  273. @prefered_ciphers[method] = prefered_cipher method
  274. end
  275. raise TLSNotAvailableException unless @prefered_ciphers.any? { |_, c| !c.nil? }
  276. end
  277. def available_ciphers(method)
  278. context = ::OpenSSL::SSL::SSLContext.new method
  279. context.ciphers = %w(ALL COMPLEMENTOFALL)
  280. context.ciphers
  281. end
  282. def supported_cipher?(method, cipher, curves = nil)
  283. cert, chain, dh = ssl_client(method, [cipher], curves) do |s|
  284. [s.peer_cert, s.peer_cert_chain, s.tmp_key]
  285. end
  286. @chains << [cert, chain]
  287. @dh << dh if dh
  288. p dh.group.curve_name
  289. cipher = Cipher.new method, cipher, dh
  290. dh = dh ? " (#{'PFS'.colorize :good} : #{Tls.key_to_s dh})" : ''
  291. states = cipher.states
  292. text = %i(critical error warning good perfect best).collect do |s|
  293. states[s].collect { |t| t.to_s.colorize s }.join ' '
  294. end.reject &:empty?
  295. text = text.join ' '
  296. Logger.info { "#{Tls.colorize method} / #{cipher.colorize}#{dh} [#{text}]" }
  297. cipher
  298. rescue => e
  299. cipher = Cipher.new method, cipher
  300. Logger.debug { "#{Tls.colorize method} / #{cipher.colorize} : Not supported (#{e})" }
  301. nil
  302. end
  303. def check_supported_cipher
  304. Logger.info { '' }
  305. @supported_ciphers = {}
  306. EXISTING_METHODS.each do |method|
  307. next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method]
  308. supported_ciphers = []
  309. available_ciphers = available_ciphers method
  310. available_ciphers.each do |c|
  311. cipher = Cipher.new method, c
  312. supported = supported_cipher? method, c.first
  313. if supported
  314. if cipher.ecdhe?
  315. SUPPORTED_CURVES.each do |curve|
  316. supported = supported_cipher? method, c.first, [curve]
  317. supported_ciphers << supported if supported
  318. end
  319. else
  320. supported_ciphers << supported
  321. end
  322. end
  323. end
  324. @supported_ciphers[method] = supported_ciphers
  325. end
  326. end
  327. def check_fallback_scsv
  328. Logger.info { '' }
  329. @fallback_scsv = false
  330. methods = @prefered_ciphers.reject { |_, v| v.nil? }.keys
  331. if methods.size > 1
  332. # We will try to connect to the not better supported method
  333. method = methods[1]
  334. begin
  335. ssl_client method, fallback: true
  336. rescue InappropriateFallback
  337. @fallback_scsv = true
  338. end
  339. else
  340. @fallback_scsv = nil
  341. end
  342. text, color = case @fallback_scsv
  343. when true
  344. ['Supported', :good]
  345. when false
  346. ['Not supported', :error]
  347. when nil
  348. ['Not applicable', :unknown]
  349. end
  350. Logger.info { "Fallback SCSV : #{text.colorize color}" }
  351. end
  352. def verify_trust(chain, cert)
  353. store = ::OpenSSL::X509::Store.new
  354. store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
  355. store.set_default_paths
  356. %w(/etc/ssl/certs).each do |directory|
  357. ::Dir.glob(::File.join directory, '*.pem').each do |file|
  358. cert = ::OpenSSL::X509::Certificate.new ::File.read file
  359. begin
  360. store.add_cert cert
  361. rescue ::OpenSSL::X509::StoreError
  362. end
  363. end
  364. end
  365. chain.each do |cert|
  366. begin
  367. store.add_cert cert
  368. rescue ::OpenSSL::X509::StoreError
  369. end
  370. end
  371. trusted = store.verify cert
  372. p store.error_string unless trusted
  373. trusted
  374. end
  375. def uniq_dh
  376. dh, find = [], []
  377. @dh.each do |k|
  378. f = [k.type, k.size]
  379. unless find.include? f
  380. dh << k
  381. find << f
  382. end
  383. end
  384. @dh = dh
  385. end
  386. end
  387. class TcpServer < Server
  388. private
  389. def sock_type
  390. ::Socket::SOCK_STREAM
  391. end
  392. end
  393. class UdpServer < Server
  394. private
  395. def sock_type
  396. ::Socket::SOCK_DGRAM
  397. end
  398. end
  399. end
  400. end