Unit tests
parent
eb82f8e1ae
commit
a2c38b05b0
|
@ -4,7 +4,6 @@ Gemfile.lock
|
|||
.rakeTasks
|
||||
.idea/
|
||||
/html/
|
||||
.rspec
|
||||
/rakefile
|
||||
/output/*.html
|
||||
/db/*.sqlite3
|
||||
|
|
|
@ -5,9 +5,21 @@ require 'yaml'
|
|||
require 'cryptcheck/tls/fixture'
|
||||
|
||||
module CryptCheck
|
||||
MAX_ANALYSIS_DURATION = 600
|
||||
MAX_ANALYSIS_DURATION = 120
|
||||
PARALLEL_ANALYSIS = 10
|
||||
|
||||
class AnalysisFailure
|
||||
attr_reader :error
|
||||
|
||||
def initialize(error)
|
||||
@error = error
|
||||
end
|
||||
|
||||
def to_s
|
||||
@error.to_s
|
||||
end
|
||||
end
|
||||
|
||||
autoload :Logger, 'cryptcheck/logger'
|
||||
autoload :Tls, 'cryptcheck/tls'
|
||||
module Tls
|
||||
|
@ -50,38 +62,54 @@ module CryptCheck
|
|||
def self.addresses(host)
|
||||
begin
|
||||
ip = IPAddr.new host
|
||||
[[ip.family, ip.to_s, nil]]
|
||||
return [[ip.family, ip.to_s, nil]]
|
||||
rescue IPAddr::InvalidAddressError
|
||||
end
|
||||
::Addrinfo.getaddrinfo(host, nil, nil, :STREAM)
|
||||
.collect { |a| [a.afamily, a.ip_address, host] }
|
||||
end
|
||||
|
||||
def self.analyze_addresses(host, addresses, port, server, grade, *args, **kargs)
|
||||
first = true
|
||||
addresses.collect do |family, ip|
|
||||
first ? (first = false) : Logger.info { '' }
|
||||
key = [host, ip, port]
|
||||
a = [host, family, ip, port, *args]
|
||||
begin
|
||||
::Addrinfo.getaddrinfo(host, nil, nil, :STREAM)
|
||||
.collect { |a| [a.afamily, a.ip_address, host] }
|
||||
rescue ::SocketError => e
|
||||
Logger.error { "Unable to resolv #{host} : #{e}" }
|
||||
[]
|
||||
::Timeout::timeout MAX_ANALYSIS_DURATION do
|
||||
s = if kargs.empty?
|
||||
server.new *a
|
||||
else
|
||||
server.new *a, **kargs
|
||||
end
|
||||
g = grade.new s
|
||||
Logger.info { '' }
|
||||
g.display
|
||||
[key, g]
|
||||
end
|
||||
rescue Exception => e
|
||||
e = "Too long analysis (max #{MAX_ANALYSIS_DURATION.humanize})" if e.message == 'execution expired'
|
||||
Logger.error e
|
||||
[key, AnalysisFailure.new(e)]
|
||||
end
|
||||
end
|
||||
end.to_h
|
||||
end
|
||||
|
||||
def self.analyze_addresses(host, addresses, port, on_error = TLS_NOT_AVAILABLE, &block)
|
||||
begin
|
||||
::Timeout::timeout MAX_ANALYSIS_DURATION do
|
||||
addresses.each { |family, ip, host| return block.call family, ip, host }
|
||||
end
|
||||
rescue ::Exception => e
|
||||
Logger.error e
|
||||
def self.analyze(host, port, server, grade, *args, **kargs)
|
||||
addresses = begin
|
||||
addresses host
|
||||
rescue ::SocketError => e
|
||||
Logger::error e
|
||||
return AnalysisFailure.new "Unable to resolve #{host}"
|
||||
end
|
||||
on_error.call host, port
|
||||
end
|
||||
|
||||
def self.analyze(host, port, on_error = Tls::TLS_NOT_AVAILABLE, &block)
|
||||
analyze_addresses host, addresses(host), port, on_error, &block
|
||||
analyze_addresses host, addresses, port, server, grade, *args, **kargs
|
||||
end
|
||||
|
||||
def self.analyze_hosts(hosts, template, output, groups: nil, &block)
|
||||
results = {}
|
||||
semaphore = ::Mutex.new
|
||||
::Parallel.each hosts, progress: 'Analysing', in_threads: PARALLEL_ANALYSIS, finish: lambda { |item, _, _| puts item[1] } do |description, host|
|
||||
#hosts.each do |description, host|
|
||||
#hosts.each do |description, host|
|
||||
result = block.call host.strip
|
||||
semaphore.synchronize do
|
||||
if results.include? description
|
||||
|
|
|
@ -2,14 +2,6 @@ require 'socket'
|
|||
|
||||
module CryptCheck
|
||||
module Ssh
|
||||
class SshNotSupportedServer
|
||||
attr_reader :host, :port
|
||||
|
||||
def initialize(host, port)
|
||||
@host, @port = host, port
|
||||
end
|
||||
end
|
||||
|
||||
class Server
|
||||
TCP_TIMEOUT = 10
|
||||
class SshNotAvailableException < Exception
|
||||
|
|
|
@ -3,18 +3,8 @@ require 'parallel'
|
|||
|
||||
module CryptCheck
|
||||
module Tls
|
||||
TLS_NOT_AVAILABLE = Proc.new { |host, port|
|
||||
TlsNotSupportedGrade.new TlsNotSupportedServer.new host, port
|
||||
}
|
||||
|
||||
def self.analyze(host, port)
|
||||
::CryptCheck.analyze host, port do |family, ip, host|
|
||||
s = TcpServer.new family, ip, port, hostname: host
|
||||
g = Grade.new s
|
||||
Logger.info { '' }
|
||||
g.display
|
||||
g
|
||||
end
|
||||
::CryptCheck.analyze host, port, TcpServer, Grade
|
||||
end
|
||||
|
||||
def self.colorize(cipher)
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
require 'openssl'
|
||||
|
||||
class Integer
|
||||
def humanize
|
||||
secs = self
|
||||
[[60, :second], [60, :minute], [24, :hour], [30, :day], [12, :month]].map { |count, name|
|
||||
if secs > 0
|
||||
secs, n = self.divmod count
|
||||
n = n.to_i
|
||||
"#{n} #{name}#{n > 1 ? 's' : ''}"
|
||||
end
|
||||
}.compact.reverse.join(' ')
|
||||
end
|
||||
end
|
||||
|
||||
class ::OpenSSL::PKey::EC
|
||||
def type
|
||||
:ecc
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
module CryptCheck
|
||||
module Tls
|
||||
class TlsNotSupportedGrade
|
||||
attr_reader :server, :score, :grade
|
||||
|
||||
def initialize(server)
|
||||
@server, @score, @grade = server, -1, 'X'
|
||||
end
|
||||
end
|
||||
|
||||
class Grade
|
||||
attr_reader :server, :protocol_score, :key_exchange_score, :cipher_strengths_score, :score, :grade, :error, :danger, :warning, :success
|
||||
|
||||
|
|
|
@ -2,13 +2,7 @@ module CryptCheck
|
|||
module Tls
|
||||
module Https
|
||||
def self.analyze(host, port=443)
|
||||
::CryptCheck.analyze host, port do |family, ip, host|
|
||||
s = Server.new family, ip, port, hostname: host
|
||||
g = Grade.new s
|
||||
Logger.info { '' }
|
||||
g.display
|
||||
g
|
||||
end
|
||||
::CryptCheck.analyze host, port, Server, Grade
|
||||
end
|
||||
|
||||
def self.analyze_file(input, output)
|
||||
|
|
|
@ -6,7 +6,7 @@ module CryptCheck
|
|||
class Server < Tls::TcpServer
|
||||
attr_reader :hsts
|
||||
|
||||
def initialize(family, ip, port = 443, hostname: nil)
|
||||
def initialize(hostname, family, ip, port=443)
|
||||
super
|
||||
fetch_hsts
|
||||
end
|
||||
|
|
|
@ -4,14 +4,6 @@ require 'httparty'
|
|||
|
||||
module CryptCheck
|
||||
module Tls
|
||||
class TlsNotSupportedServer
|
||||
attr_reader :hostname, :port
|
||||
|
||||
def initialize(host, port)
|
||||
@hostname, @port = host, port
|
||||
end
|
||||
end
|
||||
|
||||
class Server
|
||||
TCP_TIMEOUT = 10
|
||||
SSL_TIMEOUT = 2*TCP_TIMEOUT
|
||||
|
@ -25,18 +17,18 @@ module CryptCheck
|
|||
end
|
||||
class CipherNotAvailable < TLSException
|
||||
end
|
||||
class Timeout < TLSException
|
||||
class Timeout < Exception
|
||||
end
|
||||
class TLSTimeout < TLSException
|
||||
class TLSTimeout < Timeout
|
||||
end
|
||||
class ConnectionError < TLSException
|
||||
class ConnectionError < Exception
|
||||
end
|
||||
|
||||
attr_reader :family, :ip, :port, :hostname, :prefered_ciphers, :cert, :cert_valid, :cert_trusted, :dh
|
||||
|
||||
def initialize(family, ip, port, hostname: nil)
|
||||
@family, @ip, @port, @hostname = family, ip, port, hostname
|
||||
@dh = []
|
||||
def initialize(hostname, family, ip, port)
|
||||
@hostname, @family, @ip, @port = hostname, family, ip, port
|
||||
@dh = []
|
||||
Logger.info { name.colorize :blue }
|
||||
extract_cert
|
||||
Logger.info { '' }
|
||||
|
@ -128,27 +120,21 @@ module CryptCheck
|
|||
def connect(&block)
|
||||
socket = ::Socket.new @family, sock_type
|
||||
sockaddr = ::Socket.sockaddr_in @port, @ip
|
||||
Logger.trace { "Connecting to #{name}" }
|
||||
Logger.trace { "Connecting to #{@ip}:#{@port}" }
|
||||
begin
|
||||
status = socket.connect_nonblock sockaddr
|
||||
Logger.trace { "Connecting to #{name} status : #{status}" }
|
||||
Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" }
|
||||
raise ConnectionError, status unless status == 0
|
||||
Logger.trace { "Connected to #{name}" }
|
||||
Logger.trace { "Connected to #{@ip}:#{@port}" }
|
||||
block_given? ? block.call(socket) : nil
|
||||
rescue ::IO::WaitReadable
|
||||
Logger.trace { "Waiting for read to #{name}" }
|
||||
raise Timeout unless IO.select [socket], nil, nil, TCP_TIMEOUT
|
||||
Logger.trace { "Waiting for read to #{@ip}:#{@port}" }
|
||||
raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT
|
||||
retry
|
||||
rescue ::IO::WaitWritable
|
||||
Logger.trace { "Waiting for write to #{name}" }
|
||||
raise Timeout unless IO.select nil, [socket], nil, TCP_TIMEOUT
|
||||
Logger.trace { "Waiting for write to #{@ip}:#{@port}" }
|
||||
raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select nil, [socket], nil, TCP_TIMEOUT
|
||||
retry
|
||||
rescue => e
|
||||
case e.message
|
||||
when /^Connection refused/
|
||||
raise TLSNotAvailableException, e
|
||||
end
|
||||
raise
|
||||
ensure
|
||||
socket.close
|
||||
end
|
||||
|
@ -164,14 +150,14 @@ module CryptCheck
|
|||
return block_given? ? block.call(ssl_socket) : nil
|
||||
rescue ::IO::WaitReadable
|
||||
Logger.trace { "Waiting for SSL read to #{name}" }
|
||||
raise TLSTimeout unless IO.select [socket], nil, nil, SSL_TIMEOUT
|
||||
raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT
|
||||
retry
|
||||
rescue ::IO::WaitWritable
|
||||
Logger.trace { "Waiting for SSL write to #{name}" }
|
||||
raise TLSTimeout unless IO.select nil, [socket], nil, SSL_TIMEOUT
|
||||
raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT
|
||||
retry
|
||||
rescue ::OpenSSL::SSL::SSLError => e
|
||||
case e.message
|
||||
case e
|
||||
when /state=SSLv2 read server hello A$/,
|
||||
/state=SSLv3 read server hello A: wrong version number$/
|
||||
raise MethodNotAvailable, e
|
||||
|
@ -179,13 +165,11 @@ module CryptCheck
|
|||
/state=SSLv3 read server hello A: sslv3 alert handshake failure$/
|
||||
raise CipherNotAvailable, e
|
||||
end
|
||||
raise TLSException, e
|
||||
rescue => e
|
||||
case e.message
|
||||
case e
|
||||
when /^Connection reset by peer$/
|
||||
raise MethodNotAvailable, e
|
||||
end
|
||||
raise TLSException, e
|
||||
ensure
|
||||
ssl_socket.close
|
||||
end
|
||||
|
@ -248,7 +232,7 @@ module CryptCheck
|
|||
dh = ssl_client method, [cipher] { |s| s.tmp_key }
|
||||
@dh << dh if dh
|
||||
cipher = Cipher.new method, cipher, dh
|
||||
dh = dh ? " (#{'DH'.colorize :green} : #{Tls.key_to_s dh})" : ''
|
||||
dh = dh ? " (#{'DH'.colorize :green} : #{Tls.key_to_s dh})" : ''
|
||||
Logger.info { "#{Tls.colorize method} / #{cipher.colorize} : Supported#{dh}" }
|
||||
cipher
|
||||
rescue TLSException => e
|
||||
|
|
|
@ -2,20 +2,13 @@ module CryptCheck
|
|||
module Tls
|
||||
module Smtp
|
||||
def self.analyze(host, port=25, domain: nil)
|
||||
domain ||= host
|
||||
::CryptCheck.analyze host, port do |family, ip, host|
|
||||
s = Server.new family, ip, port, hostname: host, domain: domain
|
||||
g = Grade.new s
|
||||
Logger.info { '' }
|
||||
g.display
|
||||
g
|
||||
end
|
||||
::CryptCheck.analyze host, port, Server, Grade, domain: domain
|
||||
end
|
||||
|
||||
def self.analyze_domain(domain)
|
||||
srv = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).sort_by(&:preference).first
|
||||
hostname = srv ? srv.exchange.to_s : domain
|
||||
self.analyze hostname, domain: domain
|
||||
srv = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).sort_by &:preference
|
||||
hosts = srv.empty? ? [domain] : srv.collect { |s| s.exchange.to_s }
|
||||
hosts.collect { |h| self.analyze h, domain: domain }.flatten(1)
|
||||
end
|
||||
|
||||
def self.analyze_file(input, output)
|
||||
|
|
|
@ -4,9 +4,9 @@ module CryptCheck
|
|||
class Server < Tls::TcpServer
|
||||
attr_reader :domain
|
||||
|
||||
def initialize(family, ip, port, hostname: nil, domain:)
|
||||
def initialize(hostname, family, ip, port, domain:)
|
||||
@domain = domain
|
||||
super family, ip, port, hostname: hostname
|
||||
super
|
||||
end
|
||||
|
||||
def ssl_connect(socket, context, method, &block)
|
||||
|
|
|
@ -8,7 +8,7 @@ module CryptCheck
|
|||
class Server < Tls::TcpServer
|
||||
attr_reader :domain
|
||||
|
||||
def initialize(family, ip, port=nil, hostname: nil, domain: nil, type: :s2s)
|
||||
def initialize(hostname, family, ip, port=nil, domain: nil, type: :s2s)
|
||||
domain ||= hostname
|
||||
@type, @domain = type, domain
|
||||
port = case type
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
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
|
||||
end
|
||||
end
|
|
@ -0,0 +1,162 @@
|
|||
$:.unshift File.expand_path File.join File.dirname(__FILE__), '../lib'
|
||||
require 'rubygems'
|
||||
require 'bundler/setup'
|
||||
require 'cryptcheck'
|
||||
|
||||
CryptCheck::Logger.level = ENV['LOG'] || :none
|
||||
|
||||
module Helpers
|
||||
OpenSSL::PKey::EC.send :alias_method, :private?, :private_key?
|
||||
|
||||
def key(name)
|
||||
open(File.join(File.dirname(__FILE__), 'resources', "#{name}.pem"), 'r') { |f| OpenSSL::PKey.read f }
|
||||
end
|
||||
|
||||
def dh(name)
|
||||
open(File.join(File.dirname(__FILE__), 'resources', "dh-#{name}.pem"), 'r') { |f| OpenSSL::PKey::DH.new f }
|
||||
end
|
||||
|
||||
def certificate(key, domain)
|
||||
cert = OpenSSL::X509::Certificate.new
|
||||
cert.version = 2
|
||||
cert.serial = rand 2**(20*8-1) .. 2**(20*8)
|
||||
cert.not_before = Time.now
|
||||
cert.not_after = cert.not_before + 60*60
|
||||
|
||||
cert.public_key = case key
|
||||
when OpenSSL::PKey::EC
|
||||
curve = key.group.curve_name
|
||||
public = OpenSSL::PKey::EC.new curve
|
||||
public.public_key = key.public_key
|
||||
public
|
||||
else
|
||||
key.public_key
|
||||
end
|
||||
|
||||
name = OpenSSL::X509::Name.parse "CN=#{domain}"
|
||||
cert.subject = name
|
||||
cert.issuer = name
|
||||
|
||||
extension_factory = OpenSSL::X509::ExtensionFactory.new nil, cert
|
||||
extension_factory.subject_certificate = cert
|
||||
extension_factory.issuer_certificate = cert
|
||||
|
||||
cert.add_extension extension_factory.create_extension 'basicConstraints', 'CA:TRUE', true
|
||||
cert.add_extension extension_factory.create_extension 'keyUsage', 'keyEncipherment, dataEncipherment, digitalSignature,nonRepudiation,keyCertSign'
|
||||
cert.add_extension extension_factory.create_extension 'extendedKeyUsage', 'serverAuth, clientAuth'
|
||||
cert.add_extension extension_factory.create_extension 'subjectKeyIdentifier', 'hash'
|
||||
cert.add_extension extension_factory.create_extension 'authorityKeyIdentifier', 'keyid:always'
|
||||
cert.add_extension extension_factory.create_extension 'subjectAltName', "DNS:#{domain}"
|
||||
|
||||
cert.sign key, OpenSSL::Digest::SHA512.new
|
||||
end
|
||||
|
||||
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
|
||||
key = key key
|
||||
cert = certificate key, domain
|
||||
|
||||
context = OpenSSL::SSL::SSLContext.new version
|
||||
context.cert = cert
|
||||
context.key = key
|
||||
context.ciphers = ciphers
|
||||
|
||||
if dh
|
||||
dh = dh dh
|
||||
context.tmp_dh_callback = proc { dh }
|
||||
end
|
||||
if ecdh
|
||||
ecdh = key ecdh
|
||||
context.tmp_ecdh_callback = proc { ecdh }
|
||||
end
|
||||
|
||||
IO.pipe do |stop_pipe_r, stop_pipe_w|
|
||||
threads = []
|
||||
|
||||
mutex = Mutex.new
|
||||
started = ConditionVariable.new
|
||||
|
||||
threads << Thread.start do
|
||||
tcp_server = TCPServer.new host, port
|
||||
ssl_server = OpenSSL::SSL::SSLServer.new tcp_server, context
|
||||
|
||||
mutex.synchronize { started.signal }
|
||||
|
||||
loop do
|
||||
readable, = IO.select [ssl_server, stop_pipe_r]
|
||||
break if readable.include? stop_pipe_r
|
||||
begin
|
||||
ssl_server.accept
|
||||
rescue
|
||||
end
|
||||
end
|
||||
ssl_server.close
|
||||
tcp_server.close
|
||||
end
|
||||
|
||||
mutex.synchronize { started.wait mutex }
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
stop_pipe_w.close
|
||||
threads.each &:join
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def plain_server(host: '127.0.0.1', port: 5000)
|
||||
IO.pipe do |stop_pipe_r, stop_pipe_w|
|
||||
threads = []
|
||||
|
||||
mutex = Mutex.new
|
||||
started = ConditionVariable.new
|
||||
|
||||
threads << Thread.start do
|
||||
tcp_server = TCPServer.new host, port
|
||||
mutex.synchronize { started.signal }
|
||||
|
||||
loop do
|
||||
readable, = IO.select [tcp_server, stop_pipe_r]
|
||||
break if readable.include? stop_pipe_r
|
||||
begin
|
||||
tcp_server.accept
|
||||
rescue
|
||||
end
|
||||
end
|
||||
tcp_server.close
|
||||
end
|
||||
|
||||
mutex.synchronize { started.wait mutex }
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
stop_pipe_w.close
|
||||
threads.each &:join
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expect_grade(grades, host, ip, port, family)
|
||||
server = grades[[host, ip, port]].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
|
||||
end
|
||||
end
|
||||
|
||||
def expect_grade_error(grades, host, ip, port, error)
|
||||
server = grades[[host, ip, port]]
|
||||
expect(server).to be_a CryptCheck::AnalysisFailure
|
||||
expect(server.to_s).to eq error
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |c|
|
||||
c.include Helpers
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN DH PARAMETERS-----
|
||||
MIGHAoGBANBrEWPccAuXK3fq8VtE1KeDmY1vk1dJ94ht8UPEqJTdsQEtAS2g7UKm
|
||||
s49RRb7mG4JOVGWy1FWi32FrZTDUxInOP7k0wz8oqQSNBJWeAZjzETd1bjYutoSx
|
||||
F1DMUlw650faSS2dSXalOXyRfY+2dqR9sa7FQNlOztYFCrtwXMb7AgEC
|
||||
-----END DH PARAMETERS-----
|
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQC0xBo+MgjnYqvszjUpslonvcQVI1TG7yxlGCWqpvN0a3zdgBpV
|
||||
lpXv7q/821jUtlLc2BhNohRXuoejc2oiG7IOv7Md204NnoTQbxLo6gehnMyo86il
|
||||
Q7KNAAW4tam79xNgOfdkkV0d80AfG148j+N6jDZCOoZ3dFwH4a6vcSWRgQIDAQAB
|
||||
AoGBAJ7j+MVOqbDpdIG0R9qc4M4p6Z9C7RPny7gY35L/KOPeT2VLYtp0gNrjjWHP
|
||||
VGe002U3tTUYEJWEahFsM5BDk+ASqyzesPD5lWzi6QSO3cIkvNSYLdBezNprcPk4
|
||||
PEy1pX+IXrRFeDXE/wncovuYP2STF18SSP7YgCMBAAwgeZAhAkEA7xLuNz6Qt7HK
|
||||
euShzsvmzNUIaoBXa9qiOWoIb7aHa/uK87SwXpy6iV85TdWowD34JPnPiRx6FSPk
|
||||
4rOXYBq0lQJBAMGQYF/ItKUGYnwj7z2Q7N3/Pz5fTyoqzQI7Nza8aCEabFNzAdMv
|
||||
nZ2ROyWC/qXZ1osgPuwTBBfu9ty7GH2p4j0CQQCk+jJLCzDAor7waV/Dne+qQAQr
|
||||
wl8RfXFfH22s8Y+oE5CCtpjS4WLUM1MPBDcMWncnxP/TRUR13CwxyO7YEfW1AkBv
|
||||
VRqJnUiB7sUwv/54O+Zx3cFDn9BJ4apfES411nJSL/+ElA7FqIqQuZr6fXj4be5f
|
||||
wWFPqbReC72Dwj1Y8iDFAkAXpo8CtvqtxQYdbIh0Jmdj2xHWppkbBs9dT/qVAOdO
|
||||
RIA5UKKyyweZc+6ZFbAMeouhHGljcL73zOZt5V4YloT7
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN EC PARAMETERS-----
|
||||
BggqhkjOPQMBBw==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIIg6KuMLLVhcR7IIU+joH9npRN5eVYfBQo6pRL56xUCuoAoGCCqGSM49
|
||||
AwEHoUQDQgAE+h78G/a32+1ICT/euHP0Z5INER9Rh1nJNyn0HUSR0yWCistpoX1K
|
||||
yCHpVwb0SAqB/6WrwOFKnrKIdI/HX1edGQ==
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -1,91 +0,0 @@
|
|||
require 'webmock/rspec'
|
||||
|
||||
# This file was generated by the `rspec --init` command. Conventionally, all
|
||||
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
||||
# The generated `.rspec` file contains `--require spec_helper` which will cause this
|
||||
# file to always be loaded, without a need to explicitly require it in any files.
|
||||
#
|
||||
# Given that it is always loaded, you are encouraged to keep this file as
|
||||
# light-weight as possible. Requiring heavyweight dependencies from this file
|
||||
# will add to the boot time of your test suite on EVERY test run, even for an
|
||||
# individual file that may not need all of that loaded. Instead, consider making
|
||||
# a separate helper file that requires the additional dependencies and performs
|
||||
# the additional setup, and require it from the spec files that actually need it.
|
||||
#
|
||||
# The `.rspec` file also contains a few flags that are not defaults but that
|
||||
# users commonly want.
|
||||
#
|
||||
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
||||
RSpec.configure do |config|
|
||||
# rspec-expectations config goes here. You can use an alternate
|
||||
# assertion/expectation library such as wrong or the stdlib/minitest
|
||||
# assertions if you prefer.
|
||||
config.expect_with :rspec do |expectations|
|
||||
# This option will default to `true` in RSpec 4. It makes the `description`
|
||||
# and `failure_message` of custom matchers include text for helper methods
|
||||
# defined using `chain`, e.g.:
|
||||
# be_bigger_than(2).and_smaller_than(4).description
|
||||
# # => "be bigger than 2 and smaller than 4"
|
||||
# ...rather than:
|
||||
# # => "be bigger than 2"
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
end
|
||||
|
||||
# rspec-mocks config goes here. You can use an alternate test double
|
||||
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
||||
config.mock_with :rspec do |mocks|
|
||||
# Prevents you from mocking or stubbing a method that does not exist on
|
||||
# a real object. This is generally recommended, and will default to
|
||||
# `true` in RSpec 4.
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
# The settings below are suggested to provide a good initial experience
|
||||
# with RSpec, but feel free to customize to your heart's content.
|
||||
=begin
|
||||
# These two settings work together to allow you to limit a spec run
|
||||
# to individual examples or groups you care about by tagging them with
|
||||
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
||||
# get run.
|
||||
config.filter_run :focus
|
||||
config.run_all_when_everything_filtered = true
|
||||
|
||||
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
||||
# For more details, see:
|
||||
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
||||
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
||||
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
||||
config.disable_monkey_patching!
|
||||
|
||||
# This setting enables warnings. It's recommended, but in some cases may
|
||||
# be too noisy due to issues in dependencies.
|
||||
config.warnings = true
|
||||
|
||||
# Many RSpec users commonly either run the entire suite or an individual
|
||||
# file, and it's useful to allow more verbose output when running an
|
||||
# individual spec file.
|
||||
if config.files_to_run.one?
|
||||
# Use the documentation formatter for detailed output,
|
||||
# unless a formatter has already been configured
|
||||
# (e.g. via a command-line flag).
|
||||
config.default_formatter = 'doc'
|
||||
end
|
||||
|
||||
# Print the 10 slowest examples and example groups at the
|
||||
# end of the spec run, to help surface which specs are running
|
||||
# particularly slow.
|
||||
config.profile_examples = 10
|
||||
|
||||
# Run specs in random order to surface order dependencies. If you find an
|
||||
# order dependency and want to debug it, you can fix the order by providing
|
||||
# the seed, which is printed after each run.
|
||||
# --seed 1234
|
||||
config.order = :random
|
||||
|
||||
# Seed global randomization in this process using the `--seed` CLI option.
|
||||
# Setting this allows you to use `--seed` to deterministically reproduce
|
||||
# test failures related to randomization by passing the same `--seed` value
|
||||
# as the one that triggered the failure.
|
||||
Kernel.srand config.seed
|
||||
=end
|
||||
end
|
Loading…
Reference in New Issue