Refactoring

new-scoring
aeris 2017-10-29 11:25:11 +01:00
parent c640e26674
commit 3d176613c6
17 changed files with 251 additions and 180 deletions

View File

@ -96,7 +96,7 @@ module CryptCheck
def self.compare(a, b)
a = LEVELS.find_index(a.status) || (LEVELS.size - 1) / 2.0
b = LEVELS.find_index(b.status) || (LEVELS.size - 1) / 2.0
a <=> b
b <=> a
end
protected

View File

@ -3,20 +3,21 @@ require 'parallel'
module CryptCheck
module Tls
def self.analyze(host, port)
::CryptCheck.analyze host, port, TcpServer
def self.aggregate(hosts)
hosts = [hosts] unless hosts.respond_to? :collect
hosts.inject([]) { |l, h| l + h.to_h }
end
def self.key_to_s(key)
size, color = case key.type
when :ecc
["#{key.group.curve_name} #{key.size}", :good]
when :rsa
[key.size, nil]
when :dsa
[key.size, :critical]
when :dh
[key.size, :warning]
when :ecc
["#{key.group.curve_name} #{key.size}", :good]
when :rsa
[key.size, nil]
when :dsa
[key.size, :critical]
when :dh
[key.size, :warning]
end
"#{key.type.to_s.upcase.colorize color} #{size.to_s.colorize key.status} bits"
end

View File

@ -26,15 +26,17 @@ module CryptCheck
des: %w(DES-CBC),
des3: %w(3DES DES-CBC3),
aes: %w(AES(128|256) AES-(128|256)),
aes128: %w(AES128 AES-128),
aes256: %w(AES256 AES-256),
camellia: %w(CAMELLIA(128|256)),
seed: %w(SEED),
idea: %w(IDEA),
chacha20: %w(CHACHA20),
#cbc: %w(CBC),
# cbc: %w(CBC),
gcm: %w(GCM),
ccm: %w(CCM)
}
}.freeze
attr_reader :method, :name
@ -49,6 +51,7 @@ module CryptCheck
end
def self.[](method)
method = Method[method] if method.is_a? Symbol
SUPPORTED[method]
end
@ -63,6 +66,15 @@ module CryptCheck
RUBY_EVAL
end
def self.aes?(cipher)
aes?(cipher) or aes?(cipher)
end
def aes?
aes128? or aes256?
end
def self.cbc?(cipher)
!aead? cipher
end
@ -76,7 +88,7 @@ module CryptCheck
end
def aead?
gcm? or ccm?
gcm? or ccm? or chacha20?
end
def ssl?
@ -96,7 +108,7 @@ module CryptCheck
end
def sweet32?
size = self.block_size
size = self.encryption[1]
return false unless size # Not block encryption
size <= 64
end
@ -115,7 +127,7 @@ module CryptCheck
hmac = self.hmac
{
protocol: @method, name: self.name, key_exchange: self.kex, authentication: self.auth,
encryption: { name: self.encryption, mode: self.mode, block_size: self.block_size },
encryption: self.encryption,
hmac: { name: hmac.first, size: hmac.last }, states: self.states
}
end
@ -177,23 +189,27 @@ module CryptCheck
def encryption
case
when chacha20?
:chacha20
when aes?
:aes
[:chacha20, nil, 128, self.mode]
when aes128?
[:aes, 128, 128, self.mode]
when aes256?
[:aes, 128, 128, self.mode]
when camellia?
:camellia
[:camellia, 128, 128, self.mode]
when seed?
:seed
[:seed, 128, 128, self.mode]
when idea?
:idea
[:idea, 64, 128, self.mode]
when des3?
:'3des'
[:'3des', 64, 112, self.mode]
when des?
:des
[:des, 64, 56, self.mode]
when rc4?
:rc4
[:rc4, nil, nil, self.mode]
when rc2?
:rc2
[:rc2, 64, 64, self.mode]
when null?
[nil, nil, nil, nil]
end
end
@ -203,24 +219,15 @@ module CryptCheck
:gcm
when ccm?
:ccm
when rc4? || chacha20?
when chacha20?
:aead
when rc4?
nil
else
:cbc
end
end
def block_size
case self.encryption
when :'3des', :idea, :rc2
64
when :aes, :camellia, :seed
128
else
nil
end
end
def hmac
case
when poly1305?
@ -265,7 +272,7 @@ module CryptCheck
@name <=> other.name
end
ALL = 'ALL:COMPLEMENTOFALL'
ALL = 'ALL:COMPLEMENTOFALL'.freeze
SUPPORTED = Method.collect do |m|
context = ::OpenSSL::SSL::SSLContext.new m.to_sym
context.ciphers = ALL

