Bladeren bron

Refactor test checks

new-scoring
aeris 3 jaren geleden
bovenliggende
commit
e604c11e13

+ 6
- 10
lib/cryptcheck/tls.rb Bestand weergeven

@@ -9,23 +9,19 @@ module CryptCheck

def self.colorize(cipher)
colors = case
when /^SSL/ =~ cipher then { color: :white, background: :red }
when :TLSv1_2 == cipher then { color: :green }
when /^SSL/ =~ cipher then :critical
when :TLSv1_2 == cipher then :good
end
cipher.to_s.colorize colors
end

def self.key_to_s(key)
type_color = case key.type
when :ecc then { color: :green }
when :dsa then { color: :red }
when :ecc then :good
when :dh then :warning
when :dsa then :critical
end
size_color = case key.status
when :error then { color: :white, background: :red }
when :warning then { color: :yellow }
when :success then { color: :green }
end
"#{key.type.to_s.upcase.colorize type_color} #{key.size.to_s.colorize size_color} bits"
"#{key.type.to_s.upcase.colorize type_color} #{key.size.to_s.colorize key.status} bits"
end
end
end

+ 72
- 28
lib/cryptcheck/tls/cipher.rb Bestand weergeven

@@ -31,7 +31,7 @@ module CryptCheck
idea: %w(IDEA),
chacha20: %w(CHACHA20),

cbc: %w(CBC),
#cbc: %w(CBC),
gcm: %w(GCM),
ccm: %w(CCM)
}
@@ -54,6 +54,19 @@ module CryptCheck
RUBY_EVAL
end

def self.cbc?(cipher)
!aead? cipher
end
def cbc?
!aead?
end
def aead?(cipher)
gcm?(cipher) or ccm?(cipher)
end
def aead?
gcm? or ccm?
end

def ssl?
sslv2? or sslv3?
end
@@ -66,40 +79,71 @@ module CryptCheck
dhe? or ecdhe?
end

def sweet32?
enc = params[:enc]
return false unless enc # No encryption
block = enc[2]
return false unless block # No block encryption
block <= 64
end

def sweet32?
enc = params[:enc]
return false unless enc # No encryption
block = enc[2]
return false unless block # No block encryption
block <= 64
end

def colorize
colors = case self.score
when :error then
{ color: :white, background: :red }
when :danger then
{ color: :red }
when :warning then
{ color: :yellow }
when :success then
{ color: :green }
end
@name.colorize colors
end

def state
ok = Proc.new { |n| self.send "#{n}?" }
{
success: %i(pfs).select { |n| ok.call n },
warning: %i().select { |n| ok.call n },
danger: %i().select { |n| ok.call n },
error: %i(dss md5 psk srp anonymous null export des des3 rc2 rc4 idea).select { |n| ok.call n }
}
@name.colorize self.score
end

CHECKS = [
[:dss, Proc.new { |s| s.dss? }, :critical],
[:anonymous, Proc.new { |s| s.anonymous? }, :critical],
[:null, Proc.new { |s| s.null? }, :critical],
[:export, Proc.new { |s| s.export? }, :critical],
[:des, Proc.new { |s| s.des? }, :critical],
[:md5, Proc.new { |s| s.md5? }, :critical],

[:rc4, Proc.new { |s| s.rc4? }, :error],
[:sweet32, Proc.new { |s| s.sweet32? }, :error],

#[:cbc, Proc.new { |s| s.cbc? }, :warning],
#[:dhe, Proc.new { |s| s.dhe? }, :warning],
[:weak_dh, Proc.new do |s|
dh = s.dh
next nil unless dh
status = dh.status
next status if %i(critical error warning).include? status
nil
end ],
[:no_pfs, Proc.new { |s| not s.pfs? }, :warning],

[:pfs, Proc.new { |s| s.pfs? }, :good],
[:ecdhe, Proc.new { |s| s.ecdhe? }, :good],
[:aead, Proc.new { |s| s.aead? }, :good],
]

def states
return @states if @states
@states = { critical: [], error: [], warning: [], good: [], perfect: [], best: [] }
CHECKS.each do |name, check, status|
result = check.call self
states[status ? status : result] << name if result
end
@states
end

def score
state = self.state
return :error unless state[:error].empty?
return :danger unless state[:danger].empty?
return :warning unless state[:warning].empty?
return :success unless state[:success].empty?
%i(critical error warning good perfect best).each do |s|
return s unless self.states[s].empty?
end
:none
end

