From 2105242e0ae1af32d695e3b026732d477ac1f16b Mon Sep 17 00:00:00 2001 From: aeris Date: Sun, 5 Feb 2017 19:20:01 +0100 Subject: [PATCH] Move TLS engine outside server --- lib/cryptcheck.rb | 1 + lib/cryptcheck/tls/engine.rb | 453 +++++++++++++++++++++++++++++++++++ lib/cryptcheck/tls/server.rb | 433 +-------------------------------- 3 files changed, 455 insertions(+), 432 deletions(-) create mode 100644 lib/cryptcheck/tls/engine.rb diff --git a/lib/cryptcheck.rb b/lib/cryptcheck.rb index cd43944..7865999 100644 --- a/lib/cryptcheck.rb +++ b/lib/cryptcheck.rb @@ -39,6 +39,7 @@ module CryptCheck autoload :Cipher, 'cryptcheck/tls/cipher' autoload :Curve, 'cryptcheck/tls/curve' autoload :Cert, 'cryptcheck/tls/cert' + autoload :Engine, 'cryptcheck/tls/engine' autoload :Server, 'cryptcheck/tls/server' autoload :TcpServer, 'cryptcheck/tls/server' autoload :UdpServer, 'cryptcheck/tls/server' diff --git a/lib/cryptcheck/tls/engine.rb b/lib/cryptcheck/tls/engine.rb new file mode 100644 index 0000000..7a21610 --- /dev/null +++ b/lib/cryptcheck/tls/engine.rb @@ -0,0 +1,453 @@ +require 'socket' +require 'openssl' + +module CryptCheck + module Tls + module Engine + TCP_TIMEOUT = 10 + SSL_TIMEOUT = 2*TCP_TIMEOUT + + class TLSException < ::StandardError + end + class TLSNotAvailableException < TLSException + def to_s + 'TLS seems not supported on this server' + end + end + class MethodNotAvailable < TLSException + end + class CipherNotAvailable < TLSException + end + class InappropriateFallback < TLSException + end + class Timeout < ::StandardError + end + class TLSTimeout < Timeout + end + class ConnectionError < ::StandardError + end + + attr_reader :certs, :keys, :dh, :supported_methods, :supported_ciphers, :supported_curves, :curves_preference + + def initialize(hostname, family, ip, port) + @hostname, @family, @ip, @port = hostname, family, ip, port + @dh = [] + + @name = "#@ip:#@port" + @name += " [#@hostname]" if @hostname + + Logger.info { @name.colorize :blue } + + fetch_supported_methods + fetch_supported_ciphers + fetch_dh + fetch_ciphers_preferences + fetch_ecdsa_certs + fetch_supported_curves + fetch_curves_preference + + check_fallback_scsv + + verify_certs + end + + def supported_method?(method) + ssl_client method + Logger.info { " Method #{method}" } + true + rescue TLSException + Logger.debug { " Method #{method} : not supported" } + false + end + + def fetch_supported_methods + Logger.info { '' } + Logger.info { 'Supported methods' } + @supported_methods = Method.select { |m| supported_method? m } + end + + def supported_cipher?(method, cipher) + connection = ssl_client method, cipher + Logger.info { " Cipher #{cipher}" } + dh = connection.tmp_key + if dh + Logger.info { " PFS : #{dh}" } + end + connection + rescue TLSException + Logger.debug { " Cipher #{cipher} : not supported" } + nil + end + + def fetch_supported_ciphers + Logger.info { '' } + Logger.info { 'Supported ciphers' } + @supported_ciphers = @supported_methods.collect do |method| + ciphers = Cipher[method].collect do |cipher| + connection = supported_cipher? method, cipher + next nil unless connection + [cipher, connection] + end.compact.to_h + [method, ciphers] + end.to_h + end + + def fetch_ciphers_preferences + Logger.info { '' } + Logger.info { 'Cipher suite preferences' } + + @preferences = @supported_ciphers.collect do |method, ciphers| + ciphers = ciphers.keys + preferences = if ciphers.size < 2 + Logger.info { " #{method} : " + 'not applicable'.colorize(:unknown) } + nil + else + a, b, _ = ciphers + ab = ssl_client(method, [a, b]).cipher.first + ba = ssl_client(method, [b, a]).cipher.first + if ab != ba + Logger.info { " #{method} : " + 'client preference'.colorize(:warning) } + :client + else + sort = -> (a, b) do + connection = ssl_client method, [a, b] + cipher = connection.cipher.first + cipher == a.name ? -1 : 1 + end + preferences = ciphers.sort &sort + Logger.info { " #{method} : " + preferences.collect { |c| c.to_s :short }.join(', ') } + preferences + end + end + [method, preferences] + end.to_h + end + + def fetch_dh + @dh = @supported_ciphers.collect do |_, ciphers| + ciphers.values.collect(&:tmp_key).select { |d| d.is_a? OpenSSL::PKey::DH } + end.flatten + end + + def fetch_ecdsa_certs + @ecdsa_certs = {} + + @supported_ciphers.each do |method, ciphers| + ecdsa = ciphers.keys.detect &:ecdsa? + next unless ecdsa + + @ecdsa_certs = Curve.collect do |curve| + begin + connection = ssl_client method, ecdsa, curves: curve + [curve, connection] + rescue TLSException + nil + end + end.compact.to_h + + break + end + end + + def fetch_supported_curves + Logger.info { '' } + Logger.info { 'Supported elliptic curves' } + @supported_curves = [] + + ecdsa_curve = @ecdsa_certs.keys.first + if ecdsa_curve + # If we have an ECDSA cipher, we need at least the certificate curve to do handshake, + # but with lowest priority to check for ECHDE and not just ECDSA + + @supported_ciphers.each do |method, ciphers| + ecdsa = ciphers.keys.detect &:ecdsa? + next unless ecdsa + @supported_curves = Curve.select do |curve| + next true if curve == ecdsa_curve # ECDSA curve is always supported + begin + connection = ssl_client method, ecdsa, curves: [curve, ecdsa_curve] + # Not too fast !!! + # Handshake will **always** succeed, because ECDSA + # curve is always supported. + # So, we need to test for the real curve! + # Treaky case : if server preference is enforced, + # ECDSA curve can be prefered over ECDHE one and so + # really supported curve can be detected as not supported :( + + dh = connection.tmp_key + negociated_curve = dh.curve + supported = ecdsa_curve != negociated_curve + if supported + Logger.info { " ECC curve #{curve.name}" } + else + Logger.debug { " ECC curve #{curve.name} : not supported" } + end + supported + rescue TLSException + false + end + end + break + end + else + # If we have no ECDSA ciphers, ECC supported are only ECDH ones + # So peak an ECDH cipher and test all curves + @supported_ciphers.each do |method, ciphers| + ecdh = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? } + next unless ecdh + @supported_curves = Curve.select do |curve| + begin + ssl_client method, ecdh, curves: curve + Logger.info { " ECC curve #{curve.name}" } + true + rescue TLSException + Logger.debug { " ECC curve #{curve.name} : not supported" } + false + end + end + break + end + end + end + + def fetch_curves_preference + @curves_preference = if @supported_curves.size < 2 + Logger.info { 'Curves preference : ' + 'not applicable'.colorize(:unknown) } + nil + else + method, cipher = @supported_ciphers.collect do |method, ciphers| + cipher = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? } + [method, cipher] + end.detect { |n| !n.nil? } + + a, b, _ = @supported_curves + ab, ba = [a, b], [b, a] + if cipher.ecdsa? + # In case of ECDSA, add the cert key at the end + # Or no negociation possible + ecdsa_curve = @ecdsa_certs.keys.first + ab << ecdsa_curve + ba << ecdsa_curve + end + ab = ssl_client(method, cipher, curves: ab).tmp_key.curve + ba = ssl_client(method, cipher, curves: ba).tmp_key.curve + if ab != ba + Logger.info { 'Curves preference : ' + 'client preference'.colorize(:warning) } + :client + else + sort = -> (a, b) do + curves = [a, b] + if cipher.ecdsa? + # In case of ECDSA, add the cert key at the end + # Or no negociation possible + curves << ecdsa_curve + end + connection = ssl_client method, cipher, curves: curves + curve = connection.tmp_key.curve + a == curve ? -1 : 1 + end + preferences = @supported_curves.sort &sort + Logger.info { 'Curves preference : ' + preferences.collect { |c| c.name }.join(', ') } + preferences + end + end + end + + def check_fallback_scsv + Logger.info { '' } + + @fallback_scsv = false + if @supported_methods.size > 1 + # We will try to connect to the not better supported method + method = @supported_methods[1] + + begin + ssl_client method, fallback: true + rescue InappropriateFallback + @fallback_scsv = true + end + else + @fallback_scsv = nil + end + + text, color = case @fallback_scsv + when true + ['supported', :good] + when false + ['not supported', :error] + when nil + ['not applicable', :unknown] + end + Logger.info { 'Fallback SCSV : ' + text.colorize(color) } + end + + Method.each do |method| + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{method.to_sym.downcase}? + @supported_methods.detect { |m| m == :#{method.to_sym} } + end + RUBY_EVAL + end + + Cipher::TYPES.each do |type, _| + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{type}? + uniq_supported_ciphers.any? { |c| c.#{type}? } + end + RUBY_EVAL + end + + private + def connect(&block) + socket = ::Socket.new @family, sock_type + sockaddr = ::Socket.sockaddr_in @port, @ip + #Logger.trace { "Connecting to #{@ip}:#{@port}" } + begin + status = socket.connect_nonblock sockaddr + #Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" } + raise ConnectionError, status unless status == 0 + #Logger.trace { "Connected to #{@ip}:#{@port}" } + block_given? ? block.call(socket) : nil + rescue ::IO::WaitReadable + #Logger.trace { "Waiting for read to #{@ip}:#{@port}" } + raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT + retry + rescue ::IO::WaitWritable + #Logger.trace { "Waiting for write to #{@ip}:#{@port}" } + raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" 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 if @hostname and method != :SSLv2 + #Logger.trace { "SSL connecting to #{name}" } + begin + ssl_socket.connect_nonblock + #Logger.trace { "SSL connected to #{name}" } + return block_given? ? block.call(ssl_socket) : nil + rescue ::OpenSSL::SSL::SSLErrorWaitReadable + #Logger.trace { "Waiting for SSL read to #{name}" } + raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT + retry + rescue ::OpenSSL::SSL::SSLErrorWaitWritable + #Logger.trace { "Waiting for SSL write to #{name}" } + raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT + retry + rescue ::OpenSSL::SSL::SSLError => e + case e.message + when /state=SSLv2 read server hello A$/, + /state=SSLv3 read server hello A$/, + /state=SSLv3 read server hello A: wrong version number$/, + /state=SSLv3 read server hello A: tlsv1 alert protocol version$/, + /state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/ + raise MethodNotAvailable, e + when /state=SSLv2 read server hello A: peer error no cipher$/, + /state=error: no ciphers available$/, + /state=SSLv3 read server hello A: sslv3 alert handshake failure$/, + /state=error: missing export tmp dh key$/, + /state=error: wrong curve$/ + raise CipherNotAvailable, e + when /state=SSLv3 read server hello A: tlsv1 alert inappropriate fallback$/ + raise InappropriateFallback, e + end + raise + rescue ::SystemCallError => e + case e.message + when /^Connection reset by peer - SSL_connect$/ + raise TLSNotAvailableException, e + end + raise + ensure + ssl_socket.close + end + end + + def ssl_client(method, ciphers = nil, curves: nil, fallback: false, &block) + ssl_context = ::OpenSSL::SSL::SSLContext.new method.to_sym + ssl_context.enable_fallback_scsv if fallback + + if ciphers + ciphers = [ciphers] unless ciphers.is_a? Enumerable + ciphers = ciphers.collect(&:name).join ':' + else + ciphers = Cipher::ALL + end + ssl_context.ciphers = ciphers + + if curves + curves = [curves] unless curves.is_a? Enumerable + # OpenSSL fails if the same curve is selected multiple times + # So because Array#uniq preserves order, remove the less prefered ones + curves = curves.collect(&:name).uniq.join ':' + ssl_context.ecdh_curves = curves + end + + Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" } + connect do |socket| + ssl_connect socket, ssl_context, method do |ssl_socket| + return block_given? ? block.call(ssl_socket) : ssl_socket + end + end + end + + def verify_certs + Logger.info { '' } + Logger.info { 'Certificates' } + + # Let's begin the fun + # First, collect "standard" connections + # { method => { cipher => connection, ... }, ... } + certs = @supported_ciphers.values.collect(&:values).flatten 1 + # Then, collect "ecdsa" connections + # { curve => connection, ... } + certs += @ecdsa_certs.values + # For anonymous cipher, there is no certificate at all + certs = certs.reject { |c| c.peer_cert.nil? } + # Then, fetch cert + certs = certs.collect { |c| Cert.new c } + # Then, filter cert to keep uniq fingerprint + @certs = certs.uniq { |c| c.fingerprint } + + @certs.each do |cert| + key = cert.key + identity = cert.valid?(@hostname || @ip) + trust = cert.trusted? + Logger.info { " Certificate #{cert.subject} [#{cert.serial}] issued by #{cert.issuer}" } + Logger.info { ' Key : ' + Tls.key_to_s(key) } + if identity + Logger.info { ' Identity : ' + 'valid'.colorize(:good) } + else + Logger.info { ' Identity : ' + 'invalid'.colorize(:error) } + end + if trust == :trusted + Logger.info { ' Trust : ' + 'trusted'.colorize(:good) } + else + Logger.info { ' Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust } + end + end + @keys = @certs.collect &:key + end + + def uniq_dh + dh, find = [], [] + @dh.each do |k| + f = [k.type, k.size] + unless find.include? f + dh << k + find << f + end + end + @dh = dh + end + + private + def uniq_supported_ciphers + @supported_ciphers.values.collect(&:keys).flatten.uniq + end + end + end +end diff --git a/lib/cryptcheck/tls/server.rb b/lib/cryptcheck/tls/server.rb index 4726613..d788cc1 100644 --- a/lib/cryptcheck/tls/server.rb +++ b/lib/cryptcheck/tls/server.rb @@ -1,287 +1,6 @@ -require 'socket' -require 'openssl' -require 'httparty' - module CryptCheck module Tls class Server - TCP_TIMEOUT = 10 - SSL_TIMEOUT = 2*TCP_TIMEOUT - - class TLSException < ::StandardError - end - class TLSNotAvailableException < TLSException - def to_s - 'TLS seems not supported on this server' - end - end - class MethodNotAvailable < TLSException - end - class CipherNotAvailable < TLSException - end - class InappropriateFallback < TLSException - end - class Timeout < ::StandardError - end - class TLSTimeout < Timeout - end - class ConnectionError < ::StandardError - end - - attr_reader :certs, :keys, :dh, :supported_methods, :supported_ciphers, :supported_curves, :curves_preference - - def initialize(hostname, family, ip, port) - @hostname, @family, @ip, @port = hostname, family, ip, port - @dh = [] - - @name = "#@ip:#@port" - @name += " [#@hostname]" if @hostname - - Logger.info { @name.colorize :blue } - - fetch_supported_methods - fetch_supported_ciphers - fetch_dh - fetch_ciphers_preferences - fetch_ecdsa_certs - fetch_supported_curves - fetch_curves_preference - - check_fallback_scsv - - verify_certs - end - - def supported_method?(method) - ssl_client method - Logger.info { " Method #{method}" } - true - rescue TLSException - Logger.debug { " Method #{method} : not supported" } - false - end - - def fetch_supported_methods - Logger.info { '' } - Logger.info { 'Supported methods' } - @supported_methods = Method.select { |m| supported_method? m } - end - - def supported_cipher?(method, cipher) - connection = ssl_client method, cipher - Logger.info { " Cipher #{cipher}" } - dh = connection.tmp_key - if dh - Logger.info { " PFS : #{dh}" } - end - connection - rescue TLSException - Logger.debug { " Cipher #{cipher} : not supported" } - nil - end - - def fetch_supported_ciphers - Logger.info { '' } - Logger.info { 'Supported ciphers' } - @supported_ciphers = @supported_methods.collect do |method| - ciphers = Cipher[method].collect do |cipher| - connection = supported_cipher? method, cipher - next nil unless connection - [cipher, connection] - end.compact.to_h - [method, ciphers] - end.to_h - end - - def fetch_ciphers_preferences - Logger.info { '' } - Logger.info { 'Cipher suite preferences' } - - @preferences = @supported_ciphers.collect do |method, ciphers| - ciphers = ciphers.keys - preferences = if ciphers.size < 2 - Logger.info { " #{method} : " + 'not applicable'.colorize(:unknown) } - nil - else - a, b, _ = ciphers - ab = ssl_client(method, [a, b]).cipher.first - ba = ssl_client(method, [b, a]).cipher.first - if ab != ba - Logger.info { " #{method} : " + 'client preference'.colorize(:warning) } - :client - else - sort = -> (a, b) do - connection = ssl_client method, [a, b] - cipher = connection.cipher.first - cipher == a.name ? -1 : 1 - end - preferences = ciphers.sort &sort - Logger.info { " #{method} : " + preferences.collect { |c| c.to_s :short }.join(', ') } - preferences - end - end - [method, preferences] - end.to_h - end - - def fetch_dh - @dh = @supported_ciphers.collect do |_, ciphers| - ciphers.values.collect(&:tmp_key).select { |d| d.is_a? OpenSSL::PKey::DH } - end.flatten - end - - def fetch_ecdsa_certs - @ecdsa_certs = {} - - @supported_ciphers.each do |method, ciphers| - ecdsa = ciphers.keys.detect &:ecdsa? - next unless ecdsa - - @ecdsa_certs = Curve.collect do |curve| - begin - connection = ssl_client method, ecdsa, curves: curve - [curve, connection] - rescue TLSException - nil - end - end.compact.to_h - - break - end - end - - def fetch_supported_curves - Logger.info { '' } - Logger.info { 'Supported elliptic curves' } - @supported_curves = [] - - ecdsa_curve = @ecdsa_certs.keys.first - if ecdsa_curve - # If we have an ECDSA cipher, we need at least the certificate curve to do handshake, - # but with lowest priority to check for ECHDE and not just ECDSA - - @supported_ciphers.each do |method, ciphers| - ecdsa = ciphers.keys.detect &:ecdsa? - next unless ecdsa - @supported_curves = Curve.select do |curve| - next true if curve == ecdsa_curve # ECDSA curve is always supported - begin - connection = ssl_client method, ecdsa, curves: [curve, ecdsa_curve] - # Not too fast !!! - # Handshake will **always** succeed, because ECDSA - # curve is always supported. - # So, we need to test for the real curve! - # Treaky case : if server preference is enforced, - # ECDSA curve can be prefered over ECDHE one and so - # really supported curve can be detected as not supported :( - - dh = connection.tmp_key - negociated_curve = dh.curve - supported = ecdsa_curve != negociated_curve - if supported - Logger.info { " ECC curve #{curve.name}" } - else - Logger.debug { " ECC curve #{curve.name} : not supported" } - end - supported - rescue TLSException - false - end - end - break - end - else - # If we have no ECDSA ciphers, ECC supported are only ECDH ones - # So peak an ECDH cipher and test all curves - @supported_ciphers.each do |method, ciphers| - ecdh = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? } - next unless ecdh - @supported_curves = Curve.select do |curve| - begin - ssl_client method, ecdh, curves: curve - Logger.info { " ECC curve #{curve.name}" } - true - rescue TLSException - Logger.debug { " ECC curve #{curve.name} : not supported" } - false - end - end - break - end - end - end - - def fetch_curves_preference - @curves_preference = if @supported_curves.size < 2 - Logger.info { 'Curves preference : ' + 'not applicable'.colorize(:unknown) } - nil - else - method, cipher = @supported_ciphers.collect do |method, ciphers| - cipher = ciphers.keys.detect { |c| c.ecdh? or c.ecdhe? } - [method, cipher] - end.detect { |n| !n.nil? } - - a, b, _ = @supported_curves - ab, ba = [a, b], [b, a] - if cipher.ecdsa? - # In case of ECDSA, add the cert key at the end - # Or no negociation possible - ecdsa_curve = @ecdsa_certs.keys.first - ab << ecdsa_curve - ba << ecdsa_curve - end - ab = ssl_client(method, cipher, curves: ab).tmp_key.curve - ba = ssl_client(method, cipher, curves: ba).tmp_key.curve - if ab != ba - Logger.info { 'Curves preference : ' + 'client preference'.colorize(:warning) } - :client - else - sort = -> (a, b) do - curves = [a, b] - if cipher.ecdsa? - # In case of ECDSA, add the cert key at the end - # Or no negociation possible - curves << ecdsa_curve - end - connection = ssl_client method, cipher, curves: curves - curve = connection.tmp_key.curve - a == curve ? -1 : 1 - end - preferences = @supported_curves.sort &sort - Logger.info { 'Curves preference : ' + preferences.collect { |c| c.name }.join(', ') } - preferences - end - end - end - - def check_fallback_scsv - Logger.info { '' } - - @fallback_scsv = false - if @supported_methods.size > 1 - # We will try to connect to the not better supported method - method = @supported_methods[1] - - begin - ssl_client method, fallback: true - rescue InappropriateFallback - @fallback_scsv = true - end - else - @fallback_scsv = nil - end - - text, color = case @fallback_scsv - when true - ['supported', :good] - when false - ['not supported', :error] - when nil - ['not applicable', :unknown] - end - Logger.info { 'Fallback SCSV : ' + text.colorize(color) } - end - Method.each do |method| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{method.to_sym.downcase}? @@ -396,157 +115,7 @@ module CryptCheck @certs + @dh end - private - def connect(&block) - socket = ::Socket.new @family, sock_type - sockaddr = ::Socket.sockaddr_in @port, @ip - #Logger.trace { "Connecting to #{@ip}:#{@port}" } - begin - status = socket.connect_nonblock sockaddr - #Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" } - raise ConnectionError, status unless status == 0 - #Logger.trace { "Connected to #{@ip}:#{@port}" } - block_given? ? block.call(socket) : nil - rescue ::IO::WaitReadable - #Logger.trace { "Waiting for read to #{@ip}:#{@port}" } - raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT - retry - rescue ::IO::WaitWritable - #Logger.trace { "Waiting for write to #{@ip}:#{@port}" } - raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" 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 if @hostname and method != :SSLv2 - #Logger.trace { "SSL connecting to #{name}" } - begin - ssl_socket.connect_nonblock - #Logger.trace { "SSL connected to #{name}" } - return block_given? ? block.call(ssl_socket) : nil - rescue ::OpenSSL::SSL::SSLErrorWaitReadable - #Logger.trace { "Waiting for SSL read to #{name}" } - raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT - retry - rescue ::OpenSSL::SSL::SSLErrorWaitWritable - #Logger.trace { "Waiting for SSL write to #{name}" } - raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT - retry - rescue ::OpenSSL::SSL::SSLError => e - case e.message - when /state=SSLv2 read server hello A$/, - /state=SSLv3 read server hello A$/, - /state=SSLv3 read server hello A: wrong version number$/, - /state=SSLv3 read server hello A: tlsv1 alert protocol version$/, - /state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/ - raise MethodNotAvailable, e - when /state=SSLv2 read server hello A: peer error no cipher$/, - /state=error: no ciphers available$/, - /state=SSLv3 read server hello A: sslv3 alert handshake failure$/, - /state=error: missing export tmp dh key$/, - /state=error: wrong curve$/ - raise CipherNotAvailable, e - when /state=SSLv3 read server hello A: tlsv1 alert inappropriate fallback$/ - raise InappropriateFallback, e - end - raise - rescue ::SystemCallError => e - case e.message - when /^Connection reset by peer - SSL_connect$/ - raise TLSNotAvailableException, e - end - raise - ensure - ssl_socket.close - end - end - - def ssl_client(method, ciphers = nil, curves: nil, fallback: false, &block) - ssl_context = ::OpenSSL::SSL::SSLContext.new method.to_sym - ssl_context.enable_fallback_scsv if fallback - - if ciphers - ciphers = [ciphers] unless ciphers.is_a? Enumerable - ciphers = ciphers.collect(&:name).join ':' - else - ciphers = Cipher::ALL - end - ssl_context.ciphers = ciphers - - if curves - curves = [curves] unless curves.is_a? Enumerable - # OpenSSL fails if the same curve is selected multiple times - # So because Array#uniq preserves order, remove the less prefered ones - curves = curves.collect(&:name).uniq.join ':' - ssl_context.ecdh_curves = curves - end - - Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" } - connect do |socket| - ssl_connect socket, ssl_context, method do |ssl_socket| - return block_given? ? block.call(ssl_socket) : ssl_socket - end - end - end - - def verify_certs - Logger.info { '' } - Logger.info { 'Certificates' } - - # Let's begin the fun - # First, collect "standard" connections - # { method => { cipher => connection, ... }, ... } - certs = @supported_ciphers.values.collect(&:values).flatten 1 - # Then, collect "ecdsa" connections - # { curve => connection, ... } - certs += @ecdsa_certs.values - # For anonymous cipher, there is no certificate at all - certs = certs.reject { |c| c.peer_cert.nil? } - # Then, fetch cert - certs = certs.collect { |c| Cert.new c } - # Then, filter cert to keep uniq fingerprint - @certs = certs.uniq { |c| c.fingerprint } - - @certs.each do |cert| - key = cert.key - identity = cert.valid?(@hostname || @ip) - trust = cert.trusted? - Logger.info { " Certificate #{cert.subject} [#{cert.serial}] issued by #{cert.issuer}" } - Logger.info { ' Key : ' + Tls.key_to_s(key) } - if identity - Logger.info { ' Identity : ' + 'valid'.colorize(:good) } - else - Logger.info { ' Identity : ' + 'invalid'.colorize(:error) } - end - if trust == :trusted - Logger.info { ' Trust : ' + 'trusted'.colorize(:good) } - else - Logger.info { ' Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust } - end - end - @keys = @certs.collect &:key - end - - def uniq_dh - dh, find = [], [] - @dh.each do |k| - f = [k.type, k.size] - unless find.include? f - dh << k - find << f - end - end - @dh = dh - end - - private - def uniq_supported_ciphers - @supported_ciphers.values.collect(&:keys).flatten.uniq - end + include Engine end class TcpServer < Server