1
0
mirror of https://github.com/tomav/docker-mailserver.git synced 2024-06-21 06:57:12 +02:00
docker-mailserver/test/test_helper/tls.bash
Brennan Kinney 1b1877f025
refactor: letsencrypt implicit location discovery (#2525)
* chore: Extract letsencrypt logic into methods

This allows other scripts to share the functionality to discover the correct letsencrypt folder from the 3 possible locations (where specific order is important).

As these methods should now return a string value, the `return 1` after a panic is now dropped.

* chore: Update comments

The todo is resolved with this PR, `_setup_ssl` will be called by both cert conditional statements with purpose for each better documented to maintainers at the start of the logic block.

* refactor: Defer most logic to helper/ssl.sh

The loop is no longer required, extraction is delegated to `_setup_ssl` now.

For the change event prevention, we retrieve the relevant FQDN via the new helper method, beyond that it's just indentation diff.

`check-for-changes.sh` adjusted to allow locally scoped var declarations by wrapping a function. Presently no loop control flow is needed so this seems fine. Made it clear that `CHANGED` is local and `CHKSUM_FILE` is not.

Panic scope doesn't require `SSL_TYPE` for context, it's clearly`letsencrypt`.

* fix: Correctly match wildcard results

Now that the service configs are properly updated, when the services restart they will return a cert with the SAN `DNS:*.example.test`,  which is valid for `mail.example.test`, however the test function did not properly account for this in the regexp query.

Resolved by truncating the left-most DNS label from FQDN and adding a third check to match a returned wildcard DNS result.

Extracted out the common logic to create the regexp query and renamed the methods to communicate more clearly that they check the FQDN is supported, not necessarily explicitly listed by the cert.

* tests(letsencrypt): Enable remaining tests

These will now pass. Adjusted comments accordingly.

Added an additional test on a fake FQDN that should still be valid to a wildcard cert (SNI validation in a proper setup would reject the connection afterwards).

Co-authored-by: Georg Lauterbach <44545919+georglauterbach@users.noreply.github.com>
2022-04-18 22:52:50 +12:00

136 lines
4.9 KiB
Bash

#! /bin/bash
load 'test_helper/common'
# Helper methods for testing TLS.
# `_should_*` methods are useful for common high-level functionality.
# ? --------------------------------------------- Negotiate TLS
# For certs actually provisioned from LetsEncrypt the Root CA cert should not need to be provided,
# as it would already be available by default in `/etc/ssl/certs`, requiring only the cert chain (fullchain.pem).
function _should_succesfully_negotiate_tls() {
local FQDN=${1}
local CONTAINER_NAME=${2:-${TEST_NAME}}
# shellcheck disable=SC2031
local CA_CERT=${3:-${TEST_CA_CERT}}
# Postfix and Dovecot are ready:
wait_for_smtp_port_in_container_to_respond "${CONTAINER_NAME}"
wait_for_tcp_port_in_container 993 "${CONTAINER_NAME}"
# Root CA cert should be present in the container:
assert docker exec "${CONTAINER_NAME}" [ -f "${CA_CERT}" ]
local PORTS=(25 587 465 143 993)
for PORT in "${PORTS[@]}"
do
_negotiate_tls "${FQDN}" "${PORT}"
done
}
# Basically runs commands like:
# docker exec "${TEST_NAME}" sh -c "timeout 1 openssl s_client -connect localhost:587 -starttls smtp -CAfile ${CA_CERT} 2>/dev/null | grep 'Verification'"
function _negotiate_tls() {
local FQDN=${1}
local PORT=${2}
local CONTAINER_NAME=${3:-${TEST_NAME}}
# shellcheck disable=SC2031
local CA_CERT=${4:-${TEST_CA_CERT}}
local CMD_OPENSSL_VERIFY
CMD_OPENSSL_VERIFY=$(_generate_openssl_cmd "${PORT}")
# Should fail as a chain of trust is required to verify successfully:
run docker exec "${CONTAINER_NAME}" sh -c "${CMD_OPENSSL_VERIFY}"
assert_output --partial 'Verification error: unable to verify the first certificate'
# Provide the Root CA cert for successful verification:
CMD_OPENSSL_VERIFY=$(_generate_openssl_cmd "${PORT}" "-CAfile ${CA_CERT}")
run docker exec "${CONTAINER_NAME}" sh -c "${CMD_OPENSSL_VERIFY}"
assert_output --partial 'Verification: OK'
_should_support_fqdn_in_cert "${FQDN}" "${PORT}"
}
function _generate_openssl_cmd() {
# Using a HOST of `localhost` will not have issues with `/etc/hosts` matching,
# since hostname may not be match correctly in `/etc/hosts` during tests when checking cert validity.
local HOST='localhost'
local PORT=${1}
local EXTRA_ARGS=${2}
# `echo '' | openssl ...` is a common approach for providing input to `openssl` command which waits on input to exit.
# While the command is still successful it does result with `500 5.5.2 Error: bad syntax` being included in the response.
# `timeout 1` instead of the empty echo pipe approach seems to work better instead.
local CMD_OPENSSL="timeout 1 openssl s_client -connect ${HOST}:${PORT}"
# STARTTLS ports need to add a hint:
if [[ ${PORT} =~ ^(25|587)$ ]]
then
CMD_OPENSSL="${CMD_OPENSSL} -starttls smtp"
elif [[ ${PORT} == 143 ]]
then
CMD_OPENSSL="${CMD_OPENSSL} -starttls imap"
elif [[ ${PORT} == 110 ]]
then
CMD_OPENSSL="${CMD_OPENSSL} -starttls pop3"
fi
# `2>/dev/null` prevents openssl interleaving output to stderr that shouldn't be captured:
echo "${CMD_OPENSSL} ${EXTRA_ARGS} 2>/dev/null"
}
# ? --------------------------------------------- Verify FQDN
function _get_fqdn_match_query() {
local FQDN
FQDN=$(escape_fqdn "${1}")
# 3rd check is for wildcard support by replacing the 1st DNS label of the FQDN with a `*`,
# eg: `mail.example.test` will become `*.example.test` matching `DNS:*.example.test`.
echo "Subject: CN = ${FQDN}|DNS:${FQDN}|DNS:\*\.${FQDN#*.}"
}
function _should_support_fqdn_in_cert() {
_get_fqdns_for_cert "$@"
assert_output --regexp "$(_get_fqdn_match_query "${1}")"
}
function _should_not_support_fqdn_in_cert() {
_get_fqdns_for_cert "$@"
refute_output --regexp "$(_get_fqdn_match_query "${1}")"
}
# Escapes `*` and `.` so the FQDN literal can be used in regex queries
# `sed` will match those two chars and `\\&` says to prepend a `\` to the sed match (`&`)
function escape_fqdn() {
# shellcheck disable=SC2001
sed 's|[\*\.]|\\&|g' <<< "${1}"
}
function _get_fqdns_for_cert() {
local FQDN=${1}
local PORT=${2:-'25'}
local CONTAINER_NAME=${3:-${TEST_NAME}}
# shellcheck disable=SC2031
local CA_CERT=${4:-${TEST_CA_CERT}}
# `-servername` is for SNI, where the port may be for a service that serves multiple certs,
# and needs a specific FQDN to return the correct cert. Such as a reverse-proxy.
local EXTRA_ARGS="-servername ${FQDN} -CAfile ${CA_CERT}"
local CMD_OPENSSL_VERIFY
# eg: "timeout 1 openssl s_client -connect localhost:25 -starttls smtp ${EXTRA_ARGS} 2>/dev/null"
CMD_OPENSSL_VERIFY=$(_generate_openssl_cmd "${PORT}" "${EXTRA_ARGS}")
# Takes the result of the openssl output to return the x509 certificate,
# We then check that for any matching FQDN entries:
# main == `Subject CN = <FQDN>`, sans == `DNS:<FQDN>`
local CMD_FILTER_FQDN="openssl x509 -noout -text | grep -E 'Subject: CN = |DNS:'"
run docker exec "${CONTAINER_NAME}" sh -c "${CMD_OPENSSL_VERIFY} | ${CMD_FILTER_FQDN}"
}