Переглянути джерело

Prepare for stuff outside HTTPS

master
Nicolas Vinot 4 роки тому
джерело
коміт
777d220b28

+ 19
- 0
bin/check_https Переглянути файл

@@ -0,0 +1,19 @@
#!/usr/bin/env ruby
$:.unshift 'lib'
require 'logging'
require 'cryptcheck'

name = ARGV[0] || 'index'
file = ::File.join 'output', "#{name}.yml"

if ::File.exist? file
::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

server = ::CryptCheck::Tls::Https::Server.new(ARGV[0], ARGV[1] || 443)
p grade = ::CryptCheck::Tls::Https::Grade.new(server)
end



+ 19
- 0
bin/check_https_alexa Переглянути файл

@@ -0,0 +1,19 @@
#!/usr/bin/env ruby
$:.unshift 'lib'
require 'logging'
require 'cryptcheck'

::Logging.logger.root.appenders = ::Logging.appenders.stdout
::Logging.logger.root.level = :error

hosts = []
::File.open('top-1m.csv', 'r') do |file|
i = 0
while line = file.gets
hosts << ['Top 100 Alexa', line.strip.split(',')[1]]
i += 1
break if i == 100
end
end

::CryptCheck::Tls::Https.analyze hosts, 'output/alexa.html'

+ 12
- 0
lib/cryptcheck.rb Переглянути файл

@@ -0,0 +1,12 @@
module CryptCheck
module Tls
autoload :Server, 'cryptcheck/tls/server'
autoload :Grade, 'cryptcheck/tls/grade'
autoload :Https, 'cryptcheck/tls/https'

module Https
autoload :Server, 'cryptcheck/tls/https/server'
autoload :Grade, 'cryptcheck/tls/https/grade'
end
end
end

+ 111
- 0
lib/cryptcheck/tls/grade.rb Переглянути файл

@@ -0,0 +1,111 @@
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

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_grade
warning
success
perfect
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

@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 = 'M' unless @server.cert_valid
@grade = 'T' unless @server.cert_trusted
end

def 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
@success = []
@success << :pfs if @server.pfs_only?
end

ALL_WARNING = %i(md5_sig md5 rc4 des)
def all_warning
ALL_WARNING
end
ALL_SUCCESS = %i(pfs hsts hsts_long)
def all_success
ALL_SUCCESS
end

def perfect
@grade = 'A+' if @grade == 'A' and (all_warning & @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]
@protocol_score = (METHODS_SCORES[worst] + METHODS_SCORES[best]) / 2
end

def 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
end

def cipher_strength_score(cipher_strength)
case cipher_strength
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
end
end
end
end

+ 73
- 0
lib/cryptcheck/tls/https.rb Переглянути файл

@@ -0,0 +1,73 @@
require 'erb'
require 'logging'
require 'parallel'

module CryptCheck
module Tls
module Https
MAX_ANALYSIS_DURATION = 600
PARALLEL_ANALYSIS = 10
@@log = ::Logging.logger[Https]

def self.grade(hostname, port=443)
timeout MAX_ANALYSIS_DURATION do
Grade.new Server.new hostname, port
end
rescue ::Exception => e
@@log.error { "Error during #{hostname}:#{port} analysis : #{e}" }
TlsNotSupportedGrade.new TlsNotSupportedServer.new hostname, port
end

def self.analyze(hosts, output, groups = nil)
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
semaphore.synchronize do
if results.include? description
results[description] << result
else
results[description] = [result]
end
end
end

results = ::Hash[groups.collect { |g| [g, results[g]] }] if groups

results.each do |d, _|
results[d].sort! do |a, b|
cmp = score(a) <=> score(b)
if cmp == 0
cmp = b.score <=> a.score
if cmp == 0
cmp = a.server.hostname <=> b.server.hostname
end
end
cmp
end
end

::File.write output, ::ERB.new(::File.read('output/https.erb')).result(binding)
end

def self.analyze_from_file(file, output)
config = ::YAML.load_file file
hosts = []
groups = []
config.each do |c|
d, hs = c['description'], c['hostnames']
groups << d
hs.each { |host| hosts << [d, host] }
end
self.analyze hosts, output, groups
end

private
SCORES = %w(A+ A A- B C D E F T M X)
def self.score(a)
SCORES.index a.grade
end
end
end
end