PRIORITY = { success: 1, none: 2, warning: 3, danger: 4, error: 5 }
PRIORITY = { good: 1, none: 2, warning: 3, error: 4, critical: 5 }

def self.sort(ciphers)
ciphers.sort do |a, b|

+ 29
- 10
lib/cryptcheck/tls/fixture.rb Bestand weergeven

@@ -1,5 +1,23 @@
require 'openssl'

class String
alias :colorize_old :colorize

COLORS = {
critical: { color: :white, background: :red },
error: :red,
warning: :light_red,
good: :green,
perfect: :blue,
best: :magenta,
unknown: { background: :black }
}

def colorize(state)
self.colorize_old COLORS[state]
end
end

class Integer
def humanize
secs = self
@@ -28,9 +46,10 @@ class ::OpenSSL::PKey::EC

def status
case self.size
when 0...160 then :error
when 160...256 then :warning
when 384...::Float::INFINITY then :success
when 0...160 then :critical
when 160...192 then :error
when 192...256 then :warning
when 384...::Float::INFINITY then :good
end
end
end
@@ -50,9 +69,9 @@ class ::OpenSSL::PKey::RSA

def status
case self.size
when 0...1024 then :error
when 1024...2048 then :warning
when 4096...::Float::INFINITY then :success
when 0...1024 then :critical
when 1024...2048 then :error
when 4096...::Float::INFINITY then :good
end
end
end
@@ -71,7 +90,7 @@ class ::OpenSSL::PKey::DSA
end

def status
return :error
return :critical
end
end

@@ -90,9 +109,9 @@ class ::OpenSSL::PKey::DH

def status
case self.size
when 0...1024 then :error
when 1024...2048 then :warning
when 4096...::Float::INFINITY then :success
when 0...1024 then :critical
when 1024...2048 then :error
when 4096...::Float::INFINITY then :good
end
end
end

+ 110
- 53
lib/cryptcheck/tls/grade.rb Bestand weergeven

@@ -1,80 +1,137 @@
module CryptCheck
module Tls
class Grade
attr_reader :server, :score, :grade, :error, :danger, :warning, :success
attr_reader :server, :grade, :states

def initialize(server)
@server = server
calculate_states
calculate_grade
@checks = checks
@states = calculate_states
@grade = calculate_grade
end

def display
color = case self.grade
when 'A+' then :blue
when 'A' then :green
when 'B', 'C' then :yellow
when 'E', 'F' then :red
when 'M', 'T' then { color: :white, background: :red }
color = case @grade
when 'A', 'A+'
:best
when 'B', 'B+'
:perfect
when 'C', 'C+'
nil
when 'E'
:warning
when 'F'
:error
when 'G'
:critical
when 'M', 'T'
:unknown
end

Logger.info { "Grade : #{self.grade.colorize color }" }
Logger.info { '' }
Logger.info { "Errors : #{self.error.join(' ').colorize :red }" } unless self.error.empty?
Logger.info { "Warnings : #{self.warning.join(' ').colorize :yellow }" } unless self.warning.empty?
Logger.info { "Best practices : #{self.success.join(' ').colorize :green }" } unless self.success.empty?
[
['Critical', :critical],
['Error', :error],
['Warning', :warning],
['Good', :good],
['Perfect', :perfect],
['Best', :best],
].each do |text, color|
states = @states[color]
Logger.info { "#{text} : #{states.collect { |s| s.to_s.colorize color }.join ' '}" } unless states.empty?
end
end

private
def calculate_grade
@grade = case @score
when 0...20 then 'F'
when 20...35 then 'E'
when 35...50 then 'D'
when 50...65 then 'C'
when 65...80 then 'B'
else 'A'
end

@grade = [@grade, 'B'].max if !@server.tlsv1_2? or %i(error warning).include? @server.key.status
@grade = [@grade, 'F'].max unless @error.empty?
@grade = [@grade, 'F'].max unless @error.empty?

@grade = 'M' unless @server.cert_valid
@grade = 'T' unless @server.cert_trusted

@grade = 'A+' if @grade == 'A' and @error.empty? and @warning.empty? and (all_success & @success) == all_success
end
case
when !@states[:critical].empty?
return 'G'
when !@states[:error].empty?
return 'F'
when !@states[:warning].empty?
return 'E'
end

