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.

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