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.

310 lines
8.0KB

  1. require 'socket'
  2. require 'openssl'
  3. require 'httparty'
  4. require 'parallel'
  5. require 'tcp_timeout'
  6. module SSLCheck
  7. class NoSslTlsServer
  8. attr_reader :hostname, :port
  9. def initialize(hostname, port=443)
  10. @hostname, @port = hostname, port
  11. end
  12. end
  13. class Server
  14. TCP_TIMEOUT = 60
  15. SSL_TIMEOUT = 2*TCP_TIMEOUT
  16. EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2)
  17. SUPPORTED_METHODS = OpenSSL::SSL::SSLContext::METHODS
  18. class TLSNotAvailableException < Exception; end
  19. class CipherNotAvailable < Exception; end
  20. class Timeout < Exception; end
  21. class ConnectionError < Exception; end
  22. attr_reader :hostname, :port, :prefered_ciphers, :cert, :hsts
  23. def initialize(hostname, port=443, methods: EXISTING_METHODS)
  24. @log = Logging.logger[hostname]
  25. @hostname = hostname
  26. @port = port
  27. @methods = methods
  28. @log.error { "Begin analysis" }
  29. extract_cert
  30. fetch_prefered_ciphers
  31. check_supported_cipher
  32. fetch_hsts
  33. @log.error { "End analysis" }
  34. end
  35. def supported_methods
  36. worst = EXISTING_METHODS.find { |method| !@prefered_ciphers[method].nil? }
  37. best = EXISTING_METHODS.reverse.find { |method| !@prefered_ciphers[method].nil? }
  38. {worst: worst, best: best}
  39. end
  40. def key
  41. key = @cert.public_key
  42. case key
  43. when OpenSSL::PKey::RSA then
  44. [:rsa, key.n.num_bits]
  45. when OpenSSL::PKey::DSA then
  46. [:dsa, key.p.num_bits]
  47. when OpenSSL::PKey::EC then
  48. [:ecc, key.group.degree]
  49. end
  50. end
  51. def key_size
  52. type, size = self.key
  53. if type == :ecc
  54. size = case size
  55. when 160 then 1024
  56. when 224 then 2048
  57. when 256 then 3072
  58. when 384 then 7680
  59. when 521 then 15360
  60. end
  61. end
  62. size
  63. end
  64. def cipher_size
  65. cipher_strengths = supported_ciphers.collect { |c| c[2] }.uniq.sort
  66. worst, best = cipher_strengths.first, cipher_strengths.last
  67. {worst: worst, best: best}
  68. end
  69. EXISTING_METHODS.each do |method|
  70. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  71. def #{method.to_s.downcase}?
  72. !prefered_ciphers[:#{method}].nil?
  73. end
  74. RUBY_EVAL
  75. end
  76. {
  77. md2: %w(md2WithRSAEncryption),
  78. md5: %w(md5WithRSAEncryption md5WithRSA),
  79. sha1: %w(sha1WithRSAEncryption sha1WithRSA dsaWithSHA1 dsaWithSHA1_2 ecdsa_with_SHA1)
  80. }.each do |name, signature|
  81. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  82. def #{name}_sig?
  83. #{signature}.include? @cert.signature_algorithm
  84. end
  85. RUBY_EVAL
  86. end
  87. {
  88. md5: %w(MD5),
  89. sha1: %w(SHA),
  90. rc4: %w(RC4),
  91. des3: %w(3DES DES-CBC3),
  92. des: %w(DES-CBC)
  93. }.each do |name, ciphers|
  94. class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  95. def #{name}?
  96. supported_ciphers.any? { |supported| #{ciphers}.any? { |available| /(^|-)#\{available\}(-|$)/ =~ supported[0] } }
  97. end
  98. RUBY_EVAL
  99. end
  100. def any_des?
  101. des? or des3?
  102. end
  103. def ssl?
  104. sslv2? or sslv3?
  105. end
  106. def tls?
  107. tlsv1? or tlsv1_1? or tlsv1_2?
  108. end
  109. def tls_only?
  110. tls? and !ssl?
  111. end
  112. PFS_CIPHERS = [/^DHE-RSA-/, /^DHE-DSS-/, /^ECDHE-RSA-/, /^ECDHE-ECDSA-/]
  113. def pfs?
  114. supported_ciphers.any? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } }
  115. end
  116. def pfs_only?
  117. supported_ciphers.all? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } }
  118. end
  119. def supported_ciphers
  120. @supported_ciphers.values.flatten(1).uniq
  121. end
  122. def supported_ciphers_by_method
  123. @supported_ciphers
  124. end
  125. def hsts?
  126. !@hsts.nil?
  127. end
  128. def hsts_long?
  129. hsts? and @hsts >= 6*30*24*60*60
  130. end
  131. private
  132. def connect(family, host, port, &block)
  133. socket = Socket.new family, Socket::SOCK_STREAM
  134. sockaddr = Socket.sockaddr_in port, host
  135. @log.debug { "Connecting to #{host}:#{port}" }
  136. begin
  137. status = socket.connect_nonblock sockaddr
  138. @log.debug { "Connecting to #{host}:#{port} status : #{status}" }
  139. raise ConnectionError.new status unless status == 0
  140. @log.debug { "Connected to #{host}:#{port}" }
  141. block_given? ? block.call(socket) : nil
  142. rescue IO::WaitReadable
  143. @log.debug { "Waiting for read to #{host}:#{port}" }
  144. raise Timeout.new unless IO.select [socket], nil, nil, TCP_TIMEOUT
  145. retry
  146. rescue IO::WaitWritable
  147. @log.debug { "Waiting for write to #{host}:#{port}" }
  148. raise Timeout.new unless IO.select nil, [socket], nil, TCP_TIMEOUT
  149. retry
  150. ensure
  151. socket.close
  152. end
  153. end
  154. def ssl_connect(socket, context, method, &block)
  155. ssl_socket = OpenSSL::SSL::SSLSocket.new socket, context
  156. ssl_socket.hostname = @hostname unless method == :SSLv2
  157. @log.debug { "SSL connecting to #{@hostname}:#{@port}" }
  158. begin
  159. ssl_socket.connect_nonblock
  160. @log.debug { "SSL connected to #{@hostname}:#{@port}" }
  161. return block_given? ? block.call(ssl_socket) : nil
  162. rescue IO::WaitReadable
  163. @log.debug { "Waiting for SSL read to #{@hostname}:#{@port}" }
  164. raise Timeout.new unless IO.select [socket], nil, nil, SSL_TIMEOUT
  165. retry
  166. rescue IO::WaitWritable
  167. @log.debug { "Waiting for SSL write to #{@hostname}:#{@port}" }
  168. raise Timeout.new unless IO.select nil, [socket], nil, SSL_TIMEOUT
  169. retry
  170. ensure
  171. ssl_socket.close
  172. end
  173. end
  174. def ssl_client(method, ciphers = nil, &block)
  175. ssl_context = OpenSSL::SSL::SSLContext.new method
  176. ssl_context.ciphers = ciphers if ciphers
  177. @log.debug { "Try #{method} connection with #{ciphers}" }
  178. [Socket::AF_INET, Socket::AF_INET6].each do |family|
  179. @log.debug { "Try connection for family #{family}" }
  180. addrs = begin
  181. Socket.getaddrinfo @hostname, nil, family, :STREAM
  182. rescue SocketError => e
  183. @log.debug { "Unable to resolv #{@hostname} : #{e}" }
  184. next
  185. end
  186. addrs.each do |addr|
  187. connect family, addr[3], @port do |socket|
  188. ssl_connect socket, ssl_context, method do |ssl_socket|
  189. return block_given? ? block.call(ssl_socket) : nil
  190. end
  191. end
  192. end
  193. end
  194. @log.debug { "No SSL available on #{@hostname}" }
  195. raise CipherNotAvailable.new
  196. end
  197. def extract_cert
  198. @methods.each do |method|
  199. next unless SUPPORTED_METHODS.include? method
  200. begin
  201. @cert = ssl_client(method) { |s| s.peer_cert }
  202. @log.warn { "Certificate #{@cert.subject}" }
  203. break
  204. rescue Exception => e
  205. @log.info { "Method #{method} not supported : #{e}" }
  206. end
  207. end
  208. raise TLSNotAvailableException.new unless @cert
  209. end
  210. def prefered_cipher(method)
  211. cipher = ssl_client(method, %w(ALL:COMPLEMENTOFALL)) { |s| s.cipher }
  212. @log.warn { "Prefered cipher for #{method} : #{cipher[0]}" }
  213. cipher
  214. rescue Exception => e
  215. @log.info { "Method #{method} not supported : #{e}" }
  216. nil
  217. end
  218. def fetch_prefered_ciphers
  219. @prefered_ciphers = {}
  220. @methods.each do |method|
  221. next unless SUPPORTED_METHODS.include? method
  222. @prefered_ciphers[method] = prefered_cipher method
  223. end
  224. raise TLSNotAvailableException.new unless @prefered_ciphers.any? { |_, c| !c.nil? }
  225. end
  226. def available_ciphers(method)
  227. OpenSSL::SSL::SSLContext.new(method).ciphers
  228. end
  229. def supported_cipher?(method, cipher)
  230. ssl_client method, [cipher]
  231. @log.warn { "Verify #{method} / #{cipher[0]} : OK" }
  232. true
  233. rescue Exception => e
  234. @log.info { "Verify #{method} / #{cipher[0]} : NOK (#{e})" }
  235. false
  236. end
  237. def check_supported_cipher
  238. @supported_ciphers = {}
  239. @methods.each do |method|
  240. next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method]
  241. @supported_ciphers[method] = available_ciphers(method).select { |cipher| supported_cipher? method, cipher }
  242. end
  243. end
  244. def fetch_hsts
  245. port = @port == 443 ? '' : ":#{@port}"
  246. response = nil
  247. @methods.each do |method|
  248. begin
  249. next unless SUPPORTED_METHODS.include? method
  250. @log.debug { "Check HSTS with #{method}" }
  251. response = HTTParty.head "https://#{@hostname}#{port}/", {follow_redirects: false, verify: false, ssl_version: method, timeout: SSL_TIMEOUT}
  252. break
  253. rescue Exception => e
  254. @log.debug { "#{method} not supported : #{e}" }
  255. end
  256. end
  257. if response and header = response.headers['strict-transport-security']
  258. name, value = header.split '='
  259. if name == 'max-age'
  260. @hsts = value.to_i
  261. @log.info { "HSTS : #{@hsts}" }
  262. return
  263. end
  264. end
  265. @log.info { 'No HSTS' }
  266. @hsts = nil
  267. end
  268. end
  269. end