+ 18
- 0
lib/cryptcheck/tls/https/grade.rb Переглянути файл

@@ -0,0 +1,18 @@
module CryptCheck
module Tls
module Https
class Grade < Tls::Grade
private
def success
super
@success << :hsts if @server.hsts?
@success << :hsts_long if @server.hsts_long?
end

def all_success
super + %i(hsts hsts_long)
end
end
end
end
end

+ 54
- 0
lib/cryptcheck/tls/https/server.rb Переглянути файл

@@ -0,0 +1,54 @@
require 'socket'
require 'openssl'
require 'httparty'

module CryptCheck
module Tls
module Https
class Server < Tls::Server
attr_reader :hsts

def initialize(hostname, port=443, methods: EXISTING_METHODS)
super
fetch_hsts
end

def fetch_hsts
port = @port == 443 ? '' : ":#{@port}"

response = nil
@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']
name, value = header.split '='
if name == 'max-age'
@hsts = value.to_i
@log.info { "HSTS : #{@hsts}" }
return
end
end

@log.info { 'No HSTS' }
@hsts = nil
end

def hsts?
!@hsts.nil?
end

def hsts_long?
hsts? and @hsts >= 6*30*24*60*60
end
end
end
end
end

+ 309
- 0
lib/cryptcheck/tls/server.rb Переглянути файл

@@ -0,0 +1,309 @@
require 'socket'
require 'openssl'
require 'httparty'

module CryptCheck
module Tls
class TlsNotSupportedServer
attr_reader :hostname, :port

def initialize(hostname, port)
@hostname, @port = hostname, port
end
end

class Server
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
class TLSNotAvailableException < TLSException
end
class CipherNotAvailable < TLSException
end
class Timeout < TLSException
end
class TLSTimeout < TLSException
end
class ConnectionError < TLSException
end

attr_reader :hostname, :port, :prefered_ciphers, :cert, :cert_valid, :cert_trusted

def initialize(hostname, port, methods: EXISTING_METHODS)
@log = Logging.logger[hostname]
@hostname = hostname
@port = port
@methods = methods
@log.error { "Begin analysis" }
extract_cert
fetch_prefered_ciphers
check_supported_cipher
fetch_hsts
@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? }
{ worst: worst, best: best }
end

def key
key = @cert.public_key
case key
when ::OpenSSL::PKey::RSA then
[:rsa, key.n.num_bits]
when ::OpenSSL::PKey::DSA then
[:dsa, key.p.num_bits]
when ::OpenSSL::PKey::EC then
[:ecc, key.group.degree]
end
end

def key_size
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
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: worst, best: best }
end

EXISTING_METHODS.each do |method|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method.to_s.downcase}?
!prefered_ciphers[:#{method}].nil?
end
RUBY_EVAL
end

{
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
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|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{name}?
supported_ciphers.any? { |supported| #{ciphers}.any? { |available| /(^|-)#\{available\}(-|$)/ =~ supported[0] } }
end
RUBY_EVAL
end

def ssl?
sslv2? or sslv3?
end

def tls?
tlsv1? or tlsv1_1? or tlsv1_2?
end

def tls_only?
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] } }
end

def pfs_only?
supported_ciphers.all? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } }
end

def supported_ciphers
@supported_ciphers.values.flatten(1).uniq
end

def supported_ciphers_by_method
@supported_ciphers
end

private
def connect(family, host, port, &block)
socket = ::Socket.new family, ::Socket::SOCK_STREAM
sockaddr = ::Socket.sockaddr_in port, host
@log.debug { "Connecting to #{host}:#{port}" }
begin
status = socket.connect_nonblock sockaddr
@log.debug { "Connecting to #{host}:#{port} status : #{status}" }
raise ConnectionError, status unless status == 0
@log.debug { "Connected to #{host}:#{port}" }
block_given? ? block.call(socket) : nil
rescue ::IO::WaitReadable
@log.debug { "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}" }
raise Timeout unless IO.select nil, [socket], nil, TCP_TIMEOUT
retry
ensure
socket.close
end
end

