diff --git a/.gitignore b/.gitignore index da369d0..df03ac2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.iml +*.gem Gemfile.lock /.idea/ /html/ diff --git a/Gemfile b/Gemfile index 6d82d50..851fabc 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/bin/check_https_alexa.rb b/bin/check_https_alexa.rb index ee78231..82989db 100755 --- a/bin/check_https_alexa.rb +++ b/bin/check_https_alexa.rb @@ -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' diff --git a/bin/check_smtp.rb b/bin/check_smtp.rb index 3e0dbc3..538a7c5 100755 --- a/bin/check_smtp.rb +++ b/bin/check_smtp.rb @@ -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] diff --git a/bin/check_xmpp.rb b/bin/check_xmpp.rb index c9a8698..012c07f 100755 --- a/bin/check_xmpp.rb +++ b/bin/check_xmpp.rb @@ -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] diff --git a/cryptcheck.gemspec b/cryptcheck.gemspec new file mode 100644 index 0000000..9eb3808 --- /dev/null +++ b/cryptcheck.gemspec @@ -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 diff --git a/lib/cryptcheck.rb b/lib/cryptcheck.rb index 759361a..d354122 100644 --- a/lib/cryptcheck.rb +++ b/lib/cryptcheck.rb @@ -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' diff --git a/lib/cryptcheck/tls.rb b/lib/cryptcheck/tls.rb index 4f1fd50..63d5305 100644 --- a/lib/cryptcheck/tls.rb +++ b/lib/cryptcheck/tls.rb @@ -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 diff --git a/lib/cryptcheck/tls/cipher.rb b/lib/cryptcheck/tls/cipher.rb new file mode 100644 index 0000000..4255dd6 --- /dev/null +++ b/lib/cryptcheck/tls/cipher.rb @@ -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 diff --git a/lib/cryptcheck/tls/fixture.rb b/lib/cryptcheck/tls/fixture.rb index c8ac750..bce5452 100644 --- a/lib/cryptcheck/tls/fixture.rb +++ b/lib/cryptcheck/tls/fixture.rb @@ -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 diff --git a/lib/cryptcheck/tls/grade.rb b/lib/cryptcheck/tls/grade.rb index a902525..cffe3e8 100644 --- a/lib/cryptcheck/tls/grade.rb +++ b/lib/cryptcheck/tls/grade.rb @@ -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 diff --git a/lib/cryptcheck/tls/https/server.rb b/lib/cryptcheck/tls/https/server.rb index 68f5ce4..ac23992 100644 --- a/lib/cryptcheck/tls/https/server.rb +++ b/lib/cryptcheck/tls/https/server.rb @@ -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 } diff --git a/lib/cryptcheck/tls/server.rb b/lib/cryptcheck/tls/server.rb index d58830a..adbf59e 100644 --- a/lib/cryptcheck/tls/server.rb +++ b/lib/cryptcheck/tls/server.rb @@ -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 diff --git a/lib/cryptcheck/tls/xmpp.rb b/lib/cryptcheck/tls/xmpp.rb index 796cf90..b333557 100644 --- a/lib/cryptcheck/tls/xmpp.rb +++ b/lib/cryptcheck/tls/xmpp.rb @@ -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 diff --git a/output/https.erb b/output/https.erb index f49eb93..2c3ea6c 100644 --- a/output/https.erb +++ b/output/https.erb @@ -97,10 +97,10 @@ <%= n.grade %> -