Refactoring for usage on RoR application

v1
Nicolas Vinot 8 years ago
parent 7753d023a2
commit 67b2654e22
  1. 1
      .gitignore
  2. 21
      Gemfile
  3. 1
      bin/check_https_alexa.rb
  4. 1
      bin/check_smtp.rb
  5. 1
      bin/check_xmpp.rb
  6. 38
      cryptcheck.gemspec
  7. 1
      lib/cryptcheck.rb
  8. 59
      lib/cryptcheck/tls.rb
  9. 69
      lib/cryptcheck/tls/cipher.rb
  10. 3
      lib/cryptcheck/tls/fixture.rb
  11. 81
      lib/cryptcheck/tls/grade.rb
  12. 17
      lib/cryptcheck/tls/https/server.rb
  13. 71
      lib/cryptcheck/tls/server.rb
  14. 2
      lib/cryptcheck/tls/xmpp.rb
  15. 15
      output/https.erb
  16. 1
      output/porn.yml
  17. 15
      output/smtp.erb
  18. 15
      output/xmpp.erb

1
.gitignore vendored

@ -1,4 +1,5 @@
*.iml
*.gem
Gemfile.lock
/.idea/
/html/

@ -1,21 +1,2 @@
source 'https://rubygems.org'
gem 'rake'
gem 'httparty'
gem 'nokogiri'
gem 'net-ssh', '>= 2.9.2.beta'
gem 'net-scp'
gem 'tcp_timeout'
gem 'parallel'
gem 'ruby-progressbar'
gem 'logging'
#gem 'activerecord'
#gem 'sqlite3'
gem 'colorize'
group :test do
gem 'rspec'
gem 'webmock'
end
gem 'debase'
gemspec

@ -2,7 +2,6 @@
$:.unshift File.expand_path File.join File.dirname(__FILE__), '../lib'
require 'rubygems'
require 'bundler/setup'
require 'logging'
require 'cryptcheck'
GROUP_NAME = 'Top 100 Alexa'

@ -2,7 +2,6 @@
$:.unshift File.expand_path File.join File.dirname(__FILE__), '../lib'
require 'rubygems'
require 'bundler/setup'
require 'logging'
require 'cryptcheck'
name = ARGV[0]

@ -2,7 +2,6 @@
$:.unshift File.expand_path File.join File.dirname(__FILE__), '../lib'
require 'rubygems'
require 'bundler/setup'
require 'logging'
require 'cryptcheck'
name = ARGV[0]

@ -0,0 +1,38 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
Gem::Specification.new do |spec|
spec.name = 'cryptcheck'
spec.version = '1.0.0'
spec.authors = ['Aeris']
spec.email = ['aeris+tls@imirhil.fr']
spec.summary = %q{Check best practices on crypto-stack implementation}
spec.description = %q{Verify if best practices are well implemented on current crypto-stack (TLS & SSH) protocol (HTTPS, SMTP, XMPP, SSH & VPN)}
spec.homepage = 'https://tls.imirhil.fr'
spec.license = 'AGPLv3+'
if spec.respond_to?(:metadata)
spec.metadata['allowed_push_host'] = 'TODO: Set to "http://mygemserver.com"'
else
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
end
spec.files = { '*.rb' => %w(lib) }
.collect_concat { |e, ds| ds.collect_concat { |d| Dir[File.join d, '**', e] } }
# spec.bindir = 'bin'
# spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
# spec.test_files = spec.files.grep(%r{^spec/})
spec.require_paths = %w(lib)
spec.add_development_dependency 'bundler', '~> 1.9', '>= 1.9.8'
spec.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
spec.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
spec.add_dependency 'httparty', '~> 0.13', '>= 0.13.3'
spec.add_dependency 'nokogiri', '~> 1.6', '>= 1.6.6'
spec.add_dependency 'parallel', '~> 1.3', '>= 1.3.4'
spec.add_dependency 'ruby-progressbar', '~> 1.7', '>= 1.7.1'
spec.add_dependency 'colorize', '~> 0.7', '>= 0.7.7'
end