def ssl_connect(socket, context, method, &block)
ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context
ssl_socket.hostname = @hostname unless method == :SSLv2
@log.debug { "SSL connecting to #{@hostname}:#{@port}" }
begin
ssl_socket.connect_nonblock
@log.debug { "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}" }
raise TLSTimeout unless IO.select [socket], nil, nil, SSL_TIMEOUT
retry
rescue ::IO::WaitWritable
@log.debug { "Waiting for SSL write to #{@hostname}:#{@port}" }
raise TLSTimeout unless IO.select nil, [socket], nil, SSL_TIMEOUT
retry
rescue ::OpenSSL::SSL::SSLError => e
raise TLSException, e
ensure
ssl_socket.close
end
end

def ssl_client(method, ciphers = nil, &block)
ssl_context = ::OpenSSL::SSL::SSLContext.new method
ssl_context.ciphers = ciphers if ciphers
@log.debug { "Try #{method} connection with #{ciphers}" }

[::Socket::AF_INET, ::Socket::AF_INET6].each do |family|
@log.debug { "Try connection for family #{family}" }
addrs = begin
::Socket.getaddrinfo @hostname, nil, family, :STREAM
rescue ::SocketError => e
@log.debug { "Unable to resolv #{@hostname} : #{e}" }
next
end

addrs.each do |addr|
connect family, addr[3], @port do |socket|
ssl_connect socket, ssl_context, method do |ssl_socket|
return block_given? ? block.call(ssl_socket) : nil
end
end
end
end

@log.debug { "No SSL available on #{@hostname}" }
raise CipherNotAvailable
end

def extract_cert
@methods.each do |method|
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}" }
break
rescue TLSException => e
@log.info { "Method #{method} not supported : #{e}" }
end
end
raise TLSNotAvailableException unless @cert
@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
rescue Exception => e
@log.info { "Method #{method} not supported : #{e}" }
nil
end

def fetch_prefered_ciphers
@prefered_ciphers = {}
@methods.each do |method|
next unless SUPPORTED_METHODS.include? method
@prefered_ciphers[method] = prefered_cipher method
end
raise TLSNotAvailableException.new unless @prefered_ciphers.any? { |_, c| !c.nil? }
end

def available_ciphers(method)
::OpenSSL::SSL::SSLContext.new(method).ciphers
end

def supported_cipher?(method, cipher)
ssl_client method, [cipher]
@log.warn { "Verify #{method} / #{cipher[0]} : OK" }
true
rescue TLSException => e
@log.info { "Verify #{method} / #{cipher[0]} : NOK (#{e})" }
false
end

def check_supported_cipher
@supported_ciphers = {}
@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 }
end
end

def verify_trust(chain, cert)
store = ::OpenSSL::X509::Store.new
store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
%w(mozilla cacert).each do |directory|
::Dir.glob(::File.join '/usr/share/ca-certificates', directory, '*').each do |file|
::File.open file, 'r' do |file|
cert = ::OpenSSL::X509::Certificate.new file.read
begin
store.add_cert cert
rescue ::OpenSSL::X509::StoreError
end
end
end
end
chain.each do |cert|
begin
store.add_cert cert
rescue ::OpenSSL::X509::StoreError
end
end
store.verify cert
end
end
end
end

+ 0
- 77
lib/sslcheck.rb Переглянути файл

@@ -1,77 +0,0 @@
require 'erb'
require 'logging'
require 'parallel'
require 'thread'
require 'yaml'

module SSLCheck
module SSLLabs
autoload :API, 'sslcheck/ssllabs/api'
end
autoload :Server, 'sslcheck/server'
autoload :Grade, 'sslcheck/grade'

PARALLEL_ANALYSIS = 20
SYN_TIMEOUT = 600
@@log = Logging.logger[SSLCheck]

def self.grade(hostname, port=443)
timeout SYN_TIMEOUT do
Grade.new Server.new hostname, port
end
rescue Exception => e
@@log.error { "Error during #{hostname}:#{port} analysis : #{e}" }
NoSslTlsGrade.new NoSslTlsServer.new hostname, port
end

def self.analyze(hosts, output, groups = nil)
results = {}
semaphore = Mutex.new
Parallel.each hosts, progress: 'Analysing', in_threads: PARALLEL_ANALYSIS,
finish: lambda { |item, _, _| puts item[1] } do |description, host|
result = SSLCheck.grade host.strip
semaphore.synchronize do
if results.include? description
results[description] << result
else
results[description] = [result]
end
end
end

