Compare commits

...

49 Commits

Author SHA1 Message Date
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 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 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 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
Daniel Kahn Gillmor ad8f396db0 Expand test suite to cover RSA as well as ed25519 2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor 479820a07d tests: test DKIM signing and verification
This test makes use of DNSOverride and the new verifying milter to
ensure that signatures can be verified properly.

It doesn't test the actual interaction with the public DNS, but
getting that kind of test to work on arbitrary platforms might be more
trouble than it's worth.

I note that the DNSOverride only works as long as testkey.dns is a
single line, which is fine for ed25519, but maybe not for RSA.
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor 7bfb87fab7 Set up __main__.py, use it in tests
This allows us to invoke dkimpy-milter as "python -m dkimpy_milter
dkimpy-milter.conf", which makes running the test suite easier.
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor 5c1d5d6e52 tests: Run a verifying milter as well as a signing milter
Having a verifying milter will come in handy when we want to test both
sides of the DKIM process.
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor ae31730593 check for actions claimed by the filter 2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor 72ed000ccf simple testing framework 2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor b3db013754 config: Reassemble strings sensibly
If a string-based configuation entry had whitespace in it, it would be
reassembled via a round-trip through the python interpreter, resulting
in a line like this:

    PidFile /home/dkimpy-milter/pid file

produces a string like "['/home/dkimpy-milter/pid', 'file']", which is
clearly wrong.

I don't want to encourage people to use paths or other strings with
whitespace in them, but if we're going to fail on them we should be
failing explicitly, not doing a weird transformation that will just
break.

This is concretely useful for the DNSOverride mechanism, which is
where i ran into the problem when trying to set up testing that could
work without setting up an emulated DNS system.
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor bd1d25d83e Set up correct AuthservID defaults
Without this fix, a verifying dkimpy-milter that has no explicit
AuthservID produces the following crashing behavior as it tries to
create the authres header:

Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/Milter/__init__.py", line 772, in <lambda>
    milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
  File "…/dkimpy_milter/__init__.py", line 199, in eom
    h = fold(str(h))
  File "/usr/lib/python2.7/dist-packages/authres/core.py", line 476, in __str__
    return ''.join((self.HEADER_FIELD_NAME, ': ', self.header_value()))
  File "/usr/lib/python2.7/dist-packages/authres/core.py", line 496, in header_value
    return ''.join(strs)
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor 1c6030024d add DNSOverride configuration for testing 2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor 71c0c3f20a Avoid failing to chown non-existent Unix-domain sockets
Changing ownership of sockets that doesn't exist isn't a great
practice.

A better approach would be to apply os.chown() to the file descriptor
of the open socket, but at the very least dkimpy-milter shouldn't
crash the way it currently does if the socket isn't already present.
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor a9a6893c89 Handle unix: socket prefix the same as local:
sendmail's milter.c treats these two declarations the same way, so
what we do for one should also be done for the other.
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor bb44f36519 When Socket is absolute path, do not strip leading /
This appears to just be an untested codepath.
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor 9e11b75ec3 Avoid AttributeError on simple connection and disconnection
Without this patch, this simple script for miltertest:

----
conn = mt.connect("unix:milter.sock")
if conn == nil then
  error "mt.connect() failed"
end
if mt.conninfo(conn, nil, "unspec") ~= nil then
  error "mt.conninfo() failed"
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
     error "mt.conninfo() unexpected reply"
end
mt.disconnect(conn)
----

Produces the following error:

Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/Milter/__init__.py", line 702, in connect_callback
    return m.connect(hostname,family,hostaddr)
  File "/usr/lib/python2.7/dist-packages/Milter/__init__.py", line 173, in wrapper
    rc = func(self,*args)
  File "/home/dkg/src/dkimpy-milter/dkimpy-milter/dkimpy_milter/__init__.py", line 64, in connect
    self.receiver = self.getsymval('j').strip()