def calculate_states
ok = Proc.new { |n| @server.send "#{n}?" }
state = {
success: all_success.select { |n| ok.call n },
warning: all_warning.select { |n| ok.call n },
danger: all_danger.select { |n| ok.call n },
error: all_error.select { |n| ok.call n }
}
@success, @warning, @danger, @error = state[:success], state[:warning], state[:danger], state[:error]
end
goods = @checks.select { |c| c.last == :good }.collect &:first
unless goods.empty?
return 'D' if @states[:good].empty?
return 'C' if @states[:good] != goods
end

ALL_ERROR = %i(md5_sig md5 anonymous dss null export des des3 rc4)
def all_error
ALL_ERROR
end
perfects = @checks.select { |c| c.last == :perfect }.collect &:first
unless perfects.empty?
return 'C+' if @states[:perfect].empty?
return 'B' if @states[:perfect] != perfects
end

ALL_DANGER = %i()
def all_danger
ALL_DANGER
bests = @checks.select { |c| c.last == :best }.collect &:first
unless bests.empty?
return 'B+' if @states[:best].empty?
return 'A' if @states[:best] != bests
end

'A+'
end

ALL_WARNING = %i(sha1_sig)
def all_warning
ALL_WARNING
CHECKS = [
# Keys
[:dss_sign, Proc.new { |s| s.dss_sig? }, :critical],
[:weak_key, Proc.new { |s| %i(critical error warning).include? s.key.status } ],

# DH
[:weak_dh, Proc.new { |s| (%i(critical error warning) & s.dh.collect(&:status).uniq).first } ],

# Certificates
[:md2_sign, Proc.new { |s| s.md2_sig? }, :critical],
[:mdc2_sign, Proc.new { |s| s.mdc2_sig? }, :critical],
[:md4_sign, Proc.new { |s| s.md4_sig? }, :critical],
[:md5_sign, Proc.new { |s| s.md5_sig? }, :critical],
[:sha_sign, Proc.new { |s| s.sha_sig? }, :critical],

[:sha1_sign, Proc.new { |s| s.sha1_sig? }, :warning],

# Protocols
[:ssl, Proc.new { |s| s.ssl? }, :critical],
[:tls12, Proc.new { |s| s.tlsv1_2? }, :good],
[:tls12_only, Proc.new { |s| s.tlsv1_2_only? }, :perfect],

# Ciphers
[:dss, Proc.new { |s| s.dss? }, :critical],
[:anonymous, Proc.new { |s| s.anonymous? }, :critical],
[:null, Proc.new { |s| s.null? }, :critical],
[:export, Proc.new { |s| s.export? }, :critical],
[:des, Proc.new { |s| s.des? }, :critical],
[:md5, Proc.new { |s| s.md5? }, :critical],

[:rc4, Proc.new { |s| s.rc4? }, :error],
[:sweet32, Proc.new { |s| s.sweet32? }, :error],

[:no_pfs, Proc.new { |s| not s.pfs_only? }, :warning],
[:pfs, Proc.new { |s| s.pfs? }, :good],
[:pfs_only, Proc.new { |s| s.pfs_only? }, :perfect],
[:ecdhe, Proc.new { |s| s.ecdhe? }, :good],
[:ecdhe_only, Proc.new { |s| s.ecdhe_only? }, :perfect],

[:aead, Proc.new { |s| s.aead_only? }, :good],
#[:aead_only, Proc.new { |s| s.aead_only? }, :best],
]

def checks
CHECKS
end

ALL_SUCCESS = %i(pfs_only)
def all_success
ALL_SUCCESS
def calculate_states
states = { critical: [], error: [], warning: [], good: [], perfect: [], best: [] }
@checks.each do |name, check, status|
result = check.call @server
if result
state = states[status ? status : result]
state << name if state
end
end
states
end
end
end

+ 5
- 2
lib/cryptcheck/tls/https/grade.rb Bestand weergeven

@@ -2,8 +2,11 @@ module CryptCheck
module Tls
module Https
class Grade < Tls::Grade
def all_success
super + %i(hsts hsts_long)
def checks
super + [
[:hsts, Proc.new { |s| s.hsts? }, :good],
[:hsts_long, Proc.new { |s| s.hsts_long? }, :perfect],
]
end
end
end

+ 2
- 2
lib/cryptcheck/tls/https/server.rb Bestand weergeven

@@ -27,14 +27,14 @@ module CryptCheck
name, value = header.split '='
if name == 'max-age'
@hsts = value.to_i
Logger.info { "HSTS : #{@hsts.to_s.colorize hsts_long? ? :green : nil}" }
Logger.info { "HSTS : #{@hsts.to_s.colorize hsts_long? ? :good : nil}" }
return
end
end
rescue
end

