Commit 4ecc5461 authored by Alberto Bertogli's avatar Alberto Bertogli

Add driusan/dkim integration example and tests

This patch adds DKIM signing using tools
to the example hook.

It also adds an optional integration test to exercise signing and
verification, and corresponding documentation.
parent ebad590c
# DKIM integration
[chasquid] supports generating [DKIM] signatures via the [hooks](
## Signing
The example hook in this repository contains an example of integration with
[driusan/dkim]( tools, and assumes the
- The [selector]( for a domain
can be found in the file `domains/$DOMAIN/dkim_selector`.
- The private key to use for signing can be found in the file
Only authenticated email will be signed.
## Verification
Verifying signatures is technically supported as well, and can be done in the
same hook. However, it's not recommended for SMTP servers to reject mail on
verification failures
([source 1](,
[source 2](, so it is not
included in the example.
......@@ -6,6 +6,7 @@
# - greylist (from greylistd) to do greylisting.
# - spamc (from Spamassassin) to filter spam.
# - clamdscan (from ClamAV) to filter virus.
# - dkimsign (from driusan/dkim) to do DKIM signing.
# If it exits with code 20, it will be considered a permanent error.
# Otherwise, temporary.
......@@ -53,3 +54,23 @@ if command -v clamdscan >/dev/null; then
echo "X-Virus-Scanned: pass"
# DKIM sign with
# Do it only if all the following are true:
# - User has authenticated.
# - dkimsign binary exists.
# - domains/$DOMAIN/dkim_selector file exists.
# - certs/$DOMAIN/dkim_privkey.pem file exists.
# Note this has not been thoroughly tested, so might need further adjustments.
if [ "$AUTH_AS" != "" ] && command -v dkimsign; then
DOMAIN=$( echo "$MAIL_FROM" | cut -d '@' -f 2 )
if [ -f "domains/$DOMAIN/dkim_selector" ] \
&& [ -f "certs/$DOMAIN/dkim_privkey.pem" ]; then
dkimsign -n -hd \
-key "certs/$DOMAIN/dkim_privkey.pem" \
-s $(cat "domains/$DOMAIN/dkim_selector") \
-d "$DOMAIN" \
< "$TF"
......@@ -32,9 +32,15 @@ RUN cd test/t-02-exim && mkdir -p .exim4 && ln -s /usr/sbin/exim4 .exim4/
# Packages for the (optional) TLS tracking test.
RUN apt-get install -y -q dnsmasq
RUN go get -d ./...
RUN go install ./...
# Packages for the (optional) DKIM integration test.
RUN go get \
&& go install \
&& go install \
&& go install
# Install chasquid and its dependencies.
RUN go get -d -v ./...
RUN go install -v ./...
# Don't run the tests as root: it makes some integration tests more difficult,
# as for example Exim has hard-coded protections against running as root.
smtp_address: ":1025"
submission_address: ":1587"
monitoring_address: ":1099"
mail_delivery_agent_bin: "test-mda"
mail_delivery_agent_args: "%to%"
data_dir: "../.data"
mail_log_path: "../.logs/mail_log"
# If authenticated, sign; otherwise, verify.
# It is not recommended that we fail delivery on dkim verification failures,
# but leave it to the MUA to handle verifications.
# We do a verification here so we have a stronger integration test (check
# encodings/dot-stuffing/etc. works ok), but it's not recommended for general
# purposes.
if [ "$AUTH_AS" != "" ]; then
DOMAIN=$( echo "$MAIL_FROM" | cut -d '@' -f 2 )
exec dkimsign -n -hd -key ../.dkimcerts/private.pem \
-s $(cat "domains/$DOMAIN/dkim_selector") -d "$DOMAIN"
exec dkimverify -txt ../.dkimcerts/dns.txt
Subject: Prueba desde el test
To: someone@testserver
Crece desde el test el futuro
Crece desde el test
El punto de arriba testea el dot-stuffing, que es importante para DKIM.
testserver localhost
account default
host testserver
port 1587
tls on
tls_trust_file config/certs/testserver/fullchain.pem
from user@testserver
auth on
user user@testserver
password secretpassword
# Test integration with driusan's DKIM tools.
set -e
. $(dirname ${0})/../util/
for binary in dkimsign dkimverify dkimkeygen; do
if ! which $binary > /dev/null; then
skip "$binary binary not found"
exit 0
generate_certs_for testserver
( mkdir -p .dkimcerts; cd .dkimcerts; dkimkeygen )
add_user user@testserver secretpassword
add_user someone@testserver secretpassword
mkdir -p .logs
chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config &
wait_until_ready 1025
# Authenticated: user@testserver -> someone@testserver
# Should be signed.
run_msmtp someone@testserver < content
wait_for_file .mail/someone@testserver
mail_diff content .mail/someone@testserver
grep -q "DKIM-Signature:" .mail/someone@testserver
# Verify the signature manually, just in case.
dkimverify -txt .dkimcerts/dns.txt < .mail/someone@testserver
# Save the signed mail so we can verify it later.
# Drop the first line ("From blah") so it can be used as email contents.
tail -n +2 .mail/someone@testserver > .signed_content
# Not authenticated: someone@testserver -> someone@testserver --server=localhost:1025 < .signed_content
# Check that the signature fails on modified content.
echo "Added content, invalid and not signed" >> .signed_content
if --server=localhost:1025 < .signed_content 2> /dev/null; then
fail "DKIM verification succeeded on modified content"
......@@ -5,6 +5,7 @@
import argparse
import email.parser
import email.policy
import re
import smtplib
import sys
......@@ -16,15 +17,27 @@ args = ap.parse_args()
# Parse the email using the "default" policy, which is not really the default.
# If unspecified, compat32 is used, which does not support UTF8.
msg = email.parser.Parser(policy=email.policy.default).parse(sys.stdin)
rawmsg =
msg = email.parser.Parser(policy=email.policy.default).parsestr(
s = smtplib.SMTP(args.server)
s.login(args.user, args.password)
if args.user:
s.login(args.user, args.password)
# Note this does NOT support non-ascii message payloads transparently (headers
# are ok).
# Send the raw message, not parsed, because the parser does not handle some
# corner cases that well (for example, DKIM-Signature headers get mime-encoded
# incorrectly).
# Replace \n with \r\n, which is normally done by the library, but will not do
# it in this case because we are giving it bytes and not a string (which we
# cannot do because it tries to incorrectly escape the headers).
crlfmsg = re.sub(br'(?:\r\n|\n|\r(?!\n))', b"\r\n", rawmsg)
from_addr=msg['from'], to_addrs=msg.get_all('to'),
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment