author | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-10-21 12:45:45 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-11-30 10:03:48 UTC |
parent | ebad590c31094a8e5e3dbfad8e49510392cf358d |
docs/dkim.md | +33 | -0 |
etc/chasquid/hooks/post-data | +21 | -0 |
test/Dockerfile | +9 | -3 |
test/t-15-driusan_dkim/config/chasquid.conf | +9 | -0 |
test/t-15-driusan_dkim/config/domains/testserver/dkim_selector | +1 | -0 |
test/t-15-driusan_dkim/config/hooks/post-data | +19 | -0 |
test/t-15-driusan_dkim/content | +9 | -0 |
test/t-15-driusan_dkim/hosts | +1 | -0 |
test/t-15-driusan_dkim/msmtprc | +14 | -0 |
test/t-15-driusan_dkim/run.sh | +51 | -0 |
test/util/smtpc.py | +18 | -5 |
diff --git a/docs/dkim.md b/docs/dkim.md new file mode 100644 index 0000000..4889e21 --- /dev/null +++ b/docs/dkim.md @@ -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 diff --git a/etc/chasquid/hooks/post-data b/etc/chasquid/hooks/post-data index b6a937c..6f2ea6e 100755 --- a/etc/chasquid/hooks/post-data +++ b/etc/chasquid/hooks/post-data @@ -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" 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 diff --git a/test/Dockerfile b/test/Dockerfile index 41c0d5d..146f0aa 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -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 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 + +# 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. diff --git a/test/t-15-driusan_dkim/config/chasquid.conf b/test/t-15-driusan_dkim/config/chasquid.conf new file mode 100644 index 0000000..2da8942 --- /dev/null +++ b/test/t-15-driusan_dkim/config/chasquid.conf @@ -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" diff --git a/test/t-15-driusan_dkim/config/domains/testserver/dkim_selector b/test/t-15-driusan_dkim/config/domains/testserver/dkim_selector new file mode 100644 index 0000000..59ccb93 --- /dev/null +++ b/test/t-15-driusan_dkim/config/domains/testserver/dkim_selector @@ -0,0 +1 @@ +testselector1 diff --git a/test/t-15-driusan_dkim/config/hooks/post-data b/test/t-15-driusan_dkim/config/hooks/post-data new file mode 100755 index 0000000..354a849 --- /dev/null +++ b/test/t-15-driusan_dkim/config/hooks/post-data @@ -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 diff --git a/test/t-15-driusan_dkim/content b/test/t-15-driusan_dkim/content new file mode 100644 index 0000000..fa095d3 --- /dev/null +++ b/test/t-15-driusan_dkim/content @@ -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. diff --git a/test/t-15-driusan_dkim/hosts b/test/t-15-driusan_dkim/hosts new file mode 100644 index 0000000..2b9b623 --- /dev/null +++ b/test/t-15-driusan_dkim/hosts @@ -0,0 +1 @@ +testserver localhost diff --git a/test/t-15-driusan_dkim/msmtprc b/test/t-15-driusan_dkim/msmtprc new file mode 100644 index 0000000..8d191e1 --- /dev/null +++ b/test/t-15-driusan_dkim/msmtprc @@ -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 + diff --git a/test/t-15-driusan_dkim/run.sh b/test/t-15-driusan_dkim/run.sh new file mode 100755 index 0000000..c0825f1 --- /dev/null +++ b/test/t-15-driusan_dkim/run.sh @@ -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 diff --git a/test/util/smtpc.py b/test/util/smtpc.py index 8d459c2..bca1c13 100755 --- a/test/util/smtpc.py +++ b/test/util/smtpc.py @@ -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 = sys.stdin.buffer.read() +msg = email.parser.Parser(policy=email.policy.default).parsestr( + rawmsg.decode('utf8')) s = smtplib.SMTP(args.server) 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 -# are ok). -s.send_message(msg) +# 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) + +s.sendmail( + from_addr=msg['from'], to_addrs=msg.get_all('to'), + msg=crlfmsg, + mail_options=['SMTPUTF8']) s.quit()