Compare commits

...

47 Commits

Author SHA1 Message Date
Scott Kitterman abba014c5c Add release date for 1.1.4 2019-11-22 20:35:38 -05:00
Scott Kitterman 807b1391b2 - Delete own_socketfile to resolve race condition where the permissions
change fails on a Unix socket because it hasn't been created yet (libmilter
   will do this correctly on its own based on umask, the milter doesn't need
   to do it) (LP: #1849712)
2019-10-29 07:15:51 -04:00
Scott Kitterman dcb781c365 Make error logging more explicit to aid debugging 2019-10-18 23:31:34 -04:00
Scott Kitterman 577a971f1a Fix typo in README 2019-10-07 00:41:43 -04:00
Scott Kitterman 59296be0cf Bump version to 1.1.3 2019-10-06 00:14:54 -04:00
Scott Kitterman 4e1fa3c8ff Update README now that sysv init is tested 2019-10-06 00:14:27 -04:00
Scott Kitterman 99899062bb Fix sysv init so it works (LP: #1839487) 2019-10-05 21:48:44 -04:00
Scott Kitterman 6a1705926f Update version/release date for 1.1.2 2019-09-23 13:36:56 -04:00
Scott Kitterman e6f8db9f94 More reslience fixes 2019-09-23 11:36:10 -04:00
Scott Kitterman e63867d517 Merge branch 'stable1.1' of git+ssh://git.launchpad.net/dkimpy-milter into stable1.1 2019-09-23 11:27:33 -04:00
Scott Kitterman 209ad11661 Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
2019-09-23 11:26:10 -04:00
Scott Kitterman 795a914845 Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
2019-09-23 11:04:02 -04:00
Scott Kitterman be92e5c5b1 Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
2019-09-23 10:48:04 -04:00
Scott Kitterman 6910ff1f9a Fix variable initialization so mailformed mails missing body From do not
cause a traceback (LP: #1844161)
2019-09-16 20:08:34 -04:00
Scott Kitterman 7953e54ffb CHANGES and setup.py updates for 1.1.1 release prep 2019-09-06 00:52:18 -04:00
Scott Kitterman bc98f9180f Fix startup logging so it provides information at a useful time 2019-09-06 00:41:42 -04:00
Scott Kitterman a144791f2a Minor README corrections 2019-09-06 00:37:46 -04:00
Scott Kitterman 55e1a6b54e Fixup missing i= processing 2019-09-06 00:27:52 -04:00
Scott Kitterman 7c3ff1905a - Fix verify processing so missing (optional) i= tag doesn't cause the milter
to fail
2019-09-06 00:22:00 -04:00
Scott Kitterman 7ec97a6001 - Fix message extraction so that signing in the same pass through the milter
as verifying works correctly
2019-09-05 23:55:34 -04:00
Scott Kitterman aba9c0aa0c Update TODO 2019-04-12 22:43:18 -04:00
Scott Kitterman 19b6ce7a68 Update CHANGES and man/dkimpy-milter.conf.5 for SubDomains option. 2019-04-12 22:34:59 -04:00
Scott Kitterman 8dc3ac6474 Add option to sign for subdomains - Thanks to Sagi for the patch 2019-04-12 22:31:24 -04:00
Scott Kitterman 290a37b99c Update CHANGES to current 2019-04-12 22:27:23 -04:00
Scott Kitterman 564799402a Catch and log config items missing values 2019-04-12 22:01:56 -04:00
Scott Kitterman 357905bb68 Fix debugLevel for config._readConfigFile 2019-04-12 21:45:31 -04:00
Scott Kitterman 6b851f18df Changes in dkimpy_milter/__init__.py to harmonize logging messages and log text instead of bytestring. 2019-04-12 21:29:09 -04:00
Scott Kitterman 7ab58edb1b Consisently use /run vice /var/run for documentation and init system integration 2019-04-12 20:57:08 -04:00
Scott Kitterman 23b0e8a386 README updates for python3 switch 2019-04-12 20:51:33 -04:00
Scott Kitterman 2e105bd18c Py3DNS vice PyDNS because we're python3 now 2019-04-12 20:34:42 -04:00
Scott Kitterman fb72b9f6e7 Merge 1_0 into master 2019-03-12 22:06:42 -04:00
Scott Kitterman 7eed8995a2 Merge branch 'dkg/socket-activation' into 1_0 2019-03-12 22:01:00 -04:00
Scott Kitterman a16d887ac6 Fix merge conflicts 2019-02-24 07:22:58 -05:00
Scott Kitterman 23d91b2b50 Update CHANGES for merge of dkg/test-suite 2019-02-24 07:19:18 -05:00
Scott Kitterman 481fbdae29 Merge branch 'dkg/test-suite' into 1_0 2019-02-24 07:13:30 -05:00
Scott Kitterman 4b0c39b0c7 Start changes for python3 update 2019-02-24 06:57:47 -05:00
Daniel Kahn Gillmor 7092874729 Enable sd_listen_fds(3)-style socket-activation support
I've added straightforward systemd unit files in
system/socket-activation/ that make use of this approach, and a
README.md in the same location that describes the tradeoffs.
2019-02-21 19:22:11 -05:00
Daniel Kahn Gillmor ea09bab1a8 Convert __init__.py to python3
The main work here is about bytes vs. strings.  This work was
confusing for several reasons:

 * pymilter thinks that headers are all strings, but body is bytes

 * dkimpy wants to deal with bytes objects generally (though it
   accepts a string object as an ed25519 secret key for some reason,
   despite requiring bytes as an RSA secret key)

 * authres.AuthenticationResultsHeader object converts easily to a
   string, but has no direct bytes conversion.  meanwhile, it wants
   its arguments as strings, but will accept them if they are bytes
   and convert them with something like str(), which leaves weird
   cruft like "header.a=b'ed25519-sha256'"

 * dkimpy_milter/utils.py contains fold() which expects bytes

 * self.fp needs to accumulate the on-the-wire version of the message
   as a whole (so it needs to be bytes).  That means converting the
   headers.  Header names and values are US-ASCII, per §2.2 of RFC
   5322, so they should be convertible cleanly, but we still have to
   convert them explicitly so that python knows the right thing to do.

At any rate, tests/runtests all passes with these changes, and the
output for both Authentication-Results: and DKIM-Signature headers
looks the same.
2019-02-21 19:22:11 -05:00
Daniel Kahn Gillmor 25fdd3b81c Do not create PidFile by default
By default, avoid creating a PIDFile.

PIDFiles are racy and potentially dangerous.  Modern system
supervision systems don't need them, because they manage the process
groups directly.

If the configuration file doesn't specify a PidFile, dkimpy-milter
shouldn't try to create one.
2019-02-21 19:22:11 -05:00
Daniel Kahn Gillmor 9d5316ca0e Handle defaults for Socket differently
We want to be able to select the default for Socket differently in the
future.

This change augments the API for dkimpy_milter.util.own_socketfile()
by adding an optional sockname argument.  This is a
backward-compatible change.  If we aren't committed to API stability
for this function, we could make a more invasive change that would
probably be a more reasonable API going forward, but this is probably
good enough.
2019-02-21 19:22:11 -05:00
Daniel Kahn Gillmor 391b5352f3 Convert mostly to python3 (still need strings/bytes conversions)
This covers conversion of the whole project to python3, *except* for
the strings/bytes distinction in __init__.py, which i'm leaving for a
second commit.

The changes in this commit are intended to be relatively
uncontroversial, so that the following commit contains the tricky
bits.
2019-02-21 19:22:09 -05:00
Scott Kitterman 851f8ff9c9 Update references in man/dkimpy-milter.8 2019-02-08 04:02:34 -05:00
Scott Kitterman cbb6098dd8 Remove obsolete .IX macro from man pages 2019-02-08 01:12:21 -05:00
Scott Kitterman c90d694fff Fix grammar error in README 2019-02-01 15:15:50 -05:00
Scott Kitterman 8d8cd15cba Fix spelling error in README 2019-02-01 15:11:00 -05:00
Scott Kitterman 4f21623f92 Add read_keytable function to util.py 2018-12-24 16:08:50 -05:00
Scott Kitterman bf2548f891 Documentation updates for 1.1.0 2018-10-11 21:45:17 -04:00
17 changed files with 346 additions and 148 deletions
+34 -1
View File
@@ -1,5 +1,38 @@
1.1.4 2019-11-22
- Make error logging more explicit to aid debugging
- Delete own_socketfile to resolve race condition where the permissions
change fails on a Unix socket because it hasn't been created yet (libmilter
will do this correctly on its own based on umask, the milter doesn't need
to do it) (LP: #1849712)
1.1.3 2019-10-06
- Fix sysv init so it works (LP: #1839487)
1.1.2 2019-09-23
- Fix variable initialization so mailformed mails missing body From do not
cause a traceback (LP: #1844161)
- Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
1.1.1 2019-09-06
- Fix startup logging so it provides information at a useful time
- Fix verify processing so missing (optional) i= tag doesn't cause the milter
to fail (LP: #1842250)
- Fix message extraction so that signing in the same pass through the milter
as verifying works correctly
1.1.0 2019-04-12
- Add SubDomains option to enable signing for sub-domains (LP: #1811535)
- Port to python3 (LP: #1815502)
- Add test suite using opendkim miltertest
- When Socket is absolute path, do not strip leading /
- Handle unix: socket prefix the same as local:
- Set up correct AuthservID defaults
- config: Reassemble strings sensibly
- Consistently prefer dnspython to Py3DNS (LP: #1815558)
1.0.1 2019-02-11 1.0.1 2019-02-11
* Reorder milter start and dropping privileges so permissions on Unix socket - Reorder milter start and dropping privileges so permissions on Unix socket
are correct (LP: 1797720) are correct (LP: 1797720)
- Make domain checks case insensitive for determining if signing should be - Make domain checks case insensitive for determining if signing should be
done (LP: #1815311) done (LP: #1815311)
+10 -11
View File
@@ -17,13 +17,13 @@ to be installed when installing using setup.py, the following incantation is
required because setuptools developers decided not being able to do this by required because setuptools developers decided not being able to do this by
default is a feature: default is a feature:
python setup.py install --single-version-externally-managed --record=/dev/null python3 setup.py install --single-version-externally-managed --record=/dev/null
For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are
available in either the main or backports repositories: available in either the main or backports repositories:
[sudo] apt install python-milter python-nacl python-ipaddress python-dnspython [sudo] apt install python3-milter python3-nacl python3-dnspython
[sudo] apt install -t stretch-backports python-authres python-dkim [sudo] apt install -t stretch-backports python3-authres python3-dkim
The preferred method of installation is from PyPi using pip (if distribution The preferred method of installation is from PyPi using pip (if distribution
packages are not available): packages are not available):
@@ -33,11 +33,11 @@ packages are not available):
Using pip will cause required packages to be installed via easy_install if they Using pip will cause required packages to be installed via easy_install if they
have not been previously installed. Because pymilter and PyNaCl are compiled have not been previously installed. Because pymilter and PyNaCl are compiled
Python extensions, the system will need appropriate development packages and Python extensions, the system will need appropriate development packages and
an C compiler. Alternately, install these dependencies from dsitribution/OS an C compiler. Alternately, install these dependencies from distribution/OS
packages and then pip install dkimpy_milter. packages and then pip install dkimpy_milter.
The milter will work with either pydns (DNS) or dnspython (dns), preferring The milter will work with either py3dns (DNS) or dnspython (dns), preferring
dnspython is both are available. The dkimpy DKIM module also works with dnspython if both are available. The dkimpy DKIM module also works with
either. either.
@@ -84,9 +84,8 @@ MTA INTEGRATION
Both a systemd unit file and a sysv init file are provided. Both make Both a systemd unit file and a sysv init file are provided. Both make
assumptions about defaults being used, e.g. if a non-standard pidfile name is assumptions about defaults being used, e.g. if a non-standard pidfile name is
used, they will need to be updated. The sysv init file is Debian specific and used, they will need to be updated. The sysv init file uses start-stop-deamon
untested, since the developers are not using sysv init. Feedback/patches from Debian. It is not portable to systems without that available.
welcome.
The dkimpy-milter drops priviledges after setup to the user/group specified in The dkimpy-milter drops priviledges after setup to the user/group specified in
UserID. During initial setup, this system user needs to be manually created. UserID. During initial setup, this system user needs to be manually created.
@@ -94,7 +93,7 @@ As an example, using the default dkimpy-user on Debian, the command would be:
[sudo] adduser --system --no-create-home --quiet --disabled-password \ [sudo] adduser --system --no-create-home --quiet --disabled-password \
--disabled-login --shell /bin/false --group \ --disabled-login --shell /bin/false --group \
--home /var/run/dkimpy-milter dkimpy-milter --home /run/dkimpy-milter dkimpy-milter
Since /var/run or /run is sometimes on a tempfs, if the PID file directory is Since /var/run or /run is sometimes on a tempfs, if the PID file directory is
missing, the milter will create it on startup. missing, the milter will create it on startup.
@@ -117,7 +116,7 @@ Configuration is very similar to opendkim, but needs some adjustment for
dkimpy-milter. Here's an example configuration line to include in your dkimpy-milter. Here's an example configuration line to include in your
sendmail.mc: sendmail.mc:
INPUT_MAIL_FILTER(`dkimpy-milter', `S=local:/var/run/dkimpy-milter/dkimpy-milter.sock')dnl INPUT_MAIL_FILTER(`dkimpy-milter', `S=local:/run/dkimpy-milter/dkimpy-milter.sock')dnl
Changing the sendmail.mc file requires a Make (to compile it into sendmail.cf) Changing the sendmail.mc file requires a Make (to compile it into sendmail.cf)
and a restart of sendmail. Note that S= needs to match the value of Socket in and a restart of sendmail. Note that S= needs to match the value of Socket in
+8 -6
View File
@@ -44,15 +44,20 @@ No additional features
1.0.1 1.0.1
Bug fix only, improved documentation Bug fix only, improved documentation
1.1.0 (planned) 1.1.0
Port to Python 3 Port to Python 3 implemented verified
Subdomain support Subdomain support implemented verified
Test suite implemented verified
Planned dataset type support (if needed): Planned dataset type support (if needed):
mdb: mdb:
Considered for near-term feature release Considered for near-term feature release
KeyTable
KeytableEd25519
SigningTable
SigningTableEd25519
AlwaysAddARHeader AlwaysAddARHeader
ChangeRootDirectory ChangeRootDirectory
ClockDrift (requires dkimpy change) ClockDrift (requires dkimpy change)
@@ -73,8 +78,6 @@ ExternalIgnoreList
FixCRLF FixCRLF
KeepAuthResults KeepAuthResults
KeepTemporaryFiles KeepTemporaryFiles
KeyTable
KeytableEd25519
LogResults LogResults
LogWhy LogWhy
MaximumHeaders MaximumHeaders
@@ -98,7 +101,6 @@ RequireSafeKeys
SignatureAlgorithm SignatureAlgorithm
SignatureTTL SignatureTTL
SignHeaders SignHeaders
SigningTable
SoftwareHeader SoftwareHeader
StrictHeaders StrictHeaders
SubDomains SubDomains
+99 -55
View File
@@ -1,4 +1,4 @@
#! /usr/bin/python2 #! /usr/bin/python3
# Original dkim-milter.py code: # Original dkim-milter.py code:
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2007 Business Management Systems, Inc. # Copyright 2007 Business Management Systems, Inc.
@@ -28,15 +28,15 @@ import dkim
import authres import authres
import os import os
import tempfile import tempfile
import StringIO import io
import re import re
import codecs
from Milter.utils import parse_addr, parseaddr from Milter.utils import parse_addr, parseaddr
import dkimpy_milter.config as config import dkimpy_milter.config as config
from dkimpy_milter.util import drop_privileges from dkimpy_milter.util import drop_privileges
from dkimpy_milter.util import setExceptHook from dkimpy_milter.util import setExceptHook
from dkimpy_milter.util import write_pid from dkimpy_milter.util import write_pid
from dkimpy_milter.util import read_keyfile from dkimpy_milter.util import read_keyfile
from dkimpy_milter.util import own_socketfile
from dkimpy_milter.util import fold from dkimpy_milter.util import fold
__version__ = "1.0.1" __version__ = "1.0.1"
@@ -54,6 +54,7 @@ class dkimMilter(Milter.Base):
self.privatersa = privateRSA self.privatersa = privateRSA
self.privateed25519 = privateEd25519 self.privateed25519 = privateEd25519
self.fp = None self.fp = None
self.fdomain = ''
@Milter.noreply @Milter.noreply
def connect(self, hostname, unused, hostaddr): def connect(self, hostname, unused, hostaddr):
@@ -110,7 +111,7 @@ class dkimMilter(Milter.Base):
def envfrom(self, f, *str): def envfrom(self, f, *str):
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2: if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2:
syslog.syslog("mail from: {0} {1}".format(f, str)) syslog.syslog("mail from: {0} {1}".format(f, str))
self.fp = StringIO.StringIO() self.fp = io.BytesIO()
self.mailfrom = f self.mailfrom = f
t = parse_addr(f) t = parse_addr(f)
if len(t) == 2: if len(t) == 2:
@@ -135,20 +136,24 @@ class dkimMilter(Milter.Base):
try: try:
self.fdomain = self.author.split('@')[1].lower() self.fdomain = self.author.split('@')[1].lower()
except IndexError as er: except IndexError as er:
self.fdomain = '' # self.author was not a proper email address pass # self.author was not a proper email address
if (milterconfig.get('Syslog') and if (milterconfig.get('Syslog') and
milterconfig.get('debugLevel') >= 1): milterconfig.get('debugLevel') >= 1):
syslog.syslog("{0}: {1}".format(name, val)) syslog.syslog("{0}: {1}".format(name, val))
elif lname == 'authentication-results': elif lname == 'authentication-results':
self.arheaders.append(val) self.arheaders.append(val)
if self.fp: if self.fp:
self.fp.write("%s: %s\n" % (name, val)) try:
self.fp.write(b"%s: %s\n" % (codecs.encode(name, 'ascii'), codecs.encode(val, 'ascii')))
except:
# Don't choke on header fields with non-ascii garbage in them.
pass
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @Milter.noreply
def eoh(self): def eoh(self):
if self.fp: if self.fp:
self.fp.write("\n") # terminate headers self.fp.write(b"\n") # terminate headers
self.bodysize = 0 self.bodysize = 0
return Milter.CONTINUE return Milter.CONTINUE
@@ -176,39 +181,40 @@ class dkimMilter(Milter.Base):
except: except:
# Don't error out on unparseable AR header fiels # Don't error out on unparseable AR header fiels
pass pass
# Check or sign DKIM # Check and/or sign DKIM
self.fp.seek(0) self.fp.seek(0)
txt = self.fp.read()
if milterconfig.get('Domain'): if milterconfig.get('Domain'):
domain = milterconfig.get('Domain') domain = milterconfig.get('Domain')
else: else:
domain = '' domain = ''
if milterconfig.get('SubDomains'):
self.fdomain = _get_parent_domain(self.fdomain, domain)
if ((self.fdomain in domain) and not milterconfig.get('Mode') == 'v' if ((self.fdomain in domain) and not milterconfig.get('Mode') == 'v'
and not self.external_connection): and not self.external_connection):
txt = self.fp.read()
self.sign_dkim(txt) self.sign_dkim(txt)
if ((self.has_dkim) and (not self.internal_connection) and if ((self.has_dkim) and (not self.internal_connection) and
(milterconfig.get('Mode') == 'v' or (milterconfig.get('Mode') == 'v' or
milterconfig.get('Mode') == 'sv')): milterconfig.get('Mode') == 'sv')):
txt = self.fp.read()
self.check_dkim(txt) self.check_dkim(txt)
if self.arresults: if self.arresults:
h = authres.AuthenticationResultsHeader(authserv_id= h = authres.AuthenticationResultsHeader(authserv_id=
self.AuthservID, self.AuthservID,
results=self.arresults) results=self.arresults)
h = fold(str(h)) h = fold(codecs.encode(str(h), 'ascii'))
if (milterconfig.get('Syslog') and if (milterconfig.get('Syslog') and
milterconfig.get('debugLevel') >= 2): milterconfig.get('debugLevel') >= 2):
syslog.syslog(str(h)) syslog.syslog(codecs.decode(h, 'ascii'))
name, val = str(h).split(': ', 1) name, val = codecs.decode(h, 'ascii').split(': ', 1)
self.addheader(name, val, 0) self.addheader(name, val, 0)
return Milter.CONTINUE return Milter.CONTINUE
def sign_dkim(self, txt): def sign_dkim(self, txt):
canon = milterconfig.get('Canonicalization') canon = codecs.encode(milterconfig.get('Canonicalization'), 'ascii')
canonicalize = [] canonicalize = []
if len(canon.split('/')) == 2: if len(canon.split(b'/')) == 2:
canonicalize.append(canon.split('/')[0]) canonicalize.append(canon.split(b'/')[0])
canonicalize.append(canon.split('/')[1]) canonicalize.append(canon.split(b'/')[1])
else: else:
canonicalize.append(canon) canonicalize.append(canon)
canonicalize.append(canon) canonicalize.append(canon)
@@ -218,35 +224,36 @@ class dkimMilter(Milter.Base):
try: try:
if privateRSA: if privateRSA:
d = dkim.DKIM(txt) d = dkim.DKIM(txt)
h = d.sign(milterconfig.get('Selector'), self.fdomain, h = d.sign(codecs.encode(milterconfig.get('Selector'), 'ascii'), codecs.encode(self.fdomain, 'ascii'),
privateRSA, canonicalize=(canonicalize[0], codecs.encode(privateRSA, 'ascii'),
canonicalize[1])) canonicalize=(canonicalize[0],
name, val = h.split(': ', 1) canonicalize[1]))
self.addheader(name, val.strip().replace('\r\n', '\n'), 0) name, val = h.split(b': ', 1)
self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
if (milterconfig.get('Syslog') and if (milterconfig.get('Syslog') and
(milterconfig.get('SyslogSuccess') (milterconfig.get('SyslogSuccess')
or milterconfig.get('debugLevel') >= 1)): or milterconfig.get('debugLevel') >= 1)):
syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} ' syslog.syslog('{0}: {1} DKIM signature added (s={2} '
'd={3})'.format(self.getsymval('i'), 'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a'), d.signature_fields.get(b'a').decode(),
d.signature_fields.get(b's'), d.signature_fields.get(b's').decode(),
d.domain.lower())) d.domain.decode().lower()))
if privateEd25519: if privateEd25519:
d = dkim.DKIM(txt) d = dkim.DKIM(txt)
h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain, h = d.sign(codecs.encode(milterconfig.get('SelectorEd25519'), 'ascii'), codecs.encode(self.fdomain, 'ascii'),
privateEd25519, canonicalize=(canonicalize[0], privateEd25519, canonicalize=(canonicalize[0],
canonicalize[1]), canonicalize[1]),
signature_algorithm='ed25519-sha256') signature_algorithm=b'ed25519-sha256')
name, val = h.split(': ', 1) name, val = h.split(b': ', 1)
self.addheader(name, val.strip().replace('\r\n', '\n'), 0) self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
if (milterconfig.get('Syslog') and if (milterconfig.get('Syslog') and
(milterconfig.get('SyslogSuccess') (milterconfig.get('SyslogSuccess')
or milterconfig.get('debugLevel') >= 1)): or milterconfig.get('debugLevel') >= 1)):
syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} ' syslog.syslog('{0}: {1} DKIM signature added (s={2} '
'd={3})'.format(self.getsymval('i'), 'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a'), d.signature_fields.get(b'a').decode(),
d.signature_fields.get(b's'), d.signature_fields.get(b's').decode(),
d.domain.lower())) d.domain.decode().lower()))
except dkim.DKIMException as x: except dkim.DKIMException as x:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('DKIM: {0}'.format(x)) syslog.syslog('DKIM: {0}'.format(x))
@@ -257,6 +264,7 @@ class dkimMilter(Milter.Base):
def check_dkim(self, txt): def check_dkim(self, txt):
res = False res = False
self.header_a = None
for y in range(self.has_dkim): # Verify _ALL_ the signatures for y in range(self.has_dkim): # Verify _ALL_ the signatures
d = dkim.DKIM(txt) d = dkim.DKIM(txt)
try: try:
@@ -266,20 +274,17 @@ class dkimMilter(Milter.Base):
res = d.verify(idx=y, dnsfunc=lambda _x: dnsoverride) res = d.verify(idx=y, dnsfunc=lambda _x: dnsoverride)
else: else:
res = d.verify(idx=y) res = d.verify(idx=y)
algo = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
if res: if res:
if d.signature_fields.get(b'a') == 'ed25519-sha256': if algo == 'ed25519-sha256':
self.dkim_comment = ('Good {0} signature' self.dkim_comment = ('Good {0} signature'
.format(d.signature_fields .format(algo))
.get(b'a')))
else: else:
self.dkim_comment = ('Good {0} bit {1} signature' self.dkim_comment = ('Good {0} bit {1} signature'
.format(d.keysize, .format(d.keysize, algo))
d.signature_fields
.get(b'a')))
else: else:
self.dkim_comment = ('Bad {0} bit {1} signature.' self.dkim_comment = ('Bad {0} bit {1} signature.'
.format(d.keysize, .format(d.keysize, algo))
d.signature_fields.get(b'a')))
except dkim.DKIMException as x: except dkim.DKIMException as x:
self.dkim_comment = str(x) self.dkim_comment = str(x)
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
@@ -287,19 +292,31 @@ class dkimMilter(Milter.Base):
except Exception as x: except Exception as x:
self.dkim_comment = str(x) self.dkim_comment = str(x)
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog("check_dkim: {0}".format(x)) syslog.syslog("check_dkim: Internal program fault while verifying: {0}".format(x))
self.header_i = d.signature_fields.get(b'i') try:
self.header_d = d.signature_fields.get(b'd') # i= is optional and dkimpy is fine if it's not provided
self.header_a = d.signature_fields.get(b'a') self.header_i = codecs.decode(d.signature_fields.get(b'i'), 'ascii')
except TypeError as x:
self.header_i = None
try:
self.header_d = codecs.decode(d.signature_fields.get(b'd'), 'ascii')
self.header_a = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
except Exception as x:
self.dkim_comment = str(x)
if milterconfig.get('Syslog'):
syslog.syslog("check_dkim: Internal proram fuault extracting header a or d: {0}".format(x))
self.header_d = None
if not self.header_a:
self.header_a = 'rsa-sha256'
if res: if res:
if (milterconfig.get('Syslog') and if (milterconfig.get('Syslog') and
(milterconfig.get('SyslogSuccess') or (milterconfig.get('SyslogSuccess') or
milterconfig.get('debugLevel') >= 1)): milterconfig.get('debugLevel') >= 1)):
syslog.syslog('{0}: {1} DKIM signature verified (s={2} ' syslog.syslog('{0}: {1} DKIM signature verified (s={2} '
'd={3})'.format(self.getsymval('i'), 'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a'), d.signature_fields.get(b'a').decode(),
d.signature_fields.get(b's'), d.signature_fields.get(b's').decode(),
d.domain.lower())) d.domain.decode().lower()))
self.dkim_domain = d.domain.lower() self.dkim_domain = d.domain.lower()
else: else:
if milterconfig.get('DiagnosticDirectory'): if milterconfig.get('DiagnosticDirectory'):
@@ -310,22 +327,39 @@ class dkimMilter(Milter.Base):
syslog.syslog('DKIM: Fail (saved as {0})' syslog.syslog('DKIM: Fail (saved as {0})'
.format(fname)) .format(fname))
else: else:
syslog.syslog('DKIM: Fail ({0})'.format(d.domain.lower())) if milterconfig.get('Syslog'):
if d.domain:
syslog.syslog('DKIM: Fail ({0})'
.format(d.domain.lower()))
else:
syslog.syslog('DKIM: Fail, unextractable domain')
if res: if res:
result = 'pass' result = 'pass'
else: else:
result = 'fail' result = 'fail'
res = False res = False
self.arresults.append( if self.header_d:
authres.DKIMAuthenticationResult(result=result, self.arresults.append(
authres.DKIMAuthenticationResult(result=result,
header_i=self.header_i, header_i=self.header_i,
header_d=self.header_d, header_d=self.header_d,
header_a=self.header_a, header_a=self.header_a,
result_comment= result_comment=
self.dkim_comment) self.dkim_comment)
) )
self.header_a = None
return return
# get parent domain to be signed for if fdomain is a subdomain
def _get_parent_domain(fdomain, domains):
for domain in domains:
rhs = '.'+domain
# compare right hand side of fdomain against .domain
if fdomain[-len(rhs):] == rhs:
# return parent domain on match
return domain
# or return the fdomain itself
return fdomain
def main(): def main():
# Ugh, but there's no easy way around this. # Ugh, but there's no easy way around this.
@@ -355,13 +389,23 @@ def main():
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS) Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
miltername = 'dkimpy-filter' miltername = 'dkimpy-filter'
socketname = milterconfig.get('Socket') socketname = milterconfig.get('Socket')
own_socketfile(milterconfig) if socketname is None:
if int(os.environ.get('LISTEN_PID', '0')) == os.getpid():
lfds = os.environ.get('LISTEN_FDS')
if lfds is not None:
if lfds != '1':
syslog.syslog('LISTEN_FDS is set to "{0}", but we only know how to deal with "1", ignoring it'.
format(lfds))
else:
socketname = 'fd:3'
if socketname is None:
socketname = 'local:/var/run/dkimpy-milter/dkimpy-milter.sock'
drop_privileges(milterconfig) drop_privileges(milterconfig)
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter(miltername, socketname, 240)
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('dkimpy-milter started:{0} user:{1}' syslog.syslog('dkimpy-milter starting:{0} user:{1}'
.format(pid, milterconfig.get('UserID'))) .format(pid, milterconfig.get('UserID')))
Milter.runmilter(miltername, socketname, 240)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2 #!/usr/bin/python3
from dkimpy_milter import main from dkimpy_milter import main
+25 -15
View File
@@ -31,16 +31,16 @@ import stat
import dkim import dkim
import socket import socket
import ipaddress import ipaddress
from dnsplug import Session from .dnsplug import Session
# default values # default values
defaultConfigData = { defaultConfigData = {
'Syslog': 'yes', 'Syslog': 'yes',
'SyslogFacility': 'mail', 'SyslogFacility': 'mail',
'UMask': 007, 'UMask': 0o07,
'Mode': 'sv', 'Mode': 'sv',
'Socket': 'local:/var/run/dkimpy-milter/dkimpy-milter.sock', 'Socket': None,
'PidFile': '/var/run/dkimpy-milter/dkimpy-milter.pid', 'PidFile': None,
'UserID': 'dkimpy-milter', 'UserID': 'dkimpy-milter',
'Canonicalization': 'relaxed/simple', 'Canonicalization': 'relaxed/simple',
'InternalHosts': '127.0.0.1', 'InternalHosts': '127.0.0.1',
@@ -49,6 +49,7 @@ defaultConfigData = {
'MacroList': '', 'MacroList': '',
'MacroListVerify': '', 'MacroListVerify': '',
'DNSOverride': None, 'DNSOverride': None,
'SubDomains': False,
'debugLevel': 0 # Undocumented config item for developer use 'debugLevel': 0 # Undocumented config item for developer use
} }
@@ -85,14 +86,14 @@ class HostsDataset(object):
self.item = item[1:] self.item = item[1:]
self.negative = True self.negative = True
try: try:
self.item = ipaddress.ip_address(unicode(self.item, "utf-8")) self.item = ipaddress.ip_address(str(self.item, "utf-8"))
if isinstance(self.item, ipaddress.IPv4Address): if isinstance(self.item, ipaddress.IPv4Address):
self.isipv4 = True self.isipv4 = True
elif isinstance(self.item, ipaddress.IPv6Address): elif isinstance(self.item, ipaddress.IPv6Address):
self.isipv6 = True self.isipv6 = True
except ValueError as e: except ValueError as e:
try: try:
self.item = ipaddress.ip_network(unicode self.item = ipaddress.ip_network(str
(self.item, "utf-8"), (self.item, "utf-8"),
strict=False) strict=False)
if isinstance(self.item, ipaddress.IPv4Network): if isinstance(self.item, ipaddress.IPv4Network):
@@ -110,7 +111,7 @@ class HostsDataset(object):
def match(self, connectip): def match(self, connectip):
'''Check if the connect IP is part of the dataset''' '''Check if the connect IP is part of the dataset'''
source = ipaddress.ip_address(unicode(connectip, "utf-8")) source = ipaddress.ip_address(str(connectip, "utf-8"))
for item in self.dataset: for item in self.dataset:
if item.isdomain or item.ishostname: if item.isdomain or item.ishostname:
result = self.matchname(source) # Match host/domains first result = self.matchname(source) # Match host/domains first
@@ -160,13 +161,13 @@ class HostsDataset(object):
if isinstance(source, ipaddress.IPv4Address): if isinstance(source, ipaddress.IPv4Address):
ips = s.dns(name, 'A') ips = s.dns(name, 'A')
for ip in ips: for ip in ips:
ip = ipaddress.IPv4Address(unicode(ip, 'UTF-8')) ip = ipaddress.IPv4Address(str(ip, 'UTF-8'))
if ip == source: if ip == source:
results.append(name) results.append(name)
if isinstance(source, ipaddress.IPv6Address): if isinstance(source, ipaddress.IPv6Address):
ips = s.dns(name, 'AAAA') ips = s.dns(name, 'AAAA')
for ip in ips: for ip in ips:
ip = ipaddress.IPv6Address(unicode(ip, 'UTF-8')) ip = ipaddress.IPv6Address(str(ip, 'UTF-8'))
if ip == source: if ip == source:
results.append(name) results.append(name)
return results return results
@@ -225,13 +226,13 @@ def _processConfigFile(filename=None, configdata=None, useSyslog=1,
'''Load the specified config file, exit and log errors if it fails, '''Load the specified config file, exit and log errors if it fails,
otherwise return a config dictionary.''' otherwise return a config dictionary.'''
import config from . import config
if configdata is None: if configdata is None:
configdata = config.defaultConfigData configdata = config.defaultConfigData
if filename is not None: if filename is not None:
try: try:
_readConfigFile(filename, configdata) _readConfigFile(filename, configdata)
except Exception, e: except Exception as e:
raise raise
if useSyslog: if useSyslog:
syslog.syslog(e.args[0]) syslog.syslog(e.args[0])
@@ -309,7 +310,9 @@ def _readConfigFile(path, configData=None, configGlobal={}):
dictionary of name/value pairs based on configData and the values dictionary of name/value pairs based on configData and the values
read from path.''' read from path.'''
debugLevel = configGlobal.get('debugLevel', 0) # No config file data is available yet, so to debug _readConfigFile, set
# the value here.
debugLevel = 0
if debugLevel >= 5: if debugLevel >= 5:
syslog.syslog('readConfigFile: Loading "%s"' % path) syslog.syslog('readConfigFile: Loading "%s"' % path)
if configData is None: if configData is None:
@@ -325,6 +328,7 @@ def _readConfigFile(path, configData=None, configGlobal={}):
'PidFile': 'str', 'PidFile': 'str',
'UserID': 'str', 'UserID': 'str',
'Domain': 'dataset', 'Domain': 'dataset',
'SubDomains': 'bool',
'KeyFile': 'str', 'KeyFile': 'str',
'KeyFileEd25519': 'str', 'KeyFileEd25519': 'str',
'Selector': 'str', 'Selector': 'str',
@@ -342,7 +346,7 @@ def _readConfigFile(path, configData=None, configGlobal={}):
# check to see if it's a file # check to see if it's a file
try: try:
mode = os.stat(path)[0] mode = os.stat(path)[0]
except OSError, e: except OSError as e:
syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s' syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s'
% (path, e.strerror)) % (path, e.strerror))
return(configData) return(configData)
@@ -378,9 +382,15 @@ def _readConfigFile(path, configData=None, configGlobal={}):
value = data[1:] value = data[1:]
# check validity of name # check validity of name
conversion = nameConversion.get(name) try:
conversion = nameConversion.get(name)
except TypeError:
name = name[0]
syslog.syslog('Config item "%s" does not provide a value in file "%s"'
% (name, path))
conversion = None
if conversion is None: if conversion is None:
syslog.syslog('ERROR: Unknown name "%s" in file "%s"' syslog.syslog('ERROR: Unknown name or name missing value "%s" in file "%s"'
% (name, path)) % (name, path))
continue continue
+8 -8
View File
@@ -84,7 +84,7 @@ class Session(object):
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME) raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
cnames[name] = cname cnames[name] = cname
if cname in cnames: if cname in cnames:
raise DNSError, 'CNAME loop' raise DNSError('CNAME loop')
result = self.dns(cname, qtype, cnames=cnames) result = self.dns(cname, qtype, cnames=cnames)
return result return result
@@ -103,16 +103,16 @@ def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=30):
# #
if resp.header['tc'] == True: if resp.header['tc'] == True:
if not tcpfallback: if not tcpfallback:
raise DNS.DNSError, 'DNS: Truncated UDP Reply, SPF records should fit in a UDP packet' raise DNS.DNSError('DNS: Truncated UDP Reply, SPF records should fit in a UDP packet')
try: try:
req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp', req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp',
timeout=timeout) timeout=timeout)
resp = req.req() resp = req.req()
except DNS.DNSError, x: except DNS.DNSError as x:
raise DNS.DNSError, 'TCP Fallback error: ' + str(x) raise DNS.DNSError('TCP Fallback error: ' + str(x))
return [((a['name'], a['typename']), a['data']) for a in resp.answers] return [((a['name'], a['typename']), a['data']) for a in resp.answers]
except IOError, x: except IOError as x:
raise DNS.DNSError, 'DNS: ' + str(x) raise DNS.DNSError('DNS: ' + str(x))
def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30): def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30):
retVal = [] retVal = []
@@ -164,5 +164,5 @@ if __name__ == '__main__':
import sys import sys
s = Session() s = Session()
for n,t in zip(*[iter(sys.argv[1:])]*2): for n,t in zip(*[iter(sys.argv[1:])]*2):
print n,t print(n,t)
print s.dns(n,t) print(s.dns(n,t))
+34 -26
View File
@@ -115,55 +115,40 @@ def write_pid(milterconfig):
"""Write PID in pidfile. Will not overwrite an existing file.""" """Write PID in pidfile. Will not overwrite an existing file."""
import os import os
import syslog import syslog
if not os.path.isfile(milterconfig.get('PidFile')): pidfile = milterconfig.get('PidFile')
if pidfile is None:
return
if not os.path.isfile(pidfile):
pid = str(os.getpid()) pid = str(os.getpid())
try: try:
f = open(milterconfig.get('PidFile'), 'w') f = open(pidfile, 'w')
except IOError as e: except IOError as e:
if str(e)[:35] == '[Errno 2] No such file or directory': if str(e)[:35] == '[Errno 2] No such file or directory':
piddir = milterconfig.get('PidFile').rsplit('/', 1)[0] piddir = pidfile.rsplit('/', 1)[0]
os.mkdir(piddir) os.mkdir(piddir)
user, group = user_group(milterconfig.get('UserID')) user, group = user_group(milterconfig.get('UserID'))
os.chown(piddir, user, group) os.chown(piddir, user, group)
f = open(milterconfig.get('PidFile'), 'w') f = open(pidfile, 'w')
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('PID dir created: {0}'.format(piddir)) syslog.syslog('PID dir created: {0}'.format(piddir))
else: else:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('Unable to write pidfle {0}. IOError: {1}' syslog.syslog('Unable to write pidfle {0}. IOError: {1}'
.format(milterconfig.get('PidFile'), e)) .format(pidfile, e))
raise raise
f.write(pid) f.write(pid)
f.close() f.close()
user, group = user_group(milterconfig.get('UserID')) user, group = user_group(milterconfig.get('UserID'))
os.chown(milterconfig.get('PidFile'), user, group) os.chown(pidfile, user, group)
else: else:
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
syslog.syslog('Unable to write pidfle {0}. File exists.' syslog.syslog('Unable to write pidfle {0}. File exists.'
.format(milterconfig.get('PidFile'))) .format(pidfile))
raise RuntimeError('Unable to write pidfle {0}. File exists.' raise RuntimeError('Unable to write pidfle {0}. File exists.'
.format(milterconfig.get('PidFile'))) .format(pidfile))
return pid return pid
def own_socketfile(milterconfig):
"""If socket is Unix socket, chown to UserID before dropping privileges"""
import os
user, group = user_group(milterconfig.get('UserID'))
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): def read_keyfile(milterconfig, keytype):
"""Read private key from file.""" """Read private key from file."""
import syslog import syslog
@@ -184,3 +169,26 @@ def read_keyfile(milterconfig, keytype):
for line in keylist: for line in keylist:
key += line key += line
return key return key
def read_keytable(milterconfig, tabletype):
"""Read keytables into in memory configuration data so all keys are read
before priviledges are dropped."""
import syslog
if tabletype == "RSA":
tablefile = milterconfig.get('KeyTable')
if tabletype == "Ed25519":
tablefile = milterconfig.get('KeyTableEd25519')
if milterconfig.get(tablefile):
keytabledata = []
try:
f = open(milterconfig.get(tablefile))
for row in f:
keytabledata.append(row)
f.close()
except IOError as e:
if milterconfig.get('Syslog'):
syslog.syslog('Unable to read keytable {0}. IOError: {1}'
.format(tablefile, e))
raise
return keytabledata
+1 -1
View File
@@ -38,7 +38,7 @@ Socket inet:8892@localhost
### Name of the file where the filter should write its pid before beginning ### Name of the file where the filter should write its pid before beginning
### normal operations. ### normal operations.
# #
PidFile /var/run/dkimpy-milter/dkimpy-milter.pid PidFile /run/dkimpy-milter/dkimpy-milter.pid
## Userid userid ## Userid userid
### default dkimpy-milter ### default dkimpy-milter
+54 -9
View File
@@ -131,7 +131,7 @@
.SH "NAME" .SH "NAME"
dkimpy-milter \- Python milter for DKIM signing and validation dkimpy-milter \- Python milter for DKIM signing and validation
.SH "VERSION" .SH "VERSION"
0\.9\.2 1\.1\.0
.SH "DESCRIPTION" .SH "DESCRIPTION"
.I dkimpy-milter(8) .I dkimpy-milter(8)
@@ -221,12 +221,16 @@ domains will be verified rather than being signed.
This parameter is not required if a This parameter is not required if a
.I SigningTable .I SigningTable
or
.I SigningTableEd25519
is in use; in that case, the list of signed domains is implied by the is in use; in that case, the list of signed domains is implied by the
lines in that file. [SigningTable NOT IMPLEMENTED] lines in that file.
This parameter is ignored if a This parameter is ignored if a
.I KeyTable .I KeyTable
is defined. [KeyTable NOT IMPLEMENTED] or
.I KeyTableD25119
is defined.
.TP .TP
.I InternalHosts (dataset) .I InternalHosts (dataset)
@@ -244,7 +248,7 @@ address explicitly. [PeerList NOT IMPLEMENTED]
Gives the location of a PEM-formatted private key to be used for RSA signing Gives the location of a PEM-formatted private key to be used for RSA signing
all messages. Ignored if a all messages. Ignored if a
.I KeyTable .I KeyTable
is defined. [KeyTable NOT IMPLEMENTED] is defined.
.TP .TP
.I KeyFileEd25519 (string) .I KeyFileEd25519 (string)
@@ -252,7 +256,17 @@ Gives the location of a Ed25519 private key to be used for Ed25519 signing
all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key
generation (as used in dkimpy). Ignored if a generation (as used in dkimpy). Ignored if a
.I KeyTableEd25519 .I KeyTableEd25519
is defined. [KeyTableEd25519 NOT IMPLEMENTED] is defined.
.TP
.I KeyTable (dataset)
Gives the location of a file mapping key names to RSA signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signatures "d=" value; (b) the name of the selector to use in the signatures "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender. NOTE: direct specification of keys in the table as is done by OpenDKIM is not supported.
.TP
.I KeyTableEd25519 (dataset)
Gives the location of a file mapping key names to Ed25519 signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signatures "d=" value; (b) the name of the selector to use in the signatures "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender. NOTE: direct specification of keys in the table as is done by OpenDKIM is not support
ed.
.TP .TP
.I MacroList (dataset) .I MacroList (dataset)
@@ -309,7 +323,6 @@ When signing mode is enabled, one of the following combinations must also
be set: be set:
(a) Domain, KeyFile, Selector, no KeyTable, no SigningTable; (a) Domain, KeyFile, Selector, no KeyTable, no SigningTable;
(b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector; (b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector;
[fooTable options NOT IMPLEMENTED]
.TP .TP
.I DNSOverride (string) .I DNSOverride (string)
@@ -338,7 +351,7 @@ will be checked. [PeerList NOT IMPLEMENTED - included for reference only]
.TP .TP
.I PidFile (string) .I PidFile (string)
Specifies the path to a file that should be created at process start Specifies the path to a file that should be created at process start
containing the process ID. containing the process ID. If not specified, no such file will be created.
.TP .TP
.I Selector (string) .I Selector (string)
@@ -352,7 +365,7 @@ parameter below for more information.
This parameter is ignored if a This parameter is ignored if a
.I KeyTable .I KeyTable
is defined. [KeyTable NOT IMPLEMENTED] is defined.
.TP .TP
.I SelectorEd25519 (string) .I SelectorEd25519 (string)
@@ -366,7 +379,33 @@ parameter below for more information.
This parameter is ignored if a This parameter is ignored if a
.I KeyTableEd25519 .I KeyTableEd25519
is defined. [KeyTable NOT IMPLEMENTED] is defined.
.TP
.I SigningTable (dataset)
Defines a table used to select one or more signatures to apply to a message based on the address found in the From: header field. Keys in this table vary depending on the type of table used; values in this data set should include one field that contains a name found in the KeyTable (see above) that identifies which key should be used in generating the signature, and an optional second field naming the signer of the message that will be included in the "i=" tag in the generated signature. Note that the "i=" value will not be included in the signature if it conflicts with the signing domain (the "d=" value).
If the first field contains only a "%" character, it will be replaced by the domain found in the From: header field. Similarly, within the optional second field, any "%" character will be replaced by the domain found in the From: header field.
If this table specifies a regular expression file ("refile"), then the keys are wildcard patterns that are matched against the address found in the From: header field. Entries are checked in the order in which they appear in the file. ["refile support not implemented"].
For all other database types, the full user@host is checked first, then simply host, then user@.domain (with all superdomains checked in sequence, so "foo.example.com" would first check "user@foo.example.com", then "user@.example.com", then "user@.com"), then .domain, then user@*, and finally *.
In any case, only the first match is applied.
.TP
.I SigningTableEd25519 (dataset)
Defines a table used to select one or more signatures to apply to a message based on the address found in the From: header field. Keys in this table vary depending on the type of table used; values in this data set should include one field that contains a name found in the KeyTable (see above) that identifies which key should be used in generating the signature, and an optional second field naming the signer of the message that will be included in the "i=" tag in the generated signature. Note that the "i=" value will not be included in the signature if it conflicts with the signing domain (the "d=" value).
If the first field contains only a "%" character, it will be replaced by the domain found in the From: header field. Similarly, within the optional second field, any "%" character will be replaced by the domain found in the From: header field.
If this table specifies a regular expression file ("refile"), then the keys are wildcard patterns that are matched against the address found in the From: header field. Entries are checked in the order in which they appear in the file. ["refile support not implemented"].
For all other database types, the full user@host is checked first, then simply host, then user@.domain (with all superdomains checked in sequence, so "foo.example.com" would first check "user@foo.example.com", then "user@.example.com", then "user@.com"), then .domain, then user@*, and finally *.
In any case, only the first match is applied.
.TP .TP
.I Socket (string) .I Socket (string)
@@ -391,6 +430,12 @@ is not given as either a hostname or an IP address, the socket will be
listening on all interfaces. A literal IP address must be enclosed in listening on all interfaces. A literal IP address must be enclosed in
square brackets. This option is mandatory in the configuration file. square brackets. This option is mandatory in the configuration file.
.TP
.I SubDomains (Boolean)
Sign subdomains of those listed by the
.I Domain
parameter as well as the actual domains.
.TP .TP
.I Syslog (Boolean) .I Syslog (Boolean)
Log via calls to Log via calls to
+6 -6
View File
@@ -1,7 +1,7 @@
#! /usr/bin/python #! /usr/bin/python3
# dkimpy-milter: A DKIM signing/verification Milter application # dkimpy-milter: A DKIM signing/verification Milter application
# Author: Scott Kitterman <scott@kitterman.com> # Author: Scott Kitterman <scott@kitterman.com>
# Copyright 2018 Scott Kitterman # Copyright 2018,2019 Scott Kitterman
""" This program is free software; you can redistribute it and/or modify """ This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or the Free Software Foundation; either version 2 of the License, or
@@ -24,13 +24,13 @@ description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for P
kw = {} # Work-around for lack of 'or' requires in setuptools. kw = {} # Work-around for lack of 'or' requires in setuptools.
try: try:
import dns import dns
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dnspython'] kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'dnspython']
except ImportError: # If PyDNS is not installed, prefer dnspython except ImportError: # If PyDNS is not installed, prefer dnspython
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'PyDNS'] kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'Py3DNS']
setup( setup(
name='dkimpy-milter', name='dkimpy-milter',
version='1.0.1', version='1.1.4',
author='Scott Kitterman', author='Scott Kitterman',
author_email='scott@kitterman.com', author_email='scott@kitterman.com',
url='https://launchpad.net/dkimpy-milter', url='https://launchpad.net/dkimpy-milter',
@@ -43,7 +43,7 @@ setup(
'License :: OSI Approved :: GNU General Public License (GPL)', 'License :: OSI Approved :: GNU General Public License (GPL)',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: Python :: 2 :: Only', 'Programming Language :: Python :: 3 :: Only',
'Topic :: Communications :: Email :: Mail Transport Agents', 'Topic :: Communications :: Email :: Mail Transport Agents',
'Topic :: Communications :: Email :: Filters', 'Topic :: Communications :: Email :: Filters',
'Topic :: Security', 'Topic :: Security',
+9 -7
View File
@@ -20,9 +20,9 @@
### END INIT INFO ### END INIT INFO
prefix="/usr/local" prefix="/usr/local"
exec_prefix=${prefix} exec_prefix=${prefix}
sysconfdir="/etc/dkimpy-milter" sysconfdir="/usr/local/etc"
bindir="${exec_prefix}/bin/" bindir="${exec_prefix}/bin/"
RUNDIR="/var/run/dkimpy-milter" RUNDIR="/run/dkimpy-milter"
DAEMON=${bindir}/dkimpy-milter DAEMON=${bindir}/dkimpy-milter
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin: PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:
NAME=dkimpy-milter NAME=dkimpy-milter
@@ -67,14 +67,14 @@ case "$1" in
fi fi
fi fi
fi fi
start-stop-daemon --start --background --quiet --pidfile \
start-stop-daemon --start --quiet --pidfile $RUNDIR/$NAME.pid --startas \ $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
$DAEMON $sysconfdir/$NAME.conf --name $NAME --test > /dev/null \
echo "$NAME." echo "$NAME."
;; ;;
stop) stop)
echo -n "Stopping $DESC: " echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid
#echo $SOCKET #echo $SOCKET
@@ -87,6 +87,7 @@ case "$1" in
force-reload) force-reload)
echo -n "Force reloading $DESC: " echo -n "Force reloading $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid
#echo $SOCKET #echo $SOCKET
@@ -95,7 +96,7 @@ case "$1" in
fi fi
fi fi
sleep 1 sleep 1
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \ start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME." echo "$NAME."
;; ;;
@@ -103,6 +104,7 @@ case "$1" in
echo "Restarting $DESC: " echo "Restarting $DESC: "
echo -n "Stopping $DESC: " echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid
#echo $SOCKET #echo $SOCKET
@@ -113,7 +115,7 @@ case "$1" in
echo "$NAME." echo "$NAME."
sleep 1 sleep 1
echo -n "Starting $DESC: " echo -n "Starting $DESC: "
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \ start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME." echo "$NAME."
;; ;;
+1 -1
View File
@@ -5,7 +5,7 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
PIDFile=/var/run/dkimpy-milter/dkimpy-milter.pid PIDFile=/run/dkimpy-milter/dkimpy-milter.pid
ExecStart=/usr/local/bin/dkimpy-milter /usr/local/etc/dkimpy-milter.conf ExecStart=/usr/local/bin/dkimpy-milter /usr/local/etc/dkimpy-milter.conf
[Install] [Install]
+32
View File
@@ -0,0 +1,32 @@
This directory contains example systemd unit files for running a
supervised, socket-activated instance of dkimpy-milter.
There are several advantages of using socket activation:
- dkimpy-milter never runs with elevated privileges, they are dropped
before any dkimpy-milter code is executed.
- The socket is opened before dkimpy-milter runs. This means that
clients can connect() to the socket immediately. So even if there
is a delay in dkimpy-milter startup, or in libmilter itself, the
connection will not fail.
- You can set the privileges of a listening Unix-domain socket by an
override of ListenGroup= in dkimpy-milter.socket (see
systemd.unit(5) for how to override). This lets you control who has
access to the daemon with finer granularity than is available with
dkimpy-milter on its own.
- dkimpy-milter will not consume system resources if it is not used.
- A fully-supervised dkimpy-milter needs no PIDFile, UMask, UserID, or
Socket configuation. This eliminates common race conditions and
startup failures, and simplifies the resulting configuration file.
There is one downside to using socket activation:
- it will only work on systems where libmilter can support connection
strings like "fd:3". This has been supported on Debian and derived
systems since sendmail 8.14.4-6 (before Debian Jessie, in early
2014), see for example:
https://sources.debian.org/src/sendmail/8.15.2-8/debian/patches/socket_activation.patch/
@@ -0,0 +1,11 @@
[Unit]
Description=DKIMpy Milter
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
Requires=dkimpy-milter.socket
[Service]
ExecStart=/usr/bin/dkimpy-milter /etc/dkimpy-milter.conf
User=dkimpy-milter
[Install]
Also=dkimpy-milter.socket
@@ -0,0 +1,12 @@
[Unit]
Description=DKIMpy Milter socket
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
[Socket]
ListenStream=/run/dkimpy-milter/dkimpy-milter.sock
SocketMode=0660
# override SocketGroup to grant access to members of another system group:
SocketGroup=dkimpy-milter
[Install]
WantedBy=sockets.target
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
python2 -m dkimpy_milter "$@" python3 -m dkimpy_milter "$@"