More unit tests (HTTPS)
parent
a2c38b05b0
commit
5aa9a975fe
|
@ -87,7 +87,7 @@ module CryptCheck
|
|||
g.display
|
||||
[key, g]
|
||||
end
|
||||
rescue Exception => e
|
||||
rescue => e
|
||||
e = "Too long analysis (max #{MAX_ANALYSIS_DURATION.humanize})" if e.message == 'execution expired'
|
||||
Logger.error e
|
||||
[key, AnalysisFailure.new(e)]
|
||||
|
|
|
@ -41,8 +41,9 @@ module CryptCheck
|
|||
!@hsts.nil?
|
||||
end
|
||||
|
||||
LONG_HSTS = 6*30*24*60*60
|
||||
def hsts_long?
|
||||
hsts? and @hsts >= 6*30*24*60*60
|
||||
hsts? and @hsts >= LONG_HSTS
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,15 +9,18 @@ module CryptCheck
|
|||
SSL_TIMEOUT = 2*TCP_TIMEOUT
|
||||
EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2)
|
||||
SUPPORTED_METHODS = ::OpenSSL::SSL::SSLContext::METHODS
|
||||
class TLSException < ::Exception
|
||||
class TLSException < ::StandardError
|
||||
end
|
||||
class TLSNotAvailableException < TLSException
|
||||
def to_s
|
||||
'TLS seems not supported on this server'
|
||||
end
|
||||
end
|
||||
class MethodNotAvailable < TLSException
|
||||
end
|
||||
class CipherNotAvailable < TLSException
|
||||
end
|
||||
class Timeout < Exception
|
||||
class Timeout < ::StandardError
|
||||
end
|
||||
class TLSTimeout < Timeout
|
||||
end
|
||||
|
@ -112,8 +115,8 @@ module CryptCheck
|
|||
|
||||
private
|
||||
def name
|
||||
name = "#{@hostname || @ip}:#@port"
|
||||
name += " [#@ip]" if @hostname
|
||||
name = "#@ip:#@port"
|
||||
name += " [#@hostname]" if @hostname
|
||||
name
|
||||
end
|
||||
|
||||
|
@ -165,7 +168,7 @@ module CryptCheck
|
|||
/state=SSLv3 read server hello A: sslv3 alert handshake failure$/
|
||||
raise CipherNotAvailable, e
|
||||
end
|
||||
rescue => e
|
||||
rescue SystemCallError => e
|
||||
case e
|
||||
when /^Connection reset by peer$/
|
||||
raise MethodNotAvailable, e
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
describe CryptCheck::Tls::Https do
|
||||
def process
|
||||
proc do |socket|
|
||||
socket.print [
|
||||
'HTTP/1.1 200 OK',
|
||||
'Content-Type: text/plain',
|
||||
'Content-Length: 0',
|
||||
'Connection: close'
|
||||
].join "\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
def analyze(*args)
|
||||
CryptCheck::Tls::Https.analyze *args
|
||||
end
|
||||
|
||||
include_examples :analysis
|
||||
|
||||
describe '#hsts?' do
|
||||
it 'has no hsts' do
|
||||
grades = server host: '127.0.0.1', process: process do
|
||||
analyze '127.0.0.1', 5000
|
||||
end
|
||||
|
||||
_, server = expect_grade grades, '127.0.0.1', '127.0.0.1', 5000, :ipv4
|
||||
expect(server.hsts?).to be false
|
||||
end
|
||||
|
||||
it 'has hsts' do
|
||||
process = proc do |socket|
|
||||
socket.print [
|
||||
'HTTP/1.1 200 OK',
|
||||
'Strict-transport-security: max-age=31536000; includeSubdomains; preload',
|
||||
'Content-Type: text/plain',
|
||||
'Content-Length: 0',
|
||||
'Connection: close'
|
||||
].join "\r\n"
|
||||
end
|
||||
|
||||
grades = server host: '127.0.0.1', process: process do
|
||||
analyze '127.0.0.1', 5000
|
||||
end
|
||||
|
||||
_, server = expect_grade grades, '127.0.0.1', '127.0.0.1', 5000, :ipv4
|
||||
expect(server.hsts?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hsts_long?' do
|
||||
it 'has no hsts' do
|
||||
grades = server host: '127.0.0.1', process: process do
|
||||
analyze '127.0.0.1', 5000
|
||||
end
|
||||
|
||||
_, server = expect_grade grades, '127.0.0.1', '127.0.0.1', 5000, :ipv4
|
||||
expect(server.hsts_long?).to be false
|
||||
end
|
||||
|
||||
it 'has hsts but not long' do
|
||||
process = proc do |socket|
|
||||
socket.print [
|
||||
'HTTP/1.1 200 OK',
|
||||
"Strict-transport-security: max-age=#{CryptCheck::Tls::Https::Server::LONG_HSTS-1}; includeSubdomains; preload",
|
||||
'Content-Type: text/plain',
|
||||
'Content-Length: 0',
|
||||
'Connection: close'
|
||||
].join "\r\n"
|
||||
end
|
||||
|
||||
grades = server host: '127.0.0.1', process: process do
|
||||
analyze '127.0.0.1', 5000
|
||||
end
|
||||
|
||||
_, server = expect_grade grades, '127.0.0.1', '127.0.0.1', 5000, :ipv4
|
||||
expect(server.hsts_long?).to be false
|
||||
end
|
||||
|
||||
it 'has long hsts' do
|
||||
process = proc do |socket|
|
||||
socket.print [
|
||||
'HTTP/1.1 200 OK',
|
||||
"Strict-transport-security: max-age=#{CryptCheck::Tls::Https::Server::LONG_HSTS}; includeSubdomains; preload",
|
||||
'Content-Type: text/plain',
|
||||
'Content-Length: 0',
|
||||
'Connection: close'
|
||||
].join "\r\n"
|
||||
end
|
||||
|
||||
grades = server host: '127.0.0.1', process: process do
|
||||
analyze '127.0.0.1', 5000
|
||||
end
|
||||
|
||||
_, server = expect_grade grades, '127.0.0.1', '127.0.0.1', 5000, :ipv4
|
||||
expect(server.hsts_long?).to be true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,136 @@
|
|||
RSpec.shared_examples :analysis do
|
||||
describe '#analyze' do
|
||||
it 'return 1 grade with IPv4' do
|
||||
grades = server host: '127.0.0.1', process: process do
|
||||
analyze '127.0.0.1', 5000
|
||||
end
|
||||
|
||||
expect(grades.size).to be 1
|
||||
expect_grade grades, '127.0.0.1', '127.0.0.1', 5000, :ipv4
|
||||
end
|
||||
|
||||
it 'return 1 grade with IPv6' do
|
||||
grades = server host: '::1', process: process do
|
||||
analyze '::1', 5000
|
||||
end
|
||||
|
||||
expect(grades.size).to be 1
|
||||
expect_grade grades, '::1', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return 2 grades with hostname (IPv4 & IPv6)' do
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', nil, nil, :STREAM) do
|
||||
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
|
||||
end
|
||||
|
||||
grades = server host: '::', process: process do
|
||||
analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade grades, 'localhost', '127.0.0.1', 5000, :ipv4
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return error if DNS resolution problem' do
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', nil, nil, :STREAM)
|
||||
.and_raise SocketError, 'getaddrinfo: Name or service not known'
|
||||
|
||||
grades = server process: process do
|
||||
analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect(grades).to be_a CryptCheck::AnalysisFailure
|
||||
expect(grades.to_s).to eq 'Unable to resolve localhost'
|
||||
end
|
||||
|
||||
it 'return error if analysis too long' do
|
||||
stub_const 'CryptCheck::MAX_ANALYSIS_DURATION', 1
|
||||
allow(CryptCheck::Tls::Server).to receive(:new) { sleep 2 }
|
||||
|
||||
grades = server process: process do
|
||||
analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Too long analysis (max 1 second)'
|
||||
end
|
||||
|
||||
it 'return error if unable to connect' do
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', nil, nil, :STREAM) do
|
||||
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
|
||||
end
|
||||
|
||||
grades = server host: '::1', process: process do
|
||||
analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Connection refused - connect(2) for 127.0.0.1:5000'
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return error if TCP timeout' do
|
||||
stub_const 'CryptCheck::Tls::Server::TCP_TIMEOUT', 1
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', 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
|
||||
|
||||
grades = server host: '::', process: process do
|
||||
analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Timeout when connect to 127.0.0.1:5000 (max 1 second)'
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return error if TLS timeout' do
|
||||
stub_const 'CryptCheck::Tls::Server::SSL_TIMEOUT', 1
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', 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
|
||||
|
||||
grades = server host: '::', process: process do
|
||||
analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Timeout when TLS connect to 127.0.0.1:5000 (max 1 second)'
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return error if plain server' do
|
||||
stub_const 'CryptCheck::Tls::Server::SSL_TIMEOUT', 1
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', nil, nil, :STREAM) do
|
||||
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
|
||||
end
|
||||
|
||||
grades = plain_server host: '127.0.0.1', process: process do
|
||||
server host: '::1', process: process do
|
||||
analyze 'localhost', 5000
|
||||
end
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'TLS seems not supported on this server'
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,136 +1,10 @@
|
|||
describe CryptCheck::Tls do
|
||||
describe '#analyze' do
|
||||
it 'return 1 grade with IPv4' do
|
||||
grades = server(host: '127.0.0.1') do
|
||||
CryptCheck::Tls.analyze '127.0.0.1', 5000
|
||||
end
|
||||
|
||||
expect(grades.size).to be 1
|
||||
expect_grade grades, '127.0.0.1', '127.0.0.1', 5000, :ipv4
|
||||
end
|
||||
|
||||
it 'return 1 grade with IPv6' do
|
||||
grades = server(host: '::1') do
|
||||
CryptCheck::Tls.analyze '::1', 5000
|
||||
end
|
||||
|
||||
expect(grades.size).to be 1
|
||||
expect_grade grades, '::1', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return 2 grades with hostname (IPv4 & IPv6)' do
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', nil, nil, :STREAM) do
|
||||
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
|
||||
end
|
||||
|
||||
grades = server(host: '::') do
|
||||
CryptCheck::Tls.analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade grades, 'localhost', '127.0.0.1', 5000, :ipv4
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return error if DNS resolution problem' do
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', nil, nil, :STREAM)
|
||||
.and_raise SocketError, 'getaddrinfo: Name or service not known'
|
||||
|
||||
grades = server do
|
||||
CryptCheck::Tls.analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect(grades).to be_a CryptCheck::AnalysisFailure
|
||||
expect(grades.to_s).to eq 'Unable to resolve localhost'
|
||||
end
|
||||
|
||||
it 'return error if analysis too long' do
|
||||
stub_const 'CryptCheck::MAX_ANALYSIS_DURATION', 1
|
||||
allow(CryptCheck::Tls::Server).to receive(:new) { sleep 2 }
|
||||
|
||||
grades = server do
|
||||
CryptCheck::Tls.analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Too long analysis (max 1 second)'
|
||||
end
|
||||
|
||||
it 'return error if unable to connect' do
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', nil, nil, :STREAM) do
|
||||
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
|
||||
end
|
||||
|
||||
grades = server(host: '::1') do
|
||||
CryptCheck::Tls.analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Connection refused - connect(2) for 127.0.0.1:5000'
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return error if TCP timeout' do
|
||||
stub_const 'CryptCheck::Tls::Server::TCP_TIMEOUT', 1
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', 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
|
||||
|
||||
grades = server(host: '::') do
|
||||
CryptCheck::Tls.analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Timeout when connect to 127.0.0.1:5000 (max 1 second)'
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return error if TLS timeout' do
|
||||
stub_const 'CryptCheck::Tls::Server::SSL_TIMEOUT', 1
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', 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
|
||||
|
||||
grades = server(host: '::') do
|
||||
CryptCheck::Tls.analyze 'localhost', 5000
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Timeout when TLS connect to 127.0.0.1:5000 (max 1 second)'
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
|
||||
it 'return error if plain server' do
|
||||
stub_const 'CryptCheck::Tls::Server::SSL_TIMEOUT', 1
|
||||
addresses = %w(127.0.0.1 ::1)
|
||||
allow(Addrinfo).to receive(:getaddrinfo).with('localhost', nil, nil, :STREAM) do
|
||||
addresses.collect { |a| Addrinfo.new Socket.sockaddr_in(nil, a) }
|
||||
end
|
||||
|
||||
grades = plain_server(host: '127.0.0.1') do
|
||||
server(host: '::1') do
|
||||
CryptCheck::Tls.analyze 'localhost', 5000
|
||||
end
|
||||
end
|
||||
|
||||
expect_grade_error grades, 'localhost', '127.0.0.1', 5000,
|
||||
'Timeout when TLS connect to 127.0.0.1:5000 (max 1 second)'
|
||||
expect_grade grades, 'localhost', '::1', 5000, :ipv6
|
||||
end
|
||||
def process
|
||||
end
|
||||
|
||||
def analyze(*args)
|
||||
CryptCheck::Tls.analyze *args
|
||||
end
|
||||
|
||||
include_examples :analysis
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ $:.unshift File.expand_path File.join File.dirname(__FILE__), '../lib'
|
|||
require 'rubygems'
|
||||
require 'bundler/setup'
|
||||
require 'cryptcheck'
|
||||
Dir['./spec/**/support/**/*.rb'].sort.each { |f| require f }
|
||||
|
||||
CryptCheck::Logger.level = ENV['LOG'] || :none
|
||||
|
||||
|
@ -54,7 +55,8 @@ module Helpers
|
|||
def server(key: 'rsa-1024', domain: 'localhost', # Key & certificate
|
||||
host: '127.0.0.1', port: 5000, # Binding
|
||||
version: :TLSv1_2, ciphers: 'AES128-SHA', # TLS version and ciphers
|
||||
dh: 1024, ecdh: 'secp256r1') # DHE & ECDHE
|
||||
dh: 1024, ecdh: 'secp256r1', # DHE & ECDHE
|
||||
process: nil)
|
||||
key = key key
|
||||
cert = certificate key, domain
|
||||
|
||||
|
@ -75,7 +77,7 @@ module Helpers
|
|||
IO.pipe do |stop_pipe_r, stop_pipe_w|
|
||||
threads = []
|
||||
|
||||
mutex = Mutex.new
|
||||
mutex = Mutex.new
|
||||
started = ConditionVariable.new
|
||||
|
||||
threads << Thread.start do
|
||||
|
@ -88,7 +90,12 @@ module Helpers
|
|||
readable, = IO.select [ssl_server, stop_pipe_r]
|
||||
break if readable.include? stop_pipe_r
|
||||
begin
|
||||
ssl_server.accept
|
||||
socket = ssl_server.accept
|
||||
begin
|
||||
process.call socket if process
|
||||
ensure
|
||||
socket.close
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
@ -106,11 +113,11 @@ module Helpers
|
|||
end
|
||||
end
|
||||
|
||||
def plain_server(host: '127.0.0.1', port: 5000)
|
||||
def plain_server(host: '127.0.0.1', port: 5000, process: nil)
|
||||
IO.pipe do |stop_pipe_r, stop_pipe_w|
|
||||
threads = []
|
||||
|
||||
mutex = Mutex.new
|
||||
mutex = Mutex.new
|
||||
started = ConditionVariable.new
|
||||
|
||||
threads << Thread.start do
|
||||
|
@ -120,8 +127,14 @@ module Helpers
|
|||
loop do
|
||||
readable, = IO.select [tcp_server, stop_pipe_r]
|
||||
break if readable.include? stop_pipe_r
|
||||
|
||||
begin
|
||||
tcp_server.accept
|
||||
socket = tcp_server.accept
|
||||
begin
|
||||
process.call socket if process
|
||||
ensure
|
||||
socket.close
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
@ -138,16 +151,25 @@ module Helpers
|
|||
end
|
||||
end
|
||||
|
||||
def grade(grades, host, ip, port)
|
||||
grades[[host, ip, port]]
|
||||
end
|
||||
|
||||
def expect_grade(grades, host, ip, port, family)
|
||||
server = grades[[host, ip, port]].server
|
||||
grade = grade grades, host, ip, port
|
||||
expect(grade).to_not be nil
|
||||
server = grade.server
|
||||
expect(server).to be_a CryptCheck::Tls::Server
|
||||
expect(server.hostname).to eq host
|
||||
expect(server.ip).to eq ip
|
||||
expect(server.port).to eq port
|
||||
expect(server.family).to eq case family
|
||||
when :ipv4 then Socket::AF_INET
|
||||
when :ipv6 then Socket::AF_INET6
|
||||
when :ipv4 then
|
||||
Socket::AF_INET
|
||||
when :ipv6 then
|
||||
Socket::AF_INET6
|
||||
end
|
||||
[grade, server]
|
||||
end
|
||||
|
||||
def expect_grade_error(grades, host, ip, port, error)
|
||||
|
|
Loading…
Reference in New Issue