More unit tests (HTTPS)

v1
Aeris 2016-05-06 19:59:13 +02:00
parent a2c38b05b0
commit 5aa9a975fe
7 changed files with 282 additions and 149 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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