123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- require 'socket'
- require 'openssl'
- require 'httparty'
-
- module CryptCheck
- module Tls
- class TlsNotSupportedServer
- attr_reader :hostname, :port
-
- def initialize(hostname, port)
- @hostname, @port = hostname, port
- end
- end
-
- class Server
- TCP_TIMEOUT = 10
- SSL_TIMEOUT = 2*TCP_TIMEOUT
- EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2)
- SUPPORTED_METHODS = ::OpenSSL::SSL::SSLContext::METHODS
- class TLSException < ::Exception
- end
- class TLSNotAvailableException < TLSException
- end
- class CipherNotAvailable < TLSException
- end
- class Timeout < TLSException
- end
- class TLSTimeout < TLSException
- end
- class ConnectionError < TLSException
- end
-
- attr_reader :hostname, :port, :prefered_ciphers, :cert, :cert_valid, :cert_trusted
-
- def initialize(hostname, port, methods: EXISTING_METHODS)
- @log = Logging.logger[hostname]
- @hostname = hostname
- @port = port
- @methods = methods
- @log.error { "Begin analysis" }
- extract_cert
- fetch_prefered_ciphers
- check_supported_cipher
- fetch_hsts
- @log.error { "End analysis" }
- end
-
- def supported_methods
- worst = EXISTING_METHODS.find { |method| !@prefered_ciphers[method].nil? }
- best = EXISTING_METHODS.reverse.find { |method| !@prefered_ciphers[method].nil? }
- { worst: worst, best: best }
- end
-
- def key
- key = @cert.public_key
- case key
- when ::OpenSSL::PKey::RSA then
- [:rsa, key.n.num_bits]
- when ::OpenSSL::PKey::DSA then
- [:dsa, key.p.num_bits]
- when ::OpenSSL::PKey::EC then
- [:ecc, key.group.degree]
- end
- end
-
- def key_size
- type, size = self.key
- if type == :ecc
- size = case size
- when 160 then
- 1024
- when 224 then
- 2048
- when 256 then
- 3072
- when 384 then
- 7680
- when 521 then
- 15360
- end
- end
- size
- end
-
- def cipher_size
- cipher_strengths = supported_ciphers.collect { |c| c[2] }.uniq.sort
- worst, best = cipher_strengths.first, cipher_strengths.last
- { worst: worst, best: best }
- end
-
- EXISTING_METHODS.each do |method|
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{method.to_s.downcase}?
- !prefered_ciphers[:#{method}].nil?
- end
- RUBY_EVAL
- end
-
- {
- md2: %w(md2WithRSAEncryption),
- md5: %w(md5WithRSAEncryption md5WithRSA),
- sha1: %w(sha1WithRSAEncryption sha1WithRSA dsaWithSHA1 dsaWithSHA1_2 ecdsa_with_SHA1)
- }.each do |name, signature|
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{name}_sig?
- #{signature}.include? @cert.signature_algorithm
- end
- RUBY_EVAL
- end
-
- {
- md5: %w(MD5),
- sha1: %w(SHA),
-
- rc4: %w(RC4),
- des3: %w(3DES DES-CBC3),
- des: %w(DES-CBC)
- }.each do |name, ciphers|
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{name}?
- supported_ciphers.any? { |supported| #{ciphers}.any? { |available| /(^|-)#\{available\}(-|$)/ =~ supported[0] } }
- end
- RUBY_EVAL
- end
-
- def ssl?
- sslv2? or sslv3?
- end
-
- def tls?
- tlsv1? or tlsv1_1? or tlsv1_2?
- end
-
- def tls_only?
- tls? and !ssl?
- end
-
- PFS_CIPHERS = [/^DHE-RSA-/, /^DHE-DSS-/, /^ECDHE-RSA-/, /^ECDHE-ECDSA-/]
-
- def pfs?
- supported_ciphers.any? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } }
- end
-
- def pfs_only?
- supported_ciphers.all? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } }
- end
-
- def supported_ciphers
- @supported_ciphers.values.flatten(1).uniq
- end
-
- def supported_ciphers_by_method
- @supported_ciphers
- end
-
- private
- def connect(family, host, port, &block)
- socket = ::Socket.new family, ::Socket::SOCK_STREAM
- sockaddr = ::Socket.sockaddr_in port, host
- @log.debug { "Connecting to #{host}:#{port}" }
- begin
- status = socket.connect_nonblock sockaddr
- @log.debug { "Connecting to #{host}:#{port} status : #{status}" }
- raise ConnectionError, status unless status == 0
- @log.debug { "Connected to #{host}:#{port}" }
- block_given? ? block.call(socket) : nil
- rescue ::IO::WaitReadable
- @log.debug { "Waiting for read to #{host}:#{port}" }
- raise Timeout unless IO.select [socket], nil, nil, TCP_TIMEOUT
- retry
- rescue ::IO::WaitWritable
- @log.debug { "Waiting for write to #{host}:#{port}" }
- raise Timeout unless IO.select nil, [socket], nil, TCP_TIMEOUT
- retry
- ensure
- socket.close
- end
- end
-
- def ssl_connect(socket, context, method, &block)
- ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context
- ssl_socket.hostname = @hostname unless method == :SSLv2
- @log.debug { "SSL connecting to #{@hostname}:#{@port}" }
- begin
- ssl_socket.connect_nonblock
- @log.debug { "SSL connected to #{@hostname}:#{@port}" }
- return block_given? ? block.call(ssl_socket) : nil
- rescue ::IO::WaitReadable
- @log.debug { "Waiting for SSL read to #{@hostname}:#{@port}" }
- raise TLSTimeout unless IO.select [socket], nil, nil, SSL_TIMEOUT
- retry
- rescue ::IO::WaitWritable
- @log.debug { "Waiting for SSL write to #{@hostname}:#{@port}" }
- raise TLSTimeout unless IO.select nil, [socket], nil, SSL_TIMEOUT
- retry
- rescue ::OpenSSL::SSL::SSLError => e
- raise TLSException, e
- ensure
- ssl_socket.close
- end
- end
-
- def ssl_client(method, ciphers = nil, &block)
- ssl_context = ::OpenSSL::SSL::SSLContext.new method
- ssl_context.ciphers = ciphers if ciphers
- @log.debug { "Try #{method} connection with #{ciphers}" }
-
- [::Socket::AF_INET, ::Socket::AF_INET6].each do |family|
- @log.debug { "Try connection for family #{family}" }
- addrs = begin
- ::Socket.getaddrinfo @hostname, nil, family, :STREAM
- rescue ::SocketError => e
- @log.debug { "Unable to resolv #{@hostname} : #{e}" }
- next
- end
-
- addrs.each do |addr|
- connect family, addr[3], @port do |socket|
- ssl_connect socket, ssl_context, method do |ssl_socket|
- return block_given? ? block.call(ssl_socket) : nil
- end
- end
- end
- end
-
- @log.debug { "No SSL available on #{@hostname}" }
- raise CipherNotAvailable
- end
-
- def extract_cert
- @methods.each do |method|
- next unless SUPPORTED_METHODS.include? method
- begin
- @cert, @chain = ssl_client(method) { |s| [s.peer_cert, s.peer_cert_chain] }
- @log.warn { "Certificate #{@cert.subject}" }
- break
- rescue TLSException => e
- @log.info { "Method #{method} not supported : #{e}" }
- end
- end
- raise TLSNotAvailableException unless @cert
- @cert_valid = ::OpenSSL::SSL.verify_certificate_identity @cert, @hostname
- @cert_trusted = verify_trust @chain, @cert
- end
-
- def prefered_cipher(method)
- cipher = ssl_client(method, %w(ALL:COMPLEMENTOFALL)) { |s| s.cipher }
- @log.warn { "Prefered cipher for #{method} : #{cipher[0]}" }
- cipher
- rescue Exception => e
- @log.info { "Method #{method} not supported : #{e}" }
- nil
- end
-
- def fetch_prefered_ciphers
- @prefered_ciphers = {}
- @methods.each do |method|
- next unless SUPPORTED_METHODS.include? method
- @prefered_ciphers[method] = prefered_cipher method
- end
- raise TLSNotAvailableException.new unless @prefered_ciphers.any? { |_, c| !c.nil? }
- end
-
- def available_ciphers(method)
- ::OpenSSL::SSL::SSLContext.new(method).ciphers
- end
-
- def supported_cipher?(method, cipher)
- ssl_client method, [cipher]
- @log.warn { "Verify #{method} / #{cipher[0]} : OK" }
- true
- rescue TLSException => e
- @log.info { "Verify #{method} / #{cipher[0]} : NOK (#{e})" }
- false
- end
-
- def check_supported_cipher
- @supported_ciphers = {}
- @methods.each do |method|
- next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method]
- @supported_ciphers[method] = available_ciphers(method).select { |cipher| supported_cipher? method, cipher }
- end
- end
-
- def verify_trust(chain, cert)
- store = ::OpenSSL::X509::Store.new
- store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
- %w(mozilla cacert).each do |directory|
- ::Dir.glob(::File.join '/usr/share/ca-certificates', directory, '*').each do |file|
- ::File.open file, 'r' do |file|
- cert = ::OpenSSL::X509::Certificate.new file.read
- begin
- store.add_cert cert
- rescue ::OpenSSL::X509::StoreError
- end
- end
- end
- end
- chain.each do |cert|
- begin
- store.add_cert cert
- rescue ::OpenSSL::X509::StoreError
- end
- end
- store.verify cert
- end
- end
- end
- end
|