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