@ -5,6 +5,7 @@ module CryptCheck
autoload :Logger, 'cryptcheck/logger'
autoload :Tls, 'cryptcheck/tls'
module Tls
autoload :Cipher, 'cryptcheck/tls/cipher'
autoload :Server, 'cryptcheck/tls/server'
autoload :TcpServer, 'cryptcheck/tls/server'
autoload :UdpServer, 'cryptcheck/tls/server'

@ -1,5 +1,4 @@
require 'erb'
require 'logging'
require 'parallel'
module CryptCheck
@ -7,33 +6,6 @@ module CryptCheck
MAX_ANALYSIS_DURATION = 600
PARALLEL_ANALYSIS = 10
TYPES = {
md5: %w(MD5),
sha1: %w(SHA),
psk: %w(PSK),
srp: %w(SRP),
anonymous: %w(ADH AECDH),
dss: %w(DSS),
null: %w(NULL),
export: %w(EXP),
des: %w(DES-CBC),
rc4: %w(RC4),
des3: %w(3DES DES-CBC3),
pfs: %w(DHE EDH ECDHE ECDH)
}
TYPES.each do |name, ciphers|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def self.#{name}?(cipher)
#{ciphers}.any? { |c| /(^|-)#\{c\}(-|$)/ =~ cipher }
end
RUBY_EVAL
end
def self.grade(hostname, port, server_class:, grade_class:)
timeout MAX_ANALYSIS_DURATION do
grade_class.new server_class.new hostname, port
@ -89,20 +61,8 @@ module CryptCheck
def self.colorize(cipher)
colors = case
when /^SSL/ =~ cipher,
dss?(cipher),
anonymous?(cipher),
null?(cipher),
export?(cipher),
md5?(cipher),
des?(cipher),
rc4?(cipher)
{ color: :white, background: :red }
when des3?(cipher)
{ color: :yellow }
when :TLSv1_2 == cipher,
pfs?(cipher)
{ color: :green }
when /^SSL/ =~ cipher then { color: :white, background: :red }
when :TLSv1_2 == cipher then { color: :green }
end
cipher.to_s.colorize colors
end
@ -110,18 +70,13 @@ module CryptCheck
def self.key_to_s(key)
size = key.rsa_equivalent_size
type_color = case key.type
when :ecc
{ color: :green }
when :dsa
{ color: :yellow }
when :ecc then { color: :green }
when :dsa then { color: :yellow }
end
size_color = case size
when 0...1024
{ color: :white, background: :red }
when 1024...2048
{ color: :yellow }
when 4096...::Float::INFINITY
{ color: :green }
when 0...1024 then { color: :white, background: :red }
when 1024...2048 then { color: :yellow }
when 4096...::Float::INFINITY then { color: :green }
end
"#{key.type.to_s.upcase.colorize type_color} #{key.size.to_s.colorize size_color} bits"
end

@ -0,0 +1,69 @@
module CryptCheck
module Tls
class Cipher
TYPES = {
md5: %w(MD5),
sha1: %w(SHA),
psk: %w(PSK),
srp: %w(SRP),
anonymous: %w(ADH AECDH),
dss: %w(DSS),
null: %w(NULL),
export: %w(EXP),
des: %w(DES-CBC),
rc2: %w(RC2),
rc4: %w(RC4),
des3: %w(3DES DES-CBC3),
pfs: %w(DHE EDH ECDHE ECDH)
}
attr_reader :protocol, :name, :size, :dh
def initialize(protocol, cipher, dh)
@protocol, @dh = protocol, dh
@name, _, @size = cipher
end
TYPES.each do |name, ciphers|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def self.#{name}?(cipher)
#{ciphers}.any? { |c| /(^|-)#\{c\}(-|$)/ =~ cipher }
end
def #{name}?
#{ciphers}.any? { |c| /(^|-)#\{c\}(-|$)/ =~ @name }
end
RUBY_EVAL
end
def ssl?
sslv2? or sslv3?
end
def tls?
tlsv1? or tlsv1_1? or tlsv1_2?
end
def colorize
colors = case
when dss?,
anonymous?,
null?,
export?,
md5?,
des?,
rc4?
{ color: :white, background: :red }
when des3?
{ color: :yellow }
when pfs?
{ color: :green }
end
@name.colorize colors
end
end
end
end

