parent
eb82f8e1ae
commit
a2c38b05b0
@ -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