git » chasquid » commit 4ecc546

Add driusan/dkim integration example and tests

author Alberto Bertogli
2018-10-21 12:45:45 UTC
committer Alberto Bertogli
2018-11-30 10:03:48 UTC
parent ebad590c31094a8e5e3dbfad8e49510392cf358d

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.

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()