diff --git a/.gitignore b/.gitignore index 9e59230..8bd7e59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist dkimpy_milter.egg-info *.pyc +*~ diff --git a/dkimpy_milter/__init__.py b/dkimpy_milter/__init__.py index 3791748..5345fc7 100644 --- a/dkimpy_milter/__init__.py +++ b/dkimpy_milter/__init__.py @@ -61,7 +61,9 @@ class dkimMilter(Milter.Base): self.external_connection = False self.hello_name = None # sometimes people put extra space in sendmail config, so we strip - self.receiver = self.getsymval('j').strip() + self.receiver = self.getsymval('j') + if self.receiver is not None: + self.receiver = self.receiver.strip() try: self.AuthservID = milterconfig['AuthservID'] except: @@ -258,7 +260,12 @@ class dkimMilter(Milter.Base): for y in range(self.has_dkim): # Verify _ALL_ the signatures d = dkim.DKIM(txt) try: - res = d.verify(idx=y) + dnsoverride = milterconfig.get('DNSOverride') + if isinstance(dnsoverride, str): + syslog.syslog("DNSOverride: {0}".format(dnsoverride)) + res = d.verify(idx=y, dnsfunc=lambda _x: dnsoverride) + else: + res = d.verify(idx=y) if res: if d.signature_fields.get(b'a') == 'ed25519-sha256': self.dkim_comment = ('Good {0} signature' diff --git a/dkimpy_milter/__main__.py b/dkimpy_milter/__main__.py new file mode 100644 index 0000000..8c5cf9c --- /dev/null +++ b/dkimpy_milter/__main__.py @@ -0,0 +1,6 @@ +#!/usr/bin/python2 + +from dkimpy_milter import main + +if __name__ == "__main__": + main() diff --git a/dkimpy_milter/config.py b/dkimpy_milter/config.py index 9f42af2..d562e97 100644 --- a/dkimpy_milter/config.py +++ b/dkimpy_milter/config.py @@ -48,6 +48,7 @@ defaultConfigData = { 'DiagnosticDirectory': '', 'MacroList': '', 'MacroListVerify': '', + 'DNSOverride': None, 'debugLevel': 0 # Undocumented config item for developer use } @@ -334,6 +335,7 @@ def _readConfigFile(path, configData=None, configGlobal={}): 'DiagnosticDirectory': 'str', 'MacroList': 'dataset', 'MacroListVerify': 'dataset', + 'DNSOverride': 'str', 'debugLevel': 'int' } @@ -388,7 +390,10 @@ def _readConfigFile(path, configData=None, configGlobal={}): if conversion == 'bool': configData[name] = _find_boolean(value) elif conversion == 'str': - configData[name] = str(value) + if isinstance(value, list): + configData[name] = line.split(None, 1)[1] + else: + configData[name] = str(value) elif conversion == 'int': configData[name] = int(value) elif conversion == 'dataset': @@ -399,7 +404,7 @@ def _readConfigFile(path, configData=None, configGlobal={}): configData[name] = conversion(value) fp.close() try: - configData['AuthservID'] = _make_authserv_id(configData['AuthservID']) + configData['AuthservID'] = _make_authserv_id(configData.get('AuthservID', 'HOSTNAME')) configData['IntHosts'] = HostsDataset(configData['InternalHosts']) except: pass diff --git a/dkimpy_milter/util.py b/dkimpy_milter/util.py index 5d3f69d..17857d6 100644 --- a/dkimpy_milter/util.py +++ b/dkimpy_milter/util.py @@ -150,10 +150,18 @@ def own_socketfile(milterconfig): """If socket is Unix socket, chown to UserID before dropping privileges""" import os user, group = user_group(milterconfig.get('UserID')) - if milterconfig.get('Socket')[:1] == '/': - os.chown(milterconfig.get('Socket')[1:], user, group) - if milterconfig.get('Socket')[:6] == "local:": - os.chown(milterconfig.get('Socket')[6:], user, group) + offset = None + sockname = milterconfig.get('Socket') + if sockname[:1] == '/': + offset = 0 + elif sockname[:6] == "local:": + offset = 6 + elif sockname[:5] == "unix:": + offset = 5 + + if offset is not None: + if os.path.exists(sockname[offset:]): + os.chown(sockname[offset:], user, group) def read_keyfile(milterconfig, keytype): diff --git a/man/dkimpy-milter.conf.5 b/man/dkimpy-milter.conf.5 index 3dd7612..a7e5d31 100644 --- a/man/dkimpy-milter.conf.5 +++ b/man/dkimpy-milter.conf.5 @@ -311,6 +311,13 @@ be set: (b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector; [fooTable options NOT IMPLEMENTED] +.TP +.I DNSOverride (string) +Provide a text string that a verifying milter should use instead of +consulting the DNS on each message. This is useful primarily for +testing purposes in environments where it is awkward to modify the +system DNS resolution. It should not be used in production. + .TP .I PeerList (dataset) Identifies a set of "peers" that identifies clients whose connections diff --git a/setup.py b/setup.py index f3c9f26..c117221 100644 --- a/setup.py +++ b/setup.py @@ -23,10 +23,10 @@ description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for P kw = {} # Work-around for lack of 'or' requires in setuptools. try: - import DNS - kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'PyDNS'] -except ImportError: # If PyDNS is not installed, prefer dnspython + import dns kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dnspython'] +except ImportError: # If PyDNS is not installed, prefer dnspython + kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'PyDNS'] setup( name='dkimpy-milter', diff --git a/tests/00_minimal.miltertest b/tests/00_minimal.miltertest new file mode 100644 index 0000000..fbe0849 --- /dev/null +++ b/tests/00_minimal.miltertest @@ -0,0 +1,12 @@ +-- -*- lua -*- +for _, keytype in ipairs({"ed25519", "rsa"}) do + for _, func in ipairs({"signing", "verify"}) do + mt.echo("testing "..keytype.." "..func) + conn = mt.connect("unix:"..keytype.."."..func..".sock") + if conn == nil then + error("mt.connect() failed "..keytype.." "..func) + end + mt.disconnect(conn) + mt.echo(keytype.." "..func.." complete") + end +end diff --git a/tests/01_connect.miltertest b/tests/01_connect.miltertest new file mode 100755 index 0000000..2f43eff --- /dev/null +++ b/tests/01_connect.miltertest @@ -0,0 +1,40 @@ +-- -*- lua -*- +for _, keytype in ipairs({"ed25519", "rsa"}) do + for _, func in ipairs({"signing", "verify"}) do + mt.echo("testing "..keytype.." "..func) + conn = mt.connect("unix:"..keytype.."."..func..".sock") + if conn == nil then + error("mt.connect() failed "..keytype.." "..func) + end + if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then + error("mt.conninfo() failed "..keytype.." "..func) + end + if mt.getreply(conn) ~= SMFIR_CONTINUE then + error("mt.conninfo() unexpected reply "..keytype.." "..func) + end + + if mt.test_action(conn, SMFIF_ADDHDRS) then + print("could add headers "..keytype.." "..func) + else + error("mt.test_action() says could not add headers "..keytype.." "..func) + end + + if mt.test_action(conn, SMFIF_CHGHDRS) then + print("could change headers "..keytype.." "..func) + else + error("mt.test_action() says could not change headers "..keytype.." "..func) + end + +-- -- FIXME: this part of the test fails, as apparently the +-- -- dkimpy-milter claims the right to change the body of a message, +-- -- even though it shouldn't. How can we fix the negotiation? +-- if mt.test_action(conn, SMFIF_CHGBODY) then +-- error("mt.test_action() says could change body "..keytype.." "..func) +-- else +-- print("could not change body "..keytype.." "..func) +-- end + + mt.disconnect(conn) + mt.echo(keytype.." "..func.." test complete") + end +end diff --git a/tests/02_sign_message.miltertest b/tests/02_sign_message.miltertest new file mode 100644 index 0000000..cb5e7ff --- /dev/null +++ b/tests/02_sign_message.miltertest @@ -0,0 +1,100 @@ +-- -*- lua -*- + +msg = { + ['headers'] = { + ['From'] = 'Alice ', + ['Message-Id'] = '', + ['To'] = 'Bob ', + ['Date'] = 'Mon, 18 Feb 2019 08:32:50 -0500', + ['Subject'] = 'Signing test', + ['Content-Type'] = 'text/plain', + }, + ['body'] = "This is a test!\r\n", +} + +-- returns miltertest connection object +function connect_and_send (sockname, headers, body) + conn = mt.connect(sockname) + if conn == nil then + error "mt.connect() failed" + end + if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then + error "mt.conninfo() failed" + end + if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.conninfo() unexpected reply" + end + + -- mt.macro(conn, SMFIC_MAIL, "i", "simple-message") + if mt.mailfrom(conn, "") ~= nil then + error "mt.mailfrom() failed" + end + if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.mailfrom() unexpected reply" + end + -- mt.rcptto() is called implicitly + + -- send headers + for key,value in pairs(headers) do + if mt.header(conn, key, value) ~= nil then + error("mt.header(" .. key .. ") failed") + end + if mt.getreply(conn) ~= SMFIR_CONTINUE then + error("mt.header(" .. key .. ") unexpected reply") + end + end + -- send EOH + if mt.eoh(conn) ~= nil then + error "mt.eoh() failed" + end + if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.eoh() unexpected reply" + end + + -- send body + if mt.bodystring(conn, body) ~= nil then + error "mt.bodystring() failed" + end + if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.bodystring() unexpected reply" + end + -- end of message; let the filter react + if mt.eom(conn) ~= nil then + error "mt.eom() failed" + end + reply = mt.getreply(conn) + if reply ~= SMFIR_CONTINUE then + error ("mt.eom() unexpected reply: " .. reply) + end + return conn +end + +for _, keytype in ipairs({"ed25519", "rsa"}) do + mt.echo("testing "..keytype) + signing = connect_and_send("unix:"..keytype..".signing.sock", msg.headers, msg.body) + -- verify that a test header field got added + if not mt.eom_check(signing, MT_HDRINSERT) then + error "no header added by signer" + end + + signature = mt.getheader(signing, "DKIM-Signature", 0) + + mt.disconnect(signing) + + mt.echo("DKIM-Signature: " .. signature) + + msg.headers['DKIM-Signature'] = signature + + verify = connect_and_send("unix:"..keytype..".verify.sock", msg.headers, msg.body) + + if not mt.eom_check(verify, MT_HDRINSERT) then + error "no header added in verify" + end + + authres = mt.getheader(verify, "Authentication-Results", 0) + mt.echo("Authentication-Results: "..authres) + + mt.disconnect(verify) + + mt.echo(keytype.." complete") +end diff --git a/tests/dkimpy-milter b/tests/dkimpy-milter new file mode 100755 index 0000000..39b64d5 --- /dev/null +++ b/tests/dkimpy-milter @@ -0,0 +1,2 @@ +#!/bin/sh +python2 -m dkimpy_milter "$@" diff --git a/tests/runtests b/tests/runtests new file mode 100755 index 0000000..7878f17 --- /dev/null +++ b/tests/runtests @@ -0,0 +1,84 @@ +#!/bin/bash + +set -e +WORKDIR=$(mktemp -d) +TESTDIR=$(realpath "$(dirname "$0")") +DKIMPY_MILTER=${DKIMPY_MILTER:-"$TESTDIR/dkimpy-milter"} +KEY_TYPES=(ed25519 rsa) + +cd "$WORKDIR" + +printf "Testing %s from directory %s\n" "$DKIMPY_MILTER" "$WORKDIR" + +for keytype in "${KEY_TYPES[@]}"; do + dknewkey --ktype "$keytype" "testkey.$keytype" + if [ "$keytype" = ed25519 ]; then + keyfile=KeyFileEd25519 + selector=SelectorEd25519 + else + keyfile=KeyFile + selector=Selector + fi + cat > "$keytype.signing.conf" < "$keytype.verify.conf" < %s:\n" "$errdata" + cat "$errdata" + printf -- "-> end %s\n" "$errdata" + fi + done + done + rm -rf "$WORKDIR" +} + +for keytype in "${KEY_TYPES[@]}"; do + for func in signing verify; do + PYTHONPATH="$(dirname "$TESTDIR")" "$DKIMPY_MILTER" "$keytype.$func.conf" 2>"$keytype.$func.stderr" & + done +done +trap cleanup EXIT + +# ugly ugly (how are we supposed to know that the milters are all ready?): +sleep 2 + +# uses miltertest from opendkim: +for x in ${TESTS:-"$TESTDIR"/*.miltertest}; do + if ! [ -e "$x" ]; then + if [ -e "$TESTDIR/$x" ]; then + x="$TESTDIR/$x" + fi + fi + printf -- "-> running %s...\n" "$x" + miltertest -s "$x" +done