mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
Add driusan/dkim integration example and tests
This patch adds DKIM signing using https://github.com/driusan/dkim tools to the example hook. It also adds an optional integration test to exercise signing and verification, and corresponding documentation.
This commit is contained in:
33
docs/dkim.md
Normal file
33
docs/dkim.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
# DKIM integration
|
||||||
|
|
||||||
|
[chasquid] supports generating [DKIM] signatures via the [hooks](hooks.md)
|
||||||
|
mechanism.
|
||||||
|
|
||||||
|
|
||||||
|
## Signing
|
||||||
|
|
||||||
|
The example hook in this repository contains an example of integration with
|
||||||
|
[driusan/dkim](https://github.com/driusan/dkim) tools, and assumes the
|
||||||
|
following:
|
||||||
|
|
||||||
|
- The [selector](https://tools.ietf.org/html/rfc6376#section-3.1) 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
|
||||||
|
`certs/$DOMAIN/dkim_privkey.pem`.
|
||||||
|
|
||||||
|
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](https://tools.ietf.org/html/rfc6376#section-6.3),
|
||||||
|
[source 2](https://tools.ietf.org/html/rfc7601#section-2.7.1)), so it is not
|
||||||
|
included in the example.
|
||||||
|
|
||||||
|
|
||||||
|
[chasquid]: https://blitiri.com.ar/p/chasquid
|
||||||
|
[DKIM]: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
# - greylist (from greylistd) to do greylisting.
|
# - greylist (from greylistd) to do greylisting.
|
||||||
# - spamc (from Spamassassin) to filter spam.
|
# - spamc (from Spamassassin) to filter spam.
|
||||||
# - clamdscan (from ClamAV) to filter virus.
|
# - 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.
|
# If it exits with code 20, it will be considered a permanent error.
|
||||||
# Otherwise, temporary.
|
# Otherwise, temporary.
|
||||||
@@ -53,3 +54,23 @@ if command -v clamdscan >/dev/null; then
|
|||||||
echo "X-Virus-Scanned: pass"
|
echo "X-Virus-Scanned: pass"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# DKIM sign with https://github.com/driusan/dkim.
|
||||||
|
#
|
||||||
|
# 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"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
@@ -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.
|
# Packages for the (optional) TLS tracking test.
|
||||||
RUN apt-get install -y -q dnsmasq
|
RUN apt-get install -y -q dnsmasq
|
||||||
|
|
||||||
|
# Packages for the (optional) DKIM integration test.
|
||||||
|
RUN go get github.com/driusan/dkim/... \
|
||||||
|
&& go install github.com/driusan/dkim/cmd/dkimsign \
|
||||||
|
&& go install github.com/driusan/dkim/cmd/dkimverify \
|
||||||
|
&& go install github.com/driusan/dkim/cmd/dkimkeygen
|
||||||
|
|
||||||
RUN go get -d ./...
|
# Install chasquid and its dependencies.
|
||||||
RUN go install ./...
|
RUN go get -d -v ./...
|
||||||
|
RUN go install -v ./...
|
||||||
|
|
||||||
# Don't run the tests as root: it makes some integration tests more difficult,
|
# 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.
|
# as for example Exim has hard-coded protections against running as root.
|
||||||
|
|||||||
9
test/t-15-driusan_dkim/config/chasquid.conf
Normal file
9
test/t-15-driusan_dkim/config/chasquid.conf
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
testselector1
|
||||||
19
test/t-15-driusan_dkim/config/hooks/post-data
Executable file
19
test/t-15-driusan_dkim/config/hooks/post-data
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# https://tools.ietf.org/html/rfc6376#section-2.2
|
||||||
|
#
|
||||||
|
# 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"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec dkimverify -txt ../.dkimcerts/dns.txt
|
||||||
9
test/t-15-driusan_dkim/content
Normal file
9
test/t-15-driusan_dkim/content
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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.
|
||||||
1
test/t-15-driusan_dkim/hosts
Normal file
1
test/t-15-driusan_dkim/hosts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
testserver localhost
|
||||||
14
test/t-15-driusan_dkim/msmtprc
Normal file
14
test/t-15-driusan_dkim/msmtprc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
|
|
||||||
51
test/t-15-driusan_dkim/run.sh
Executable file
51
test/t-15-driusan_dkim/run.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Test integration with driusan's DKIM tools.
|
||||||
|
# https://github.com/driusan/dkim
|
||||||
|
|
||||||
|
set -e
|
||||||
|
. $(dirname ${0})/../util/lib.sh
|
||||||
|
|
||||||
|
init
|
||||||
|
|
||||||
|
for binary in dkimsign dkimverify dkimkeygen; do
|
||||||
|
if ! which $binary > /dev/null; then
|
||||||
|
skip "$binary binary not found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
|
smtpc.py --server=localhost:1025 < .signed_content
|
||||||
|
|
||||||
|
# Check that the signature fails on modified content.
|
||||||
|
echo "Added content, invalid and not signed" >> .signed_content
|
||||||
|
if smtpc.py --server=localhost:1025 < .signed_content 2> /dev/null; then
|
||||||
|
fail "DKIM verification succeeded on modified content"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import email.parser
|
import email.parser
|
||||||
import email.policy
|
import email.policy
|
||||||
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -16,15 +17,27 @@ args = ap.parse_args()
|
|||||||
|
|
||||||
# Parse the email using the "default" policy, which is not really the default.
|
# Parse the email using the "default" policy, which is not really the default.
|
||||||
# If unspecified, compat32 is used, which does not support UTF8.
|
# If unspecified, compat32 is used, which does not support UTF8.
|
||||||
msg = email.parser.Parser(policy=email.policy.default).parse(sys.stdin)
|
rawmsg = sys.stdin.buffer.read()
|
||||||
|
msg = email.parser.Parser(policy=email.policy.default).parsestr(
|
||||||
|
rawmsg.decode('utf8'))
|
||||||
|
|
||||||
s = smtplib.SMTP(args.server)
|
s = smtplib.SMTP(args.server)
|
||||||
s.starttls()
|
s.starttls()
|
||||||
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
|
# Send the raw message, not parsed, because the parser does not handle some
|
||||||
# are ok).
|
# corner cases that well (for example, DKIM-Signature headers get mime-encoded
|
||||||
s.send_message(msg)
|
# 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)
|
||||||
|
|
||||||
|
s.sendmail(
|
||||||
|
from_addr=msg['from'], to_addrs=msg.get_all('to'),
|
||||||
|
msg=crlfmsg,
|
||||||
|
mail_options=['SMTPUTF8'])
|
||||||
s.quit()
|
s.quit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user