Compare commits

...

8 Commits

  1. 18
      Makefile
  2. 2
      bin/cryptcheck
  3. 7
      bin/rspec.sh
  4. 22
      lib/cryptcheck/grade.rb
  5. 7
      lib/cryptcheck/host.rb
  6. 94
      lib/cryptcheck/tls/curve.rb
  7. 210
      lib/cryptcheck/tls/engine.rb
  8. 106
      lib/cryptcheck/tls/https/server.rb
  9. 2
      lib/fixtures/01_openssl/ec.rb
  10. 165
      spec/cryptcheck/grade_spec.rb
  11. 184
      spec/cryptcheck/tls/cert_spec.rb
  12. 149
      spec/cryptcheck/tls/grade_spec.rb
  13. 224
      spec/cryptcheck/tls/host_spec.rb
  14. 371
      spec/cryptcheck/tls/server_spec.rb
  15. 46
      spec/fake.rb
  16. 216
      spec/fake/fake.c
  17. 7
      spec/fake/fake.h
  18. 82
      spec/fake/test.c
  19. 27
      spec/faketime.rb
  20. 35
      spec/faketime/faketime.c
  21. 2
      spec/faketime/faketime.h
  22. BIN
      spec/faketime/libfaketime.so
  23. 390
      spec/helpers.rb
  24. 34
      spec/lib/basic_server.rb
  25. 9
      spec/lib/tcp_server.rb
  26. 78
      spec/lib/tls_server.rb

@ -2,7 +2,7 @@ ROOT_DIR := $(dir $(realpath $(firstword $(MAKEFILE_LIST))))
BUILD_DIR := $(ROOT_DIR)/build
export RBENV_ROOT ?= $(ROOT_DIR)/build/rbenv
RBENV_VERSION := v1.2.0
_RBENV_VERSION := v1.2.0
RUBY_BUILD_VERSION = v20220218
OPENSSL_1_0_VERSION := 1.0.2j
@ -39,7 +39,7 @@ clean:
rm -rf build/
$(RBENV_ROOT)/:
git clone https://github.com/rbenv/rbenv/ "$@" -b "$(RBENV_VERSION)" --depth 1
git clone https://github.com/rbenv/rbenv/ "$@" -b "$(_RBENV_VERSION)" --depth 1
$(RBENV_ROOT)/plugins/ruby-build/: | $(RBENV_ROOT)/
git clone https://github.com/rbenv/ruby-build/ "$@" -b "$(RUBY_BUILD_VERSION)" --depth 1
rbenv: | $(RBENV_ROOT)/plugins/ruby-build/
@ -107,16 +107,18 @@ ruby-1.0: $(RBENV_ROOT)/versions/$(RUBY_1_0_VERSION)-cryptcheck
ruby-1.1: $(RBENV_ROOT)/versions/$(RUBY_1_1_VERSION)-cryptcheck
ruby: ruby-1.0 ruby-1.1
build/libfaketime.so: spec/faketime/faketime.c spec/faketime/faketime.h
$(CC) $^ -o "$@" -shared -fPIC -ldl -std=c99 -Werror -Wall
faketime: build/libfaketime.so
.PHONY: faketime
build/libfake.so: spec/fake/fake.c spec/fake/fake.h
LANG=C $(CC) $^ -g -o "$@" -shared -fPIC -ldl -std=c99 -Werror -Wall -pedantic
fake: build/libfake.so
build/test: spec/fake/test.c
LANG=C $(CC) $^ -g -o "$@" -Werror -Wall -pedantic
test-material:
bin/generate-test-material.rb
test: spec/faketime/libfaketime.so
LD_LIBRARY_PATH="$(LIBRARY_PATH_1_0):$(BUILD_DIR)" bin/rspec
test: build/libfake.so
LD_LIBRARY_PATH="$(LIBRARY_PATH_1_0):$(BUILD_DIR)" LD_PRELOAD="$(ROOT_DIR)/$^" bin/rspec
.PHONY: test
docker-1.0:

