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.

331 lines
8.9KB

  1. require 'socket'
  2. require 'openssl'
  3. require 'httparty'
  4. module CryptCheck
  5. module Tls
  6. class TlsNotSupportedServer
  7. attr_reader :hostname, :port
  8. def initialize(hostname, port)
  9. @hostname, @port = hostname, port
  10. end
  11. end
  12. class Server
  13. TCP_TIMEOUT = 10
  14. SSL_TIMEOUT = 2*TCP_TIMEOUT
  15. EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2)
  16. SUPPORTED_METHODS = ::OpenSSL::SSL::SSLContext::METHODS
  17. class TLSException < ::Exception
  18. end
  19. class TLSNotAvailableException < TLSException
  20. end
  21. class MethodNotAvailable < TLSException
  22. end
  23. class CipherNotAvailable < TLSException
  24. end
  25. class Timeout < TLSException
  26. end
  27. class TLSTimeout < TLSException
  28. end
  29. class ConnectionError < TLSException
  30. end
  31. attr_reader :hostname, :port, :prefered_ciphers, :cert, :cert_valid, :cert_trusted, :dh
  32. def initialize(hostname, port)
  33. @hostname, @port = hostname, port
  34. @dh = []
  35. Logger.info { "#{hostname}:#{port}".colorize :blue }
  36. extract_cert
  37. Logger.info { '' }
  38. Logger.info { "Key : #{Tls.key_to_s self.key}" }
  39. fetch_prefered_ciphers
  40. check_supported_cipher
  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 }.sort.last
  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. {
  66. md2: %w(md2WithRSAEncryption),
  67. md5: %w(md5WithRSAEncryption md5WithRSA),
  68. sha1: %w(sha1WithRSAEncryption sha1WithRSA dsaWithSHA1 dsaWithSHA1_2 ecdsa_with_SHA1)
  69. }.each do |name, signature|
  70. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  71. def #{name}_sig?
  72. #{signature}.include? @cert.signature_algorithm
  73. end
  74. RUBY_EVAL
  75. end
  76. Cipher::TYPES.each do |type, _|
  77. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  78. def #{type}?
  79. supported_ciphers.any? { |c| c.#{type}? }
  80. end
  81. RUBY_EVAL
  82. end
  83. def key_size
  84. @cert.public_key.rsa_equivalent_size
  85. end
  86. def ssl?
  87. sslv2? or sslv3?
  88. end
  89. def tls?
  90. tlsv1? or tlsv1_1? or tlsv1_2?
  91. end
  92. def tls_only?
  93. tls? and !ssl?
  94. end
  95. def pfs?
  96. supported_ciphers.any? { |c| c.pfs? }
  97. end
  98. def pfs_only?
  99. supported_ciphers.all? { |c| c.pfs? }
  100. end
  101. private
  102. def connect(family, host, port, &block)
  103. socket = ::Socket.new family, sock_type
  104. sockaddr = ::Socket.sockaddr_in port, host
  105. Logger.trace { "Connecting to #{host}:#{port}" }
  106. begin
  107. status = socket.connect_nonblock sockaddr
  108. Logger.trace { "Connecting to #{host}:#{port} status : #{status}" }
  109. raise ConnectionError, status unless status == 0
  110. Logger.trace { "Connected to #{host}:#{port}" }
  111. block_given? ? block.call(socket) : nil
  112. rescue ::IO::WaitReadable
  113. Logger.trace { "Waiting for read to #{host}:#{port}" }
  114. raise Timeout unless IO.select [socket], nil, nil, TCP_TIMEOUT
  115. retry
  116. rescue ::IO::WaitWritable
  117. Logger.trace { "Waiting for write to #{host}:#{port}" }
  118. raise Timeout unless IO.select nil, [socket], nil, TCP_TIMEOUT
  119. retry
  120. rescue => e
  121. case e.message
  122. when /^Connection refused/
  123. raise TLSNotAvailableException, e
  124. end
  125. raise
  126. ensure
  127. socket.close
  128. end
  129. end
  130. def ssl_connect(socket, context, method, &block)
  131. ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context
  132. ssl_socket.hostname = @hostname unless method == :SSLv2
  133. Logger.trace { "SSL connecting to #{@hostname}:#{@port}" }
  134. begin
  135. ssl_socket.connect_nonblock
  136. Logger.trace { "SSL connected to #{@hostname}:#{@port}" }
  137. return block_given? ? block.call(ssl_socket) : nil
  138. rescue ::IO::WaitReadable
  139. Logger.trace { "Waiting for SSL read to #{@hostname}:#{@port}" }
  140. raise TLSTimeout unless IO.select [socket], nil, nil, SSL_TIMEOUT
  141. retry
  142. rescue ::IO::WaitWritable
  143. Logger.trace { "Waiting for SSL write to #{@hostname}:#{@port}" }
  144. raise TLSTimeout unless IO.select nil, [socket], nil, SSL_TIMEOUT
  145. retry
  146. rescue ::OpenSSL::SSL::SSLError => e
  147. case e.message
  148. when /state=SSLv2 read server hello A$/,
  149. /state=SSLv3 read server hello A: wrong version number$/
  150. raise MethodNotAvailable, e
  151. when /state=error: no ciphers available$/,
  152. /state=SSLv3 read server hello A: sslv3 alert handshake failure$/
  153. raise CipherNotAvailable, e
  154. end
  155. raise TLSException, e
  156. rescue => e
  157. case e.message
  158. when /^Connection reset by peer$/
  159. raise MethodNotAvailable, e
  160. end
  161. raise TLSException, e
  162. ensure
  163. ssl_socket.close
  164. end
  165. end
  166. def ssl_client(method, ciphers = nil, &block)
  167. ssl_context = ::OpenSSL::SSL::SSLContext.new method
  168. ssl_context.ciphers = ciphers if ciphers
  169. Logger.trace { "Try #{method} connection with #{ciphers}" }
  170. [::Socket::AF_INET, ::Socket::AF_INET6].each do |family|
  171. Logger.trace { "Try connection for family #{family}" }
  172. addrs = begin
  173. ::Socket.getaddrinfo @hostname, nil, family, :STREAM
  174. rescue ::SocketError => e
  175. Logger.error { "Unable to resolv #{@hostname} : #{e}" }
  176. next
  177. end
  178. addrs.each do |addr|
  179. connect family, addr[3], @port do |socket|
  180. ssl_connect socket, ssl_context, method do |ssl_socket|
  181. return block_given? ? block.call(ssl_socket) : nil
  182. end
  183. end
  184. end
  185. end
  186. Logger.debug { "No SSL available on #{@hostname}" }
  187. raise CipherNotAvailable
  188. end
  189. def extract_cert
  190. EXISTING_METHODS.each do |method|
  191. next unless SUPPORTED_METHODS.include? method
  192. begin
  193. @cert, @chain = ssl_client(method) { |s| [s.peer_cert, s.peer_cert_chain] }
  194. Logger.debug { "Certificate #{@cert.subject}" }
  195. break
  196. rescue TLSException
  197. end
  198. end
  199. raise TLSNotAvailableException unless @cert
  200. @cert_valid = ::OpenSSL::SSL.verify_certificate_identity @cert, @hostname
  201. @cert_trusted = verify_trust @chain, @cert
  202. end
  203. def prefered_cipher(method)
  204. cipher = ssl_client(method, 'ALL:COMPLEMENTOFALL') { |s| Cipher.new method, s.cipher, s.tmp_key }
  205. Logger.info { "Prefered cipher for #{Tls.colorize method} : #{cipher.colorize}" }
  206. cipher
  207. rescue TLSException => e
  208. Logger.debug { "Method #{Tls.colorize method} not supported : #{e}" }
  209. nil
  210. end
  211. def fetch_prefered_ciphers
  212. @prefered_ciphers = {}
  213. EXISTING_METHODS.each do |method|
  214. next unless SUPPORTED_METHODS.include? method
  215. @prefered_ciphers[method] = prefered_cipher method
  216. end
  217. raise TLSNotAvailableException unless @prefered_ciphers.any? { |_, c| !c.nil? }
  218. end
  219. def available_ciphers(method)
  220. context = ::OpenSSL::SSL::SSLContext.new method
  221. context.ciphers = 'ALL:COMPLEMENTOFALL'
  222. context.ciphers
  223. end
  224. def supported_cipher?(method, cipher)
  225. dh = ssl_client method, [cipher] { |s| s.tmp_key }
  226. @dh << dh if dh
  227. cipher = Cipher.new method, cipher, dh
  228. dh = dh ? " (#{'DH'.colorize :green} : #{Tls.key_to_s dh})" : ''
  229. Logger.info { "#{Tls.colorize method} / #{cipher.colorize} : Supported#{dh}" }
  230. cipher
  231. rescue TLSException => e
  232. cipher = Cipher.new method, cipher
  233. Logger.debug { "#{Tls.colorize method} / #{cipher.colorize} : Not supported (#{e})" }
  234. nil
  235. end
  236. def check_supported_cipher
  237. Logger.info { '' }
  238. @supported_ciphers = {}
  239. EXISTING_METHODS.each do |method|
  240. next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method]
  241. supported_ciphers = available_ciphers(method).collect { |c| supported_cipher? method, c }.reject { |c| c.nil? }
  242. Logger.info { '' } unless supported_ciphers.empty?
  243. @supported_ciphers[method] = supported_ciphers
  244. end
  245. end
  246. def verify_trust(chain, cert)
  247. store = ::OpenSSL::X509::Store.new
  248. store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
  249. store.set_default_paths
  250. %w(cacert mozilla).each do |directory|
  251. ::Dir.glob(::File.join '/usr/share/ca-certificates', directory, '*').each do |file|
  252. cert = ::OpenSSL::X509::Certificate.new ::File.read file
  253. begin
  254. store.add_cert cert
  255. rescue ::OpenSSL::X509::StoreError
  256. end
  257. end
  258. end
  259. chain.each do |cert|
  260. begin
  261. store.add_cert cert
  262. rescue ::OpenSSL::X509::StoreError
  263. end
  264. end
  265. trusted = store.verify cert
  266. p store.error_string unless trusted
  267. trusted
  268. end
  269. def uniq_dh
  270. dh, find = [], []
  271. @dh.each do |k|
  272. f = [k.type, k.size]
  273. unless find.include? f
  274. dh << k
  275. find << f
  276. end
  277. end
  278. @dh = dh
  279. end
  280. end
  281. class TcpServer < Server
  282. private
  283. def sock_type
  284. ::Socket::SOCK_STREAM
  285. end
  286. end
  287. class UdpServer < Server
  288. private
  289. def sock_type
  290. ::Socket::SOCK_DGRAM
  291. end
  292. end
  293. end
  294. end