Browse Source

Prepare for stuff outside HTTPS

v1
Nicolas Vinot 7 years ago
parent
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

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

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

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

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

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

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

5
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

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>

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

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)

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'

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"

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)
Loading…
Cancel
Save