From fc63f58e0c3f0013f01c6e1281ad7fdbb5f3f503 Mon Sep 17 00:00:00 2001 From: aeris Date: Tue, 26 Apr 2022 17:27:33 +0200 Subject: [PATCH] Detect ECC curves --- lib/cryptcheck/tls/curve.rb | 94 ++++++++-------- lib/cryptcheck/tls/engine.rb | 204 ++++++++++++++-------------------- lib/fixtures/01_openssl/ec.rb | 2 +- 3 files changed, 132 insertions(+), 168 deletions(-) diff --git a/lib/cryptcheck/tls/curve.rb b/lib/cryptcheck/tls/curve.rb index cd4b44f..972b142 100644 --- a/lib/cryptcheck/tls/curve.rb +++ b/lib/cryptcheck/tls/curve.rb @@ -1,52 +1,54 @@ module CryptCheck - module Tls - class Curve - attr_reader :name + module Tls + class Curve + attr_reader :name - def initialize(name) - name = name.to_sym if name.is_a? String - @name = name - end + def initialize(name) + name = name.to_sym if name.is_a? String + @name = name + end - SUPPORTED = %i(secp256k1 sect283k1 sect283r1 secp384r1 + SUPPORTED = (%i(secp256k1 sect283k1 sect283r1 secp384r1 sect409k1 sect409r1 secp521r1 sect571k1 sect571r1 prime192v1 prime256v1 - brainpoolP256r1 brainpoolP384r1 brainpoolP512r1 x25519).collect { |c| self.new c }.freeze - - extend Enumerable - - def self.each(&block) - SUPPORTED.each &block - end - - def to_s - @name - end - - def to_h - { name: @name, states: self.states } - end - - def ==(other) - case other - when String - @name == other.to_sym - when Symbol - @name == other - else - @name == other.name - end - end - - protected - include State - - CHECKS = [].freeze - - protected - def available_checks - CHECKS - end - end - end + brainpoolP256r1 brainpoolP384r1 brainpoolP512r1 x25519) & OpenSSL::PKey::EC::builtin_curves.collect { |c| c.first.to_sym }).collect { |c| self.new c }.freeze + + extend Enumerable + + def self.each(&block) + SUPPORTED.each &block + end + + def to_s + @name + end + + def to_h + { name: @name, states: self.states } + end + + def ==(other) + case other + when String + @name == other.to_sym + when Symbol + @name == other + else + @name == other.name + end + end + + protected + + include State + + CHECKS = [].freeze + + protected + + def available_checks + CHECKS + end + end + end end diff --git a/lib/cryptcheck/tls/engine.rb b/lib/cryptcheck/tls/engine.rb index 06d7a89..a93c4a7 100644 --- a/lib/cryptcheck/tls/engine.rb +++ b/lib/cryptcheck/tls/engine.rb @@ -62,7 +62,6 @@ module CryptCheck fetch_supported_ciphers fetch_dh fetch_ciphers_preferences - fetch_ecdsa_certs fetch_supported_curves fetch_curves_preference @@ -150,133 +149,99 @@ module CryptCheck end.flatten.uniq &:fingerprint end - def fetch_ecdsa_certs - @ecdsa_certs = {} - - @supported_ciphers.each do |method, ciphers| - ecdsa = ciphers.keys.detect &:ecdsa? - next unless ecdsa - ecdsa_curve = Curve.new ciphers[ecdsa].tmp_key.curve - - @ecdsa_certs = Curve.collect do |curve| - begin - connection = ssl_client method, ecdsa, curves: [curve, ecdsa_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| - if curve == ecdsa_curve - # ECDSA curve is always supported - Logger.info { " ECC curve #{curve.name}" } - next true - end - 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 + ecdsa = @supported_ciphers.find do |method, ciphers| + cipher, connection = ciphers.find { |c, _| c.ecdsa? } + break [method, cipher, connection] if cipher + end + ecdh = @supported_ciphers.find do |method, ciphers| + cipher, connection = ciphers.find { |c, _| c.ecdh? or c.ecdhe? } + break [method, cipher, connection] if cipher + end + cipher, curves = if ecdsa + # 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 + _, _, connection = ecdsa + key = connection.peer_cert.public_key + ecdsa_curve = Curve.new key.group.curve_name + curves = Curve.collect { |c| [c, ecdsa_curve] } + [ecdsa, curves] + else + # If we have no ECDSA ciphers, ECC supported are + # only ECDH ones, so peak an ECDH cipher and test + # all curves + curves = Curve.collect { |c| [c] } + [ecdh, curves] + end + method, cipher, _ = cipher + + supported_curves = curves.collect do |curve| + begin + ssl_client method, cipher, curves: curve + connection = ssl_client method, cipher, curves: curve + connection.tmp_key.curve + rescue TLSException + nil end + end.compact.uniq + + @supported_curves = supported_curves.collect do |curve| + Logger.info { " ECC curve #{curve}" } + Curve.new curve 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 = lambda do |a, b| - 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 + @curves_preference = nil + + if @supported_curves.size < 2 + Logger.info { 'Curves preference : ' + 'not applicable'.colorize(:unknown) } + return + end + + method, cipher, connection = @supported_ciphers.find do |method, ciphers| + cipher, connection = ciphers.find { |c, _| c.ecdh? or c.ecdhe? } + break [method, cipher, connection] if cipher + end + + 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 = Curve.new connection.peer_cert.public_key.group.curve_name + 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) } + @curves_preference = :client + return + end + + sort = lambda do |a, b| + curves = [a, b] + if cipher.ecdsa? + # In case of ECDSA, add the cert key at the end + # Or no negociation possible + ecdsa_curve = Curve.new connection.tmp_key.curve + curves << ecdsa_curve + end + connection = ssl_client method, cipher, curves: curves + curve = connection.tmp_key.curve + a == curve ? -1 : 1 + end + + @curves_preference = @supported_curves.sort &sort + Logger.info { 'Curves preference : ' + @curves_preference.collect { |c| c.name }.join(', ') } end def check_fallback_scsv @@ -440,9 +405,6 @@ module CryptCheck # 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 diff --git a/lib/fixtures/01_openssl/ec.rb b/lib/fixtures/01_openssl/ec.rb index 21dd9fc..82fdee1 100644 --- a/lib/fixtures/01_openssl/ec.rb +++ b/lib/fixtures/01_openssl/ec.rb @@ -14,7 +14,7 @@ module Fixture end def to_s - "ECC #{self.size} bits" + "ECC #{self.size} bits (#{self.curve})" end def to_h