#!/usr/bin/env bats load 'test_helper/common' # Globals ${BATS_TMPDIR} and ${NAME} # `${NAME}` defaults to `mailserver-testing:ci` function setup() { run_setup_file_if_necessary } function teardown() { docker rm -f tls_test_cipherlists run_teardown_file_if_necessary } function setup_file() { export DOMAIN="example.test" export NETWORK="test-network" # Shared config for TLS testing (read-only) export TLS_CONFIG_VOLUME TLS_CONFIG_VOLUME="$(pwd)/test/test-files/ssl/${DOMAIN}/:/config/ssl/:ro" # `${BATS_TMPDIR}` maps to `/tmp` export TLS_RESULTS_DIR="${BATS_TMPDIR}/results" # NOTE: If the network already exists, test will fail to start. docker network create "${NETWORK}" # Copies all of `./test/config/` to specific directory for testing # `${PRIVATE_CONFIG}` becomes `$(pwd)/test/duplicate_configs/` export PRIVATE_CONFIG PRIVATE_CONFIG="$(duplicate_config_for_container .)" # Pull `testssl.sh` image in advance to avoid it interfering with the `run` captured output. # Only interferes (potential test failure) with `assert_output` not `assert_success`? docker pull drwetter/testssl.sh:3.1dev } function teardown_file() { docker network rm "${NETWORK}" } @test "first" { skip 'this test must come first to reliably identify when to run setup_file' } @test "checking tls: cipher list - rsa intermediate" { check_ports 'rsa' 'intermediate' } @test "checking tls: cipher list - rsa modern" { check_ports 'rsa' 'modern' } @test "checking tls: cipher list - ecdsa intermediate" { check_ports 'ecdsa' 'intermediate' } @test "checking tls: cipher list - ecdsa modern" { check_ports 'ecdsa' 'modern' } function check_ports() { local KEY_TYPE=$1 local TLS_LEVEL=$2 local RESULTS_PATH="${KEY_TYPE}/${TLS_LEVEL}" collect_cipherlist_data # SMTP: Opportunistic STARTTLS Explicit(25) # Needs to test against cipher lists specific to Port 25 ('_p25' parameter) check_cipherlists "${RESULTS_PATH}/port_25.json" '_p25' # SMTP Submission: Mandatory STARTTLS Explicit(587) and Implicit(465) TLS check_cipherlists "${RESULTS_PATH}/port_587.json" check_cipherlists "${RESULTS_PATH}/port_465.json" # IMAP: Mandatory STARTTLS Explicit(143) and Implicit(993) TLS check_cipherlists "${RESULTS_PATH}/port_143.json" check_cipherlists "${RESULTS_PATH}/port_993.json" # POP3: Mandatory STARTTLS Explicit(110) and Implicit(995) check_cipherlists "${RESULTS_PATH}/port_110.json" check_cipherlists "${RESULTS_PATH}/port_995.json" } function collect_cipherlist_data() { run docker run -d --name tls_test_cipherlists \ --volume "${PRIVATE_CONFIG}/:/tmp/docker-mailserver/" \ --volume "${TLS_CONFIG_VOLUME}" \ --env DMS_DEBUG=0 \ --env ENABLE_POP3=1 \ --env SSL_TYPE="manual" \ --env SSL_CERT_PATH="/config/ssl/cert.${KEY_TYPE}.pem" \ --env SSL_KEY_PATH="/config/ssl/key.${KEY_TYPE}.pem" \ --env TLS_LEVEL="${TLS_LEVEL}" \ --network "${NETWORK}" \ --network-alias "${DOMAIN}" \ --hostname "mail.${DOMAIN}" \ --tty \ "${NAME}" # Image name assert_success wait_for_finished_setup_in_container tls_test_cipherlists # NOTE: An rDNS query for the container IP will resolve to `..` # Make directory with test user ownership. Avoids Docker creating with root ownership. # TODO: Can switch to filename prefix for JSON output when this is resolved: https://github.com/drwetter/testssl.sh/issues/1845 mkdir -p "${TLS_RESULTS_DIR}/${RESULTS_PATH}" # For non-CI test runs, instead of removing prior test files after this test suite completes, # they're retained and overwritten by future test runs instead. Useful for inspection. # `--preference` reduces the test scope to the cipher suites reported as supported by the server. Completes in ~35% of the time. local TESTSSL_CMD="--quiet --file /config/ssl/testssl.txt --mode parallel --overwrite --preference" # NOTE: Batch testing ports via `--file` doesn't properly bubble up failure. # If the failure for a test is misleading consider testing a single port with: # local TESTSSL_CMD="--quiet --jsonfile-pretty ${RESULTS_PATH}/port_${PORT}.json --starttls smtp ${DOMAIN}:${PORT}" # TODO: Can use `jq` to check for failure when this is resolved: https://github.com/drwetter/testssl.sh/issues/1844 # `--user ":"` is a workaround: Avoids `permission denied` write errors for json output, uses `id` to match user uid & gid. # shellcheck disable=SC2086 # ${TESTSSL_CMD} doesn't work with double quotes run docker run --rm \ --user "$(id -u):$(id -g)" \ --network "${NETWORK}" \ --volume "${TLS_CONFIG_VOLUME}" \ --volume "${TLS_RESULTS_DIR}/${RESULTS_PATH}/:/output" \ --workdir "/output" \ drwetter/testssl.sh:3.1dev ${TESTSSL_CMD} assert_success } # Use `jq` to extract a specific cipher list from the target`testssl.sh` results json output file function compare_cipherlist() { local TARGET_CIPHERLIST=$1 local RESULTS_FILE=$2 local EXPECTED_CIPHERLIST=$3 run jq '.scanResult[0].fs[] | select(.id=="'"${TARGET_CIPHERLIST}"'") | .finding' "${TLS_RESULTS_DIR}/${RESULTS_FILE}" assert_success assert_output "${EXPECTED_CIPHERLIST}" } # Compares the expected cipher lists against logged test results from `testssl.sh` function check_cipherlists() { local RESULTS_FILE=$1 local p25=$2 # optional suffix # TLS_LEVEL `modern` doesn't have TLS v1.0 or v1.1 cipher suites. Sets TLS v1.2 as minimum. if [[ "${TLS_LEVEL}" == "intermediate" ]] then compare_cipherlist "cipherorder_TLSv1" "${RESULTS_FILE}" "$(get_cipherlist "TLSv1${p25}")" compare_cipherlist "cipherorder_TLSv1_1" "${RESULTS_FILE}" "$(get_cipherlist "TLSv1_1${p25}")" fi compare_cipherlist "cipherorder_TLSv1_2" "${RESULTS_FILE}" "$(get_cipherlist "TLSv1_2${p25}")" compare_cipherlist "cipherorder_TLSv1_3" "${RESULTS_FILE}" "$(get_cipherlist 'TLSv1_3')" } # Expected cipher lists. Should match `TLS_LEVEL` cipher lists set in `start-mailserver.sh`. # Excluding Port 25 which uses defaults from Postfix after applying `smtpd_tls_exclude_ciphers` rules. function get_cipherlist() { local TLS_VERSION=$1 if [[ "${TLS_VERSION}" == "TLSv1_3" ]] then # TLS v1.3 cipher suites are not user defineable and not unique to the available certificate(s). # They do not support server enforced order either. echo '"TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256"' else # Associative array for easy querying of required cipher list declare -A CIPHER_LIST # Our TLS v1.0 and v1.1 cipher suites should be the same: CIPHER_LIST["rsa_intermediate_TLSv1"]='"ECDHE-RSA-AES128-SHA ECDHE-RSA-AES256-SHA DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA"' CIPHER_LIST["rsa_intermediate_TLSv1_1"]=${CIPHER_LIST["rsa_intermediate_TLSv1"]} CIPHER_LIST["rsa_intermediate_TLSv1_2"]='"ECDHE-RSA-CHACHA20-POLY1305 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA256 DHE-RSA-AES256-SHA"' # `modern` doesn't have TLS v1.0 or v1.1 cipher suites: CIPHER_LIST["rsa_modern_TLSv1_2"]='"ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384"' # ECDSA CIPHER_LIST["ecdsa_intermediate_TLSv1"]='"ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA"' CIPHER_LIST["ecdsa_intermediate_TLSv1_1"]=${CIPHER_LIST["ecdsa_intermediate_TLSv1"]} CIPHER_LIST["ecdsa_intermediate_TLSv1_2"]='"ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA384 ECDHE-ECDSA-AES256-SHA"' CIPHER_LIST["ecdsa_modern_TLSv1_2"]='"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305"' # Port 25 # TLSv1 and TLSv1_1 share the same cipher suites as other ports have. The server order differs. # TLSv1_2 has different server order and ARIA, CCM, DHE+CHACHA20-POLY1305 cipher suites CIPHER_LIST["rsa_intermediate_TLSv1_p25"]='"ECDHE-RSA-AES256-SHA DHE-RSA-AES256-SHA ECDHE-RSA-AES128-SHA DHE-RSA-AES128-SHA"' CIPHER_LIST["rsa_intermediate_TLSv1_1_p25"]=${CIPHER_LIST["rsa_intermediate_TLSv1_p25"]} CIPHER_LIST["rsa_intermediate_TLSv1_2_p25"]='"ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-CHACHA20-POLY1305 DHE-RSA-AES256-CCM8 DHE-RSA-AES256-CCM ECDHE-ARIA256-GCM-SHA384 DHE-RSA-ARIA256-GCM-SHA384 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES256-SHA256 ECDHE-RSA-AES256-SHA DHE-RSA-AES256-SHA ARIA256-GCM-SHA384 ECDHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-CCM8 DHE-RSA-AES128-CCM ECDHE-ARIA128-GCM-SHA256 DHE-RSA-ARIA128-GCM-SHA256 ECDHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA256 ECDHE-RSA-AES128-SHA DHE-RSA-AES128-SHA ARIA128-GCM-SHA256"' CIPHER_LIST["rsa_modern_TLSv1_2_p25"]=${CIPHER_LIST["rsa_intermediate_TLSv1_2_p25"]} # ECDSA CIPHER_LIST["ecdsa_intermediate_TLSv1_p25"]='"ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA"' CIPHER_LIST["ecdsa_intermediate_TLSv1_1_p25"]=${CIPHER_LIST["ecdsa_intermediate_TLSv1_p25"]} CIPHER_LIST["ecdsa_intermediate_TLSv1_2_p25"]='"ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-CCM8 ECDHE-ECDSA-AES256-CCM ECDHE-ECDSA-ARIA256-GCM-SHA384 ECDHE-ECDSA-AES256-SHA384 ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES128-CCM8 ECDHE-ECDSA-AES128-CCM ECDHE-ECDSA-ARIA128-GCM-SHA256 ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES128-SHA"' CIPHER_LIST["ecdsa_modern_TLSv1_2_p25"]=${CIPHER_LIST["ecdsa_intermediate_TLSv1_2_p25"]} local TARGET_QUERY="${KEY_TYPE}_${TLS_LEVEL}_${TLS_VERSION}" echo "${CIPHER_LIST[${TARGET_QUERY}]}" fi } @test "last" { skip 'this test is only there to reliably mark the end for the teardown_file' }