@ -2,7 +2,7 @@
require 'rubygems'
require 'bundler/setup'
require 'thor'
require 'awesome_print'
require 'amazing_print'
begin
require 'pry-byebug'
rescue LoadError

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
eval "$(bin/load-rbenv "$1")"
BASE_DIR="$(dirname "$(dirname "$0")")"
shift
LD_PRELOAD="$BASE_DIR/build/libfake.so" LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$BASE_DIR/build/" \
exec rspec "$@"

@ -1,6 +1,6 @@
module CryptCheck
module Grade
GRADES = %i(A+ A B+ B C+ C D E F G V T X)
GRADES = %i(A+ A B+ B C+ C D E F G V T X).freeze
GRADE_STATUS = {
:'A+' => :best,
A: :best,
@ -16,7 +16,7 @@ module CryptCheck
V: :critical,
T: :critical,
X: :critical
}
}.freeze
STATUS_GRADES = {
critical: :G,
error: :F,
@ -25,7 +25,7 @@ module CryptCheck
good: :C,
great: :B,
best: :A
}
}.freeze
def grade
@grade ||= calculate_grade
@ -34,5 +34,21 @@ module CryptCheck
def grade_status
GRADE_STATUS.fetch self.grade, :unknown
end
def self.compare(a, b)
GRADES.index(a.to_sym) <=> GRADES.index(b.to_sym)
end
def self.sort(grades)
grades.sort &self.method(:compare)
end
def self.better(grades)
self.sort(grades).first
end
def self.worst(grades)
self.sort(grades).last
end
end
end

@ -3,7 +3,7 @@ require 'timeout'
module CryptCheck
class Host
MAX_ANALYSIS_DURATION = 600
MAX_ANALYSIS_DURATION = ENV.fetch('MAX_ANALYSIS_DURATION', '600').to_i
attr_reader :servers, :error
@ -22,6 +22,9 @@ module CryptCheck
Logger.info { "Grade : #{server.grade.to_s.colorize server.grade_status}" }
Logger.info { server.states.ai }
server
rescue ::Timeout::Error => e
Logger.error { e }
StandardError.new "Too long analysis (max #{MAX_ANALYSIS_DURATION.humanize})"
rescue => e
Logger.error { e }
raise if ENV['DEV_MODE']
@ -75,7 +78,7 @@ module CryptCheck
end
::Addrinfo.getaddrinfo(@hostname, nil, nil, :STREAM)
.collect { |a| [@hostname, a.ip_address, a.afamily, @port] }
end.reject do |_1, _2, family, *_3|
end.reject do |_, _, family, *_|
(ENV['DISABLE_IPv6'] && family == Socket::AF_INET6) ||
(ENV['DISABLE_IPv4'] && family == Socket::AF_INET)
end

@ -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

@ -5,15 +5,15 @@ module CryptCheck
module Tls
module Engine
SLOW_DOWN = ENV.fetch('SLOW_DOWN', '0').to_i
TCP_TIMEOUT = 10
TLS_TIMEOUT = 2 * TCP_TIMEOUT
TCP_TIMEOUT = ENV.fetch('TCP_TIMEOUT', '10').to_i
TLS_TIMEOUT = ENV.fetch('TLS_TIMEOUT', '10').to_i
class TLSException < ::StandardError
end
class TLSNotAvailableException < TLSException
def to_s
'TLS seems not supported on this server'
'TLS seems not supported on this server'
end
end
@ -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

