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.

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