19 changed files with 417 additions and 195 deletions
@ -0,0 +1,2 @@ |
|||
--color |
|||
--require helpers |
@ -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