@ -1,67 +1,67 @@
require 'httparty'
module CryptCheck
module Tls
module Https
class Server < Tls::Server
attr_reader :hsts
module Tls
module Https
class Server < Tls::Server
attr_reader :hsts
def initialize(hostname, ip, family, port = 443)
super
fetch_hsts
end
def initialize(hostname, ip, family, port = 443)
super
fetch_hsts
end
def fetch_hsts
port = @port == 443 ? '' : ":#{@port}"
def fetch_hsts
port = @port == 443 ? '' : ":#{@port}"
begin
response = ::HTTParty.head "https://#{@hostname}#{port}/",
{
follow_redirects: false,
verify: false,
timeout: TLS_TIMEOUT,
ssl_version: @supported_methods.first.to_sym,
ciphers: Cipher::ALL
}
if header = response.headers['strict-transport-security']
name, value = header.split '='
if name == 'max-age'
@hsts = value.to_i
Logger.info { 'HSTS : ' + @hsts.to_s.colorize(hsts_long? ? :good : nil) }
return
end
end
rescue Exception => e
Logger.debug { e }
end
begin
response = ::HTTParty.head "https://#{@hostname}#{port}/",
{
follow_redirects: false,
verify: false,
timeout: TLS_TIMEOUT,
ssl_version: @supported_methods.first.to_sym,
ciphers: Cipher::ALL
}
if header = response.headers['strict-transport-security']
name, value = header.split '='
if name == 'max-age'
@hsts = value.to_i
Logger.info { 'HSTS : ' + @hsts.to_s.colorize(hsts_long? ? :good : nil) }
return
end
end
rescue Exception => e
Logger.debug { e }
end
Logger.info { 'No HSTS'.colorize :warning }
@hsts = nil
end
Logger.info { 'No HSTS'.colorize :warning }
@hsts = nil
end
def hsts?
!@hsts.nil?
end
def hsts?
!@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
end
def hsts_long?
hsts? and @hsts >= LONG_HSTS
end
def to_h
super.merge({ hsts: @hsts })
end
def to_h
super.merge({ hsts: @hsts })
end
protected
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? }],
]
end
end
end
end
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? }],
]
end
end
end
end
end

@ -14,7 +14,7 @@ module Fixture
end
def to_s
"ECC #{self.size} bits"
"ECC #{self.size} bits (#{self.curve})"
end
def to_h

@ -1,149 +1,20 @@
require 'awesome_print'
module CryptCheck
describe Grade do
describe '#grade' do
def obj(trust: true, valid: true, **states)
Class.new do
def initialize(trust, valid, states)
@trust, @valid, @states = trust, valid, states
end
include Grade
def trusted?
@trust
end
def valid?
@valid
end
def states
State.empty.merge @states
end
end.new trust, valid, states
end
it 'must return :V if not valid' do
obj = obj valid: false, critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :V
end
it 'must return :T if not trusted' do
obj = obj trust: false, critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :T
end
it 'must return :G if critical' do
obj = obj critical: { foo: false, bar: nil, baz: true },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :G
end
it 'must return :F if error' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil, baz: true },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :F
end
it 'must return :E if warning' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil, baz: true },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :E
end
it 'must return :D if nor good nor bad' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: false, bar: nil },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :D
end
it 'must return :C if some good' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: false, bar: nil, baz: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :C
end
it 'must return :C+ if all good' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: false, bar: nil },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :'C+'
end
it 'must return :B if some great' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: false, bar: nil, baz: true },
best: { foo: true, bar: nil }
expect(obj.grade).to eq :B
end
it 'must return :B+ if all great' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: false, bar: nil }
expect(obj.grade).to eq :'B+'
end
it 'must return :A if some best' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: false, bar: nil, baz: true }
expect(obj.grade).to eq :A
end
it 'must return :A+ if all best' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :'A+'
end
end
end
describe Grade do
describe '#compare' do
it 'must return correct order' do
expect(Grade.compare('A', 'B')).to be -1
expect(Grade.compare('A', 'A')).to be 0
expect(Grade.compare('B', 'A')).to be 1
expect(Grade.compare('A+', 'A')).to be -1
expect(Grade.compare('A+', 'A+')).to be 0
expect(Grade.compare('A', 'A+')).to be 1
expected = %i[A+ A B+ B C+ C D E F G V T X]
sorted = expected.shuffle
.sort &Grade.method(:compare)
expect(sorted).to eq expected
end
end
end
end

