@@ -2,7 +2,6 @@ require 'colorize' | |||
require 'ipaddr' | |||
require 'timeout' | |||
require 'yaml' | |||
require 'cryptcheck/tls/fixture' | |||
module CryptCheck | |||
MAX_ANALYSIS_DURATION = 120 | |||
@@ -96,6 +95,7 @@ module CryptCheck | |||
else | |||
server.new *a, **kargs | |||
end | |||
ap s.status | |||
exit | |||
if grade | |||
g = grade.new s | |||
@@ -181,3 +181,6 @@ module CryptCheck | |||
SCORES.index a.grade | |||
end | |||
end | |||
require 'cryptcheck/fixture' | |||
require 'cryptcheck/tls/fixture' |
@@ -0,0 +1,52 @@ | |||
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) | |||
color = COLORS[state] || state | |||
self.colorize_old color | |||
end | |||
end | |||
class Exception | |||
BACKTRACE_REGEXP = /^(.*):(\d+):in `(.*)'$/ | |||
def colorize | |||
$stderr.puts self.message.colorize(:red) | |||
self.backtrace.each do |line| | |||
line = BACKTRACE_REGEXP.match line | |||
line = '%s:%s:in `%s\'' % [ | |||
line[1].colorize(:yellow), | |||
line[2].colorize(:blue), | |||
line[3].colorize(:magenta) | |||
] | |||
$stderr.puts line | |||
end | |||
end | |||
end | |||
class Integer | |||
def humanize | |||
secs = self | |||
[[60, :second], | |||
[60, :minute], | |||
[24, :hour], | |||
[30, :day], | |||
[12, :month]].map do |count, name| | |||
if secs > 0 | |||
secs, n = secs.divmod count | |||
n = n.to_i | |||
n > 0 ? "#{n} #{name}#{n > 1 ? 's' : ''}" : nil | |||
end | |||
end.compact.reverse.join ' ' | |||
end | |||
end |
@@ -106,6 +106,16 @@ module CryptCheck | |||
def issuer | |||
@cert.issuer | |||
end | |||
include ::CryptCheck::Statused | |||
CHECKS = [:weak_sign, -> (s) do | |||
not (SIGNATURE_ALGORITHMS_X509[s.signature_algorithm] & WEAK_SIGN).empty? | |||
end, :critical].freeze | |||
def children | |||
[self.key] | |||
end | |||
end | |||
end | |||
end |
@@ -1,58 +1,5 @@ | |||
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) | |||
color = COLORS[state] || state | |||
self.colorize_old color | |||
end | |||
end | |||
class Exception | |||
BACKTRACE_REGEXP = /^(.*):(\d+):in `(.*)'$/ | |||
def colorize | |||
$stderr.puts self.message.colorize(:red) | |||
self.backtrace.each do |line| | |||
line = BACKTRACE_REGEXP.match line | |||
line = '%s:%s:in `%s\'' % [ | |||
line[1].colorize(:yellow), | |||
line[2].colorize(:blue), | |||
line[3].colorize(:magenta) | |||
] | |||
$stderr.puts line | |||
end | |||
end | |||
end | |||
class Integer | |||
def humanize | |||
secs = self | |||
[[60, :second], | |||
[60, :minute], | |||
[24, :hour], | |||
[30, :day], | |||
[12, :month]].map do |count, name| | |||
if secs > 0 | |||
secs, n = secs.divmod count | |||
n = n.to_i | |||
n > 0 ? "#{n} #{name}#{n > 1 ? 's' : ''}" : nil | |||
end | |||
end.compact.reverse.join ' ' | |||
end | |||
end | |||
class ::OpenSSL::PKey::EC | |||
def type | |||
:ecc | |||
@@ -70,18 +17,23 @@ class ::OpenSSL::PKey::EC | |||
"ECC #{self.size} bits" | |||
end | |||
def status | |||
case self.size | |||
when 0...160 | |||
:critical | |||
when 160...192 | |||
:error | |||
when 192...256 | |||
:warning | |||
when 256...364 | |||
else | |||
:good | |||
end | |||
include ::CryptCheck::Statused | |||
CHECKS = [ | |||
[:weak_key, -> (s) do | |||
case s.size | |||
when 0...160 | |||
:critical | |||
when 160...192 | |||
:error | |||
when 192...256 | |||
:warning | |||
end | |||
end] | |||
].freeze | |||
def checks | |||
CHECKS | |||
end | |||
end | |||
@@ -98,16 +50,21 @@ class ::OpenSSL::PKey::RSA | |||
"RSA #{self.size} bits" | |||
end | |||
def status | |||
case self.size | |||
when 0...1024 | |||
:critical | |||
when 1024...2048 | |||
:error | |||
when 2048...4096 | |||
else | |||
:good | |||
end | |||
include ::CryptCheck::Statused | |||
CHECKS = [ | |||
[:weak_key, -> (s) do | |||
case s.size | |||
when 0...1024 | |||
:critical | |||
when 1024...2048 | |||
:error | |||
end | |||
end] | |||
].freeze | |||
def checks | |||
CHECKS | |||
end | |||
end | |||
@@ -124,8 +81,14 @@ class ::OpenSSL::PKey::DSA | |||
"DSA #{self.size} bits" | |||
end | |||
def status | |||
return :critical | |||
include ::CryptCheck::Statused | |||
CHECKS = [ | |||
[:weak_key, -> (_) { :critical }] | |||
].freeze | |||
def checks | |||
CHECKS | |||
end | |||
end | |||
@@ -142,16 +105,23 @@ class ::OpenSSL::PKey::DH | |||
"DH #{self.size} bits" | |||
end | |||
def status | |||
case self.size | |||
when 0...1024 | |||
:critical | |||
when 1024...2048 | |||
:error | |||
when 2048...4096 | |||
else | |||
:good | |||
end | |||
include ::CryptCheck::Statused | |||
CHECKS = [ | |||
[:weak_dh, -> (s) do | |||
case s.size | |||
when 0...1024 | |||
:critical | |||
when 1024...2048 | |||
:error | |||
else | |||
:warning | |||
end | |||
end] | |||
].freeze | |||
def checks | |||
CHECKS | |||
end | |||
end | |||
@@ -67,73 +67,6 @@ module CryptCheck | |||
'A+' | |||
end | |||
CHECKS = ([ | |||
# Certificates | |||
[:weak_sign, Proc.new { |s| | |||
Cert::WEAK_SIGN[:critical] | |||
}, :critical], | |||
# Keys | |||
[:weak_key, Proc.new { |s| Status.problem s.keys.collect &:status }], | |||
# DH | |||
[:weak_dh, Proc.new { |s| Status.problem s.dh.collect &:status }], | |||
# 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], | |||
[:no_ecdhe, Proc.new { |s| not s.ecdhe? }, :warning], | |||
[:ecdhe, Proc.new { |s| s.ecdhe? }, :good], | |||
[:ecdhe_only, Proc.new { |s| s.ecdhe_only? }, :perfect], | |||
[:aead, Proc.new { |s| s.aead? }, :good], | |||
#[:aead_only, Proc.new { |s| s.aead_only? }, :best], | |||
] + Cert::WEAK_SIGN.collect do |level, hashes| | |||
hashes.collect do |hash| | |||
["#{hash}_sig?".to_sym, Proc.new { |s| s.call "#{hash}_sig?".to_sym }, level ] | |||
end | |||
end.flatten(1)).freeze | |||
def checks | |||
checks = CHECKS | |||
unless @server.fallback_scsv? == nil | |||
checks += [ | |||
[:no_fallback_scsv, Proc.new { |s| not s.fallback_scsv? }, :error], | |||
[:fallback_scsv, Proc.new { |s| s.fallback_scsv? }, :good] | |||
] | |||
end | |||
checks | |||
end | |||
def calculate_states | |||
states = Status.empty | |||
@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 | |||
end |
@@ -20,7 +20,7 @@ module CryptCheck | |||
follow_redirects: false, | |||
verify: false, | |||
timeout: SSL_TIMEOUT, | |||
ssl_version: @supported_methods.first.name, | |||
ssl_version: @supported_methods.first.to_sym, | |||
ciphers: Cipher::ALL | |||
} | |||
if header = response.headers['strict-transport-security'] | |||
@@ -43,9 +43,18 @@ module CryptCheck | |||
end | |||
LONG_HSTS = 6*30*24*60*60 | |||
def hsts_long? | |||
hsts? and @hsts >= LONG_HSTS | |||
end | |||
def checks | |||
super + [ | |||
[:hsts, -> (s) { s.hsts? }, :good], | |||
[:hsts_long, -> (s) { s.hsts_long? }, :perfect], | |||
#[:must_staple, -> (s) { s.must_staple? }, :best], | |||
] | |||
end | |||
end | |||
end | |||
end | |||
@@ -126,7 +126,7 @@ module CryptCheck | |||
def fetch_dh | |||
@dh = @supported_ciphers.collect do |_, ciphers| | |||
ciphers.values.collect(&:tmp_key).select { |d| d.is_a? OpenSSL::PKey::DH }.collect &:size | |||
ciphers.values.collect(&:tmp_key).select { |d| d.is_a? OpenSSL::PKey::DH } | |||
end.flatten | |||
end | |||
@@ -285,7 +285,7 @@ module CryptCheck | |||
Method.each do |method| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{method.to_sym.downcase}? | |||
@supported_methods.detect { |m| m == method } | |||
@supported_methods.detect { |m| m == :#{method.to_sym} } | |||
end | |||
RUBY_EVAL | |||
end | |||
@@ -293,7 +293,7 @@ module CryptCheck | |||
Cipher::TYPES.each do |type, _| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{type}? | |||
@supported_ciphers.any? { |c| c.#{type}? } | |||
uniq_supported_ciphers.any? { |c| c.#{type}? } | |||
end | |||
RUBY_EVAL | |||
end | |||
@@ -315,31 +315,31 @@ module CryptCheck | |||
end | |||
def pfs? | |||
supported_ciphers.any? { |c| c.pfs? } | |||
uniq_supported_ciphers.any? { |c| c.pfs? } | |||
end | |||
def pfs_only? | |||
supported_ciphers.all? { |c| c.pfs? } | |||
uniq_supported_ciphers.all? { |c| c.pfs? } | |||
end | |||
def ecdhe? | |||
supported_ciphers.any? { |c| c.ecdhe? } | |||
uniq_supported_ciphers.any? { |c| c.ecdhe? } | |||
end | |||
def ecdhe_only? | |||
supported_ciphers.all? { |c| c.ecdhe? } | |||
uniq_supported_ciphers.all? { |c| c.ecdhe? } | |||
end | |||
def aead? | |||
supported_ciphers.any? { |c| c.aead? } | |||
uniq_supported_ciphers.any? { |c| c.aead? } | |||
end | |||
def aead_only? | |||
supported_ciphers.all? { |c| c.aead? } | |||
uniq_supported_ciphers.all? { |c| c.aead? } | |||
end | |||
def sweet32? | |||
supported_ciphers.any? { |c| c.sweet32? } | |||
uniq_supported_ciphers.any? { |c| c.sweet32? } | |||
end | |||
def fallback_scsv? | |||
@@ -350,6 +350,52 @@ module CryptCheck | |||
@cert.extensions.any? { |e| e.oid == '1.3.6.1.5.5.7.1.24' } | |||
end | |||
include Statused | |||
CHECKS = [ | |||
# Protocols | |||
[:ssl, -> (s) { s.ssl? }, :critical], | |||
[:tls12, -> (s) { s.tlsv1_2? }, :good], | |||
[:tls12_only, -> (s) { s.tlsv1_2_only? }, :perfect], | |||
# Ciphers | |||
[:dss, -> (s) { s.dss? }, :critical], | |||
[:anonymous, -> (s) { s.anonymous? }, :critical], | |||
[:null, -> (s) { s.null? }, :critical], | |||
[:export, -> (s) { s.export? }, :critical], | |||
[:des, -> (s) { s.des? }, :critical], | |||
[:md5, -> (s) { s.md5? }, :critical], | |||
[:rc4, -> (s) { s.rc4? }, :error], | |||
[:sweet32, -> (s) { s.sweet32? }, :error], | |||
[:no_pfs, -> (s) { not s.pfs_only? }, :warning], | |||
[:pfs, -> (s) { s.pfs? }, :good], | |||
[:pfs_only, -> (s) { s.pfs_only? }, :perfect], | |||
[:no_ecdhe, -> (s) { not s.ecdhe? }, :warning], | |||
[:ecdhe, -> (s) { s.ecdhe? }, :good], | |||
[:ecdhe_only, -> (s) { s.ecdhe_only? }, :perfect], | |||
[:aead, -> (s) { s.aead? }, :good], | |||
#[:aead_only, -> (s) { s.aead_only? }, :best], | |||
].freeze | |||
def checks | |||
checks = CHECKS | |||
unless self.fallback_scsv? == nil | |||
checks += [ | |||
[:no_fallback_scsv, -> (s) { not s.fallback_scsv? }, :error], | |||
[:fallback_scsv, -> (s) { s.fallback_scsv? }, :good] | |||
] | |||
end | |||
checks | |||
end | |||
def children | |||
@certs + @dh | |||
end | |||
private | |||
def connect(&block) | |||
socket = ::Socket.new @family, sock_type | |||
@@ -497,12 +543,9 @@ module CryptCheck | |||
@dh = dh | |||
end | |||
Cert::SIGNATURE_ALGORITHMS.each do |s| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{s}_sign? | |||
@certs.any? &:#{s}? | |||
end | |||
RUBY_EVAL | |||
private | |||
def uniq_supported_ciphers | |||
@supported_ciphers.values.collect(&:keys).flatten.uniq | |||
end | |||
end | |||