AttributeError: 'NoneType' object has no attribute 'strip'
2019-02-21 19:21:38 -05:00
Daniel Kahn Gillmor f60ea12e86 Prefer dnspython over PyDNS in setup.py
README and dkimpy_milter/dnsplug.py both prefer dnspython if
available, over PyDNS.  setup.py should order the preferences in the
same way.
2019-02-21 19:21:36 -05:00
Daniel Kahn Gillmor e872bd44b0 ignore emacs turds 2019-02-19 18:03:54 -05:00
Scott Kitterman 51c8fdcb6c Bump version to 1.0.1, update TODO, set release date 2019-02-11 15:14:11 -05:00
Scott Kitterman aa4dadc22f * Reorder milter start and dropping privileges so permissions on Unix socket
are correct (LP: 1797720)
2019-02-11 15:09:34 -05:00
Scott Kitterman b1abbf9d61 - Make domain checks case insensitive for determining if signing should be
done (LP: #1815311)
2019-02-11 14:55:35 -05:00
Scott Kitterman ea2b612e8d - Add information on Ed25519 key creation to README (LP: #1815313) 2019-02-11 14:23:55 -05:00
Scott Kitterman 5945e818ca - Add additional Sendmail configuration information to README from OpenDKIM
update based on input from Дилян Палаузов (LP: #1801619)
2019-02-11 13:32:37 -05:00
Scott Kitterman f38fed3bee Rip out unused whichbd module in preparation for python3 port 2019-02-11 03:16:53 -05:00
Scott Kitterman 06948b3dbc Update references in man/dkimpy-milter.8 2019-02-09 22:20:01 -05:00
Scott Kitterman e951ab6c5e Remove obsolete .IX macro from man pages
Conflicts:
	man/dkimpy-milter.conf.5
2019-02-09 22:19:41 -05:00
Scott Kitterman 03c86a2b08 Fix grammar error in README 2019-02-09 22:18:11 -05:00
Scott Kitterman 2cda1758c1 Fix spelling error in README 2019-02-09 22:17:53 -05:00
Scott Kitterman a188bd3960 Deleted reference to obsolete syslog target in unit file 2018-05-11 14:31:15 -04:00
Scott Kitterman c91a12f0d1 Documentation updates for 1.0.0 release 2018-05-11 14:29:15 -04:00
Scott Kitterman 286ffbb6c9 Add release date for 0.9.7 to CHANGES 2018-03-19 01:07:52 -04:00
Scott Kitterman ec3252c367 - Minor sysv init improvments 2018-03-15 23:59:03 -04:00
Scott Kitterman a2ff03727d - Put version directly in setup.py and do not import dkimpy_milter to ease
install via pip
2018-03-15 23:44:31 -04:00
Scott Kitterman af4b05e242 - Add missing documentation key to system/dkimpy-milter.service 2018-03-15 20:49:35 -04:00
Scott Kitterman 6509eaad35 - Made sysv init executable 2018-03-15 20:44:16 -04:00
Scott Kitterman f9483fea8c - Added protection for malformed From addresses. If the From does not at
least have an '@' in the address, then the signing domain is not extracted
   and the message will not be signed
2018-03-15 20:42:49 -04:00
Scott Kitterman 7a3a7bfb43 Bump version to 0.9.6 2018-03-12 22:08:07 -04:00
Scott Kitterman 8a0e1bdd97 - Fixed typo in path for fallback location of the config file if one is not
provided
2018-03-12 22:03:45 -04:00
Scott Kitterman e3005aa723 Move OversignHeaders up earlier on TODO. 2018-03-12 22:03:36 -04:00
Scott Kitterman 45d3ba13ca Added more to README about first run with systemd 2018-03-11 00:42:22 -05:00
Scott Kitterman f05309437f Fix merge conflict 2018-03-11 00:28:08 -05:00
Scott Kitterman d4499f6990 Fixed typo in package installation section of README 2018-03-11 00:27:19 -05:00
Scott Kitterman 7d87309f4b Fixed typo in package installation section of README 2018-03-11 00:24:39 -05:00
Scott Kitterman 1d8c309da9 Fix setup.py install locations so they are installed correctly and drop unneeded README changes. 2018-03-10 20:06:21 -05:00
Scott Kitterman 4d5961e4d5 Bump version 2018-03-10 19:52:29 -05:00
Scott Kitterman 59448e8e57 - Add information to README about manually putting init scripts in the right
locations
2018-03-10 19:51:29 -05:00
Scott Kitterman 695de0db14 - Add conf file location to systemd unit file 2018-03-10 19:43:37 -05:00
22 changed files with 628 additions and 149 deletions
+1
View File
@@ -1,3 +1,4 @@
dist dist
dkimpy_milter.egg-info dkimpy_milter.egg-info
*.pyc *.pyc
*~
+33
View File
@@ -1,3 +1,36 @@
1.0.1 2019-02-11
* Reorder milter start and dropping privileges so permissions on Unix socket
are correct (LP: 1797720)
- Make domain checks case insensitive for determining if signing should be
done (LP: #1815311)
- Add additional Sendmail configuration information to README from OpenDKIM
update based on input from Дилян Палаузов (LP: #1801619)
- Add information on Ed25519 key creation to README (LP: #1815313)
1.0.0 2018-05-11
- Minor documentation updates
- Deleted reference to obsolete syslog target in unit file
0.9.7 2018-03-19
- Made sysv init executable
- Add missing documentation key to system/dkimpy-milter.service
- Put version directly in setup.py and do not import dkimpy_milter to ease
install via pip
- Minor sysv init improvments
0.9.6 2018-03-13
- Fixed typo in package installation section of README
- Added more to README about first run with systemd
- Fixed typo in path for fallback location of the config file if one is not
provided
- Added protection for malformed From addresses. If the From does not at
least have an '@' in the address, then the signing domain is not extracted
and the message will not be signed
0.9.5.1 2018-03-10
- Add conf file location to systemd unit file
- Fix setup.py install locations so they are installed correctly
0.9.5 2018-03-10 0.9.5 2018-03-10
- Beta 1 (updated Alpha -> Beta warning in README and trove classifiers) - Beta 1 (updated Alpha -> Beta warning in README and trove classifiers)
- Added support for MacroList option - Added support for MacroList option
+135 -19
View File
@@ -1,11 +1,17 @@
This is a DKIM signing and verification milter. In theory it works with both OVERVIEW
Postfix and Sendmail, but the author has zero experience with Sendmail, so ========
reports of success/failure with Sendmail and patches are welcom.
This is a DKIM signing and verification milter. It has been tested with both
Postfix and Sendmail.
The configuration file is designed to be compatible with OpenDKIM, but only The configuration file is designed to be compatible with OpenDKIM, but only
a subset of OpenDKIM options are supported. If an unsupported option is a subset of OpenDKIM options are supported. If an unsupported option is
specified, an error will be raised. specified, an error will be raised.
INSTALLATION
===========
This package includes a default configuration file and man pages. For those This package includes a default configuration file and man pages. For those
to be installed when installing using setup.py, the following incantation is 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
@@ -13,11 +19,11 @@ default is a feature:
python setup.py install --single-version-externally-managed --record=/dev/null python setup.py install --single-version-externally-managed --record=/dev/null
For users of Debian Stable (Debian 9, Codename Squueze), 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 pthon-ipaddress python-dnspython [sudo] apt install python-milter python-nacl python-ipaddress python-dnspython
[sudo] apt install -t squeeze-backports python-authres python-dkim [sudo] apt install -t stretch-backports python-authres python-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):
@@ -25,12 +31,57 @@ packages are not available):
[sudo] pip install dkimpy_milter [sudo] pip install dkimpy_milter
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. have not been previously installed. Because pymilter and PyNaCl are compiled
Python extensions, the system will need appropriate development packages and
an C compiler. Alternately, install these dependencies from dsitribution/OS
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 pydns (DNS) or dnspython (dns), preferring
dnspython is both are available. The dkimpy DKIM module also works with dnspython is both are available. The dkimpy DKIM module also works with
either. either.
SETUP
====
SIGNING KEYS
============
In order to create DKIM signatures, a private key must be available. Signing
keys should be protected (owned by root:root with permissions 600 in a
directory that is not world readable). Different keys are required for RSA
and (if used) Ed25519.
RSA
===
Both public and private keys for RSA have standard formats and there are many
tools available to create them. Keys must (RFC 8302) have a minimum size of
1024 bits and should have a size of at least 2048 bits. The dknewkey script
that is provided with dkimpy is one such tool:
dknewkey exampleprivkey
will produce both the private key file (.key suffix) and a file with the DKIM
public key record to be published DNS (.dns suffix). RSA is the default key
type. 2048 bits is the default key size.
ED25519
=======
There is no standardized non-binary representation for Ed25519 private keys,
so in order to generate Ed25519 keys for dkimpy-milter, dkimpy specific tools
must be used to be compatible. The same dknewkey script support Ed25519:
dknewkey --ktype ed25519 anothernewkey
will provide both the private key file (.key suffix) and a file with the DKIM
public key record to be published DNS (.dns suffix). Ed25519 keys do not have
variable bit lengths.
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 is Debian specific and
@@ -48,10 +99,19 @@ As an example, using the default dkimpy-user on Debian, the command would be:
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.
To start dkimpy-milter with systemd for the first time, you will need to take
the following steps:
[sudo] systemctl daemon-reload
[sudo] systemctl enable dkimpy-milter
[sudo] systemctl start dkimpy-milter
[sudo] systemctl status dkimpy-milter (to verify it started correctly)
As with all milters, dkimpy-milter needs to be integrated with your MTA of As with all milters, dkimpy-milter needs to be integrated with your MTA of
choice (Sendmail or Postfix). choice (Sendmail or Postfix).
For Sendmail: SENDMAIL
========
Configuration is very similar to opendkim, but needs some adjustment for 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
@@ -67,12 +127,64 @@ Milter support should be present by default in most versions of sendmail
these days, but if not included in your Sendmail build, see: these days, but if not included in your Sendmail build, see:
http://www.elandsys.com/resources/sendmail/milter.html http://www.elandsys.com/resources/sendmail/milter.html
For Postfix: ISSUES USING SENDMAIL TO SIGN AND VERIFY
========================================
When using the sendmail MTA in both signing and verifying mode, there are
a few issues of which to be aware that might cause operational problems
and deserve consideration.
(a) When the MTA will be used for relaying emails, e.g. delivering to other
hosts using the aliases mechanism, it is important not to break
signatures inserted by the original sender. This is particularly sensitive
particular when the sending domain has published a "reject" DMARC policy.
By default, sendmail quotes to address header fields when there are no
quotes and the display part of the address contains a period or an
apostrophe. However, opendkim only sees the raw, unmodified form of
the header field, and so the content that gets verified and what gets
signed will not be the same, guaranteeing the attached signature is not
valid.
To direct sendmail not to modify the headers, add this to your sendmail.mc:
conf(`confMUST_QUOTE_CHARS', `')
(b) As stated in sendmail's KNOWNBUGS file, sendmail truncates header field
values longer than 256 characters, which could mean truncating the domain
of a long From: header field value and invalidating the signature.
You may wish to consider increasing MAXNAME in sendmail/conf.h to mitigate
changing the messages and invalidating their signatures. This change
requires recompiling sendmail.
(c) Similar to (a) above, sendmail may wrap very long single-line recipient
fields for presentation purposes; for example:
To: very long name <a@example.org>,anotherloo...ong name b <b@example.org>
...might be rewritten as:
To: very long name <a@example.org>,
anotherloo...ong name b <b@example.org>
This rewrite is also done after opendkim has seen the message, meaning
the signature opendkim attaches to the message does not match the
content it signed. There is not a known configuration change to
mitigate this mutation.
The only known mechanism for dealing with this is to have distinct
instances of opendkim do the verifying (inbound) and signing (outbound)
so that the version that arrives at the signing instance is already
in the rewritten form, guaranteeing the input and output are the same
and thus the signature matches the payload.
POSTFIX
=======
Integration of dkimpy-milter into Postfix is like any milter (See Postfix's Integration of dkimpy-milter into Postfix is like any milter (See Postfix's
README_FILES/MILTER_README). Here's an example master.cf excerpt the talks to README_FILES/MILTER_README). Here's an example master.cf excerpt that talks
two dkimpy-milter instances, one configured for signing and one configured for to two dkimpy-milter instances, one configured for signing and one configured
verification: for verification:
smtp inet n - - - - smtpd smtp inet n - - - - smtpd
... ...
@@ -117,11 +229,15 @@ MacroListVerify daemon_name|VERIFYING
... ...
The python DKIM library, dkimpy, requires the entire message being signed or NOTES
verified to be in memory, so dkimpy-milter does not write messages out to a temp =====
file. This may impact performance on low-memory systems.
This is an beta grade release to support interoperability testing with Ed25519 The python DKIM library, dkimpy, requires the entire message being signed or
signatures sufficient functionality for basic use. The documented verified to be in memory, so dkimpy-milter does not write messages out to a
functionality has been implemented and at least partially tested. It is free temp file. This may impact performance on low-memory systems.
of known major defects, but is not fully tested in a variety of environments.
DKIM with Ed25519 signatures are described in RFC 8463. Version 1.0.0 and
later support Ed25519 signing and verification. RFC 8301 removed rsa-sha1
from DKIM. dkimpy-milter does not sign with rsa-sha1, but still considers
rsa-sha1 signatures as valid for verification because they are still in
common use and are not known to be cryptographically broken.
+11 -5
View File
@@ -39,10 +39,16 @@ MacroListVerify implemented verified
SyslogSuccess implemented verified SyslogSuccess implemented verified
1.0.0 1.0.0
No additional features planned No additional features
Plannedataset type support (if needed): 1.0.1
db:/.db Bug fix only, improved documentation
1.1.0 (planned)
Port to Python 3
Subdomain support
Planned dataset type support (if needed):
mdb: mdb:
Considered for near-term feature release Considered for near-term feature release
@@ -50,9 +56,10 @@ Considered for near-term feature release
AlwaysAddARHeader AlwaysAddARHeader
ChangeRootDirectory ChangeRootDirectory
ClockDrift (requires dkimpy change) ClockDrift (requires dkimpy change)
DNSTimeout (requires dkmpy change) DNSTimeout (requires dkimpy change)
MilterDebug MilterDebug
MinimumKeyBits MinimumKeyBits
OversignHeaders (may require dkimpy changes)
PeerList PeerList
SignatureAlgorithm SignatureAlgorithm
@@ -83,7 +90,6 @@ On-InternalError
On-KeyNotFound On-KeyNotFound
On-NoSignature On-NoSignature
On-SignatureError On-SignatureError
OversignHeaders
RemoveARAll RemoveARAll
RemoveARFrom RemoveARFrom
RemoveOldSignatures RemoveOldSignatures
+64 -44
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,8 +28,9 @@ 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
@@ -39,7 +40,7 @@ from dkimpy_milter.util import read_keyfile
from dkimpy_milter.util import own_socketfile from dkimpy_milter.util import own_socketfile
from dkimpy_milter.util import fold from dkimpy_milter.util import fold
__version__ = "0.9.5" __version__ = "1.0.1"
FWS = re.compile(r'\r?\n[ \t]+') FWS = re.compile(r'\r?\n[ \t]+')
@@ -61,7 +62,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 +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:
@@ -130,20 +133,23 @@ class dkimMilter(Milter.Base):
self.has_dkim += 1 self.has_dkim += 1
if lname == 'from': if lname == 'from':
fname, self.author = parseaddr(val) fname, self.author = parseaddr(val)
self.fdomain = self.author.split('@')[1] try:
self.fdomain = self.author.split('@')[1].lower()
except IndexError as er:
self.fdomain = '' # 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)) self.fp.write(b"%s: %s\n" % (codecs.encode(name, 'ascii'), codecs.encode(val, 'ascii')))
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
@@ -190,20 +196,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)
@@ -213,11 +219,12 @@ 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)):
@@ -225,15 +232,15 @@ class dkimMilter(Milter.Base):
'd={3})'.format(self.getsymval('i'), 'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a'), d.signature_fields.get(b'a'),
d.signature_fields.get(b's'), d.signature_fields.get(b's'),
d.domain)) d.domain.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)):
@@ -241,7 +248,7 @@ class dkimMilter(Milter.Base):
'd={3})'.format(self.getsymval('i'), 'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a'), d.signature_fields.get(b'a'),
d.signature_fields.get(b's'), d.signature_fields.get(b's'),
d.domain)) d.domain.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))
@@ -255,21 +262,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'):
@@ -278,9 +287,9 @@ class dkimMilter(Milter.Base):
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: {0}".format(x))
self.header_i = d.signature_fields.get(b'i') self.header_i = codecs.decode(d.signature_fields.get(b'i'), 'ascii')
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')
if res: if res:
if (milterconfig.get('Syslog') and if (milterconfig.get('Syslog') and
(milterconfig.get('SyslogSuccess') or (milterconfig.get('SyslogSuccess') or
@@ -289,8 +298,8 @@ class dkimMilter(Milter.Base):
'd={3})'.format(self.getsymval('i'), 'd={3})'.format(self.getsymval('i'),
d.signature_fields.get(b'a'), d.signature_fields.get(b'a'),
d.signature_fields.get(b's'), d.signature_fields.get(b's'),
d.domain)) d.domain.lower()))
self.dkim_domain = d.domain self.dkim_domain = d.domain.lower()
else: else:
if milterconfig.get('DiagnosticDirectory'): if milterconfig.get('DiagnosticDirectory'):
fd, fname = tempfile.mkstemp(".dkim") fd, fname = tempfile.mkstemp(".dkim")
@@ -300,7 +309,7 @@ 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)) syslog.syslog('DKIM: Fail ({0})'.format(d.domain.lower()))
if res: if res:
result = 'pass' result = 'pass'
else: else:
@@ -324,7 +333,7 @@ def main():
global privateEd25519 global privateEd25519
privateRSA = False privateRSA = False
privateEd25519 = False privateEd25519 = False
configFile = '/etc/dkimpy-milter.conf' configFile = '/usr/local/etc/dkimpy-milter.conf'
if len(sys.argv) > 1: if len(sys.argv) > 1:
if sys.argv[1] in ('-?', '--help', '-h'): if sys.argv[1] in ('-?', '--help', '-h'):
print('usage: dkimpy-milter [<configfilename>]') print('usage: dkimpy-milter [<configfilename>]')
@@ -345,13 +354,24 @@ 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)
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 started:{0} user:{1}'
.format(pid, milterconfig.get('UserID'))) .format(pid, milterconfig.get('UserID')))
sys.stdout.flush()
Milter.runmilter(miltername, socketname, 240)
own_socketfile(milterconfig)
drop_privileges(milterconfig)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/python3
from dkimpy_milter import main
if __name__ == "__main__":
main()
+19 -26
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',
@@ -48,6 +48,7 @@ defaultConfigData = {
'DiagnosticDirectory': '', 'DiagnosticDirectory': '',
'MacroList': '', 'MacroList': '',
'MacroListVerify': '', 'MacroListVerify': '',
'DNSOverride': None,
'debugLevel': 0 # Undocumented config item for developer use 'debugLevel': 0 # Undocumented config item for developer use
} }
@@ -84,14 +85,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 +110,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 +160,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 +225,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])
@@ -294,19 +295,7 @@ def _dataset_to_list(dataset):
else: else:
return [dataset.strip().strip(',')] return [dataset.strip().strip(',')]
if dataset[-3:] == '.db' or dataset[:3] == 'db:': if dataset[-3:] == '.db' or dataset[:3] == 'db:':
# This is a Sleepycat (Oracle) DB dataset # This is a Sleepycat (Oracle) DB dataset, which we dont support
import whichdb # Will need rewriting someday for python3
if dataset[-3:] == '.db':
dbname = dataset
elif dataset[:3] == 'db:':
dbname = dataset[3:]
else:
raise dkim.ParameterError('Unimplmented dataset type: {0}'
.format(type(dataset)))
if whichdb.whichdb(dbname) != 'dbhash':
raise dkim.ParameterError('Unimplmented dataset type: {0}'
.format(type(dataset)))
#TODO replace this with code to use db maps
raise dkim.ParameterError('Unsupported dataset db datase: {0}' raise dkim.ParameterError('Unsupported dataset db datase: {0}'
.format(type(dataset))) .format(type(dataset)))
@@ -346,13 +335,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)
@@ -400,6 +390,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)
@@ -411,7 +404,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
+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))
+27 -13
View File
@@ -115,45 +115,59 @@ 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): def own_socketfile(milterconfig, sockname=None):
"""If socket is Unix socket, chown to UserID before dropping privileges""" """If socket is Unix socket, chown to UserID before dropping privileges"""
import os import os
user, group = user_group(milterconfig.get('UserID')) user, group = user_group(milterconfig.get('UserID'))
if milterconfig.get('Socket')[:1] == '/': offset = None
os.chown(milterconfig.get('Socket')[1:], user, group) if sockname is None:
if milterconfig.get('Socket')[:6] == "local:": sockname = milterconfig.get('Socket')
os.chown(milterconfig.get('Socket')[6:], user, group) 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):
+5 -4
View File
@@ -127,7 +127,6 @@
.rm #[ #] #H #V #F C .rm #[ #] #H #V #F C
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "dkimpy-milter 8"
.TH dkimpyy-milter 8 .TH dkimpyy-milter 8
.SH NAME .SH NAME
.B dkimpy .B dkimpy
@@ -269,7 +268,7 @@ proposal, and Cisco's
.B Internet Identified Mail .B Internet Identified Mail
(IIM) proposal. (IIM) proposal.
.SH VERSION .SH VERSION
This man page covers version 0.9.4 of This man page covers version 1.1.0 of
.I dkimpy-milter. .I dkimpy-milter.
.SH COPYRIGHT .SH COPYRIGHT
Copyright (c) 2005-2008, Sendmail, Inc. and its suppliers. All rights Copyright (c) 2005-2008, Sendmail, Inc. and its suppliers. All rights
@@ -278,7 +277,7 @@ reserved.
Copyright (c) 2009-2013, 2015, The Trusted Domain Project. Copyright (c) 2009-2013, 2015, The Trusted Domain Project.
All rights reserved. All rights reserved.
Copyright (c) 2018 Scott Kitterman <scott@kitterman.com> Copyright (c) 2018, 2019 Scott Kitterman <scott@kitterman.com>
.SH SEE ALSO .SH SEE ALSO
.I dkimpy-milter.conf(5), sendmail(8) .I dkimpy-milter.conf(5), sendmail(8)
.P .P
@@ -292,4 +291,6 @@ RFC6376 - DomainKeys Identified Mail
.P .P
RFC7601 - Message Header Field for Indicating Message Authentication Status RFC7601 - Message Header Field for Indicating Message Authentication Status
.P .P
draft-ietf-dcrup-dkim-crypto - A new cryptographic signature method for DKIM RFC8301 - Cryptographic Algorithm and Key Usage Update to DomainKeys Identified Mail (DKIM)
.P
RFC8463 - A New Cryptographic Signature Method for DomainKeys Identified Mail (DKIM)
+8 -10
View File
@@ -127,16 +127,13 @@
.rm #[ #] #H #V #F C .rm #[ #] #H #V #F C
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "dkimpy-milter.conf 5"
.TH dkimpy-milter.conf 5 "2018-02-12" .TH dkimpy-milter.conf 5 "2018-02-12"
.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"
.IX Header "VERSION"
0\.9\.2 0\.9\.2
.SH "DESCRIPTION" .SH "DESCRIPTION"
.IX Header "DESCRIPTION"
.I dkimpy-milter(8) .I dkimpy-milter(8)
implements the implements the
.B DKIM .B DKIM
@@ -160,18 +157,15 @@ The provided setup.py installs this configuration file in /etc or
Command line invocation of parameters as is done by OpenDKIM is not supported. Command line invocation of parameters as is done by OpenDKIM is not supported.
.SH "USAGE" .SH "USAGE"
.IX Header "USAGE"
Usage: Usage:
dkimpy-milter [/etc/dkimpy-milter.conf] dkimpy-milter [/etc/dkimpy-milter.conf]
.SH "OTHER DOCUMENTATION" .SH "OTHER DOCUMENTATION"
.IX Header "OTHER DOCUMENTATION"
This documentation assumes you have read Postfix's README_FILES/MILTER_README This documentation assumes you have read Postfix's README_FILES/MILTER_README
(or Sendmail equivalent) and are generally familiar with Domain Keys Identified (or Sendmail equivalent) and are generally familiar with Domain Keys Identified
Mail (DKIM). See RFC 6376 for details. Mail (DKIM). See RFC 6376 for details.
.SH "SYNOPSIS" .SH "SYNOPSIS"
.IX Header "SYNOPSIS"
dkimpy-milter operates with a default installed configuration file and dkimpy-milter operates with a default installed configuration file and
set of default configuration options that are used if the configuration file set of default configuration options that are used if the configuration file
@@ -181,14 +175,12 @@ files can be used directly. Not all OpenDKIM options are supported. If an
unsupported option from OpenDKIM is specified, an error will be raised. unsupported option from OpenDKIM is specified, an error will be raised.
.SH "DESCRIPTION" .SH "DESCRIPTION"
.IX Header "DESCRIPTION"
Configuration options are described here and in the configuration file Configuration options are described here and in the configuration file
provided with the package. The provided setup.py installs this configuration provided with the package. The provided setup.py installs this configuration
file in /etc or /usr/local/etc. file in /etc or /usr/local/etc.
.SH "OPTIONS" .SH "OPTIONS"
.IX Header "OPTIONS"
.TP .TP
.I AuthservID (string) .I AuthservID (string)
@@ -319,6 +311,13 @@ be set:
(b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector; (b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector;
[fooTable options NOT IMPLEMENTED] [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)
Identifies a set of "peers" that identifies clients whose connections Identifies a set of "peers" that identifies clients whose connections
@@ -339,7 +338,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)
@@ -441,7 +440,6 @@ unless an alternate
is specified. is specified.
.SH "AUTHORS" .SH "AUTHORS"
.IX Header "AUTHORS"
\ddkimpy-milter\fR was written by Scott Kitterman <scott@kitterman.com>. \ddkimpy-milter\fR was written by Scott Kitterman <scott@kitterman.com>.
It is based on dkimpy-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc. It is based on dkimpy-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc.
Copyright (c) 2013-2015 Stuart D. Gathman Copyright (c) 2013-2015 Stuart D. Gathman
+9 -10
View File
@@ -1,4 +1,4 @@
#! /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 Scott Kitterman
@@ -18,33 +18,32 @@
from setuptools import setup from setuptools import setup
import os import os
import dkimpy_milter
description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail." description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail."
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', 'PyDNS']
setup( setup(
name='dkimpy-milter', name='dkimpy-milter',
version=dkimpy_milter.__version__, version='1.0.1',
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',
description=description, description=description,
download_url = "https://pypi.python.org/pypi/dkimpy-milter", download_url = "https://pypi.python.org/pypi/dkimpy-milter",
classifiers= [ classifiers= [
'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable',
'Environment :: No Input/Output (Daemon)', 'Environment :: No Input/Output (Daemon)',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',
'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',
@@ -59,8 +58,8 @@ setup(
data_files=[(os.path.join('share', 'man', 'man5'), data_files=[(os.path.join('share', 'man', 'man5'),
['man/dkimpy-milter.conf.5']), (os.path.join('share', 'man', 'man8'), ['man/dkimpy-milter.conf.5']), (os.path.join('share', 'man', 'man8'),
['man/dkimpy-milter.8']), ('etc', ['etc/dkimpy-milter.conf']), ['man/dkimpy-milter.8']), ('etc', ['etc/dkimpy-milter.conf']),
(os.path.join('/lib', 'systemd', 'system'), (os.path.join('lib', 'systemd', 'system'),
['system/dkimpy-milter.service']),(os.path.join('/etc', 'init.d'), ['system/dkimpy-milter.service']),(os.path.join('etc', 'init.d'),
['system/dkimpy-milter'])], ['system/dkimpy-milter'])],
zip_safe = False, zip_safe = False,
**kw **kw
Regular → Executable
+3 -5
View File
@@ -13,8 +13,6 @@
# Provides: dkim-milter dkim-milter-python dkimpy-milter # Provides: dkim-milter dkim-milter-python dkimpy-milter
# Required-Start: $remote_fs $syslog $network $time # Required-Start: $remote_fs $syslog $network $time
# Required-Stop: $remote_fs $syslog $network # Required-Stop: $remote_fs $syslog $network
# Should-Start:
# Should-Stop:
# Default-Start: 2 3 4 5 # Default-Start: 2 3 4 5
# Default-Stop: 0 1 6 # Default-Stop: 0 1 6
# Short-Description: dkimpy-milter # Short-Description: dkimpy-milter
@@ -31,7 +29,7 @@ NAME=dkimpy-milter
DESC="Python DKIM Milter" DESC="Python DKIM Milter"
USER=dkimpy-milter USER=dkimpy-milter
GROUP=dkimpy-milter GROUP=dkimpy-milter
SOCKET=$RUNDIR/dkimpy-milter.pid SOCKET=$RUNDIR/dkimpy-milter.sock
test -x $DAEMON || exit 0 test -x $DAEMON || exit 0
@@ -70,8 +68,8 @@ case "$1" in
fi fi
fi fi
start-stop-daemon --start --chuid $USER --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)
+3 -2
View File
@@ -1,11 +1,12 @@
[Unit] [Unit]
Description=DKIMpy Milter Description=DKIMpy Milter
After=syslog.target network.target Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
After=network.target
[Service] [Service]
Type=simple Type=simple
PIDFile=/var/run/dkimpy-milter/dkimpy-milter.pid PIDFile=/var/run/dkimpy-milter/dkimpy-milter.pid
ExecStart=/usr/local/bin/dkimpy-milter ExecStart=/usr/local/bin/dkimpy-milter /usr/local/etc/dkimpy-milter.conf
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
+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
+12
View File
@@ -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
+40
View File
@@ -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
+100
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
python3 -m dkimpy_milter "$@"
Executable
+84
View File
@@ -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