@@ -1,5 +1,6 @@ | |||
source 'https://rubygems.org' | |||
gem 'rake' | |||
gem 'httparty' | |||
gem 'nokogiri' | |||
gem 'net-ssh', '>= 2.9.2.beta' | |||
@@ -8,9 +9,9 @@ gem 'tcp_timeout' | |||
gem 'parallel' | |||
gem 'ruby-progressbar' | |||
gem 'logging' | |||
gem 'activerecord' | |||
gem 'sqlite3' | |||
gem 'rake' | |||
#gem 'activerecord' | |||
#gem 'sqlite3' | |||
gem 'colorize' | |||
group :test do | |||
gem 'rspec' | |||
@@ -34,13 +34,15 @@ $(OPENSSL_DIR)/: | |||
$(OPENSSL_DIR)/Makefile: | $(OPENSSL_DIR)/ | |||
cd $(OPENSSL_DIR); ./config shared | |||
$(OPENSSL_DIR)/libssl.so.1.0.0 $(OPENSSL_DIR)/libcrypto.so.1.0.0: $(OPENSSL_DIR)/Makefile | |||
$(OPENSSL_DIR)/libssl.so $(OPENSSL_DIR)/libcrypto.so: $(OPENSSL_DIR)/Makefile | |||
$(MAKE) -C $(OPENSSL_DIR) depend build_libs | |||
lib/%.so: $(OPENSSL_DIR)/%.so | |||
cp $< $@ | |||
lib/%.so.1.0.0: $(OPENSSL_DIR)/%.so.1.0.0 | |||
cp $< $@ | |||
libs: lib/libssl.so.1.0.0 lib/libcrypto.so.1.0.0 | |||
libs: lib/libssl.so lib/libcrypto.so lib/libssl.so.1.0.0 lib/libcrypto.so.1.0.0 | |||
$(RUBY_DIR)/: | |||
wget http://cache.ruby-lang.org/pub/ruby/$(RUBY_MAJOR_VERSION)/$(RUBY_DIR).tar.gz | |||
@@ -2,18 +2,18 @@ | |||
$:.unshift File.expand_path File.join File.dirname(__FILE__), '../lib' | |||
require 'rubygems' | |||
require 'bundler/setup' | |||
require 'logging' | |||
require 'cryptcheck' | |||
name = ARGV[0] || 'index' | |||
file = ::File.join 'output', "#{name}.yml" | |||
if ::File.exist? file | |||
::CryptCheck::Logger.level = :none | |||
::CryptCheck::Tls::Https.analyze_from_file "output/#{name}.yml", "output/#{name}.html" | |||
else | |||
::Logging.logger.root.appenders = ::Logging.appenders.stdout | |||
::Logging.logger.root.level = :warn | |||
::CryptCheck::Logger.level = :info | |||
server = ::CryptCheck::Tls::Https::Server.new(ARGV[0], ARGV[1] || 443) | |||
p grade = ::CryptCheck::Tls::Https::Grade.new(server) | |||
grade = ::CryptCheck::Tls::Https::Grade.new server | |||
::CryptCheck::Logger.info { '' } | |||
grade.display | |||
end |
@@ -7,8 +7,7 @@ require 'cryptcheck' | |||
GROUP_NAME = 'Top 100 Alexa' | |||
::Logging.logger.root.appenders = ::Logging.appenders.stdout | |||
::Logging.logger.root.level = :error | |||
::CryptCheck::Logger.level = :none | |||
hosts = [] | |||
::File.open('top-1m.csv', 'r') do |file| | |||
@@ -6,14 +6,15 @@ require 'logging' | |||
require 'cryptcheck' | |||
name = ARGV[0] | |||
unless name | |||
::CryptCheck::Tls::Smtp.analyze_from_file 'output/smtp.yml', 'output/smtp.html' | |||
else | |||
::Logging.logger.root.appenders = ::Logging.appenders.stdout | |||
::Logging.logger.root.level = :warn | |||
if name | |||
::CryptCheck::Logger.level = :info | |||
server = ::CryptCheck::Tls::Smtp::Server.new(ARGV[0], ARGV[1] || 25) | |||
p grade = ::CryptCheck::Tls::Smtp::Grade.new(server) | |||
grade = ::CryptCheck::Tls::Smtp::Grade.new server | |||
::CryptCheck::Logger.info { '' } | |||
grade.display | |||
else | |||
::CryptCheck::Logger.level = :none | |||
::CryptCheck::Tls::Smtp.analyze_from_file 'output/smtp.yml', 'output/smtp.html' | |||
end | |||
@@ -7,12 +7,13 @@ require 'cryptcheck' | |||
name = ARGV[0] | |||
if name | |||
::Logging.logger.root.appenders = ::Logging.appenders.stdout | |||
::Logging.logger.root.level = :warn | |||
::CryptCheck::Logger.level = :info | |||
server = ::CryptCheck::Tls::Xmpp::Server.new(name, ARGV[1] || :s2s) | |||
p grade = ::CryptCheck::Tls::Xmpp::Grade.new(server) | |||
grade = ::CryptCheck::Tls::Xmpp::Grade.new(server) | |||
::CryptCheck::Logger.info { '' } | |||
grade.display | |||
else | |||
::CryptCheck::Logger.level = :none | |||
::CryptCheck::Tls::Xmpp.analyze_from_file 'output/xmpp.yml', 'output/xmpp.html' | |||
end | |||
@@ -1,4 +1,7 @@ | |||
require 'colorize' | |||
module CryptCheck | |||
autoload :Logger, 'cryptcheck/logger' | |||
autoload :Tls, 'cryptcheck/tls' | |||
module Tls | |||
autoload :Server, 'cryptcheck/tls/server' | |||
@@ -0,0 +1,28 @@ | |||
module CryptCheck | |||
class Logger | |||
LEVELS = %i(trace debug info warning error fatal none) | |||
@@level = :info | |||
def self.level=(level) | |||
@@level = level | |||
end | |||
def self.log(level, string=nil, output: $stdout, &block) | |||
return unless enabled? level | |||
output.puts(string ? string : block.call) | |||
end | |||
LEVELS.each do |level| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def self.#{level}(string=nil, output: $stdout, &block) | |||
self.log :#{level}, string, output: output, &block | |||
end | |||
RUBY_EVAL | |||
end | |||
private | |||
def self.enabled?(level) | |||
LEVELS.index(level) >= LEVELS.index(@@level) | |||
end | |||
end | |||
end |
@@ -5,20 +5,46 @@ require 'parallel' | |||
module CryptCheck | |||
module Tls | |||
MAX_ANALYSIS_DURATION = 600 | |||
PARALLEL_ANALYSIS = 10 | |||
@@log = ::Logging.logger[Tls] | |||
PARALLEL_ANALYSIS = 10 | |||
TYPES = { | |||
md5: %w(MD5), | |||
sha1: %w(SHA), | |||
psk: %w(PSK), | |||
srp: %w(SRP), | |||
anonymous: %w(ADH AECDH), | |||
dss: %w(DSS), | |||
null: %w(NULL), | |||
export: %w(EXP), | |||
des: %w(DES-CBC), | |||
rc4: %w(RC4), | |||
des3: %w(3DES DES-CBC3), | |||
pfs: %w(DHE EDH ECDHE ECDH) | |||
} | |||
TYPES.each do |name, ciphers| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def self.#{name}?(cipher) | |||
#{ciphers}.any? { |c| /(^|-)#\{c\}(-|$)/ =~ cipher } | |||
end | |||
RUBY_EVAL | |||
end | |||
def self.grade(hostname, port, server_class:, grade_class:) | |||
timeout MAX_ANALYSIS_DURATION do | |||
grade_class.new server_class.new hostname, port | |||
end | |||
rescue ::Exception => e | |||
@@log.error { "Error during #{hostname}:#{port} analysis : #{e}" } | |||
@Logger.error { "Error during #{hostname}:#{port} analysis : #{e}" } | |||
TlsNotSupportedGrade.new TlsNotSupportedServer.new hostname, port | |||
end | |||
def self.analyze(hosts, template, output, groups = nil, port:, server_class:, grade_class:) | |||
results = {} | |||
results = {} | |||
semaphore = ::Mutex.new | |||
::Parallel.each hosts, progress: 'Analysing', in_threads: PARALLEL_ANALYSIS, finish: lambda { |item, _, _| puts item[1] } do |description, host| | |||
result = grade host.strip, port, server_class: server_class, grade_class: grade_class | |||
@@ -51,7 +77,7 @@ module CryptCheck | |||
def self.analyze_from_file(file, template, output, port:, server_class:, grade_class:) | |||
config = ::YAML.load_file file | |||
hosts = [] | |||
hosts = [] | |||
groups = [] | |||
config.each do |c| | |||
d, hs = c['description'], c['hostnames'] | |||
@@ -61,6 +87,26 @@ module CryptCheck | |||
self.analyze hosts, template, output, groups, port: port, server_class: server_class, grade_class: grade_class | |||
end | |||
def self.colorize(cipher) | |||
colors = case | |||
when /^SSL/ =~ cipher, | |||
dss?(cipher), | |||
anonymous?(cipher), | |||
null?(cipher), | |||
export?(cipher), | |||
md5?(cipher), | |||
des?(cipher), | |||
rc4?(cipher) | |||
{ color: :white, background: :red } | |||
when des3?(cipher) | |||
{ color: :yellow } | |||
when :TLSv1_2 == cipher, | |||
pfs?(cipher) | |||
{ color: :green } | |||
end | |||
cipher.to_s.colorize colors | |||
end | |||
private | |||
SCORES = %w(A+ A A- B C D E F T M X) | |||
@@ -2,109 +2,173 @@ 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, :score, :grade, :warning, :success | |||
attr_reader :server, :protocol_score, :key_exchange_score, :cipher_strengths_score, :score, :grade, :error, :warning, :success | |||
def initialize(server) | |||
@server = server | |||
protocol_score | |||
key_exchange_score | |||
cipher_strengths_score | |||
@score = @protocol_score*0.3 + @key_exchange_score*0.3 + @cipher_strengths_score*0.4 | |||
calculate_protocol_score | |||
calculate_key_exchange_score | |||
calculate_cipher_strengths_score | |||
@score = @protocol_score*0.3 + @key_exchange_score*0.3 + @cipher_strengths_score*0.4 | |||
calculate_error | |||
calculate_warning | |||
calculate_success | |||
calculate_grade | |||
warning | |||
success | |||
perfect | |||
calculate_perfect | |||
end | |||
def display | |||
color = case self.grade | |||
when 'A+' | |||
:blue | |||
when 'A' | |||
:green | |||
when 'B', 'C' | |||
:yellow | |||
when 'E', 'F' | |||
:red | |||
when 'M', 'T' | |||
{ color: :white, background: :red } | |||
end | |||
Logger.info { "Grade : #{self.grade.colorize color }" } | |||
Logger.info { '' } | |||
Logger.info { "Protocole : #{self.protocol_score} / 100" } | |||
Logger.info { "Key exchange : #{self.key_exchange_score} / 100" } | |||
Logger.info { "Ciphers strength : #{self.cipher_strengths_score} / 100" } | |||
Logger.info { "Overall score : #{self.score} / 100" } | |||
Logger.info { '' } | |||
Logger.info { "Errors : #{self.error.join(' ').colorize :red }" } unless self.error.empty? | |||
Logger.info { "Warnings : #{self.warning.join(' ').colorize :yellow }" } unless self.warning.empty? | |||
Logger.info { "Best practices : #{self.success.join(' ').colorize :green }" } unless self.success.empty? | |||
end | |||
private | |||
def calculate_grade | |||
@grade = case @score | |||
when 0...20 then 'F' | |||
when 20...35 then 'E' | |||
when 35...50 then 'D' | |||
when 50...65 then 'C' | |||
when 65...80 then 'B' | |||
else 'A' | |||
end | |||
when 0...20 then | |||
'F' | |||
when 20...35 then | |||
'E' | |||
when 35...50 then | |||
'D' | |||
when 50...65 then | |||
'C' | |||
when 65...80 then | |||
'B' | |||
else | |||
'A' | |||
end | |||
@grade = [@grade, 'B'].max if !@server.tlsv1_2? or @server.key_size < 2048 | |||
@grade = [@grade, 'C'].max if @server.des3? | |||
@grade = [@grade, 'E'].max if @server.rc4? or @server.des? | |||
@grade = [@grade, 'F'].max if @server.ssl? or @server.key_size < 1024 | |||
@grade = [@grade, 'F'].max unless @error.empty? | |||
@grade = 'M' unless @server.cert_valid | |||
@grade = 'T' unless @server.cert_trusted | |||
end | |||
def warning | |||
def calculate_error | |||
@error = [] | |||
@error << :md5_sig if @server.md5_sig? | |||
@error << :md5 if @server.md5? | |||
@error << :anonymous if @server.anonymous? | |||
@error << :dss if @server.dss? | |||
@error << :null if @server.null? | |||
@error << :export if @server.export? | |||
@error << :des if @server.des? | |||
@error << :rc4 if @server.rc4? | |||
end | |||
def calculate_warning | |||
@warning = [] | |||
@warning << :md5_sig if @server.md5_sig? | |||
@warning << :sha1_sig if @server.sha1_sig? | |||
@warning << :md5 if @server.md5? | |||
#@warning << :sha1 if @server.sha1? | |||
@warning << :rc4 if @server.rc4? | |||
@warning << :des if @server.des? | |||
@warning << :des3 if @server.des3? | |||
end | |||
def success | |||
def calculate_success | |||
@success = [] | |||
@success << :pfs if @server.pfs_only? | |||
end | |||
ALL_WARNING = %i(md5_sig md5 rc4 des) | |||
ALL_ERROR = %i(md5_sig md5 anonymous dss null export des rc4) | |||
ALL_WARNING = %i(sha1_sig des3) | |||
ALL_SUCCESS = %i(pfs) | |||
def all_error | |||
ALL_ERROR | |||
end | |||
def all_warning | |||
ALL_WARNING | |||
end | |||
ALL_SUCCESS = %i(pfs) | |||
def all_success | |||
ALL_SUCCESS | |||
end | |||
def perfect | |||
@grade = 'A+' if @grade == 'A' and (all_warning & @warning).empty? and (all_success & @success) == all_success | |||
def calculate_perfect | |||
@grade = 'A+' if @grade == 'A' and @error.empty? and @warning.empty? and (ALL_SUCCESS & @success) == ALL_SUCCESS | |||
end | |||
METHODS_SCORES = { SSLv2: 0, SSLv3: 80, TLSv1: 90, TLSv1_1: 95, TLSv1_2: 100 } | |||
def protocol_score | |||
methods = @server.supported_methods | |||
worst, best = methods[:worst], methods[:best] | |||
def calculate_protocol_score | |||
methods = @server.supported_methods | |||
worst, best = methods[:worst], methods[:best] | |||
@protocol_score = (METHODS_SCORES[worst] + METHODS_SCORES[best]) / 2 | |||
end | |||
def key_exchange_score | |||
def calculate_key_exchange_score | |||
@key_exchange_score = case @server.key_size | |||
when 0 then 0 | |||
when 0...512 then 20 | |||
when 512...1024 then 40 | |||
when 1024...2048 then 80 | |||
when 2048...4096 then 90 | |||
else 100 | |||
end | |||
when 0 then | |||
0 | |||
when 0...512 then | |||
20 | |||
when 512...1024 then | |||
40 | |||
when 1024...2048 then | |||
80 | |||
when 2048...4096 then | |||
90 | |||
else | |||
100 | |||
end | |||
end | |||
def cipher_strength_score(cipher_strength) | |||
def calculate_cipher_strength_score(cipher_strength) | |||
case cipher_strength | |||
when 0 then 0 | |||
when 0...128 then 20 | |||
when 128...256 then 80 | |||
else 100 | |||
when 0 then | |||
0 | |||
when 0...128 then | |||
20 | |||
when 128...256 then | |||
80 | |||
else | |||
100 | |||
end | |||
end | |||
def cipher_strengths_score | |||
strength = @server.cipher_size | |||
worst, best = strength[:min], strength[:max] | |||
@cipher_strengths_score = (cipher_strength_score(worst) + cipher_strength_score(best)) / 2 | |||
def calculate_cipher_strengths_score | |||
strength = @server.cipher_size | |||
worst, best = strength[:min], strength[:max] | |||
@cipher_strengths_score = (calculate_cipher_strength_score(worst) + calculate_cipher_strength_score(best)) / 2 | |||
end | |||
end | |||
end | |||
@@ -3,7 +3,7 @@ module CryptCheck | |||
module Https | |||
class Grade < Tls::Grade | |||
private | |||
def success | |||
def calculate_success | |||
super | |||
@success << :hsts if @server.hsts? | |||
@success << :hsts_long if @server.hsts_long? | |||
@@ -14,28 +14,17 @@ module CryptCheck | |||
def fetch_hsts | |||
port = @port == 443 ? '' : ":#{@port}" | |||
response = nil | |||
EXISTING_METHODS.each do |method| | |||
begin | |||
next unless SUPPORTED_METHODS.include? method | |||
@log.debug { "Check HSTS with #{method}" } | |||
response = ::HTTParty.head "https://#{@hostname}#{port}/", { follow_redirects: false, verify: false, ssl_version: method, timeout: SSL_TIMEOUT } | |||
break | |||
rescue Exception => e | |||
@log.debug { "#{method} not supported : #{e}" } | |||
end | |||
end | |||
if response and header = response.headers['strict-transport-security'] | |||
response = ::HTTParty.head "https://#{@hostname}#{port}/", { follow_redirects: false, verify: false, timeout: SSL_TIMEOUT } | |||
if header = response.headers['strict-transport-security'] | |||
name, value = header.split '=' | |||
if name == 'max-age' | |||
@hsts = value.to_i | |||
@log.info { "HSTS : #{@hsts}" } | |||
Logger.info { "HSTS : #{@hsts.to_s.colorize hsts_long? ? :green : nil}" } | |||
return | |||
end | |||
end | |||
@log.info { 'No HSTS' } | |||
Logger.info { 'No HSTS'.colorize :yellow } | |||
@hsts = nil | |||
end | |||
@@ -13,9 +13,9 @@ module CryptCheck | |||
end | |||
class Server | |||
TCP_TIMEOUT = 10 | |||
SSL_TIMEOUT = 2*TCP_TIMEOUT | |||
EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2) | |||
TCP_TIMEOUT = 10 | |||
SSL_TIMEOUT = 2*TCP_TIMEOUT | |||
EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2) | |||
SUPPORTED_METHODS = ::OpenSSL::SSL::SSLContext::METHODS | |||
class TLSException < ::Exception | |||
end | |||
@@ -33,20 +33,18 @@ module CryptCheck | |||
attr_reader :hostname, :port, :prefered_ciphers, :cert, :cert_valid, :cert_trusted | |||
def initialize(hostname, port) | |||
@log = Logging.logger[hostname] | |||
@hostname = hostname | |||
@port = port | |||
@log.error { "Begin analysis" } | |||
@port = port | |||
Logger.info { "#{hostname}:#{port}".colorize :blue } | |||
extract_cert | |||
#@prefered_ciphers = @supported_ciphers = Hash[SUPPORTED_METHODS.collect { |m| [m, []]}] | |||
fetch_prefered_ciphers | |||
check_supported_cipher | |||
@log.error { "End analysis" } | |||
end | |||
def supported_methods | |||
worst = EXISTING_METHODS.find { |method| !@prefered_ciphers[method].nil? } | |||
best = EXISTING_METHODS.reverse.find { |method| !@prefered_ciphers[method].nil? } | |||
best = EXISTING_METHODS.reverse.find { |method| !@prefered_ciphers[method].nil? } | |||
{ worst: worst, best: best } | |||
end | |||
@@ -66,24 +64,24 @@ module CryptCheck | |||
type, size = self.key | |||
if type == :ecc | |||
size = case size | |||
when 160 then | |||
1024 | |||
when 224 then | |||
2048 | |||
when 256 then | |||
3072 | |||
when 384 then | |||
7680 | |||
when 521 then | |||
15360 | |||
end | |||
when 160 then | |||
1024 | |||
when 224 then | |||
2048 | |||
when 256 then | |||
3072 | |||
when 384 then | |||
7680 | |||
when 521 then | |||
15360 | |||
end | |||
end | |||
size | |||
end | |||
def cipher_size | |||
cipher_strengths = supported_ciphers.collect { |c| c[2] }.uniq.sort | |||
worst, best = cipher_strengths.first, cipher_strengths.last | |||
worst, best = cipher_strengths.first, cipher_strengths.last | |||
{ worst: worst, best: best } | |||
end | |||
@@ -96,29 +94,22 @@ module CryptCheck | |||
end | |||
{ | |||
md2: %w(md2WithRSAEncryption), | |||
md5: %w(md5WithRSAEncryption md5WithRSA), | |||
md2: %w(md2WithRSAEncryption), | |||
md5: %w(md5WithRSAEncryption md5WithRSA), | |||
sha1: %w(sha1WithRSAEncryption sha1WithRSA dsaWithSHA1 dsaWithSHA1_2 ecdsa_with_SHA1) | |||
}.each do |name, signature| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{name}_sig? | |||
#{signature}.include? @cert.signature_algorithm | |||
end | |||
def #{name}_sig? | |||
#{signature}.include? @cert.signature_algorithm | |||
end | |||
RUBY_EVAL | |||
end | |||
{ | |||
md5: %w(MD5), | |||
sha1: %w(SHA), | |||
rc4: %w(RC4), | |||
des3: %w(3DES DES-CBC3), | |||
des: %w(DES-CBC) | |||
}.each do |name, ciphers| | |||
Tls::TYPES.each do |type, _| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{name}? | |||
supported_ciphers.any? { |supported| #{ciphers}.any? { |available| /(^|-)#\{available\}(-|$)/ =~ supported[0] } } | |||
end | |||
def #{type}? | |||
supported_ciphers.any? { |s| Tls.#{type}? s.first } | |||
end | |||
RUBY_EVAL | |||
end | |||
@@ -134,14 +125,12 @@ module CryptCheck | |||
tls? and !ssl? | |||
end | |||
PFS_CIPHERS = [/^DHE-RSA-/, /^DHE-DSS-/, /^ECDHE-RSA-/, /^ECDHE-ECDSA-/] | |||
def pfs? | |||
supported_ciphers.any? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } } | |||
supported_ciphers.any? { |c| Tls.pfs? c.first } | |||
end | |||
def pfs_only? | |||
supported_ciphers.all? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } } | |||
supported_ciphers.all? { |c| Tls.pfs? c.first } | |||
end | |||
def supported_ciphers | |||
@@ -154,21 +143,21 @@ module CryptCheck | |||
private | |||
def connect(family, host, port, &block) | |||
socket = ::Socket.new family, sock_type | |||
socket = ::Socket.new family, sock_type | |||
sockaddr = ::Socket.sockaddr_in port, host | |||
@log.debug { "Connecting to #{host}:#{port}" } | |||
Logger.trace { "Connecting to #{host}:#{port}" } | |||
begin | |||
status = socket.connect_nonblock sockaddr | |||
@log.debug { "Connecting to #{host}:#{port} status : #{status}" } | |||
Logger.trace { "Connecting to #{host}:#{port} status : #{status}" } | |||
raise ConnectionError, status unless status == 0 | |||
@log.debug { "Connected to #{host}:#{port}" } | |||
Logger.trace { "Connected to #{host}:#{port}" } | |||
block_given? ? block.call(socket) : nil | |||
rescue ::IO::WaitReadable | |||
@log.debug { "Waiting for read to #{host}:#{port}" } | |||
Logger.trace { "Waiting for read to #{host}:#{port}" } | |||
raise Timeout unless IO.select [socket], nil, nil, TCP_TIMEOUT | |||
retry | |||
rescue ::IO::WaitWritable | |||
@log.debug { "Waiting for write to #{host}:#{port}" } | |||
Logger.trace { "Waiting for write to #{host}:#{port}" } | |||
raise Timeout unless IO.select nil, [socket], nil, TCP_TIMEOUT | |||
retry | |||
ensure | |||
@@ -177,39 +166,39 @@ module CryptCheck | |||
end | |||
def ssl_connect(socket, context, method, &block) | |||
ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context | |||
ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context | |||
ssl_socket.hostname = @hostname unless method == :SSLv2 | |||
@log.debug { "SSL connecting to #{@hostname}:#{@port}" } | |||
Logger.trace { "SSL connecting to #{@hostname}:#{@port}" } | |||
begin | |||
ssl_socket.connect_nonblock | |||
@log.debug { "SSL connected to #{@hostname}:#{@port}" } | |||
Logger.trace { "SSL connected to #{@hostname}:#{@port}" } | |||
return block_given? ? block.call(ssl_socket) : nil | |||
rescue ::IO::WaitReadable | |||
@log.debug { "Waiting for SSL read to #{@hostname}:#{@port}" } | |||
Logger.trace { "Waiting for SSL read to #{@hostname}:#{@port}" } | |||
raise TLSTimeout unless IO.select [socket], nil, nil, SSL_TIMEOUT | |||
retry | |||
rescue ::IO::WaitWritable | |||
@log.debug { "Waiting for SSL write to #{@hostname}:#{@port}" } | |||
Logger.trace { "Waiting for SSL write to #{@hostname}:#{@port}" } | |||
raise TLSTimeout unless IO.select nil, [socket], nil, SSL_TIMEOUT | |||
retry | |||
rescue => e | |||
raise TLSException, e | |||
rescue => e | |||
raise TLSNotAvailableException, e | |||
ensure | |||
ssl_socket.close | |||
end | |||
end | |||
def ssl_client(method, ciphers = nil, &block) | |||
ssl_context = ::OpenSSL::SSL::SSLContext.new method | |||
ssl_context = ::OpenSSL::SSL::SSLContext.new method | |||
ssl_context.ciphers = ciphers if ciphers | |||
@log.debug { "Try #{method} connection with #{ciphers}" } | |||
Logger.trace { "Try #{method} connection with #{ciphers}" } | |||
[::Socket::AF_INET, ::Socket::AF_INET6].each do |family| | |||
@log.debug { "Try connection for family #{family}" } | |||
Logger.trace { "Try connection for family #{family}" } | |||
addrs = begin | |||
::Socket.getaddrinfo @hostname, nil, family, :STREAM | |||
rescue ::SocketError => e | |||
@log.debug { "Unable to resolv #{@hostname} : #{e}" } | |||
Logger.error { "Unable to resolv #{@hostname} : #{e}" } | |||
next | |||
end | |||
@@ -222,7 +211,7 @@ module CryptCheck | |||
end | |||
end | |||
@log.debug { "No SSL available on #{@hostname}" } | |||
Logger.debug { "No SSL available on #{@hostname}" } | |||
raise CipherNotAvailable | |||
end | |||
@@ -231,27 +220,28 @@ module CryptCheck | |||
next unless SUPPORTED_METHODS.include? method | |||
begin | |||
@cert, @chain = ssl_client(method) { |s| [s.peer_cert, s.peer_cert_chain] } | |||
@log.warn { "Certificate #{@cert.subject}" } | |||
Logger.debug { "Certificate #{@cert.subject}" } | |||
break | |||
rescue TLSException => e | |||
@log.info { "Method #{method} not supported : #{e}" } | |||
Logger.trace { "Method #{Tls.colorize method} not supported : #{e}" } | |||
end | |||
end | |||
raise TLSNotAvailableException unless @cert | |||
@cert_valid = ::OpenSSL::SSL.verify_certificate_identity @cert, @hostname | |||
@cert_valid = ::OpenSSL::SSL.verify_certificate_identity @cert, @hostname | |||
@cert_trusted = verify_trust @chain, @cert | |||
end | |||
def prefered_cipher(method) | |||
cipher = ssl_client(method, %w(ALL:COMPLEMENTOFALL)) { |s| s.cipher } | |||
@log.warn { "Prefered cipher for #{method} : #{cipher[0]}" } | |||
cipher = ssl_client(method, 'ALL:COMPLEMENTOFALL') { |s| s.cipher } | |||
Logger.info { "Prefered cipher for #{Tls.colorize method} : #{Tls.colorize cipher.first}" } | |||
cipher | |||
rescue Exception => e | |||
@log.info { "Method #{method} not supported : #{e}" } | |||
rescue Exception | |||
Logger.debug { "Method #{Tls.colorize method} not supported" } | |||
nil | |||
end | |||
def fetch_prefered_ciphers | |||
Logger.info { '' } | |||
@prefered_ciphers = {} | |||
EXISTING_METHODS.each do |method| | |||
next unless SUPPORTED_METHODS.include? method | |||
@@ -261,28 +251,33 @@ module CryptCheck | |||
end | |||
def available_ciphers(method) | |||
::OpenSSL::SSL::SSLContext.new(method).ciphers | |||
context = ::OpenSSL::SSL::SSLContext.new method | |||
context.ciphers = 'ALL:COMPLEMENTOFALL' | |||
context.ciphers | |||
end | |||
def supported_cipher?(method, cipher) | |||
ssl_client method, [cipher] | |||
@log.warn { "Verify #{method} / #{cipher[0]} : OK" } | |||
Logger.info { "#{Tls.colorize method} / #{Tls.colorize cipher[0]} : Supported" } | |||
true | |||
rescue TLSException => e | |||
@log.info { "Verify #{method} / #{cipher[0]} : NOK (#{e})" } | |||
Logger.debug { "#{Tls.colorize method} / #{Tls.colorize cipher[0]} : Not supported" } | |||
false | |||
end | |||
def check_supported_cipher | |||
Logger.info { '' } | |||
@supported_ciphers = {} | |||
EXISTING_METHODS.each do |method| | |||
next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method] | |||
@supported_ciphers[method] = available_ciphers(method).select { |cipher| supported_cipher? method, cipher } | |||
ciphers = available_ciphers(method).select { |cipher| supported_cipher? method, cipher } | |||
@supported_ciphers[method] = ciphers | |||
Logger.info { '' } unless ciphers.empty? | |||
end | |||
end | |||
def verify_trust(chain, cert) | |||
store = ::OpenSSL::X509::Store.new | |||
store = ::OpenSSL::X509::Store.new | |||
store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT | |||
store.set_default_paths | |||
@@ -7,14 +7,14 @@ module CryptCheck | |||
module Xmpp | |||
MAX_ANALYSIS_DURATION = 600 | |||
PARALLEL_ANALYSIS = 10 | |||
@@log = ::Logging.logger[Xmpp] | |||
@Logger = ::Logging.logger[Xmpp] | |||
def self.grade(hostname, type=:s2s) | |||
timeout MAX_ANALYSIS_DURATION do | |||
Grade.new Server.new hostname, type | |||
end | |||
rescue ::Exception => e | |||
@@log.error { "Error during #{hostname}:#{type} analysis : #{e}" } | |||
@Logger.error { "Error during #{hostname}:#{type} analysis : #{e}" } | |||
TlsNotSupportedGrade.new TlsNotSupportedServer.new hostname, type | |||
end | |||
@@ -2,7 +2,7 @@ module CryptCheck | |||
module Tls | |||
module Xmpp | |||
class Grade < Tls::Grade | |||
def success | |||
def calculate_success | |||
super | |||
@success << :required if @server.required? | |||
end | |||
@@ -25,6 +25,8 @@ module CryptCheck | |||
end | |||
end | |||
super hostname, port | |||
Logger.info { '' } | |||
Logger.info { self.required? ? 'Required'.colorize(:green) : 'Not required'.colorize(:yellow) } | |||
end | |||
def ssl_connect(socket, context, method, &block) | |||