results = Hash[groups.collect { |g| [g, results[g]] }] if groups

results.each do |d, _|
results[d].sort! do |a, b|
cmp = score(a) <=> score(b)
if cmp == 0
cmp = a.score <=> b.score
if cmp == 0
cmp = a.server.hostname <=> b.server.hostname
end
end
cmp
end
end

File.write output, ERB.new(File.read('output/sslcheck.erb')).result(binding)
end

def self.analyze_from_file(file, output)
config = YAML.load_file file
hosts = []
groups = []
config.each do |c|
d, hs = c['description'], c['hostnames']
groups << d
hs.each { |host| hosts << [d, host] }
end
self.analyze hosts, output, groups
end

private
SCORES = %w(A+ A A- B C D E F T M X)
def self.score(a)
SCORES.index a.grade
end
end

+ 0
- 107
lib/sslcheck/grade.rb Переглянути файл

@@ -1,107 +0,0 @@
require 'timeout'

module SSLCheck
class NoSslTlsGrade
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

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_grade
warning
success
perfect
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

@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 = 'M' unless @server.cert_valid
@grade = 'T' unless @server.cert_trusted
end

def 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
@success = []
@success << :pfs if @server.pfs_only?
@success << :hsts if @server.hsts?
@success << :hsts_long if @server.hsts_long?
end

ALL_WARNING = %i(md5_sig md5 rc4 des)
ALL_SUCCESS = %i(pfs hsts hsts_long)
def perfect
@grade = 'A+' if @grade == 'A' and (ALL_WARNING & @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]
@protocol_score = (METHODS_SCORES[worst] + METHODS_SCORES[best]) / 2
end

def 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
end

def cipher_strength_score(cipher_strength)
case cipher_strength
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
end
end
end

+ 0
- 334
lib/sslcheck/server.rb Переглянути файл

@@ -1,334 +0,0 @@
require 'socket'
require 'openssl'
require 'httparty'
require 'parallel'
require 'tcp_timeout'

module SSLCheck
class NoSslTlsServer
attr_reader :hostname, :port

def initialize(hostname, port=443)
@hostname, @port = hostname, port
end
end

class Server
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
class TLSNotAvailableException < TLSException; end
class CipherNotAvailable < TLSException; end
class Timeout < TLSException; end
class TLSTimeout < TLSException; end
class ConnectionError < TLSException; end

attr_reader :hostname, :port, :prefered_ciphers, :cert, :hsts, :cert_valid, :cert_trusted

def initialize(hostname, port=443, methods: EXISTING_METHODS)
@log = Logging.logger[hostname]
@hostname = hostname
@port = port
@methods = methods
@log.error { "Begin analysis" }
extract_cert
fetch_prefered_ciphers
check_supported_cipher
fetch_hsts
@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? }
{worst: worst, best: best}
end

def key
key = @cert.public_key
case key
when OpenSSL::PKey::RSA then
[:rsa, key.n.num_bits]
when OpenSSL::PKey::DSA then
[:dsa, key.p.num_bits]
when OpenSSL::PKey::EC then
[:ecc, key.group.degree]
end
end

def key_size
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
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: worst, best: best}
end

EXISTING_METHODS.each do |method|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method.to_s.downcase}?
!prefered_ciphers[:#{method}].nil?
end
RUBY_EVAL
end

{
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
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|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{name}?
supported_ciphers.any? { |supported| #{ciphers}.any? { |available| /(^|-)#\{available\}(-|$)/ =~ supported[0] } }
end
RUBY_EVAL
end

def ssl?
sslv2? or sslv3?
end

def tls?
tlsv1? or tlsv1_1? or tlsv1_2?
end

def tls_only?
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] } }
end

def pfs_only?
supported_ciphers.all? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } }
end

def supported_ciphers
@supported_ciphers.values.flatten(1).uniq
end

def supported_ciphers_by_method
@supported_ciphers
end

def hsts?
!@hsts.nil?
end

def hsts_long?
hsts? and @hsts >= 6*30*24*60*60
end