View File

@ -4,6 +4,7 @@ require 'openssl'
module CryptCheck
module Tls
module Engine
SLOW_DOWN = ENV.fetch('SLOW_DOWN', '0').to_i
TCP_TIMEOUT = 10
TLS_TIMEOUT = 2*TCP_TIMEOUT
@ -176,11 +177,15 @@ module CryptCheck
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
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]
connection = ssl_client method, ecdsa, curves: [curve, ecdsa_curve]
# Not too fast !!!
# Handshake will **always** succeed, because ECDSA
# 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,
@ -248,7 +253,7 @@ module CryptCheck
Logger.info { 'Curves preference : ' + 'client preference'.colorize(:warning) }
:client
else
sort = -> (a, b) do
sort = lambda do |a, b|
curves = [a, b]
if cipher.ecdsa?
# In case of ECDSA, add the cert key at the end
@ -273,10 +278,10 @@ module CryptCheck
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
rescue InappropriateFallback,
CipherNotAvailable # Seems some servers reply with "sslv3 alert handshake failure"...
@fallback_scsv = true
end
else
@ -284,12 +289,12 @@ module CryptCheck
end
text, color = case @fallback_scsv
when true
['supported', :good]
when false
['not supported', :error]
when nil
['not applicable', :unknown]
when true
['supported', :good]
when false
['not supported', :error]
when nil
['not applicable', :unknown]
end
Logger.info { 'Fallback SCSV : ' + text.colorize(color) }
end
@ -311,6 +316,7 @@ module CryptCheck
end
private
def connect(&block)
socket = ::Socket.new @family, sock_type
sockaddr = ::Socket.sockaddr_in @port, @ip
@ -354,26 +360,26 @@ module CryptCheck
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
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
when /^Connection reset by peer - SSL_connect$/
raise TLSNotAvailableException, e
end
raise
ensure
@ -382,6 +388,7 @@ module CryptCheck
end
def ssl_client(method, ciphers = nil, curves: nil, fallback: false, &block)
sleep SLOW_DOWN if SLOW_DOWN > 0
ssl_context = ::OpenSSL::SSL::SSLContext.new method.to_sym
ssl_context.enable_fallback_scsv if fallback
@ -394,7 +401,7 @@ module CryptCheck
ssl_context.ciphers = ciphers
if curves
curves = [curves] unless curves.is_a? Enumerable
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 ':'
@ -421,14 +428,14 @@ module CryptCheck
# Let's begin the fun
# First, collect "standard" connections
# { method => { cipher => connection, ... }, ... }
certs = @supported_ciphers.values.collect(&:values).flatten 1
certs = @supported_ciphers.values.collect(&:values).flatten 1
# Then, collect "ecdsa" connections
# { curve => connection, ... }
certs += @ecdsa_certs.values
certs += @ecdsa_certs.values
# For anonymous cipher, there is no certificate at all
certs = certs.reject { |c| c.peer_cert.nil? }
certs = certs.reject { |c| c.peer_cert.nil? }
# Then, fetch cert
certs = certs.collect { |c| Cert.new c }
certs = certs.collect { |c| Cert.new c }
# Then, filter cert to keep uniq fingerprint
@certs = certs.uniq { |c| c.fingerprint }

View File