@ -1,3 +1,5 @@
require 'openssl'
class ::OpenSSL::PKey::EC
def type
:ecc
@ -14,6 +16,7 @@ class ::OpenSSL::PKey::EC
when 256 then 3072
when 384 then 7680
when 521 then 15360
when 571 then 21000
end
end

@ -21,21 +21,15 @@ module CryptCheck
calculate_warning
calculate_success
calculate_grade
calculate_perfect
end
def display
color = case self.grade
when 'A+'
:blue
when 'A'
:green
when 'B', 'C'
:yellow
when 'E', 'F'
:red
when 'M', 'T'
{ color: :white, background: :red }
when 'A+' then :blue
when 'A' then :green
when 'B', 'C' then :yellow
when 'E', 'F' then :red
when 'M', 'T' then { color: :white, background: :red }
end
Logger.info { "Grade : #{self.grade.colorize color }" }
@ -53,18 +47,12 @@ module CryptCheck
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'
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
@ -73,6 +61,8 @@ module CryptCheck
@grade = 'M' unless @server.cert_valid
@grade = 'T' unless @server.cert_trusted
@grade = 'A+' if @grade == 'A' and @error.empty? and @warning.empty? and (all_success & @success) == all_success
end
def calculate_error
@ -108,63 +98,44 @@ module CryptCheck
end
ALL_ERROR = %i(md5_sig md5 anonymous dss null export des rc4)
def all_error
ALL_ERROR
end
ALL_WARNING = %i(sha1_sig des3)
def all_warning
ALL_WARNING
end
ALL_SUCCESS = %i(pfs)
def all_success
ALL_SUCCESS
end
def calculate_perfect
@grade = 'A+' if @grade == 'A' and @error.empty? and @warning.empty? and (ALL_SUCCESS & @success) == ALL_SUCCESS
end
METHODS_SCORES = { SSLv2: 0, SSLv3: 10, TLSv1: 50, TLSv1_1: 75, TLSv1_2: 100 }
METHODS_SCORES = { SSLv2: 0, SSLv3: 20, TLSv1: 60, TLSv1_1: 80, TLSv1_2: 100 }
def calculate_protocol_score
methods = @server.supported_methods
worst, best = methods.last, methods.first
@protocol_score = (METHODS_SCORES[worst] + METHODS_SCORES[best]) / 2
@protocol_score = @server.supported_protocols.collect { |p| METHODS_SCORES[p] }.min
end
def calculate_key_exchange_score
@key_exchange_score = case @server.key_size
when 0 then 0
when 0...512 then 20
when 512...1024 then 40
when 1024...2048 then 80
when 0...512 then 10
when 512...1024 then 20
when 1024...2048 then 50
when 2048...4096 then 90
when 4096...::Float::INFINITY then 100
else 100
end
end
def calculate_cipher_strength_score(cipher_strength)
case cipher_strength
when 0 then
0
when 0...128 then
20
when 128...256 then
80
else
100
end
end
def calculate_cipher_strengths_score
strength = @server.cipher_size
worst, best = strength[:min], strength[:max]
@cipher_strengths_score = (calculate_cipher_strength_score(worst) + calculate_cipher_strength_score(best)) / 2
@cipher_strengths_score = case @server.cipher_size
when 0 then 0
when 0...112 then 10
when 112...128 then 50
when 128...256 then 90
else 100
end
end
end
end