private
def connect(family, host, port, &block)
socket = Socket.new family, Socket::SOCK_STREAM
sockaddr = Socket.sockaddr_in port, host
@log.debug { "Connecting to #{host}:#{port}" }
begin
status = socket.connect_nonblock sockaddr
@log.debug { "Connecting to #{host}:#{port} status : #{status}" }
raise ConnectionError.new status unless status == 0
@log.debug { "Connected to #{host}:#{port}" }
block_given? ? block.call(socket) : nil
rescue IO::WaitReadable
@log.debug { "Waiting for read to #{host}:#{port}" }
raise Timeout.new unless IO.select [socket], nil, nil, TCP_TIMEOUT
retry
rescue IO::WaitWritable
@log.debug { "Waiting for write to #{host}:#{port}" }
raise Timeout.new unless IO.select nil, [socket], nil, TCP_TIMEOUT
retry
ensure
socket.close
end
end

def ssl_connect(socket, context, method, &block)
ssl_socket = OpenSSL::SSL::SSLSocket.new socket, context
ssl_socket.hostname = @hostname unless method == :SSLv2
@log.debug { "SSL connecting to #{@hostname}:#{@port}" }
begin
ssl_socket.connect_nonblock
@log.debug { "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}" }
raise TLSTimeout.new unless IO.select [socket], nil, nil, SSL_TIMEOUT
retry
rescue IO::WaitWritable
@log.debug { "Waiting for SSL write to #{@hostname}:#{@port}" }
raise TLSTimeout.new unless IO.select nil, [socket], nil, SSL_TIMEOUT
retry
rescue OpenSSL::SSL::SSLError => e
raise TLSException, e
ensure
ssl_socket.close
end
end

def ssl_client(method, ciphers = nil, &block)
ssl_context = OpenSSL::SSL::SSLContext.new method
ssl_context.ciphers = ciphers if ciphers
@log.debug { "Try #{method} connection with #{ciphers}" }

[Socket::AF_INET, Socket::AF_INET6].each do |family|
@log.debug { "Try connection for family #{family}" }
addrs = begin
Socket.getaddrinfo @hostname, nil, family, :STREAM
rescue SocketError => e
@log.debug { "Unable to resolv #{@hostname} : #{e}" }
next
end

addrs.each do |addr|
connect family, addr[3], @port do |socket|
ssl_connect socket, ssl_context, method do |ssl_socket|
return block_given? ? block.call(ssl_socket) : nil
end
end
end
end

@log.debug { "No SSL available on #{@hostname}" }
raise CipherNotAvailable.new
end

def extract_cert
@methods.each do |method|
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}" }
break
rescue TLSException => e
@log.info { "Method #{method} not supported : #{e}" }
end
end
raise TLSNotAvailableException.new unless @cert
@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
rescue Exception => e
@log.info { "Method #{method} not supported : #{e}" }
nil
end

def fetch_prefered_ciphers
@prefered_ciphers = {}
@methods.each do |method|
next unless SUPPORTED_METHODS.include? method
@prefered_ciphers[method] = prefered_cipher method
end
raise TLSNotAvailableException.new unless @prefered_ciphers.any? { |_, c| !c.nil? }
end

def available_ciphers(method)
OpenSSL::SSL::SSLContext.new(method).ciphers
end

def supported_cipher?(method, cipher)
ssl_client method, [cipher]
@log.warn { "Verify #{method} / #{cipher[0]} : OK" }
true
rescue TLSException => e
@log.info { "Verify #{method} / #{cipher[0]} : NOK (#{e})" }
false
end

def check_supported_cipher
@supported_ciphers = {}
@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 }
end
end

def fetch_hsts
port = @port == 443 ? '' : ":#{@port}"

response = nil
@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']
name, value = header.split '='
if name == 'max-age'
@hsts = value.to_i
@log.info { "HSTS : #{@hsts}" }
return
end
end

@log.info { 'No HSTS' }
@hsts = nil
end

def verify_trust(chain, cert)
store = OpenSSL::X509::Store.new
#store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
%w(mozilla cacert).each do |directory|
Dir.glob(File.join '/usr/share/ca-certificates', directory, '*').each do |file|
File.open file, 'r' do |file|
cert = OpenSSL::X509::Certificate.new file.read
begin
store.add_cert cert
rescue OpenSSL::X509::StoreError
end
end
end
end
chain.each do |cert|
begin
store.add_cert cert
rescue OpenSSL::X509::StoreError
end
end
store.verify cert
end
end
end