@ -33,12 +33,14 @@ class ::OpenSSL::PKey::EC
CHECKS = [
[:ecc, %i(critical error warning), -> (s) do
case s.size
when 0...160
:critical
when 160...192
:error
when 192...256
:warning
when 0...160
:critical
when 160...192
:error
when 192...256
:warning
else
false
end
end]
].freeze
@ -69,12 +71,14 @@ class ::OpenSSL::PKey::RSA
include ::CryptCheck::State
CHECKS = [
[:rsa, %i(critical error), -> (s) do
[:rsa, %i(critical error), ->(s) do
case s.size
when 0...1024
:critical
when 1024...2048
:error
when 0...1024
:critical
when 1024...2048
:error
else
false
end
end]
].freeze
@ -136,10 +140,12 @@ class ::OpenSSL::PKey::DH
CHECKS = [
[:dh, %i(critical error), -> (s) do
case s.size
when 0...1024
:critical
when 1024...2048
:error
when 0...1024
:critical
when 1024...2048
:error
else
false
end
end]
].freeze
@ -161,17 +167,17 @@ class ::OpenSSL::X509::Store
chains = [chains] unless chains.is_a? Enumerable
chains.each do |chain|
case chain
when ::OpenSSL::X509::Certificate
self.add_cert chain
when ::OpenSSL::X509::Certificate
self.add_cert chain
else
if File.directory?(chain)
Dir.entries(chain)
.collect { |e| File.join chain, e }
.select { |e| File.file? e }
.each { |f| self.add_file f }
else
if File.directory?(chain)
Dir.entries(chain)
.collect { |e| File.join chain, e }
.select { |e| File.file? e }
.each { |f| self.add_file f }
else
self.add_file chain
end
self.add_file chain
end
end
end
end

View File

@ -1,4 +1,5 @@
require 'awesome_print'
AwesomePrint.force_colors = true
require 'timeout'
module CryptCheck
@ -31,7 +32,7 @@ module CryptCheck
first = true
@servers = resolve.collect do |args|
_, ip, _, _ = args
_, ip = args
first ? (first = false) : Logger.info { '' }
result = begin
server = ::Timeout.timeout MAX_ANALYSIS_DURATION do
@ -42,24 +43,31 @@ module CryptCheck
Logger.info { server.states.ai }
server
rescue Engine::TLSException, Engine::ConnectionError, Engine::Timeout => e
# Logger.error { e.backtrace }
Logger.error { e }
AnalysisFailure.new e
rescue ::Timeout::Error
# Logger.error { e.backtrace }
Logger.error { e }
TooLongAnalysis.new
end
[[@hostname, ip, @port], result]
end.to_h
rescue => e
# Logger.error { e.backtrace }
Logger.error { e }
@error = e
end
def key
{ hostname: @hostname, port: @port }
end
def to_h
target = {
target: { hostname: @hostname, port: @port },
}
if @error
target[:error] = @error
target = { error: @error }
else
target[:hosts] = @servers.collect do |host, server|
target = @servers.collect do |host, server|
hostname, ip, port = host
host = {
hostname: hostname,
@ -72,7 +80,7 @@ module CryptCheck
host[:states] = server.states
host[:grade] = server.grade
else
host[:error] = server.message
host[:error] = server.to_s
end
host
end
@ -81,10 +89,11 @@ module CryptCheck
end
private
def resolve
begin
ip = IPAddr.new @hostname
return [[nil, ip.to_s, ip.family]]
return [[nil, ip.to_s, ip.family, @port]]
rescue IPAddr::InvalidAddressError
end
::Addrinfo.getaddrinfo(@hostname, nil, nil, :STREAM)

View File

@ -1,12 +1,11 @@
require 'resolv'
module CryptCheck
module Tls
module Https
def self.analyze(host, port=443)
::CryptCheck.analyze host, port, Server
end
def self.analyze_file(input, output)
::CryptCheck.analyze_file(input, 'output/https.erb', output) { |host| self.analyze host }
def self.analyze(hostname, port = 443)
host = Host.new hostname, port
Tls.aggregate host
end
end
end

View File

@ -3,6 +3,7 @@ module CryptCheck
module Https
class Host < Tls::Host
private
def server(*args)
Https::Server.new *args
end

View File

@ -6,7 +6,7 @@ module CryptCheck
class Server < Tls::TcpServer
attr_reader :hsts
def initialize(hostname, ip, family, port=443)
def initialize(hostname, ip, family, port = 443)
super
fetch_hsts
end
@ -19,7 +19,7 @@ module CryptCheck
{
follow_redirects: false,
verify: false,
timeout: TLS_TIMEOUT,
timeout: TLS_TIMEOUT,
ssl_version: @supported_methods.first.to_sym,
ciphers: Cipher::ALL
}
@ -31,7 +31,8 @@ module CryptCheck
return
end
end
rescue
rescue Exception => e
Logger.debug { e }
end
Logger.info { 'No HSTS'.colorize :warning }
@ -42,7 +43,7 @@ module CryptCheck
!@hsts.nil?
end
LONG_HSTS = 6*30*24*60*60
LONG_HSTS = 6 * 30 * 24 * 60 * 60
def hsts_long?
hsts? and @hsts >= LONG_HSTS
@ -53,10 +54,11 @@ module CryptCheck
end
protected
def available_checks
super + [
[:hsts, %i(warning good great), -> (s) { s.hsts_long? ? :great : s.hsts? ? :good : :warning }],
#[:must_staple, :best, -> (s) { s.must_staple? }],
#[:must_staple, :best, -> (s) { s.must_staple? }],
]
end
end

View File

@ -31,7 +31,7 @@ module CryptCheck
{ protocol: self.to_sym, states: self.states }
end
alias :to_sym :__getobj__
alias to_sym __getobj__
def <=>(other)
EXISTING.find_index(self) <=> EXISTING.find_index(other)
@ -44,7 +44,7 @@ module CryptCheck
[:sslv3, :critical, -> (s) { s == :SSLv3 }],
[:tlsv1_0, :error, -> (s) { s == :TLSv1 }],
[:tlsv1_1, :warning, -> (s) { s == :TLSv1_1 }]
]
].freeze
protected
def available_checks

