Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a1705926f | |||
| e6f8db9f94 | |||
| e63867d517 | |||
| 209ad11661 | |||
| 795a914845 | |||
| be92e5c5b1 | |||
| 6910ff1f9a | |||
| 7953e54ffb | |||
| bc98f9180f | |||
| a144791f2a | |||
| 55e1a6b54e | |||
| 7c3ff1905a | |||
| 7ec97a6001 | |||
| aba9c0aa0c | |||
| 19b6ce7a68 | |||
| 8dc3ac6474 | |||
| 290a37b99c | |||
| 564799402a | |||
| 357905bb68 | |||
| 6b851f18df | |||
| 7ab58edb1b | |||
| 23b0e8a386 | |||
| 2e105bd18c | |||
| fb72b9f6e7 | |||
| 7eed8995a2 | |||
| a16d887ac6 | |||
| 23d91b2b50 | |||
| 481fbdae29 | |||
| 4b0c39b0c7 | |||
| 7092874729 | |||
| ea09bab1a8 | |||
| 25fdd3b81c | |||
| 9d5316ca0e | |||
| 391b5352f3 | |||
| ad8f396db0 | |||
| 479820a07d | |||
| 7bfb87fab7 | |||
| 5c1d5d6e52 | |||
| ae31730593 | |||
| 72ed000ccf | |||
| b3db013754 | |||
| bd1d25d83e | |||
| 1c6030024d | |||
| 71c0c3f20a | |||
| a9a6893c89 | |||
| bb44f36519 | |||
| 9e11b75ec3 | |||
| f60ea12e86 | |||
| e872bd44b0 | |||
| 851f8ff9c9 | |||
| cbb6098dd8 | |||
| c90d694fff | |||
| 8d8cd15cba | |||
| 4f21623f92 | |||
| bf2548f891 |
@@ -1,3 +1,4 @@
|
|||||||
dist
|
dist
|
||||||
dkimpy_milter.egg-info
|
dkimpy_milter.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*~
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
1.0.3 2019-11-22
|
1.1.2 2019-09-23
|
||||||
- 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.0.2 2019-10-07
|
|
||||||
- Fix startup logging so it provides information at a useful time
|
|
||||||
- Fix message extraction so that signing in the same pass through the milter
|
|
||||||
as verifying works correctly
|
|
||||||
- Fix variable initialization so mailformed mails missing body From do not
|
- Fix variable initialization so mailformed mails missing body From do not
|
||||||
cause a traceback (LP: #1844161)
|
cause a traceback (LP: #1844161)
|
||||||
- Catch more ascii encoding errors to improve resilience against bad data
|
- Catch more ascii encoding errors to improve resilience against bad data
|
||||||
(LP: #1844189)
|
(LP: #1844189)
|
||||||
- Fix sysv init so it works (LP: #1839487)
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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 if both are available. The dkimpy DKIM module also works with
|
dnspython is both are available. The dkimpy DKIM module also works with
|
||||||
either.
|
either.
|
||||||
|
|
||||||
|
|
||||||
@@ -84,8 +84,9 @@ 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 uses start-stop-deamon
|
used, they will need to be updated. The sysv init file is Debian specific and
|
||||||
from Debian. It is not portable to systems without that available.
|
untested, since the developers are not using sysv init. Feedback/patches
|
||||||
|
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.
|
||||||
@@ -93,7 +94,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.
|
||||||
@@ -116,7 +117,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+78
-46
@@ -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,14 +28,16 @@ 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"
|
||||||
@@ -61,7 +63,9 @@ class dkimMilter(Milter.Base):
|
|||||||
self.external_connection = False
|
self.external_connection = False
|
||||||
self.hello_name = None
|
self.hello_name = None
|
||||||
# sometimes people put extra space in sendmail config, so we strip
|
# 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:
|
try:
|
||||||
self.AuthservID = milterconfig['AuthservID']
|
self.AuthservID = milterconfig['AuthservID']
|
||||||
except:
|
except:
|
||||||
@@ -108,7 +112,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:
|
||||||
@@ -141,16 +145,16 @@ class dkimMilter(Milter.Base):
|
|||||||
self.arheaders.append(val)
|
self.arheaders.append(val)
|
||||||
if self.fp:
|
if self.fp:
|
||||||
try:
|
try:
|
||||||
self.fp.write("%s: %s\n" % (name, val))
|
self.fp.write(b"%s: %s\n" % (codecs.encode(name, 'ascii'), codecs.encode(val, 'ascii')))
|
||||||
except:
|
except:
|
||||||
# Don't choke on header fields with garbage in them.
|
# Don't choke on header fields with non-ascii garbage in them.
|
||||||
pass
|
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
|
||||||
|
|
||||||
@@ -185,6 +189,8 @@ class dkimMilter(Milter.Base):
|
|||||||
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):
|
||||||
self.sign_dkim(txt)
|
self.sign_dkim(txt)
|
||||||
@@ -196,20 +202,20 @@ class dkimMilter(Milter.Base):
|
|||||||
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)
|
||||||
@@ -219,35 +225,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=(canonicalize[0],
|
||||||
canonicalize[1]))
|
canonicalize[1]))
|
||||||
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()))
|
||||||
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))
|
||||||
@@ -262,21 +269,23 @@ class dkimMilter(Milter.Base):
|
|||||||
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:
|
||||||
|
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)
|
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'):
|
||||||
@@ -284,18 +293,19 @@ 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: Internal program fault while verifying: {0}".format(x))
|
syslog.syslog("check_dkim: {0}".format(x))
|
||||||
try:
|
try:
|
||||||
self.header_i = d.signature_fields.get(b'i')
|
# i= is optional and dkimpy is fine if it's not provided
|
||||||
|
self.header_i = codecs.decode(d.signature_fields.get(b'i'), 'ascii')
|
||||||
except TypeError as x:
|
except TypeError as x:
|
||||||
self.header_i = None
|
self.header_i = None
|
||||||
try:
|
try:
|
||||||
self.header_d = d.signature_fields.get(b'd')
|
self.header_d = codecs.decode(d.signature_fields.get(b'd'), 'ascii')
|
||||||
self.header_a = d.signature_fields.get(b'a')
|
self.header_a = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
|
||||||
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: Internal proram fault extracting header a or d: {0}".format(x))
|
syslog.syslog("check_dkim: {0}".format(x))
|
||||||
self.header_d = None
|
self.header_d = None
|
||||||
if not self.header_a:
|
if not self.header_a:
|
||||||
self.header_a = 'rsa-sha256'
|
self.header_a = 'rsa-sha256'
|
||||||
@@ -305,9 +315,9 @@ class dkimMilter(Milter.Base):
|
|||||||
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'):
|
||||||
@@ -341,6 +351,16 @@ class dkimMilter(Milter.Base):
|
|||||||
self.header_a = None
|
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.
|
||||||
@@ -370,6 +390,18 @@ 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')
|
||||||
|
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'
|
||||||
|
own_socketfile(milterconfig, socketname)
|
||||||
drop_privileges(milterconfig)
|
drop_privileges(milterconfig)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from dkimpy_milter import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+30
-15
@@ -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',
|
||||||
@@ -48,6 +48,8 @@ defaultConfigData = {
|
|||||||
'DiagnosticDirectory': '',
|
'DiagnosticDirectory': '',
|
||||||
'MacroList': '',
|
'MacroList': '',
|
||||||
'MacroListVerify': '',
|
'MacroListVerify': '',
|
||||||
|
'DNSOverride': None,
|
||||||
|
'SubDomains': False,
|
||||||
'debugLevel': 0 # Undocumented config item for developer use
|
'debugLevel': 0 # Undocumented config item for developer use
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,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):
|
||||||
@@ -109,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
|
||||||
@@ -159,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
|
||||||
@@ -224,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])
|
||||||
@@ -308,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:
|
||||||
@@ -324,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',
|
||||||
@@ -334,13 +339,14 @@ def _readConfigFile(path, configData=None, configGlobal={}):
|
|||||||
'DiagnosticDirectory': 'str',
|
'DiagnosticDirectory': 'str',
|
||||||
'MacroList': 'dataset',
|
'MacroList': 'dataset',
|
||||||
'MacroListVerify': 'dataset',
|
'MacroListVerify': 'dataset',
|
||||||
|
'DNSOverride': 'str',
|
||||||
'debugLevel': 'int'
|
'debugLevel': 'int'
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -376,9 +382,15 @@ def _readConfigFile(path, configData=None, configGlobal={}):
|
|||||||
value = data[1:]
|
value = data[1:]
|
||||||
|
|
||||||
# check validity of name
|
# check validity of name
|
||||||
|
try:
|
||||||
conversion = nameConversion.get(name)
|
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
|
||||||
|
|
||||||
@@ -388,6 +400,9 @@ def _readConfigFile(path, configData=None, configGlobal={}):
|
|||||||
if conversion == 'bool':
|
if conversion == 'bool':
|
||||||
configData[name] = _find_boolean(value)
|
configData[name] = _find_boolean(value)
|
||||||
elif conversion == 'str':
|
elif conversion == 'str':
|
||||||
|
if isinstance(value, list):
|
||||||
|
configData[name] = line.split(None, 1)[1]
|
||||||
|
else:
|
||||||
configData[name] = str(value)
|
configData[name] = str(value)
|
||||||
elif conversion == 'int':
|
elif conversion == 'int':
|
||||||
configData[name] = int(value)
|
configData[name] = int(value)
|
||||||
@@ -399,7 +414,7 @@ def _readConfigFile(path, configData=None, configGlobal={}):
|
|||||||
configData[name] = conversion(value)
|
configData[name] = conversion(value)
|
||||||
fp.close()
|
fp.close()
|
||||||
try:
|
try:
|
||||||
configData['AuthservID'] = _make_authserv_id(configData['AuthservID'])
|
configData['AuthservID'] = _make_authserv_id(configData.get('AuthservID', 'HOSTNAME'))
|
||||||
configData['IntHosts'] = HostsDataset(configData['InternalHosts'])
|
configData['IntHosts'] = HostsDataset(configData['InternalHosts'])
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
+55
-8
@@ -115,37 +115,61 @@ 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, sockname=None):
|
||||||
|
"""If socket is Unix socket, chown to UserID before dropping privileges"""
|
||||||
|
import os
|
||||||
|
user, group = user_group(milterconfig.get('UserID'))
|
||||||
|
offset = None
|
||||||
|
if sockname is None:
|
||||||
|
sockname = milterconfig.get('Socket')
|
||||||
|
if sockname is None:
|
||||||
|
return
|
||||||
|
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
|
||||||
@@ -166,3 +190,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 signature’s "d=" value; (b) the name of the selector to use in the signature’s "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 signature’s "d=" value; (b) the name of the selector to use in the signature’s "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,13 @@ 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
|
||||||
|
.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
|
.TP
|
||||||
.I PeerList (dataset)
|
.I PeerList (dataset)
|
||||||
@@ -331,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)
|
||||||
@@ -345,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)
|
||||||
@@ -359,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)
|
||||||
@@ -384,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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -23,14 +23,14 @@ 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', 'PyDNS']
|
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', 'dnspython']
|
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'Py3DNS']
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='dkimpy-milter',
|
name='dkimpy-milter',
|
||||||
version='1.0.3',
|
version='1.1.2',
|
||||||
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',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
### END INIT INFO
|
### END INIT INFO
|
||||||
prefix="/usr/local"
|
prefix="/usr/local"
|
||||||
exec_prefix=${prefix}
|
exec_prefix=${prefix}
|
||||||
sysconfdir="/usr/local/etc"
|
sysconfdir="/etc/dkimpy-milter"
|
||||||
bindir="${exec_prefix}/bin/"
|
bindir="${exec_prefix}/bin/"
|
||||||
RUNDIR="/run/dkimpy-milter"
|
RUNDIR="/run/dkimpy-milter"
|
||||||
DAEMON=${bindir}/dkimpy-milter
|
DAEMON=${bindir}/dkimpy-milter
|
||||||
@@ -67,14 +67,14 @@ case "$1" in
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
start-stop-daemon --start --background --quiet --pidfile \
|
|
||||||
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
|
start-stop-daemon --start --quiet --pidfile $RUNDIR/$NAME.pid --startas \
|
||||||
|
$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,7 +87,6 @@ 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
|
||||||
@@ -96,7 +95,7 @@ case "$1" in
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
start-stop-daemon --start --background --quiet --pidfile \
|
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \
|
||||||
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
|
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
|
||||||
echo "$NAME."
|
echo "$NAME."
|
||||||
;;
|
;;
|
||||||
@@ -104,7 +103,6 @@ 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
|
||||||
@@ -115,7 +113,7 @@ case "$1" in
|
|||||||
echo "$NAME."
|
echo "$NAME."
|
||||||
sleep 1
|
sleep 1
|
||||||
echo -n "Starting $DESC: "
|
echo -n "Starting $DESC: "
|
||||||
start-stop-daemon --start --background --quiet --pidfile \
|
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \
|
||||||
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
|
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
|
||||||
echo "$NAME."
|
echo "$NAME."
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+40
@@ -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
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
-- -*- lua -*-
|
||||||
|
|
||||||
|
msg = {
|
||||||
|
['headers'] = {
|
||||||
|
['From'] = 'Alice <alice@example.net>',
|
||||||
|
['Message-Id'] = '<dkimpy-milter-test-02@example.net>',
|
||||||
|
['To'] = 'Bob <bob@example.biz>',
|
||||||
|
['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, "<alice@example.net>") ~= 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
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
python3 -m dkimpy_milter "$@"
|
||||||
Executable
+84
@@ -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" <<EOF
|
||||||
|
Domain example.net
|
||||||
|
$keyfile testkey.$keytype.key
|
||||||
|
$selector testkey
|
||||||
|
Socket unix:$keytype.signing.sock
|
||||||
|
PidFile $keytype.signing.pid
|
||||||
|
Mode s
|
||||||
|
UserID $(id --name --user):$(id --name --group)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$keytype.verify.conf" <<EOF
|
||||||
|
Socket unix:$keytype.verify.sock
|
||||||
|
PidFile $keytype.verify.pid
|
||||||
|
Mode v
|
||||||
|
DNSOverride $(cat testkey.$keytype.dns)
|
||||||
|
UserID $(id --name --user):$(id --name --group)
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo cleaning up jobs:
|
||||||
|
jobs
|
||||||
|
for keytype in "${KEY_TYPES[@]}"; do
|
||||||
|
for func in signing verify; do
|
||||||
|
if [ -s "$keytype.$func.pid" ] && kill -0 "$(cat "$keytype.$func.pid")"; then
|
||||||
|
kill "$(cat $keytype.$func.pid)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
for keytype in "${KEY_TYPES[@]}"; do
|
||||||
|
for func in signing verify; do
|
||||||
|
errdata="$keytype.$func.stderr"
|
||||||
|
if [ -s "$errdata" ]; then
|
||||||
|
printf -- "-> %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
|
||||||
Reference in New Issue
Block a user