Logger.info { 'No HSTS'.colorize :yellow }
Logger.info { 'No HSTS'.colorize :warning }
@hsts = nil
end


+ 101
- 27
lib/cryptcheck/tls/server.rb Bestand weergeven

@@ -21,6 +21,8 @@ module CryptCheck
end
class CipherNotAvailable < TLSException
end
class InappropriateFallback < TLSException
end
class Timeout < ::StandardError
end
class TLSTimeout < Timeout
@@ -33,7 +35,7 @@ module CryptCheck
def initialize(hostname, family, ip, port)
@hostname, @family, @ip, @port = hostname, family, ip, port
@dh = []
Logger.info { name.colorize :blue }
Logger.info { name.colorize :perfect }
extract_cert
Logger.info { '' }
Logger.info { "Key : #{Tls.key_to_s self.key}" }
@@ -70,14 +72,48 @@ module CryptCheck
RUBY_EVAL
end

{
md2: %w(md2WithRSAEncryption),
md5: %w(md5WithRSAEncryption md5WithRSA),
sha1: %w(sha1WithRSAEncryption sha1WithRSA dsaWithSHA1 dsaWithSHA1_2 ecdsa_with_SHA1)
}.each do |name, signature|
SIGNATURE_ALGORITHMS = {
'dsaWithSHA' => %i(sha1 dss),
'dsaWithSHA1' => %i(sha1 dss),
'dsaWithSHA1_2' => %i(sha1 dss),
'dsa_with_SHA224' => %i(sha2 dss),
'dsa_with_SHA256' => %i(sha2 dss),

'mdc2WithRSA' => %i(mdc2 rsa),

'md2WithRSAEncryption' => %i(md2 rsa),

'md4WithRSAEncryption' => %i(md4, rsa),

'md5WithRSA' => %i(md5 rsa),
'md5WithRSAEncryption' => %i(md5 rsa),

'shaWithRSAEncryption' => %i(sha rsa),
'sha1WithRSA' => %i(sha1 rsa),
'sha1WithRSAEncryption' => %i(sha1 rsa),
'sha224WithRSAEncryption' => %i(sha2 rsa),
'sha256WithRSAEncryption' => %i(sha2 rsa),
'sha384WithRSAEncryption' => %i(sha2 rsa),
'sha512WithRSAEncryption' => %i(sha2 rsa),

'ripemd160WithRSA' => %i(ripemd160 rsa),

'ecdsa-with-SHA1' => %i(sha1 ecc),
'ecdsa-with-SHA224' => %i(sha2 ecc),
'ecdsa-with-SHA256' => %i(sha2 ecc),
'ecdsa-with-SHA384' => %i(sha2 ecc),
'ecdsa-with-SHA512' => %i(sha2 ecc),

'id_GostR3411_94_with_GostR3410_2001' => %i(ghost),
'id_GostR3411_94_with_GostR3410_94' => %i(ghost),
'id_GostR3411_94_with_GostR3410_94_cc' => %i(ghost),
'id_GostR3411_94_with_GostR3410_2001_cc' => %i(ghost)
}

%i(md2 mdc2 md4 md5 ripemd160 sha sha1 sha2 rsa dss ecc ghost).each do |name|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{name}_sig?
#{signature}.include? @cert.signature_algorithm
SIGNATURE_ALGORITHMS[@cert.signature_algorithm].include? :#{name}
end
RUBY_EVAL
end
@@ -106,6 +142,10 @@ module CryptCheck
tls? and !ssl?
end

def tlsv1_2_only?
tlsv1_2? and not ssl? and not tlsv1? and not tlsv1_1?
end

def pfs?
supported_ciphers.any? { |c| c.pfs? }
end
@@ -114,6 +154,26 @@ module CryptCheck
supported_ciphers.all? { |c| c.pfs? }
end

def ecdhe?
supported_ciphers.any? { |c| c.ecdhe? }
end

def ecdhe_only?
supported_ciphers.all? { |c| c.ecdhe? }
end

def aead?
supported_ciphers.any? { |c| c.aead? }
end

def aead_only?
supported_ciphers.all? { |c| c.aead? }
end

def sweet32?
supported_ciphers.any? { |c| c.sweet32? }
end