View File

@ -62,15 +62,32 @@ module CryptCheck
end
def to_h
ciphers_preference = @preferences.collect do |p, cs|
case cs
when :client
{ protocol: p, client: true }
when nil
{ protocol: p, na: true }
else
{ protocol: p, cipher_suite: cs.collect(&:to_h) }
end
end
curves_preferences = case @curves_preference
when :client
:client
else
@curves_preference&.collect(&:name)
end
{
certs: @certs.collect(&:to_h),
dh: @dh.collect(&:to_h),
protocols: @supported_methods.collect(&:to_h),
ciphers: uniq_supported_ciphers.collect(&:to_h),
cipher_suites: @preferences.collect { |p, cs| { protocol: p, cipher_suite: cs.collect(&:name) } },
curves: @supported_curves.collect(&:to_h),
curve_preference: @curves_preference.collect(&:name),
fallback_scsv: @fallback_scsv
certs: @certs.collect(&:to_h),
dh: @dh.collect(&:to_h),
protocols: @supported_methods.collect(&:to_h),
ciphers: uniq_supported_ciphers.collect(&:to_h),
ciphers_preference: ciphers_preference,
curves: @supported_curves.collect(&:to_h),
curves_preference: curves_preferences,
fallback_scsv: @fallback_scsv
}
end
@ -78,11 +95,11 @@ module CryptCheck
include State
CHECKS = [
[:fallback_scsv, :good, -> (s) { s.fallback_scsv? }]
[:fallback_scsv, :good, -> (s) { s.fallback_scsv? }],
# [:tlsv1_2_only, -> (s) { s.tlsv1_2_only? }, :great],
# [:pfs_only, -> (s) { s.pfs_only? }, :great],
# [:ecdhe_only, -> (s) { s.ecdhe_only? }, :great],
#[:aead_only, -> (s) { s.aead_only? }, :best],
# [:aead_only, -> (s) { s.aead_only? }, :best],
].freeze
def available_checks

View File

@ -1,20 +1,18 @@
require 'resolv'
module CryptCheck
module Tls
module Smtp
def self.analyze(host, port=25, domain: nil)
::CryptCheck.analyze host, port, Server, Grade, domain: domain
end
def self.analyze(hostname, port = 25)
srv = ::Resolv::DNS.new.getresources(hostname, ::Resolv::DNS::Resource::IN::MX)
.sort_by &:preference
hosts = if srv.empty?
[hostname]
else
srv.collect { |s| s.exchange.to_s }
end
def self.analyze_domain(domain)
srv = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).sort_by &:preference
hosts = srv.empty? ? [domain] : srv.collect { |s| s.exchange.to_s }
results = {}
hosts.each { |h| results.merge! self.analyze(h, domain: domain) }
results
end
def self.analyze_file(input, output)
::CryptCheck.analyze_file(input, 'output/smtp.erb', output) { |host| self.analyze_domain host }
Tls.aggregate hosts.collect { |h| Host.new h, port }
end
end
end

