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.

server.rb 8.4KB

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