+ 0
- 148
lib/sslcheck/ssllabs/api.rb Переглянути файл

@@ -1,148 +0,0 @@
require 'httparty'
require 'nokogiri'

module SSLCheck
module SSLLabs
class Error < StandardError; end
class WaitingError < Error; end
class ServerError < Error; end
class NoEncryptionError < Error; end

class API
include HTTParty
#debug_output $stdout
base_uri 'https://www.ssllabs.com/ssltest'

attr_reader :hostname, :ip, :rank, :ssl, :tls, :bits, :rc4, :pfs, :hsts

def initialize(hostname, debug: false)
@debug = debug
@hostname = hostname
@ip = nil
html = content hostname
@rank = html.css('#rating div')[1].text.strip
parse_configuration html
end

private
def content(hostname, ip=nil)
#puts "host: #{hostname}, ip: #{ip}"
options = {query: {d: hostname}}
options[:query][:s] = ip unless ip.nil?

html = nil
loop do
response = self.class.get '/analyze.html', options
raise ServerError, response.code unless response.success?
html = Nokogiri::HTML response.body
File.write File.join('html', hostname), html if @debug
break if not resolving_domain? html
end
waiting? html

html = resolve_multiple_servers html
encrypted? html

@hostname = html.at_css('.url').content.strip
ip = html.at_css '.ip'
unless ip.nil?
@ip = ip.content.strip.gsub /[()]/, ''
else
@ip = ''
end
html
end

def waiting?(html)
warning = html.at_css '#warningBox'
raise WaitingError if not warning.nil? and warning.content.include? 'Please wait...'
end

def encrypted?(html)
warning = html.at_css '#warningBox'
raise NoEncryptionError if not warning.nil? and \
warning.content.include? 'Assessment failed: Unable to connect to server'
end

def resolving_domain?(html)
warning = html.at_css('#warningBox')
not warning.nil? and warning.content.strip == 'Please wait... (Resolving domain names)'
end

def resolve_multiple_servers(html)
servers = html.at_css('#multiTable')
return html if servers.nil?
servers.css('tr').each do |server|
td = server.css('td')[4]
next if td.nil?
rank = td.content.strip
unless rank == '-'
ip = server.at_css('.ip').content
html = content hostname, ip
waiting? html
return html
end
end
raise NoEncryptionError
end

def parse_configuration(html)
configuration = html.css('.reportSection')[2]
parse_protocols configuration
parse_handshakes configuration
parse_details configuration
end

def parse_protocols(configuration)
protocols = configuration.css('.reportTable')[0].css('tr.tableRow')
@tls = true
@ssl = false
protocols.each do |row|
cells = row.css 'td'
next unless cells.size >= 2
name = cells[0].content.strip
value = cells[1].content.strip
case name
when /^TLS 1.2/ then
@tls = value == 'Yes'
when /^SSL 2/ then
@ssl |= value != 'No'
when /^SSL 3/ then
@ssl |= value != 'No'
end
end
end

def parse_handshakes(configuration)
@bits = nil
handshakes = configuration.css('.reportTable')[2].css('td.tableRight')
handshakes.each do |cell|
value = cell.content.strip
begin
i = Integer value
@bits = @bits.nil? ? i : [@bits, i].min
rescue
end
end
end

def parse_details(configuration)
@rc4 = @pfs = @hsts = nil
details = configuration.css('.reportTable')[3].css('tr.tableRow')
details.each do |row|
cells = row.css 'td'
name = cells[0].content.strip
value = cells[1].content.strip
case name
when 'RC4' then
@rc4 = value != 'No'
when 'Forward Secrecy' then
@pfs = value == 'Yes (with most browsers)   ROBUST (more info)'
when 'Strict Transport Security (HSTS)' then
@hsts = value.start_with? 'Yes'
end
end
end
end
end
end

output/sslcheck.erb → output/https.erb Переглянути файл

@@ -84,7 +84,7 @@
<%= s.hostname %>
</a>
</th>
<% if s.is_a? SSLCheck::NoSslTlsServer %>
<% if s.is_a? SSLCheck::TlsNotSupportedServer %>
<td class="critical" colspan="16">
No SSL/TLS
</td>
@@ -94,7 +94,7 @@
when 'A+' then :info
when 'A', 'A-' then :success
when 'B', 'C' then :warning
when 'T', 'X' then :critical
when 'T', 'M' then :critical
else :danger
end
%>