private
def name
name = "#@ip:#@port"
@@ -152,39 +212,47 @@ module CryptCheck
ssl_socket.connect_nonblock
Logger.trace { "SSL connected to #{name}" }
return block_given? ? block.call(ssl_socket) : nil
rescue ::IO::WaitReadable
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 ::IO::WaitWritable
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
when /state=SSLv2 read server hello A$/,
/state=SSLv3 read server hello A: wrong version number$/
case e.message
when /state=SSLv.* read server hello A$/
raise TLSNotAvailableException, e
when /state=SSLv.* read server hello A: wrong version number$/
raise MethodNotAvailable, e
when /state=error: no ciphers available$/,
/state=SSLv3 read server hello A: sslv3 alert handshake failure$/
/state=SSLv.* read server hello A: sslv.* alert handshake failure$/
raise CipherNotAvailable, e
end
rescue SystemCallError => e
case e
when /^Connection reset by peer$/
raise MethodNotAvailable, e
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

# secp192r1 secp256r1
SUPPORTED_CURVES = %w(secp160k1 secp160r1 secp160r2 sect163k1 sect163r1 sect163r2 secp192k1 sect193r1 sect193r2 secp224k1 secp224r1 sect233k1 sect233r1 sect239k1 secp256k1 sect283k1 sect283r1 secp384r1 sect409k1 sect409r1 secp521r1 sect571k1 sect571r1)
SUPPORTED_CURVES = %w(secp160k1 secp160r1 secp160r2 sect163k1
sect163r1 sect163r2 secp192k1 sect193r1 sect193r2 secp224k1
secp224r1 sect233k1 sect233r1 sect239k1 secp256k1 sect283k1
sect283r1 secp384r1 sect409k1 sect409r1 secp521r1 sect571k1
sect571r1)

def ssl_client(method, ciphers = nil, curves = nil, &block)
ssl_context = ::OpenSSL::SSL::SSLContext.new method
ssl_context.ciphers = ciphers.join ':' if ciphers
def ssl_client(method, ciphers = nil, curves = nil, fallback: false, &block)
ssl_context = ::OpenSSL::SSL::SSLContext.new method
ssl_context.enable_fallback_scsv if fallback
ssl_context.ciphers = ciphers.join ':' if ciphers

ssl_context.ecdh_curves = curves.join ':' if curves
#ssl_context.ecdh_auto = false
@@ -197,9 +265,6 @@ module CryptCheck
return block_given? ? block.call(ssl_socket) : nil
end
end

Logger.debug { "No SSL available on #{name}" }
raise CipherNotAvailable
end

def extract_cert
@@ -209,7 +274,8 @@ module CryptCheck
@cert, @chain = ssl_client(method) { |s| [s.peer_cert, s.peer_cert_chain] }
Logger.debug { "Certificate #{@cert.subject}" }
break
rescue TLSException
rescue TLSTimeout, ::SystemCallError
raise
end
end
raise TLSNotAvailableException unless @cert
@@ -245,8 +311,16 @@ module CryptCheck
dh = ssl_client(method, [cipher], curves) { |s| s.tmp_key }
@dh << dh if dh
cipher = Cipher.new method, cipher, dh
dh = dh ? " (#{'DH'.colorize :green} : #{Tls.key_to_s dh})" : ''
Logger.info { "#{Tls.colorize method} / #{cipher.colorize} : Supported#{dh}" }
dh = dh ? " (#{'PFS'.colorize :good} : #{Tls.key_to_s dh})" : ''

states = cipher.states
text = %i(critical error warning good perfect best).collect do |s|
states[s].collect { |t| t.to_s.colorize s }.join ' '
end.reject &:empty?
text = text.join ' '

Logger.info { "#{Tls.colorize method} / #{cipher.colorize}#{dh} [#{text}]" }

cipher
rescue => e
cipher = Cipher.new method, cipher

+ 4
- 2
lib/cryptcheck/tls/xmpp/grade.rb Bestand weergeven

@@ -2,8 +2,10 @@ module CryptCheck
module Tls
module Xmpp
class Grade < Tls::Grade
def all_success
super + %i(required)
def checks
super + [
[:required, Proc.new { |s| s.required? }, :good],
]
end
end
end

+ 1
- 1
lib/cryptcheck/tls/xmpp/server.rb Bestand weergeven

@@ -19,7 +19,7 @@ module CryptCheck
end unless port
super hostname, family, ip, port
Logger.info { '' }
Logger.info { self.required? ? 'Required'.colorize(:green) : 'Not required'.colorize(:yellow) }
Logger.info { self.required? ? 'Required'.colorize(:good) : 'Not required'.colorize(:warning) }
end

def ssl_connect(socket, context, method, &block)

Laden…
Annuleren
Opslaan