@ -14,14 +14,17 @@ module CryptCheck
def fetch_hsts
port = @port == 443 ? '' : ":#{@port}"
response = ::HTTParty.head "https://#{@hostname}#{port}/", { follow_redirects: false, verify: false, timeout: SSL_TIMEOUT }
if header = response.headers['strict-transport-security']
name, value = header.split '='
if name == 'max-age'
@hsts = value.to_i
Logger.info { "HSTS : #{@hsts.to_s.colorize hsts_long? ? :green : nil}" }
return
begin
response = ::HTTParty.head "https://#{@hostname}#{port}/", { follow_redirects: false, verify: false, timeout: SSL_TIMEOUT }
if header = response.headers['strict-transport-security']
name, value = header.split '='
if name == 'max-age'
@hsts = value.to_i
Logger.info { "HSTS : #{@hsts.to_s.colorize hsts_long? ? :green : nil}" }
return
end
end
rescue ::Net::OpenTimeout
end
Logger.info { 'No HSTS'.colorize :yellow }

@ -32,7 +32,7 @@ module CryptCheck
class ConnectionError < TLSException
end
attr_reader :hostname, :port, :prefered_ciphers, :cert, :cert_valid, :cert_trusted
attr_reader :hostname, :port, :prefered_ciphers, :cert, :cert_valid, :cert_trusted, :dh
def initialize(hostname, port)
@hostname, @port = hostname, port
@ -40,19 +40,30 @@ module CryptCheck
Logger.info { "#{hostname}:#{port}".colorize :blue }
extract_cert
Logger.info { '' }
Logger.info { "Key : #{Tls.key_to_s @cert.public_key}" }
Logger.info { "Key : #{Tls.key_to_s self.key}" }
fetch_prefered_ciphers
check_supported_cipher
uniq_dh
end
def supported_methods
EXISTING_METHODS.select { |m| !@prefered_ciphers[m].nil? }
def key
@cert.public_key
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 }
supported_ciphers.collect { |c| c.size }.sort.last
end
def supported_protocols
@supported_ciphers.keys
end
def supported_ciphers
@supported_ciphers.values.flatten 1
end
def supported_ciphers_by_protocol(protocol)
@supported_ciphers[protocol]
end
EXISTING_METHODS.each do |method|
@ -75,10 +86,10 @@ module CryptCheck
RUBY_EVAL
end
Tls::TYPES.each do |type, _|
Cipher::TYPES.each do |type, _|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{type}?
supported_ciphers.any? { |s| Tls.#{type}? s.first }
supported_ciphers.any? { |c| c.#{type}? }
end
RUBY_EVAL
end
@ -100,15 +111,11 @@ module CryptCheck
end
def pfs?
supported_ciphers.any? { |c| Tls.pfs? c.first }
supported_ciphers.any? { |c| c.pfs? }
end
def pfs_only?
supported_ciphers.all? { |c| Tls.pfs? c.first }
end
def supported_ciphers
@supported_ciphers.values.flatten(1).uniq
supported_ciphers.all? { |c| c.pfs? }
end
private
@ -221,8 +228,8 @@ module CryptCheck
end
def prefered_cipher(method)
cipher = ssl_client(method, 'ALL:COMPLEMENTOFALL') { |s| s.cipher }
Logger.info { "Prefered cipher for #{Tls.colorize method} : #{Tls.colorize cipher.first}" }
cipher = ssl_client(method, 'ALL:COMPLEMENTOFALL') { |s| Cipher.new method, s.cipher, s.tmp_key }
Logger.info { "Prefered cipher for #{Tls.colorize method} : #{cipher.colorize}" }
cipher
rescue TLSException => e
Logger.debug { "Method #{Tls.colorize method} not supported : #{e}" }
@ -246,12 +253,14 @@ module CryptCheck
def supported_cipher?(method, cipher)
dh = ssl_client method, [cipher] { |s| s.tmp_key }
@dh << dh if dh
cipher = Cipher.new method, cipher, dh
dh = dh ? " (#{'DH'.colorize :green} : #{Tls.key_to_s dh})" : ''
Logger.info { "#{Tls.colorize method} / #{Tls.colorize cipher[0]} : Supported#{dh}" }
true
Logger.info { "#{Tls.colorize method} / #{cipher.colorize} : Supported#{dh}" }
cipher
rescue TLSException => e
Logger.debug { "#{Tls.colorize method} / #{Tls.colorize cipher[0]} : Not supported (#{e})" }
false
Logger.debug { "#{Tls.colorize method} / #{cipher.colorize} : Not supported (#{e})" }
nil
end
def check_supported_cipher
@ -259,9 +268,9 @@ module CryptCheck
@supported_ciphers = {}
EXISTING_METHODS.each do |method|
next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method]
ciphers = available_ciphers(method).select { |cipher| supported_cipher? method, cipher }
@supported_ciphers[method] = ciphers
Logger.info { '' } unless ciphers.empty?
supported_ciphers = available_ciphers(method).collect { |c| supported_cipher? method, c }.reject { |c| c.nil? }
Logger.info { '' } unless supported_ciphers.empty?
@supported_ciphers[method] = supported_ciphers
end
end
@ -270,7 +279,7 @@ module CryptCheck
store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
store.set_default_paths
%w(cacert).each do |directory|
%w(cacert mozilla).each do |directory|
::Dir.glob(::File.join '/usr/share/ca-certificates', directory, '*').each do |file|
cert = ::OpenSSL::X509::Certificate.new ::File.read file
begin
@ -289,6 +298,18 @@ module CryptCheck
p store.error_string unless trusted
trusted
end
def uniq_dh
dh, find = [], []
@dh.each do |k|
f = [k.type, k.size]
unless find.include? f
dh << k
find << f
end
end
@dh = dh
end
end
class TcpServer < Server

@ -1,5 +1,4 @@
require 'erb'
require 'logging'
require 'parallel'
module CryptCheck
@ -7,7 +6,6 @@ module CryptCheck
module Xmpp
MAX_ANALYSIS_DURATION = 600
PARALLEL_ANALYSIS = 10
Logger = ::Logging.logger[Xmpp]
def self.grade(hostname, type=:s2s)
timeout MAX_ANALYSIS_DURATION do

@ -97,10 +97,10 @@
<%= n.grade %>
</td>
<td class="<%= s.key_size < 2048 ? :danger : s.key_size < 4096 ? :warning : :success %>">
<% type, size = s.key %>
<%= "#{size} (#{type.to_s.upcase})" %>
<span class="sr-only">(<%= s.key_size < 2048 ? '☹' : '☺' %>)</span>
<% key = s.key %>
<td class="<%= key.rsa_equivalent_size < 2048 ? :danger : key.rsa_equivalent_size < 4096 ? :warning : :success %>">
<%= "#{key.size} (#{key.type.to_s.upcase})" %>
<span class="sr-only">(<%= key.size < 2048 ? '☹' : '☺' %>)</span>
</td>
<td class="<%= s.sha1_sig? ? :warning : :success %>">
<%= s.sha1_sig? ? '✓' : '✗' %>
@ -124,10 +124,9 @@
<span class="sr-only">(<%= s.tls? ? '☺' : '☹' %>)</span>
</td>
<% cipher_size = s.cipher_size[:worst] %>
<td class="<%= cipher_size < 112 ? :danger : cipher_size < 128 ? :warning : :success %>">
<%= cipher_size %>
<span class="sr-only">(<%= cipher_size < 128 ? '☹' : '☺' %>)</span>
<td class="<%= s.cipher_size < 112 ? :danger : s.cipher_size < 128 ? :warning : :success %>">
<%= s.cipher_size %>
<span class="sr-only">(<%= s.cipher_size < 128 ? '☹' : '☺' %>)</span>
</td>
<td class="<%= s.md5? ? :critical : :success %>">
<%= s.md5? ? '✓' : '✗' %>

@ -623,6 +623,7 @@
- www.xartmodels.com
- www.yourdirtymind.com
- your-daily-girl.com
- blog.onahole.eu
- description: Erotic Stories
hostnames:
- literotica.com

@ -97,10 +97,10 @@
<%= n.grade %>
</td>
<td class="<%= s.key_size < 2048 ? :danger : s.key_size < 4096 ? :warning : :success %>">
<% type, size = s.key %>
<%= "#{size} (#{type.to_s.upcase})" %>
<span class="sr-only">(<%= s.key_size < 2048 ? '☹' : '☺' %>)</span>
<% key = s.key %>
<td class="<%= key.rsa_equivalent_size < 2048 ? :danger : key.rsa_equivalent_size < 4096 ? :warning : :success %>">
<%= "#{key.size} (#{key.type.to_s.upcase})" %>
<span class="sr-only">(<%= key.size < 2048 ? '☹' : '☺' %>)</span>
</td>
<td class="<%= s.sha1_sig? ? :warning : :success %>">
<%= s.sha1_sig? ? '✓' : '✗' %>
@ -124,10 +124,9 @@
<span class="sr-only">(<%= s.tls? ? '☺' : '☹' %>)</span>
</td>
<% cipher_size = s.cipher_size[:worst] %>
<td class="<%= cipher_size < 112 ? :danger : cipher_size < 128 ? :warning : :success %>">
<%= cipher_size %>
<span class="sr-only">(<%= cipher_size < 128 ? '☹' : '☺' %>)</span>
<td class="<%= s.cipher_size < 112 ? :danger : s.cipher_size < 128 ? :warning : :success %>">
<%= s.cipher_size %>
<span class="sr-only">(<%= s.cipher_size < 128 ? '☹' : '☺' %>)</span>
</td>
<td class="<%= s.md5? ? :critical : :success %>">
<%= s.md5? ? '✓' : '✗' %>

@ -110,10 +110,10 @@
<%= n.grade %>
</td>
<td class="<%= s.key_size < 2048 ? :danger : s.key_size < 4096 ? :warning : :success %>">
<% type, size = s.key %>
<%= "#{size} (#{type.to_s.upcase})" %>
<span class="sr-only">(<%= s.key_size < 2048 ? '☹' : '☺' %>)</span>
<% key = s.key %>
<td class="<%= key.rsa_equivalent_size < 2048 ? :danger : key.rsa_equivalent_size < 4096 ? :warning : :success %>">
<%= "#{key.size} (#{key.type.to_s.upcase})" %>
<span class="sr-only">(<%= key.size < 2048 ? '☹' : '☺' %>)</span>
</td>
<td class="<%= s.sha1_sig? ? :warning : :success %>">
<%= s.sha1_sig? ? '✓' : '✗' %>
@ -137,10 +137,9 @@
<span class="sr-only">(<%= s.tls? ? '☺' : '☹' %>)</span>
</td>
<% cipher_size = s.cipher_size[:worst] %>
<td class="<%= cipher_size < 112 ? :danger : cipher_size < 128 ? :warning : :success %>">
<%= cipher_size %>
<span class="sr-only">(<%= cipher_size < 128 ? '☹' : '☺' %>)</span>
<td class="<%= s.cipher_size < 112 ? :danger : s.cipher_size < 128 ? :warning : :success %>">
<%= s.cipher_size %>
<span class="sr-only">(<%= s.cipher_size < 128 ? '☹' : '☺' %>)</span>
</td>
<td class="<%= s.md5? ? :critical : :success %>">
<%= s.md5? ? '✓' : '✗' %>

Loading…
Cancel
Save