View File

@ -0,0 +1,13 @@
module CryptCheck
module Tls
module Smtp
class Host < Tls::Host
private
def server(*args)
Smtp::Server.new *args
end
end
end
end
end

View File

@ -2,18 +2,10 @@ module CryptCheck
module Tls
module Smtp
class Server < Tls::TcpServer
attr_reader :domain
def initialize(hostname, family, ip, port, domain:)
@domain = domain
super hostname, family, ip, port
end
def ssl_connect(socket, context, method, &block)
socket.recv 1024
socket.write "EHLO #{Socket.gethostbyname(Socket.gethostname).first}\r\n"
features = socket.recv(1024).split "\r\n"
features
starttls = features.find { |f| /250[- ]STARTTLS/ =~ f }
raise TLSNotAvailableException unless starttls
socket.write "STARTTLS\r\n"

View File

@ -1,27 +1,24 @@
module CryptCheck
module Tls
module Xmpp
def self.analyze(host, port=nil, domain: nil, type: :s2s)
domain ||= host
::CryptCheck.analyze host, port, Server, Grade, domain: domain, type: type
end
def self.analyze_domain(domain, type: :s2s)
def self.analyze(hostname, type = :s2s)
service, port = case type
when :s2s
['_xmpp-server', 5269]
when :c2s
['_xmpp-client', 5222]
when :s2s
['_xmpp-server', 5269]
when :c2s
['_xmpp-client', 5222]
end
srv = Resolv::DNS.new.getresources("#{service}._tcp.#{hostname}",
Resolv::DNS::Resource::IN::SRV)
.sort_by &:priority
hosts = if srv.empty?
[[hostname, port]]
else
srv.collect { |s| [s.target.to_s, s.port] }
end
srv = Resolv::DNS.new.getresources("#{service}._tcp.#{domain}", Resolv::DNS::Resource::IN::SRV).sort_by &:priority
hosts = srv.empty? ? [[domain, port]] : srv.collect { |s| [s.target.to_s, s.port] }
results = {}
hosts.each { |host, port| results.merge! self.analyze(host, port, domain: domain, type: type) }
results
end
def self.analyze_file(input, output)
::CryptCheck.analyze_file(input, 'output/xmpp.erb', output) { |host| self.analyze_domain host }
hosts.collect { |args| Host.new *args, domain: hostname }
p hosts
end
end
end

View File

@ -0,0 +1,22 @@
module CryptCheck
module Tls
module Xmpp
class Host < Tls::Host
attr_reader :domain
def initialize(*args, domain: nil, type: :s2s)
@domain, @type = domain, type
super *args
Logger.info { '' }
Logger.info { self.required? ? 'Required'.colorize(:good) : 'Not required'.colorize(:warning) }
end
private
def server(*args)
Xmpp::Server.new *args, domain: @domain, type: @type
end
end
end
end
end

View File

@ -3,31 +3,31 @@ require 'nokogiri'
module CryptCheck
module Tls
module Xmpp
TLS_NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-tls'
class Server < Tls::TcpServer
attr_reader :domain
def initialize(hostname, family, ip, port=nil, domain: nil, type: :s2s)
def initialize(hostname, ip, family, port = nil, domain: nil, type: :s2s)
domain ||= hostname
@type, @domain = type, domain
@domain, @type = domain, type
port = case type
when :s2s
5269
when :c2s
5222
when :s2s
5269
when :c2s
5222
end unless port
super hostname, family, ip, port
super hostname, ip, family, port
Logger.info { '' }
Logger.info { self.required? ? 'Required'.colorize(:good) : 'Not required'.colorize(:warning) }
end
TLS_NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-tls'.freeze
def ssl_connect(socket, context, method, &block)
type = case @type
when :s2s then
'jabber:server'
when :c2s then
'jabber:client'
when :s2s then
'jabber:server'
when :c2s then
'jabber:client'
end
socket.puts "<?xml version='1.0' ?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='#{type}' to='#{@domain}' version='1.0'>"
response = ''