Prepare for stuff outside HTTPS

v1
Nicolas Vinot 8 years ago
parent a0a2dc8813
commit 777d220b28
  1. 19
      bin/check_https
  2. 19
      bin/check_https_alexa
  3. 12
      lib/cryptcheck.rb
  4. 111
      lib/cryptcheck/tls/grade.rb
  5. 73
      lib/cryptcheck/tls/https.rb
  6. 18
      lib/cryptcheck/tls/https/grade.rb
  7. 54
      lib/cryptcheck/tls/https/server.rb
  8. 309
      lib/cryptcheck/tls/server.rb
  9. 77
      lib/sslcheck.rb
  10. 107
      lib/sslcheck/grade.rb
  11. 334
      lib/sslcheck/server.rb
  12. 148
      lib/sslcheck/ssllabs/api.rb
  13. 4
      output/https.erb
  14. 5
      output/index.yml
  15. 110
      output/ssllabs.erb
  16. 51
      spec/api_spec.rb
  17. 10
      sslcheck
  18. 19
      sslcheck-alexa
  19. 5
      sslcheck-all
  20. 60
      sslcheck-ssllabs

@ -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

@ -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'

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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
%>

@ -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

@ -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>