From bba72daedf4ecf03a80eea571160371191237863 Mon Sep 17 00:00:00 2001 From: Georg Lauterbach <44545919+georglauterbach@users.noreply.github.com> Date: Wed, 3 May 2023 08:30:49 +0200 Subject: [PATCH] scripts: add DKIM helper script for Rspamd (#3286) Co-authored-by: Casper --- .../config/best-practices/dkim_dmarc_spf.md | 77 ++--- target/bin/open-dkim | 6 + target/bin/rspamd-dkim | 279 ++++++++++++++++++ .../startup/setup.d/security/rspamd.sh | 2 +- .../parallel/set1/spam_virus/rspamd_dkim.bats | 272 +++++++++++++++++ 5 files changed, 600 insertions(+), 36 deletions(-) create mode 100755 target/bin/rspamd-dkim create mode 100644 test/tests/parallel/set1/spam_virus/rspamd_dkim.bats diff --git a/docs/content/config/best-practices/dkim_dmarc_spf.md b/docs/content/config/best-practices/dkim_dmarc_spf.md index f8d17072..dfa3b5a2 100644 --- a/docs/content/config/best-practices/dkim_dmarc_spf.md +++ b/docs/content/config/best-practices/dkim_dmarc_spf.md @@ -40,6 +40,10 @@ You should have: - At least one [email account setup][docs-accounts-add] - Attached a [volume for config][docs-volumes-config] to persist the generated files to local storage +!!! warning "RSA Key Sizes >= 4096 Bit" + + Keys of 4096 bits could be denied by some mail servers. According to [RFC 6376][rfc-6376], keys are [preferably between 512 and 2048 bits][github-issue-dkimlength]. + DKIM is currently supported by either OpenDKIM or Rspamd: === "OpenDKIM" @@ -48,7 +52,7 @@ DKIM is currently supported by either OpenDKIM or Rspamd: The command `docker exec setup config dkim help` details supported config options, along with some examples. - !!! example "Create a DKIM key" + !!! example "Creating a DKIM key" Generate the DKIM files with: @@ -74,6 +78,12 @@ DKIM is currently supported by either OpenDKIM or Rspamd: setup config dkim keysize 2048 ``` + !!! info "Restart required" + + After restarting DMS, outgoing mail will now be signed with your new DKIM key(s) :tada: + + You'll need to repeat this process if you add any new domains. + === "Rspamd" Opt-in via [`ENABLE_RSPAMD=1`][docs-env-rspamd] (_and disable the default OpenDKIM: `ENABLE_OPENDKIM=0`_). @@ -83,31 +93,33 @@ DKIM is currently supported by either OpenDKIM or Rspamd: 1. [Verifying DKIM signatures from inbound mail][rspamd-docs-dkim-checks] is enabled by default. 2. [Signing outbound mail with your DKIM key][rspamd-docs-dkim-signing] needs additional setup (key + dns + config). - !!! example "Create a DKIM key" + !!! example "Creating DKIM Keys" - Presently only OpenDKIM is supported with `setup config dkim`. To generate your DKIM key and DNS files you'll need to specify: + You can simply run - - `-s` The DKIM selector (_eg: `mail`, it can be anything you like_) - - `-d` The sender address domain (_everything after `@` from the email address_) + ```bash + docker exec -ti setup config dkim help + ``` - See `rspamadm dkim_keygen -h` for an overview of the supported options. + which provides you with an overview of what the script can do. Just running + + ```bash + docker exec -ti setup config dkim + ``` + + will execute the helper script with default parameters. + + !!! info "About the Helper Script" + + The script will persist the keys in `/tmp/docker-mailserver/rspamd/dkim/`. Hence, if you are already using the default volume mounts, the keys are persisted in a volume. The script also restarts Rspamd directly, so changes take effect without restarting DMS. + + The script provides you with log messages along the way of creating keys. In case you want to read the complete log, use `-v` (verbose) or `-vv` (very verbose). --- - 1. Go inside the container with `docker exec -ti bash` - 2. Add `rspamd/dkim/` folder to your config volume and switch to it: `cd /tmp/docker-mailserver/rspamd/dkim` - 3. Run: `rspamadm dkim_keygen -s mail -b 2048 -d example.com -k mail.private > mail.txt` (_change `-d` to your domain-part_) - 4. Presently you must ensure Rspamd can read the `.private` file, run: - -`chgrp _rspamd mail.private` - -`chmod g+r mail.private` + In case you have not already provided a default DKIM signing configuration, the script will create one and write it to `/etc/rspamd/override.d/dkim_signing.conf`. If this file already exist, it will not be overwritten. When you're already using [the `rspamd/override.d/` directory][docs-rspamd-override-d], the file is created inside your volume and therefore persisted correctly. If you are not using `rspamd/override.d/`, you will need to persist the file yourself (otherwise it is lost on container restart). - --- - - !!! bug inline end "DMS config volume support is not ready for Rspamd" - - Presently you'll need to [explicitly mount `rspamd/modules/override.d/`][docs-rspamd-config-dropin] as an additional volume; do not use [`rspamd-modules.conf`][docs-rspamd-config-declarative] for this purpose. - - Create a configuration file for the DKIM signing module at `rspamd/modules/override.d/dkim_signing.conf` and populate it with config as shown in the example below: + An example of what a default configuration file for DKIM signing looks like can be found by expanding the example below. ??? example "DKIM Signing Module Configuration Examples" @@ -124,6 +136,7 @@ DKIM is currently supported by either OpenDKIM or Rspamd: use_domain = "header"; use_redis = false; # don't change unless Redis also provides the DKIM keys use_esld = true; + check_pubkey = true; # you wan't to use this in the beginning domain { @@ -134,7 +147,7 @@ DKIM is currently supported by either OpenDKIM or Rspamd: } ``` - As shown next, you can: + As shown next: - You can add more domains into the `domain { ... }` section. - A domain can also be configured with multiple selectors and keys within a `selectors [ ... ]` array. @@ -170,27 +183,19 @@ DKIM is currently supported by either OpenDKIM or Rspamd: } ``` - !!! warning "Support for DKIM keys using Ed25519" + ??? warning "Support for DKIM Keys using ED25519" - This modern elliptic curve is supported by Rspamd, but support by third-parties for [verifying Ed25519 DKIM signatures is unreliable][dkim-ed25519-support]. + This modern elliptic curve is supported by Rspamd, but support by third-parties for [verifying Ed25519 DKIM signatures is unreliable][dkim-ed25519-support]. - If you sign your mail with this key type, you should include RSA as a fallback, like shown in the above example. + If you sign your mail with this key type, you should include RSA as a fallback, like shown in the above example. - !!! tip "DKIM Signing config: `check_pubkey = true;`" + ??? tip "Let Rspamd Check Your Keys" - This setting will have Rspamd query the DNS record for each DKIM selector, verifying each public key matches the private key configured. + When `check_pubkey = true;` is set, Rspamd will query the DNS record for each DKIM selector, verifying each public key matches the private key configured. - If there is a mismatch, a warning will be omitted to the Rspamd log (`/var/log/supervisor/rspamd.log`). + If there is a mismatch, a warning will be omitted to the Rspamd log `/var/log/supervisor/rspamd.log`. -!!! info "Restart required" - - After restarting DMS, outgoing mail will now be signed with your new DKIM key(s) :tada: - - You'll need to repeat this process if you add any new domains. - -!!! warning "RSA Key Sizes >= 4096 Bit" - - Keys of 4096 bits could denied by some mail servers. According to [RFC 6376][rfc-6376] keys are [preferably between 512 and 2048 bits][github-issue-dkimlength]. + [docs-rspamd-override-d]: ../security/rspamd.md#manually ### DNS Record { #dkim-dns } @@ -211,6 +216,8 @@ When mail signed with your DKIM key is sent from your mail server, the receiver | TTL | Use the default (_otherwise [3600 seconds is appropriate][dns::digicert-ttl]_) | | Data | File content within `( ... )` (_formatted as advised below_) | + When using Rspamd, the helper script has already provided you with the contents (the "Data" field) of the DNS record you need to create - you can just copy-paste this text. + === "DNS Zone file" `.txt` is already formatted as a snippet for adding to your [DNS Zone file][dns::wikipedia-zonefile]. diff --git a/target/bin/open-dkim b/target/bin/open-dkim index 1c461ae0..b0a6b712 100755 --- a/target/bin/open-dkim +++ b/target/bin/open-dkim @@ -3,6 +3,12 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh +if [[ -f /etc/dms-settings ]] && [[ $(_get_dms_env_value 'ENABLE_RSPAMD') -eq 1 ]] +then + /usr/local/bin/rspamd-dkim "${@}" + exit +fi + KEYSIZE=4096 SELECTOR=mail DOMAINS= diff --git a/target/bin/rspamd-dkim b/target/bin/rspamd-dkim new file mode 100755 index 00000000..eb555f47 --- /dev/null +++ b/target/bin/rspamd-dkim @@ -0,0 +1,279 @@ +#!/bin/bash + +# shellcheck source=../scripts/helpers/index.sh +source /usr/local/bin/helpers/index.sh + +_trap_err_signal + +set -u -eE -o pipefail +shopt -s inherit_errexit + +# shellcheck source=/dev/null +source /etc/dms-settings + +function __usage +{ + _log 'trace' 'Showing usage message now' + echo -e "${PURPLE}RSPAMD-DKIM${RED}(${YELLOW}8${RED}) + +${ORANGE}NAME${RESET} + rspamd-dkim - Configure DomainKeys Identified Mail (DKIM) via Rspamd + +${ORANGE}SYNOPSIS${RESET} + setup config dkim [ OPTIONS${RED}...${RESET} ] + +${ORANGE}DESCRIPTION${RESET} + This script aids in creating DKIM signing keys. The keys are created and managed by Rspamd. + OPTIONS can be used to configure a more complex setup. + +${ORANGE}OPTIONS${RESET} + ${BLUE}Generic Program Information${RESET} + -v Enable verbose logging (setting the log level to 'debug'). + -vv Enable very verbose logging (setting the log level to 'trace'). + help Print the usage information. + + ${BLUE}Configuration adjustments${RESET} + keytype Set the type of key you want to use + Possible values: rsa, ed25519 + Default: rsa + keysize Set the size of the keys to be generated + Possible values: 1024, 2048 and 4096 + Default: 2048 + Only applies when using keytype=rsa + selector Set a manual selector for the key + Default: mail + domain Provide the domain for which keys are to be generated + Default: primary domain name of DMS + +${ORANGE}EXAMPLES${RESET} + ${LWHITE}setup config dkim keysize 2048${RESET} + Creates keys of length 2048 bit in a default setup where domains are obtained from + your accounts. + + ${LWHITE}setup config dkim keysize 512 selector 2023-dkim${RESET} + Creates keys of length 512 bit in a default setup where domains are obtained from + your accounts. The DKIM selector used is '2023-dkim'. + + ${LWHITE}setup config dkim keysize 1024 selector 2023-dkim domain whoami.com${RESET} + Creates keys of length 1024 bit in a default setup where domains are obtained from your accounts. + The DKIM selector used is '2023-dkim'. The domain for which DKIM keys are generated is whoami.com. + +${ORANGE}EXIT STATUS${RESET} + Exit status is 0 if command was successful. If wrong arguments are provided or arguments contain + errors, the script will exit early with a non-zero exit status. + +" +} + +function __do_as_rspamd_user +{ + local COMMAND=${1:?Command required when using __do_as_rspamd_user} + _log 'trace' "Running '${*}' as user '_rspamd' now" + shift 1 + su -l '_rspamd' -s "$(command -v "${COMMAND}")" -- "${@}" +} + +function _parse_arguments +{ + KEYTYPE='rsa' + KEYSIZE='2048' + SELECTOR='mail' + DOMAIN=${DOMAINNAME} + + _log 'trace' "Options given to this script: '${*}'" + + while [[ ${#} -gt 0 ]] + do + case "${1}" in + + ( 'keytype' ) + [[ -n ${2:-} ]] || _exit_with_error "No keytype provided after 'keytype' argument" + if [[ ${2} == 'rsa' ]] || [[ ${2} == 'ed25519' ]] + then + KEYTYPE=${2} + _log 'debug' "Keytype set to '${KEYTYPE}'" + else + _exit_with_error "Unknown keytype '${2}'" + fi + ;; + + ( 'keysize' ) + [[ -n ${2:-} ]] || _exit_with_error "No keysize provided after 'keysize' argument" + KEYSIZE=${2} + _log 'debug' "Keysize set to '${KEYSIZE}'" + ;; + + ( 'selector' ) + [[ -n ${2:-} ]] || _exit_with_error "No selector provided after 'selector' argument" + SELECTOR=${2} + _log 'debug' "Selector set to '${SELECTOR}'" + ;; + + ( 'domain' ) + [[ -n ${2:-} ]] || _exit_with_error "No domain provided after 'domain' argument" + DOMAIN=${2} + _log 'debug' "Domain set to '${DOMAIN}'" + ;; + + ( 'help' ) + __usage + exit 0 + ;; + + ( '-vv' ) + # shellcheck disable=SC2034 + LOG_LEVEL='trace' + shift 1 + _log 'trace' 'Enabled trace-logging' + continue + ;; + + ( '-v' ) + # shellcheck disable=SC2034 + LOG_LEVEL='debug' + shift 1 + _log 'debug' 'Enabled debug-logging' + continue + ;; + + ( * ) + __usage + _exit_with_error "Unknown option(s) '${1}' ${2:+"and '${2}'"}" + ;; + + esac + + shift 2 + done + + if [[ ${KEYTYPE} == 'ed25519' ]] && [[ ${KEYSIZE} -ne 2048 ]] + then + _exit_with_error "Chosen keytype does not accept the 'keysize' argument" + fi + + return 0 +} + +function _create_keys +{ + # Note: Variables not marked with `local` are used + # in other functions (after this function was called). + BASE_DIR='/tmp/docker-mailserver/rspamd/dkim' + + if [[ ${KEYTYPE} == 'rsa' ]] + then + local BASE_FILE_NAME="${BASE_DIR}/${KEYTYPE}-${KEYSIZE}-${SELECTOR}-${DOMAIN}" + KEYTYPE_OPTIONS=('-b' "${KEYSIZE}") + _log 'info' "Creating DKIM keys of type '${KEYTYPE}' and lenght '${KEYSIZE}' with selector '${SELECTOR}' for domain '${DOMAIN}'" + else + local BASE_FILE_NAME="${BASE_DIR}/${KEYTYPE}-${SELECTOR}-${DOMAIN}" + KEYTYPE_OPTIONS=('-t' "${KEYTYPE}") + _log 'info' "Creating DKIM keys of type '${KEYTYPE}' with selector '${SELECTOR}' for domain '${DOMAIN}'" + fi + + PUBLIC_KEY_FILE="${BASE_FILE_NAME}.public.txt" + PUBLIC_KEY_DNS_FILE="${BASE_FILE_NAME}.public.dns.txt" + PRIVATE_KEY_FILE="${BASE_FILE_NAME}.private.txt" + + mkdir -p "${BASE_DIR}" + chown _rspamd:_rspamd "${BASE_DIR}" + + # shellcheck disable=SC2310 + if __do_as_rspamd_user rspamadm \ + dkim_keygen \ + -s "${SELECTOR}" \ + -d "${DOMAIN}" \ + "${KEYTYPE_OPTIONS[@]}" \ + -k "${PRIVATE_KEY_FILE}" \ + >"${PUBLIC_KEY_FILE}" + then + _log 'info' 'Successfully created DKIM keys' + _log 'debug' "Public key written to '${PUBLIC_KEY_FILE}'" + _log 'debug' "Private key written to '${PRIVATE_KEY_FILE}'" + else + _exit_with_error 'Creating keys failed' + fi +} + +function _check_permissions +{ + # shellcheck disable=SC2310 + if ! __do_as_rspamd_user ls "${BASE_DIR}" >/dev/null + then + _log 'warn' "The Rspamd user ('_rspamd') seems to be unable to list files in the keys directory ('${BASE_DIR}') - Rspamd may experience permission errors later" + elif ! __do_as_rspamd_user cat "${PRIVATE_KEY_FILE}" >/dev/null + then + _log 'warn' "The Rspamd user ('_rspamd') seems to be unable to read the private key file - Rspamd may experience permission errors later" + else + _log 'debug' 'Permissions on files and directories seem ok' + fi +} + +function _setup_default_signing_conf +{ + local DEFAULT_CONFIG_FILE='/etc/rspamd/override.d/dkim_signing.conf' + if [[ -f ${DEFAULT_CONFIG_FILE} ]] + then + _log 'debug' "'${DEFAULT_CONFIG_FILE}' exists, not supplying a default" + else + _log 'info' "Supplying a default configuration ('${DEFAULT_CONFIG_FILE}')" + cat >"${DEFAULT_CONFIG_FILE}" << EOF +# documentation: https://rspamd.com/doc/modules/dkim_signing.html + +enabled = true; + +sign_authenticated = true; +sign_local = true; + +use_domain = "header"; +use_redis = false; # don't change unless Redis also provides the DKIM keys +use_esld = true; + +check_pubkey = true; # you wan't to use this in the beginning + +domain { + ${DOMAIN} { + path = "${PRIVATE_KEY_FILE}"; + selector = "${SELECTOR}"; + } +} + +EOF + chown _rspamd:_rspamd "${DEFAULT_CONFIG_FILE}" + fi +} + +function _transform_public_key_file_to_dns_record_contents +{ + _log 'trace' 'Transforming DNS zone format to DNS record content now' + : >"${PUBLIC_KEY_DNS_FILE}" + grep -o '".*"' "${PUBLIC_KEY_FILE}" | tr -d '"\n' >>"${PUBLIC_KEY_DNS_FILE}" + echo '' >>"${PUBLIC_KEY_DNS_FILE}" + + if ! _log_level_is '(warn|error)' + then + _log 'info' "Here is the content of the TXT DNS record ${SELECTOR}._domainkey.${DOMAIN} that you need to create:\n" + cat "${PUBLIC_KEY_DNS_FILE}" + printf '\n' + fi +} + +function _final_steps +{ + # We need to restart Rspamd so the changes take effect immediately. + if ! supervisorctl restart rspamd + then + _log 'warn' 'Could not restart Rspamd via Supervisord' + fi + + _log 'trace' 'Finished DKIM key creation' +} + +_obtain_hostname_and_domainname +_require_n_parameters_or_print_usage 0 "${@}" +_parse_arguments "${@}" +_create_keys +_check_permissions +_setup_default_signing_conf +_transform_public_key_file_to_dns_record_contents +_final_steps diff --git a/target/scripts/startup/setup.d/security/rspamd.sh b/target/scripts/startup/setup.d/security/rspamd.sh index 1daf0188..e171736a 100644 --- a/target/scripts/startup/setup.d/security/rspamd.sh +++ b/target/scripts/startup/setup.d/security/rspamd.sh @@ -82,7 +82,7 @@ function __rspamd__run_early_setup_and_checks then ln -s "${RSPAMD_DMS_OVERRIDE_D}" "${RSPAMD_OVERRIDE_D}" else - __rspamd__log 'warn' "Could not remove '${RSPAMD_OVERRIDE_D}' - not linking '${RSPAMD_DMS_OVERRIDE_D}'" + __rspamd__log 'warn' "Could not remove '${RSPAMD_OVERRIDE_D}' (not empty?) - not linking '${RSPAMD_DMS_OVERRIDE_D}'" fi fi diff --git a/test/tests/parallel/set1/spam_virus/rspamd_dkim.bats b/test/tests/parallel/set1/spam_virus/rspamd_dkim.bats new file mode 100644 index 00000000..0f615d94 --- /dev/null +++ b/test/tests/parallel/set1/spam_virus/rspamd_dkim.bats @@ -0,0 +1,272 @@ +load "${REPOSITORY_ROOT}/test/helper/common" +load "${REPOSITORY_ROOT}/test/helper/setup" + +BATS_TEST_NAME_PREFIX='[Rspamd] (DKIM) ' +CONTAINER_NAME='dms-test_rspamd-dkim' + +DOMAIN_NAME='fixed.com' +SIGNING_CONF_FILE='/etc/rspamd/override.d/dkim_signing.conf' + +function setup_file() { + _init_with_defaults + + # Comment for maintainers about `PERMIT_DOCKER=host`: + # https://github.com/docker-mailserver/docker-mailserver/pull/2815/files#r991087509 + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_RSPAMD=1 + --env ENABLE_OPENDKIM=0 + --env ENABLE_OPENDMARC=0 + --env ENABLE_POLICYD_SPF=0 + --env LOG_LEVEL=trace + --env OVERRIDE_HOSTNAME="mail.${DOMAIN_NAME}" + ) + + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + _wait_for_service rspamd-redis + _wait_for_service rspamd +} + +# We want each test to start with a clean state. +function teardown() { + __remove_signing_config_file + _run_in_container rm -rf /tmp/docker-mailserver/rspamd/dkim + assert_success +} + +function teardown_file() { _default_teardown ; } + +@test 'log level is applied correctly' { + _run_in_container setup config dkim -vv help + __log_is_free_of_warnings_and_errors + assert_output --partial 'Enabled trace-logging' + + _run_in_container setup config dkim -v help + __log_is_free_of_warnings_and_errors + assert_output --partial 'Enabled debug-logging' +} + +@test 'help message is properly shown' { + _run_in_container setup config dkim help + __log_is_free_of_warnings_and_errors + assert_output --partial 'Showing usage message now' + assert_output --partial 'rspamd-dkim - Configure DomainKeys Identified Mail (DKIM) via Rspamd' +} + +@test 'default signing config is created if it does not exist and not overwritten' { + # Required pre-condition: no default configuration is present + __remove_signing_config_file + + __create_key + assert_success + __log_is_free_of_warnings_and_errors + assert_output --partial "Supplying a default configuration ('${SIGNING_CONF_FILE}')" + refute_output --partial "'${SIGNING_CONF_FILE}' exists, not supplying a default" + assert_output --partial "Finished DKIM key creation" + _run_in_container_bash "[[ -f ${SIGNING_CONF_FILE} ]]" + assert_success + _exec_in_container_bash "echo "blabla" >${SIGNING_CONF_FILE}" + local INITIAL_SHA512_SUM=$(_exec_in_container sha512sum "${SIGNING_CONF_FILE}") + + __create_key + __log_is_free_of_warnings_and_errors + refute_output --partial "Supplying a default configuration ('${SIGNING_CONF_FILE}')" + assert_output --partial "'${SIGNING_CONF_FILE}' exists, not supplying a default" + assert_output --partial "Finished DKIM key creation" + local SECOND_SHA512_SUM=$(_exec_in_container sha512sum "${SIGNING_CONF_FILE}") + assert_equal "${INITIAL_SHA512_SUM}" "${SECOND_SHA512_SUM}" +} + +@test 'default directories and files are created' { + __create_key + assert_success + + _count_files_in_directory_in_container /tmp/docker-mailserver/rspamd/dkim/ 3 + _run_in_container_bash "[[ -f ${SIGNING_CONF_FILE} ]]" + assert_success + + __check_path_in_signing_config "/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-${DOMAIN_NAME}.private.txt" + __check_selector_in_signing_config 'mail' +} + +@test "argument 'domain' is applied correctly" { + for DOMAIN in 'blabla.org' 'someother.com' 'random.de' + do + _run_in_container setup config dkim domain "${DOMAIN}" + assert_success + assert_line --partial "Domain set to '${DOMAIN}'" + + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-${DOMAIN}" + __check_key_files_are_present "${BASE_FILE_NAME}" + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + __remove_signing_config_file + done +} + +@test "argument 'keytype' is applied correctly" { + _run_in_container setup config dkim keytype foobar + assert_failure + assert_line --partial "Unknown keytype 'foobar'" + + for KEYTYPE in 'rsa' 'ed25519' + do + _run_in_container setup config dkim keytype "${KEYTYPE}" + assert_success + assert_line --partial "Keytype set to '${KEYTYPE}'" + + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/ed25519-mail-${DOMAIN_NAME}" + [[ ${KEYTYPE} == 'rsa' ]] && BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-${DOMAIN_NAME}" + __check_key_files_are_present "${BASE_FILE_NAME}" + + _run_in_container grep ".*k=${KEYTYPE};.*" "${BASE_FILE_NAME}.public.txt" + assert_success + _run_in_container grep ".*k=${KEYTYPE};.*" "${BASE_FILE_NAME}.public.dns.txt" + assert_success + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + __remove_signing_config_file + done +} + +@test "argument 'selector' is applied correctly" { + for SELECTOR in 'foo' 'bar' 'baz' + do + __create_key 'rsa' "${SELECTOR}" + assert_success + assert_line --partial "Selector set to '${SELECTOR}'" + + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/rsa-2048-${SELECTOR}-${DOMAIN_NAME}" + __check_key_files_are_present "${BASE_FILE_NAME}" + _run_in_container grep "^${SELECTOR}\._domainkey.*" "${BASE_FILE_NAME}.public.txt" + assert_success + + __check_rsa_keys 2048 "${SELECTOR}-${DOMAIN_NAME}" + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + __check_selector_in_signing_config "${SELECTOR}" + __remove_signing_config_file + done +} + +@test "argument 'keysize' is applied correctly for RSA keys" { + for KEYSIZE in 512 1024 2048 4096 + do + __create_key 'rsa' 'mail' "${DOMAIN_NAME}" "${KEYSIZE}" + assert_success + __log_is_free_of_warnings_and_errors + assert_line --partial "Keysize set to '${KEYSIZE}'" + __check_rsa_keys "${KEYSIZE}" "mail-${DOMAIN_NAME}" + __remove_signing_config_file + done +} + +@test "when 'keytype=ed25519' is set, setting custom 'keysize' is rejected" { + __create_key 'ed25519' 'mail' "${DOMAIN_NAME}" 4096 + assert_failure + assert_line --partial "Chosen keytype does not accept the 'keysize' argument" +} + +@test "setting all arguments to a custom value works" { + local KEYTYPE='ed25519' + local SELECTOR='someselector' + local DOMAIN='dms.org' + + __create_key "${KEYTYPE}" "${SELECTOR}" "${DOMAIN}" + assert_success + __log_is_free_of_warnings_and_errors + + assert_line --partial "Keytype set to '${KEYTYPE}'" + assert_line --partial "Selector set to '${SELECTOR}'" + assert_line --partial "Domain set to '${DOMAIN}'" + + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/${KEYTYPE}-${SELECTOR}-${DOMAIN}" + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + __check_selector_in_signing_config 'someselector' +} + +# Create DKIM keys. +# +# @param ${1} = keytype (default: rsa) +# @param ${2} = selector (default: mail) +# @param ${3} = domain (default: ${DOMAIN}) +# @param ${4} = keysize (default: 2048) +function __create_key() { + local KEYTYPE=${1:-rsa} + local SELECTOR=${2:-mail} + local DOMAIN=${3:-${DOMAIN_NAME}} + local KEYSIZE=${4:-2048} + + _run_in_container setup config dkim \ + keytype "${KEYTYPE}" \ + keysize "${KEYSIZE}" \ + selector "${SELECTOR}" \ + domain "${DOMAIN}" +} + +# Check whether an RSA key is created successfully and correctly +# for a specific key size. +# +# @param ${1} = key size +# @param ${2} = name of the selector and domain name (as one string) +function __check_rsa_keys() { + local KEYSIZE=${1:?Keysize must be supplied to __check_rsa_keys} + local SELECTOR_AND_DOMAIN=${2:?Selector and domain name must be supplied to __check_rsa_keys} + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/rsa-${KEYSIZE}-${SELECTOR_AND_DOMAIN}" + + __check_key_files_are_present "${BASE_FILE_NAME}" + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + + # Check the private key matches the specification + _run_in_container_bash "openssl rsa -in '${BASE_FILE_NAME}.private.txt' -noout -text" + assert_success + assert_line --index 0 "RSA Private-Key: (${KEYSIZE} bit, 2 primes)" + + # Check the public key matches the specification + # + # We utilize the file for the DNS record contents which is already created + # by the Rspamd DKIM helper script. This makes parsing easier here. + local PUBKEY PUBKEY_INFO + PUBKEY=$(_exec_in_container_bash "grep -o 'p=.*' ${BASE_FILE_NAME}.public.dns.txt") + _run_in_container_bash "openssl enc -base64 -d <<< ${PUBKEY#p=} | openssl pkey -inform DER -pubin -noout -text" + assert_success + assert_line --index 0 "RSA Public-Key: (${KEYSIZE} bit)" +} + +# Verify that all DKIM key files are present. +# +# @param ${1} = base file name that all DKIM key files have +function __check_key_files_are_present() { + local BASE_FILE_NAME="${1:?Base file name must be supplied to __check_key_files_are_present}" + for FILE in ${BASE_FILE_NAME}.{public.txt,public.dns.txt,private.txt} + do + _run_in_container_bash "[[ -f ${FILE} ]]" + assert_success + done +} + +# Check whether `path = .*` is set correctly in the signing configuration file. +# +# @param ${1} = file name that `path` should be set to +function __check_path_in_signing_config() { + local BASE_FILE_NAME=${1:?Base file name must be supplied to __check_path_in_signing_config} + _run_in_container grep "[[:space:]]*path = \"${BASE_FILE_NAME}\";" "${SIGNING_CONF_FILE}" + assert_success +} + +# Check whether `selector = .*` is set correctly in the signing configuration file. +# +# @param ${1} = name that `selector` should be set to +function __check_selector_in_signing_config() { + local SELECTOR=${1:?Selector name must be supplied to __check_selector_in_signing_config} + _run_in_container grep "[[:space:]]*selector = \"${SELECTOR}\";" "${SIGNING_CONF_FILE}" + assert_success +} + +# Check whether the script output is free of warnings and errors. +function __log_is_free_of_warnings_and_errors() { + assert_success + refute_output --partial '[ WARN ]' + refute_output --partial '[ ERROR ]' +} + +# Remove the signing configuration file inside the container. +function __remove_signing_config_file() { + _exec_in_container rm -f "${SIGNING_CONF_FILE}" +}