@ -1,94 +1,94 @@
module CryptCheck::Tls
describe Cert do
around :each do |example|
FakeTime.freeze(Time.utc 2000, 6, 1) { example.run }
end
describe '::trusted?' do
it 'must accept valid certificate' do
cert, *chain, ca = chain(%w(ecdsa-prime256v1 intermediate ca))
trust = Cert.trusted? cert, chain, roots: ca
expect(trust).to eq :trusted
end
it 'must reject self signed certificate' do
cert, ca = chain(%w(self-signed ca))
trust = Cert.trusted? cert, [], roots: ca
expect(trust).to eq 'self signed certificate'
# Case for SSLv2
cert, ca = chain(%w(self-signed ca))
trust = Cert.trusted? cert, nil, roots: ca
expect(trust).to eq 'self signed certificate'
end
it 'must reject unknown CA' do
cert, *chain = chain(%w(ecdsa-prime256v1 intermediate ca))
trust = Cert.trusted? cert, chain, roots: []
expect(trust).to eq 'unable to get issuer certificate'
end
it 'must reject missing intermediate chain' do
cert, ca = chain(%w(ecdsa-prime256v1 ca))
chain = []
trust = Cert.trusted? cert, chain, roots: ca
expect(trust).to eq 'unable to get local issuer certificate'
end
it 'must reject expired certificate' do
FakeTime.freeze Time.utc(2002, 1, 1) do
cert, *chain, ca = chain(%w(ecdsa-prime256v1 intermediate ca))
trust = Cert.trusted? cert, chain, roots: ca
expect(trust).to eq 'certificate has expired'
end
end
it 'must reject not yet valid certificate' do
FakeTime.freeze Time.utc(1999, 1, 1) do
cert, *chain, ca = chain(%w(ecdsa-prime256v1 intermediate ca))
trust = Cert.trusted? cert, chain, roots: ca
expect(trust).to eq 'certificate is not yet valid'
end
end
end
describe '#md5?' do
it 'must detect md5 certificate' do
cert = Cert.new cert(:md5)
expect(cert.md5?).to be true
cert = Cert.new cert(:sha1)
expect(cert.md5?).to be false
cert = Cert.new cert(:ecdsa, :prime256v1)
expect(cert.md5?).to be false
end
end
describe '#sha1?' do
it 'must detect sha1 certificate' do
cert = Cert.new cert(:md5)
expect(cert.sha1?).to be false
cert = Cert.new cert(:sha1)
expect(cert.sha1?).to be true
cert = Cert.new cert(:ecdsa, :prime256v1)
expect(cert.sha1?).to be false
end
end
describe '#sha2?' do
it 'must detect sha2 certificate' do
cert = Cert.new cert(:md5)
expect(cert.sha2?).to be false
cert = Cert.new cert(:sha1)
expect(cert.sha2?).to be false
cert = Cert.new cert(:ecdsa, :prime256v1)
expect(cert.sha2?).to be true
end
end
end
describe Cert do
around :each do |example|
Fake.freeze(Time.utc 2000, 6, 1) { example.run }
end
describe '::trusted?' do
it 'must accept valid certificate' do
cert, *chain, ca = chain(%w(ecdsa-prime256v1 intermediate ca))
trust = Cert.trusted? cert, chain, roots: ca
expect(trust).to eq :trusted
end
it 'must reject self signed certificate' do
cert, ca = chain(%w(self-signed ca))
trust = Cert.trusted? cert, [], roots: ca
expect(trust).to eq 'self signed certificate'
# Case for SSLv2
cert, ca = chain(%w(self-signed ca))
trust = Cert.trusted? cert, nil, roots: ca
expect(trust).to eq 'self signed certificate'
end
it 'must reject unknown CA' do
cert, *chain = chain(%w(ecdsa-prime256v1 intermediate ca))
trust = Cert.trusted? cert, chain, roots: []
expect(trust).to eq 'unable to get issuer certificate'
end
it 'must reject missing intermediate chain' do
cert, ca = chain(%w(ecdsa-prime256v1 ca))
chain = []
trust = Cert.trusted? cert, chain, roots: ca
expect(trust).to eq 'unable to get local issuer certificate'
end
it 'must reject expired certificate' do
Fake.freeze Time.utc(2002, 1, 1) do
cert, *chain, ca = chain(%w(ecdsa-prime256v1 intermediate ca))
trust = Cert.trusted? cert, chain, roots: ca
expect(trust).to eq 'certificate has expired'
end
end
it 'must reject not yet valid certificate' do
Fake.freeze Time.utc(1999, 1, 1) do
cert, *chain, ca = chain(%w(ecdsa-prime256v1 intermediate ca))
trust = Cert.trusted? cert, chain, roots: ca
expect(trust).to eq 'certificate is not yet valid'
end
end
end
describe '#md5?' do
it 'must detect md5 certificate' do
cert = Cert.new cert(:md5)
expect(cert.md5?).to be true
cert = Cert.new cert(:sha1)
expect(cert.md5?).to be false
cert = Cert.new cert(:ecdsa, :prime256v1)
expect(cert.md5?).to be false
end
end
describe '#sha1?' do
it 'must detect sha1 certificate' do
cert = Cert.new cert(:md5)
expect(cert.sha1?).to be false
cert = Cert.new cert(:sha1)
expect(cert.sha1?).to be true
cert = Cert.new cert(:ecdsa, :prime256v1)
expect(cert.sha1?).to be false
end
end
describe '#sha2?' do
it 'must detect sha2 certificate' do
cert = Cert.new cert(:md5)
expect(cert.sha2?).to be false
cert = Cert.new cert(:sha1)
expect(cert.sha2?).to be false
cert = Cert.new cert(:ecdsa, :prime256v1)
expect(cert.sha2?).to be true
end
end
end
end

