From 979f3563f60bc5ad99efe159ced2846120e62ef7 Mon Sep 17 00:00:00 2001 From: Jeremy Lecour Date: Wed, 1 Feb 2017 23:32:41 +0100 Subject: [PATCH] =?UTF-8?q?Tests=20plus=20faciles=20=C3=A0=20manipuler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/security_test.rb | 89 ++++++++++++---------------------------- test/test_helper.rb | 95 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 63 deletions(-) diff --git a/test/security_test.rb b/test/security_test.rb index ecce08c..8503cf0 100644 --- a/test/security_test.rb +++ b/test/security_test.rb @@ -3,96 +3,59 @@ require 'mechanize' class SecurityTest < Minitest::Test include WebserverHelper + include SSLHelper + + def domain + "ssl.evolix.net".freeze + end def test_certificate_level level = "intermediate" - output = `#{analyze_cmd(domain, level)}` + command = analyze_cmd(domain: domain, level: level) + output = `#{command}` assert_match %r|has intermediate ssl/tls\nand complies with the '#{level}' level|, output, "Expected to comply with #{level} level :\n#{output.inspect}" refute_match %r|consider enabling OCSP Stapling|, output, 'Expected to have OCSP stapling enabled' end def test_certificate - output = `#{check_ssl_cert_cmd(domain)}` + options = { + domain: "ssl.evolix.net", + issuer: %Q("Let's Encrypt Authority X3"), + cn: "ssl.evolix.net", + } + command = check_ssl_cert_cmd(options) + output = `#{command}` assert_match(/\ASSL_CERT OK/, output, output) end def test_accepts_tls_v1 - output = `#{openssl_verify_cmd(domain, "-tls1")}` + command = openssl_verify_cmd(domain, "-tls1") + output = `#{command}` assert_match(/Verify return code: 0 \(ok\)/, output, "Expected to accept TLSv1") end def test_refuse_ssl_v3 - output = `#{openssl_verify_cmd(domain, "-ssl3")}` + command = openssl_verify_cmd(domain, "-ssl3") + output = `#{command}` assert_match(/sslv3 alert handshake failure/, output, "Expected to refuse SSLv3") end def test_hsts_header agent = Mechanize.new { |a| - a.follow_redirect = false + a.follow_redirect = true } - page = agent.get("https://#{domain}") + url = "https://#{domain}/" + page = agent.get(url) + context = "for #{url}" - assert_has_header("Strict-Transport-Security", page) - end - - def check_ssl_cert_cmd(domain) - # check_ssl_cert is a Nagios plugin, usable outside of Nagios - # cf. https://trac.id.ethz.ch/projects/nagios_plugins/wiki/check_ssl_cert - - args = [ - "--rootcert", root_certificate, - "--openssl", openssl_path(:system), - "--issuer", %Q("Gandi Standard SSL CA 2"), - "--warning", 60, - "--critical", 30, - "--cn", %Q("*.example.com"), - "--host-cn", - "--ocsp", - "--host", domain, - ].join(" ") - - "vendor/check_ssl_cert/check_ssl_cert #{args}" - end - - def analyze_cmd(domain, level = "intermediate") - # Cipherscan helps audit SSL configuration - # cf. https://github.com/jvehent/cipherscan - - args = [ - "-o", openssl_path(:local), - "-l", level, - "-t", domain - ].join(' ') - - "vendor/cipherscan/analyze.py #{args}" - end - - def openssl_verify_cmd(domain, options = "") - args = [ - "-CAfile", "#{root_certificate}", - "-connect", "#{domain}:443", - options, - "2>&1", - ].join(" ") - - "echo QUIT | #{openssl_path} s_client #{args}" - end - - def openssl_path(variant = :system) - case variant - when :local - "vendor/cipherscan/openssl-darwin64" - else - `which openssl`.chop - end - end - - def root_certificate - "test/certs/AddTrust_External_CA_Root.pem" + assert_status_ok page, context + assert_has_hsts page, context + assert_hsts_max_age "315360000", page, context + refute_hsts_include_subdomains page, context end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 197a193..34a8b6f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -3,6 +3,63 @@ require "minitest/reporters" Minitest::Reporters.use! +module SSLHelper + def check_ssl_cert_cmd(domain:, rootcert: nil, openssl: openssl_path(:system), issuer: nil, warning: 45, critical: 15, cn: nil, ocsp: true, host_cn: true) + # check_ssl_cert is a Nagios plugin, usable outside of Nagios + # cf. https://trac.id.ethz.ch/projects/nagios_plugins/wiki/check_ssl_cert + + args = [] + args.push("--host", domain) + args.push("--rootcert", rootcert) if rootcert + args.push("--openssl", openssl) if openssl + args.push("--issuer", issuer) if issuer + args.push("--warning", warning) if warning + args.push("--critical", critical) if critical + args.push("--cn", cn) if cn + args.push("--ocsp") if ocsp + args.push("--host-cn") if host_cn + + "vendor/check_ssl_cert/check_ssl_cert #{args.join(" ")}" + end + + def analyze_cmd(domain:, level: "intermediate", openssl: openssl_path(:system)) + # Cipherscan helps audit SSL configuration + # cf. https://github.com/jvehent/cipherscan + + args = [ + "-o", openssl, + "-l", level, + "-t", domain + ] + + "vendor/cipherscan/analyze.py #{args.join(' ')}" + end + + def openssl_verify_cmd(domain, options = "") + args = [ + "-CAfile", "#{root_certificate}", + "-connect", "#{domain}:443", + options, + "2>&1", + ] + + "echo QUIT | #{openssl_path} s_client #{args.join(' ')}" + end + + def openssl_path(variant = :system) + case variant + when :local + "vendor/cipherscan/openssl-darwin64" + else + `which openssl`.chop + end + end + + def root_certificate + "test/certs/AddTrust_External_CA_Root.pem" + end +end + module WebserverHelper def message_with_context(message, context = nil) @@ -91,10 +148,29 @@ module WebserverHelper assert_has_last_modified page, context end + # def assert_hsts(page, context = nil) + # assert_status_ok page, context + # assert_hsts_max_age "315360000", page, context + # assert_public page, context + # refute_hsts_include_subdomains page, context + # end + def assert_has_header(header, page, context = nil) assert page.response.key?(header), message_with_context("Expected to find '#{header}' header".freeze, context) end + def assert_has_hsts(page, context = nil) + assert_includes page.response.keys, "strict-transport-security", message_with_context("Expected to find an Strict-Transport-Security header", context) + end + + def assert_hsts_max_age(expected, page, context = nil) + assert_equal expected, hsts_max_age(page), message_with_context("Expected Strict-Transport-Security 'max-age' directive to be #{expected}", context) + end + + def assert_hsts_include_subdomains(page, context = nil) + refute hsts_include_subdomains?(page), message_with_context("Expected not to find Strict-Transport-Security 'includeSubdomains' directive", context) + end + def assert_has_etag(page, context = nil) assert_includes page.response.keys, "etag", message_with_context("Expected to find an ETag header", context) end @@ -159,6 +235,25 @@ module WebserverHelper header(page, "status".freeze) end + def hsts_header(page) + header(page, "strict-transport-security".freeze) + end + + def hsts_directives(page) + hsts_header(page).downcase.split(',').map(&:strip) + end + + def hsts_include_subdomains?(page) + cache_control_directives(page).include?("includeSubdomains".freeze) + end + + def hsts_max_age(page) + pattern = /\Amax-age\s?=\s?(\d+)\Z/ + if found = hsts_directives(page).detect("") { |v| pattern =~ v } + found[pattern, 1] + end + end + def etag_header(page) header(page, "etag".freeze) end