From 7b84511baa51b3e39488841d9b2757b0a51eabad Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Mon, 15 Jan 2018 18:52:29 +0100 Subject: [PATCH] add TLS configuration server tests This change adds non-functional tests to check whether a minio endpoint (TLS) is configured properly. This includes: - SSL/TLS version checks - Cipher suite checks To separate TLS tests from functional tests this change adds a new subdirectory `/run/tls`. Fixes #253 --- Dockerfile.dev | 4 + build/security/install.sh | 20 +++ mint.sh | 64 ++++++--- release.sh | 1 + run/security/tls/run.sh | 28 ++++ run/security/tls/server-tests.go | 229 +++++++++++++++++++++++++++++++ 6 files changed, 324 insertions(+), 22 deletions(-) create mode 100755 build/security/install.sh create mode 100755 run/security/tls/run.sh create mode 100644 run/security/tls/server-tests.go diff --git a/Dockerfile.dev b/Dockerfile.dev index 1abf55ff..97b708d3 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -16,6 +16,7 @@ RUN apt-get --yes update && apt-get --yes upgrade && apt-get --yes --quiet insta ENV MINT_ROOT_DIR /mint ENV MINT_RUN_CORE_DIR $MINT_ROOT_DIR/run/core +ENV MINT_RUN_SECURITY_DIR $MINT_ROOT_DIR/run/security ENV WGET "wget --quiet --no-check-certificate" COPY create-data-files.sh /mint @@ -60,6 +61,9 @@ RUN build/s3cmd/install.sh COPY build/minio-dotnet/ /mint/build/minio-dotnet/ RUN /mint/build/minio-dotnet/install.sh +COPY build/security /mint/build/security +RUN build/security/install.sh + COPY remove-packages.list /mint COPY postinstall.sh /mint RUN /mint/postinstall.sh diff --git a/build/security/install.sh b/build/security/install.sh new file mode 100755 index 00000000..7c883a5a --- /dev/null +++ b/build/security/install.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e +# +# Mint (C) 2018 Minio, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +test_run_dir="$MINT_RUN_SECURITY_DIR/tls" +go get -u github.com/sirupsen/logrus/... +go build -o "$test_run_dir/server-tests" "$test_run_dir/server-tests.go" diff --git a/mint.sh b/mint.sh index cca36921..00d2d189 100755 --- a/mint.sh +++ b/mint.sh @@ -29,7 +29,8 @@ if [ -z "$SERVER_ENDPOINT" ]; then fi ROOT_DIR="$PWD" -TESTS_DIR="$ROOT_DIR/run/core" +CORE_TESTS_DIR="$ROOT_DIR/run/core" +SECURITY_TESTS_DIR="$ROOT_DIR/run/security" BASE_LOG_DIR="$ROOT_DIR/log" LOG_FILE="log.json" @@ -88,6 +89,32 @@ function run_test() return $rv } +function run_tests() +{ + i=0 + for sdk_dir in "${run_list[@]}"; do + sdk_name=$(basename "$sdk_dir") + (( i++ )) + if [ ! -d "$sdk_dir" ]; then + echo "Test $sdk_name not found. Exiting Mint." + exit 1 + fi + echo -n "($i/$count) Running $sdk_name tests ... " + if ! run_test "$sdk_dir"; then + (( i-- )) + break + fi + done + + ## Report when all tests in run_list are run + if [ $i -eq "$count" ]; then + echo -e "\nAll tests ran successfully" + else + echo -e "\nExecuted $i out of $count tests successfully." + exit 1 + fi +} + function main() { export MINT_DATA_DIR @@ -113,36 +140,29 @@ function main() declare -a run_list ## Populate values from command line argument for sdk in "$@"; do - run_list=( "${run_list[@]}" "$TESTS_DIR/$sdk" ) + run_list=( "${run_list[@]}" "$CORE_TESTS_DIR/$sdk" ) done ## On empty command line argument, populate all SDK names from $TESTS_DIR if [ "${#run_list[@]}" -eq 0 ]; then - run_list=( "$TESTS_DIR"/* ) + run_list=( "$CORE_TESTS_DIR"/* ) fi + ## Run core tests + echo -e "Running core tests:\n" count="${#run_list[@]}" - i=0 - for sdk_dir in "${run_list[@]}"; do - sdk_name=$(basename "$sdk_dir") - (( i++ )) - if [ ! -d "$sdk_dir" ]; then - echo "Test $sdk_name not found. Exiting Mint." - exit 1 - fi - echo -n "($i/$count) Running $sdk_name tests ... " - if ! run_test "$sdk_dir"; then - (( i-- )) - break - fi - done + run_tests "${run_list[@]}" - ## Report when all tests in run_list are run - if [ $i -eq "$count" ]; then - echo -e "\nAll tests ran successfully" - else - echo -e "\nExecuted $i out of $count tests successfully." + if [ "$ENABLE_HTTPS" -ne 1 ]; then + echo -e "TLS is disabled. Skipping security tests.\n" + exit 0 fi + + ## Run security tests + echo -e "Running security tests:\n" + count="${#run_list[@]}" + run_list=( "$SECURITY_TESTS_DIR"/* ) + run_tests "${run_list[@]}" } main "$@" diff --git a/release.sh b/release.sh index 34bae5a3..d302455b 100755 --- a/release.sh +++ b/release.sh @@ -17,6 +17,7 @@ export MINT_ROOT_DIR=${MINT_ROOT_DIR:-/mint} export MINT_RUN_CORE_DIR="$MINT_ROOT_DIR/run/core" +export MINT_RUN_SECURITY_DIR="$MINT_ROOT_DIR/run/security" export WGET="wget --quiet --no-check-certificate" ./create-data-files.sh diff --git a/run/security/tls/run.sh b/run/security/tls/run.sh new file mode 100755 index 00000000..deb03b2d --- /dev/null +++ b/run/security/tls/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# +# Mint (C) 2018 Minio, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# handle command line arguments +if [ $# -ne 2 ]; then + echo "usage: run.sh " + exit -1 +fi + +output_log_file="$1" +error_log_file="$2" + +# run tests +/mint/run/security/tls/server-tests 1>>"$output_log_file" 2>"$error_log_file" diff --git a/run/security/tls/server-tests.go b/run/security/tls/server-tests.go new file mode 100644 index 00000000..eb3b0668 --- /dev/null +++ b/run/security/tls/server-tests.go @@ -0,0 +1,229 @@ +// Mint, (C) 2018 Minio, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software + +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "os" + "time" + + log "github.com/sirupsen/logrus" +) + +const testName = "tls-go" + +const ( + // PASS indicate that a test passed + PASS = "PASS" + // FAIL indicate that a test failed + FAIL = "FAIL" + // NA indicates that a test is not applicable + NA = "NA" +) + +func main() { + log.SetOutput(os.Stdout) + log.SetFormatter(&mintJSONFormatter{}) + log.SetLevel(log.InfoLevel) + + endpoint := os.Getenv("SERVER_ENDPOINT") + secure := os.Getenv("ENABLE_HTTPS") + if secure != "1" { + log.WithFields(log.Fields{"name:": testName, "status": NA, "message": "TLS is not enabled"}).Info() + return + } + + testTLSVersions(endpoint) + testTLSCiphers(endpoint) +} + +// Tests whether the endpoint accepts SSL3.0, TLS1.0 or TLS1.1 connections - fail if so. +// Tests whether the endpoint accepts TLS1.2 connections - fail if not. +func testTLSVersions(endpoint string) { + const function = "TLSVersions" + startTime := time.Now() + + // Tests whether the endpoint accepts SSL3.0, TLS1.0 or TLS1.1 connections + args := map[string]interface{}{ + "MinVersion": "tls.VersionSSL30", + "MaxVersion": "tls.VersionTLS11", + } + _, err := tls.Dial("tcp", endpoint, &tls.Config{ + MinVersion: tls.VersionSSL30, + MaxVersion: tls.VersionTLS11, + }) + if err == nil { + failureLog(function, args, startTime, "", "Endpoint accepts insecure connection", err).Error() + return + } + + // Tests whether the endpoint accepts TLS1.2 connections + args = map[string]interface{}{ + "MinVersion": "tls.VersionTLS12", + } + _, err = tls.Dial("tcp", endpoint, &tls.Config{ + MinVersion: tls.VersionTLS12, + }) + if err != nil { + failureLog(function, args, startTime, "", "Endpoint rejects secure connection", err).Error() + return + } + successLog(function, args, startTime) +} + +// Tests whether the endpoint accepts SSL3.0, TLS1.0 or TLS1.1 connections - fail if so. +// Tests whether the endpoint accepts TLS1.2 connections - fail if not. +func testTLSCiphers(endpoint string) { + const function = "TLSCiphers" + startTime := time.Now() + + // Tests whether the endpoint accepts insecure ciphers + args := map[string]interface{}{ + "MinVersion": "tls.VersionTLS12", + "CipherSuites": unsupportedCipherSuites, + } + _, err := tls.Dial("tcp", endpoint, &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: unsupportedCipherSuites, + }) + if err == nil { + failureLog(function, args, startTime, "", "Endpoint accepts insecure cipher suites", err).Error() + return + } + + // Tests whether the endpoint accepts at least one secure cipher + args = map[string]interface{}{ + "MinVersion": "tls.VersionTLS12", + "CipherSuites": supportedCipherSuites, + } + _, err = tls.Dial("tcp", endpoint, &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: supportedCipherSuites, + }) + if err != nil { + failureLog(function, args, startTime, "", "Endpoint rejects all secure cipher suites", err).Error() + return + } + + // Tests whether the endpoint accepts at least one default cipher + args = map[string]interface{}{ + "MinVersion": "tls.VersionTLS12", + "CipherSuites": nil, + } + _, err = tls.Dial("tcp", endpoint, &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: nil, // default value + }) + if err != nil { + failureLog(function, args, startTime, "", "Endpoint rejects default cipher suites", err).Error() + return + } + successLog(function, args, startTime) +} + +func successLog(function string, args map[string]interface{}, startTime time.Time) *log.Entry { + duration := time.Since(startTime).Nanoseconds() / 1000000 + return log.WithFields(log.Fields{ + "name": testName, + "function": function, + "args": args, + "duration": duration, + "status": PASS, + }) +} + +func failureLog(function string, args map[string]interface{}, startTime time.Time, alert string, message string, err error) *log.Entry { + duration := time.Since(startTime).Nanoseconds() / 1000000 + fields := log.Fields{ + "name": testName, + "function": function, + "args": args, + "duration": duration, + "status": FAIL, + "alert": alert, + "message": message, + } + if err != nil { + fields["error"] = err + } + return log.WithFields(fields) +} + +type mintJSONFormatter struct { +} + +func (f *mintJSONFormatter) Format(entry *log.Entry) ([]byte, error) { + data := make(log.Fields, len(entry.Data)) + for k, v := range entry.Data { + switch v := v.(type) { + case error: + // Otherwise errors are ignored by `encoding/json` + // https://github.com/sirupsen/logrus/issues/137 + data[k] = v.Error() + default: + data[k] = v + } + } + + serialized, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) + } + return append(serialized, '\n'), nil +} + +// Secure Go implementations of modern TLS ciphers +// The following ciphers are excluded because: +// - RC4 ciphers: RC4 is broken +// - 3DES ciphers: Because of the 64 bit blocksize of DES (Sweet32) +// - CBC-SHA256 ciphers: No countermeasures against Lucky13 timing attack +// - CBC-SHA ciphers: Legacy ciphers (SHA-1) and non-constant time +// implementation of CBC. +// (CBC-SHA ciphers can be enabled again if required) +// - RSA key exchange ciphers: Disabled because of dangerous PKCS1-v1.5 RSA +// padding scheme. See Bleichenbacher attacks. +var supportedCipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, +} + +var unsupportedCipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13) + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // No countermeasures against timing attacks + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13) + tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, // Broken cipher + tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, // Sweet32 + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13) + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, // No countermeasures against timing attacks + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13) + tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, // Broken cipher + + // all RSA-PKCS1-v1.5 ciphers are disabled - danger of Bleichenbacher attack variants + tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // Sweet32 + tls.TLS_RSA_WITH_AES_128_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13) + tls.TLS_RSA_WITH_AES_128_CBC_SHA256, // No countermeasures against timing attacks + tls.TLS_RSA_WITH_AES_256_CBC_SHA, // Go stack contains (some) countermeasures against timing attacks (Lucky13) + tls.TLS_RSA_WITH_RC4_128_SHA, // Broken cipher + + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // Disabled because of RSA-PKCS1-v1.5 - AES-GCM is considered secure. + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // Disabled because of RSA-PKCS1-v1.5 - AES-GCM is considered secure. +}