+ 2
- 3
output/index.yml Переглянути файл

@@ -21,13 +21,13 @@
- www.laquadrature.net
- fsf.org
- ubuntu-paris.org
- parinux.org
- www.parinux.org
- aful.org
- rmll.info
- ubuntu-fr.org
- linuxfr.org
- lea-linux.org
- framasoft.net
- framasoft.org
- gnu.org
- description: Banques en ligne
hostnames:
@@ -197,7 +197,6 @@
- spideroak.com
- hubic.com
- box.com
- mega.co.nz
- description: Divers
hostnames:
- www.mailden.net

+ 0
- 110
output/ssllabs.erb Переглянути файл

@@ -1,110 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Status SSL/TLS banque &amp; commerce en ligne</title>
<link rel="stylesheet" href="bootstrap.min.css">
<style>
body {
margin-top: 10px;
}

td {
text-align: center;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<table class="table table-bordered table-hover table-condensed">
<tbody>
<%
first = true
results.each do |r|
unless first
%>
<tr>
<th colspan="8">&nbsp;</th>
</tr>
<%
end
first = false
%>
<tr>
<th colspan="8" id="<%= r[0] %>"><%= r[0] %></th>
</tr>
<tr>
<th>Site (IP)</th>
<td>Rang</td>
<td>Sécurité (bits)</td>
<td class="danger">SSL 2/3 (obsolète)</td>
<td class="success">TLS 1.2 (actuel)</td>
<td class="danger">RC4</td>
<td class="info">PFS</td>
<td class="info">HSTS</td>
</tr>
<% r[1].each do |s|
rank_color = case s.rank
when 'A+' then :info
when 'A', 'A-' then :success
when 'B', 'C' then :warning
else :danger
end
%>
<tr>
<th id="<%= s.hostname %>">
<a href="https://www.ssllabs.com/ssltest/analyze.html?d=<%= s.hostname %>&s=<%= s.ip %>" target="_blank">
<%= s.hostname %> (<%= s.ip %>)
</a>
</th>
<td class="<%= rank_color %>">
<%= s.rank %>
</td>
<td class="<%= s.bits < 128 ? :danger : :success %>">
<%= s.bits %>
(<%= s.bits < 128 ? '☹' : '☺' %>)
</td>
<td class="<%= s.ssl ? :danger : :success %>">
<%= s.ssl ? '✓' : '✗' %>
(<%= s.ssl ? '☹' : '☺' %>)
</td>
<td class="<%= s.tls ? :success : :danger %>">
<%= s.tls ? '✓' : '✗' %>
(<%= s.tls ? '☺' : '☹' %>)
</td>
<td class="<%= s.rc4 ? :danger : :success %>">
<%= s.rc4 ? '✓' : '✗' %>
(<%= s.rc4 ? '☹' : '☺' %>)
</td>
<td class="<%= s.pfs ? :success : :danger %>">
<%= s.pfs ? '✓' : '✗' %>
(<%= s.pfs ? '☺' : '☹' %>)
</td>
<td class="<%= s.hsts ? :success : :danger %>">
<%= s.hsts ? '✓' : '✗' %>
(<%= s.hsts ? '☺' : '☹' %>)
</td>
</tr>
<% end %>
<tr>
<th>Site (IP)</th>
<td>Rang</td>
<td>Sécurité (bits)</td>
<td class="danger">SSL 2/3 (obsolète)</td>
<td class="success">TLS 1.2 (actuel)</td>
<td class="danger">RC4</td>
<td class="success">PFS</td>
<td class="success">HSTS</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>

+ 0
- 51
spec/api_spec.rb Переглянути файл

@@ -1,51 +0,0 @@
require 'sslcheck'

module SSLCheck::SSLLabs
describe API do
URL = 'https://www.ssllabs.com/ssltest/analyze.html'

it 'error' do
stub_request(:get, URL).with(query: { d: 'imirhil.fr'})
.to_return(status: 500)
expect { API.new 'imirhil.fr' } .to raise_error ServerError, '500'
end

it 'waiting' do
stub_request(:get, URL).with(query: { d: 'imirhil.fr'})
.to_return(status: 200, body: File.read('spec/html/waiting.html'))
expect { API.new 'imirhil.fr' } .to raise_error WaitingError
end

it 'single' do
stub_request(:get, URL).with(query: { d: 'imirhil.fr'})
.to_return(status: 200, body: File.read('spec/html/perfect.html'))
results = API.new 'imirhil.fr'
expect(results.hostname).to eq 'imirhil.fr'
expect(results.ip).to eq '5.135.187.37'
expect(results.rank).to eq 'A+'
expect(results.ssl).to be false
expect(results.tls).to be true
expect(results.rc4).to be false
expect(results.pfs).to be true
expect(results.hsts).to be true
expect(results.bits).to be 128
end

it 'multiple' do
stub_request(:get, URL).with(query: { d: 'fortuneo.fr'})
.to_return(status: 200, body: File.read('spec/html/multiple.html'))
stub_request(:get, URL).with(query: { d: 'fortuneo.fr', s: '93.20.46.72'})
.to_return(status: 200, body: File.read('spec/html/results.html'))
results = API.new 'fortuneo.fr'
expect(results.hostname).to eq 'fortuneo.fr'
expect(results.ip).to eq '194.51.217.72'
expect(results.rank).to eq 'B'
expect(results.ssl).to be true
expect(results.tls).to be false
expect(results.rc4).to be true
expect(results.pfs).to be false
expect(results.hsts).to be false
expect(results.bits).to be 128
end
end
end

+ 0
- 10
sslcheck Переглянути файл

@@ -1,10 +0,0 @@
#!/usr/bin/env ruby
$:.unshift 'lib'
require 'logging'
require 'sslcheck'

Logging.logger.root.appenders = Logging.appenders.stdout
Logging.logger.root.level = :warn

server = SSLCheck::Server.new(ARGV[0], ARGV[1] || 443)
p grade = SSLCheck::Grade.new(server)

+ 0
- 19
sslcheck-alexa Переглянути файл

@@ -1,19 +0,0 @@
#!/usr/bin/env ruby
require 'yaml'
$:.unshift 'lib'
require 'sslcheck'

Logging.logger.root.appenders = Logging.appenders.stdout
Logging.logger.root.level = :error

hosts = []
File.open('top-1m.csv', 'r') do |file|
i = 0
while line = file.gets
hosts << ['Top 100 Alexa', line.strip.split(',')[1]]
i += 1
break if i == 100
end
end

SSLCheck.analyze hosts, 'output/alexa.html'

+ 0
- 5
sslcheck-all Переглянути файл

@@ -1,5 +0,0 @@
#!/usr/bin/env ruby
$:.unshift 'lib'
require 'sslcheck'
name = ARGV[0] || 'index'
SSLCheck.analyze_from_file "output/#{name}.yml", "output/#{name}.html"

+ 0
- 60
sslcheck-ssllabs Переглянути файл

@@ -1,60 +0,0 @@
#!/usr/bin/env ruby
require 'erb'
require 'yaml'
$:.unshift 'lib'
require 'sslcheck'

SCORES = %w(A+ A A- B C D E F T M)
def score(a); SCORES.index a.rank; end

def check(hostname)
hostname.strip!
print ' ', hostname, ' : '
begin
result = SSLCheck::SSLLabs::API.new hostname
puts result.rank
result
rescue SSLCheck::SSLLabs::NoEncryptionError
puts 'no encryption'
raise
rescue => e
puts e
raise
end
end

config = YAML.load_file 'hosts.yml'
results = Hash[config.collect { |c| [c['description'], []] }]

loop do
waiting = false
config.each do |c|
description, hosts = c['description'], c['hostnames']
puts description
hosts.clone.each do |host|
begin
results[description] << check(host)
hosts.delete host
rescue SSLCheck::SSLLabs::WaitingError
waiting = true
rescue SSLCheck::SSLLabs::Error
rescue => e
p e.backtrace
end
end
end

break if not waiting
puts 'Waiting end of analyze'
sleep 1*60
end

results.each do |d, _|
results[d].sort! do |a, b|
cmp = score(a) <=> score(b)
cmp != 0 ? cmp : a.hostname <=> b.hostname
end
end

puts 'Generate results'
File.write 'output/index.html', ERB.new(File.read('index.erb')).result(binding)

Завантаження…
Відмінити
Зберегти