@ -0,0 +1,149 @@
module CryptCheck
module Tls
describe Grade do
describe '#grade' do
def obj(trust: true, valid: true, **states)
Class.new do
def initialize(trust, valid, states)
@trust, @valid, @states = trust, valid, states
end
include Grade
def trusted?
@trust
end
def valid?
@valid
end
def states
State.empty.merge @states
end
end.new trust, valid, states
end
it 'must return :V if not valid' do
obj = obj valid: false, critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :V
end
it 'must return :T if not trusted' do
obj = obj trust: false, critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :T
end
it 'must return :G if critical' do
obj = obj critical: { foo: false, bar: nil, baz: true },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :G
end
it 'must return :F if error' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil, baz: true },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :F
end
it 'must return :E if warning' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil, baz: true },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :E
end
it 'must return :D if nor good nor bad' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: false, bar: nil },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :D
end
it 'must return :C if some good' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: false, bar: nil, baz: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :C
end
it 'must return :C+ if all good' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: false, bar: nil },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :'C+'
end
it 'must return :B if some great' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: false, bar: nil, baz: true },
best: { foo: true, bar: nil }
expect(obj.grade).to eq :B
end
it 'must return :B+ if all great' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: false, bar: nil }
expect(obj.grade).to eq :'B+'
end
it 'must return :A if some best' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: false, bar: nil, baz: true }
expect(obj.grade).to eq :A
end
it 'must return :A+ if all best' do
obj = obj critical: { foo: false, bar: nil },
error: { foo: false, bar: nil },
warning: { foo: false, bar: nil },
good: { foo: nil, bar: true },
great: { foo: nil, bar: true },
best: { foo: nil, bar: true }
expect(obj.grade).to eq :'A+'
end
end
end
end
end

