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.

271 lines
7.0KB

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