@ -1,130 +1,98 @@
module CryptCheck::Tls
describe Host do
def host(*args, **kargs)
do_in_serv *args, **kargs do |host, port|
Host.new host, port
end
end
def servers(*args, **kargs)
host(*args, **kargs).servers
end
def error(*args, **kargs)
host(*args, **kargs).error
end
it 'return 1 grade with IPv4' do
servers = servers()
expect(servers.size).to be 1
expect_grade servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv4, Helpers::DEFAULT_PORT, :ipv4
end
it 'return 1 grade with IPv6' do
addresses = [Helpers::DEFAULT_IPv6]
allow(Addrinfo).to receive(:getaddrinfo).with(Helpers::DEFAULT_HOST, nil, nil, :STREAM) do
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
end
servers = servers(host: Helpers::DEFAULT_IPv6)
expect(servers.size).to be 1
expect_grade servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv6, Helpers::DEFAULT_PORT, :ipv6
end
it 'return 2 grades with hostname (IPv4 & IPv6)' do
addresses = [Helpers::DEFAULT_IPv4, Helpers::DEFAULT_IPv6]
allow(Addrinfo).to receive(:getaddrinfo).with(Helpers::DEFAULT_HOST, nil, nil, :STREAM) do
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
end
servers = servers(host: '::')
expect(servers.size).to be 2
expect_grade servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv4, Helpers::DEFAULT_PORT, :ipv4
expect_grade servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv6, Helpers::DEFAULT_PORT, :ipv6
end
it 'return error if DNS resolution problem' do
allow(Addrinfo).to receive(:getaddrinfo).with(Helpers::DEFAULT_HOST, nil, nil, :STREAM)
.and_raise SocketError, 'getaddrinfo: Name or service not known'
error = error()
expect_error error, ::SocketError, 'getaddrinfo: Name or service not known'
end
it 'return error if analysis too long' do
stub_const 'CryptCheck::Tls::Host::MAX_ANALYSIS_DURATION', 1
allow_any_instance_of(Host).to receive(:server) { sleep 2 }
servers = servers()
expect_grade_error servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv4, Helpers::DEFAULT_PORT,
'Too long analysis (max 1 second)'
end
it 'return error if unable to connect' do
addresses = [Helpers::DEFAULT_IPv4, Helpers::DEFAULT_IPv6]
allow(Addrinfo).to receive(:getaddrinfo).with(Helpers::DEFAULT_HOST, nil, nil, :STREAM) do
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
end
servers = servers(host: Helpers::DEFAULT_IPv6)
expect_grade_error servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv4, Helpers::DEFAULT_PORT,
'Connection refused - connect(2) for 127.0.0.1:15000'
expect_grade servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv6, Helpers::DEFAULT_PORT, :ipv6
end
it 'return error if TCP timeout' do
stub_const 'CryptCheck::Tls::Engine::TCP_TIMEOUT', 1
addresses = [Helpers::DEFAULT_IPv4, Helpers::DEFAULT_IPv6]
allow(Addrinfo).to receive(:getaddrinfo).with(Helpers::DEFAULT_HOST, nil, nil, :STREAM) do
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
end
original = IO.method :select
allow(IO).to receive(:select) do |*args, &block|
socket = [args[0]&.first, args[1]&.first].compact.first
next nil if socket.is_a?(Socket) && (socket.local_address.afamily == Socket::AF_INET)
original.call *args, &block
end
servers = servers(host: '::')
expect_grade_error servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv4, Helpers::DEFAULT_PORT,
'Timeout when connecting to 127.0.0.1:15000 (max 1 second)'
expect_grade servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv6, Helpers::DEFAULT_PORT, :ipv6
end
it 'return error if TLS timeout' do
stub_const 'CryptCheck::Tls::Engine::TLS_TIMEOUT', 1
addresses = [Helpers::DEFAULT_IPv4, Helpers::DEFAULT_IPv6]
allow(Addrinfo).to receive(:getaddrinfo).with(Helpers::DEFAULT_HOST, nil, nil, :STREAM) do
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
end
original = IO.method :select
allow(IO).to receive(:select) do |*args, &block|
socket = [args[0]&.first, args[1]&.first].compact.first
next nil if socket.is_a?(OpenSSL::SSL::SSLSocket) && (socket.io.local_address.afamily == Socket::AF_INET)
original.call *args, &block
end
servers = servers(host: '::')
expect_grade_error servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv4, Helpers::DEFAULT_PORT,
'Timeout when TLS connecting to 127.0.0.1:15000 (max 1 second)'
expect_grade servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv6, Helpers::DEFAULT_PORT, :ipv6
end
it 'return error if plain server' do
stub_const 'ENGINE::TLS_TIMEOUT', 1
addresses = [Helpers::DEFAULT_IPv4, Helpers::DEFAULT_IPv6]
allow(Addrinfo).to receive(:getaddrinfo).with(Helpers::DEFAULT_HOST, nil, nil, :STREAM) do
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
end
servers = plain_serv Helpers::DEFAULT_IPv4 do
servers(host: Helpers::DEFAULT_IPv6)
end
expect_grade_error servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv4, Helpers::DEFAULT_PORT,
'TLS seems not supported on this server'
expect_grade servers, Helpers::DEFAULT_HOST, Helpers::DEFAULT_IPv6, Helpers::DEFAULT_PORT, :ipv6
end
end
describe Host do
def host(*args, **kargs)
tls_serv(*args, **kargs) { |h, p| Host.new h, p }
end
def