Compare commits

...

151 Commits

Author SHA1 Message Date
Scott Kitterman def89db250 Set 1.2.0 release date 2020-01-03 18:12:49 -05:00
Scott Kitterman 1545d13fa0 Variable expansion related cleanup for man pages and sysv init 2020-01-03 17:11:43 -05:00
Scott Kitterman 8a2760531b Add support for specifying DNSTimeout (bumps required dkimpy version to 1.0) 2020-01-03 16:14:08 -05:00
Scott Kitterman 042005b38d Add support for storing DKIM failed mails in a specified DiagnosticDirectory 2019-12-24 14:43:14 -05:00
Scott Kitterman db268764f5 Update TODO 2019-12-24 09:47:03 -05:00
Scott Kitterman e83d4b9306 Add support for specifying MinimumKeyBits for RSA signatures 2019-10-30 16:29:00 -04:00
Scott Kitterman 472fc753e1 CHANGES entry for SigningTable/KeyTable 2019-10-30 16:24:19 -04:00
Scott Kitterman 57d92c1571 Update TODO 2019-10-30 15:42:34 -04:00
Scott Kitterman e233e0243c Add KeyTable processing, fix SigningTable matching code 2019-10-30 15:20:32 -04:00
Scott Kitterman e86b804d71 Minor test updates including fixing the table verify PID file specification 2019-10-30 15:19:59 -04:00
Scott Kitterman 9c9ab7d5d0 Support file specification in a dataset starting with './' and '../' in addition to '/'. Update man pages to indicate it is general and not just fo KeyTable*. 2019-10-30 13:56:59 -04:00
Scott Kitterman 1337ac1e1a More dkimpy-milter.conf.5 cleanups 2019-10-29 08:13:40 -04:00
Scott Kitterman e930257b6b - Delete own_socketfile to resolve race condition where the permissions
change fails on a Unix socket because it hasn't been created yet (libmilter
   will do this correctly on its own based on umask, the milter doesn't need
   to do it) (LP: #1849712)
2019-10-29 02:50:40 -04:00
Scott Kitterman 887a0c4b2a SigningTable refactor to work with the revised table structure - mostly works, but not thoroughly tested 2019-10-29 02:42:48 -04:00
Scott Kitterman 0feff9f539 man page editorial nit 2019-10-29 01:53:28 -04:00
Scott Kitterman 5b956b9c7d Refactor SigningTable and KeyTables based on more careful reading of the documentation 2019-10-29 01:50:28 -04:00
Scott Kitterman 403f8c8d1d Update description of command line options in dkimpy-milter.8 2019-10-28 09:32:17 -04:00
Scott Kitterman 6da97a07b3 Remove bdb from dkimpy-milter.8 since that is no longer a planned feature 2019-10-28 09:28:04 -04:00
Scott Kitterman f5f10f398b Add refile from opendkim.8 to dkimpy-milter.8 since we are going to support it now 2019-10-28 09:26:45 -04:00
Scott Kitterman e0dd40ff03 Update README.md for KeyTable, KeyTableEd25519, and SigningTable information based on COMPLEX SIGNING CONFIGURATIONS section of opendkim README. 2019-10-28 08:36:29 -04:00
Scott Kitterman a210032053 Clarify SigningTable description in dkimpy-milter.conf (5) 2019-10-23 16:24:49 -04:00
Scott Kitterman 82542e4ca0 Remove vestiges of SigningTableEd25519, separate per algorithm table not needed 2019-10-23 15:54:51 -04:00
Scott Kitterman b0604bf00c Add verify runtests for stable and table cases 2019-10-23 01:15:32 -04:00
Scott Kitterman 0115bf7c7c Add support for using signing table with % only - still very incomplete. 2019-10-22 23:35:58 -04:00
Scott Kitterman 5349d1b3ae Refactor multi-line datasets so it works with single and multiline (both KeyTable and SigningTable) 2019-10-22 23:33:31 -04:00
Scott Kitterman accabcf217 Add test cases for running with SigningTable and no KeyTable 2019-10-22 20:43:53 -04:00
Scott Kitterman f93dbeb966 Move _get_parent_domain into dkimMilter classs as get_parent_domain 2019-10-22 20:14:18 -04:00
Scott Kitterman d6b0acb101 Refactor signing domain determination into get_identities_sign function and add support for passing i= when signing in prepartation for parsing the signing table 2019-10-22 19:45:18 -04:00
Scott Kitterman 3061215f49 Run signing tests for table cases as well 2019-10-22 00:03:16 -04:00
Scott Kitterman 05038261f4 More tests/runtests updates for testing with tables 2019-10-22 00:02:29 -04:00
Scott Kitterman a752a9c829 Add test data and configuration for testing with signing and key tables 2019-10-21 14:08:23 -04:00
Scott Kitterman ec55aac974 Refactor util.read_keytable to work with multi-line dataset format from util._dataset_to_list 2019-10-21 14:05:58 -04:00
Scott Kitterman 43f6272b0d Complete multi-row dataset implementation for KeyTable* and SingingTable* 2019-10-21 14:00:17 -04:00
Scott Kitterman 5588748795 Refactor and update loading keys (file and table) to both are now loaded 2019-10-20 02:43:06 -04:00
Scott Kitterman 1097894eac Editorial nits 2019-10-19 12:34:22 -04:00
Scott Kitterman e9f95e0937 Refactor: move reading keys into util.get_keys in preparation for table variants 2019-10-19 03:50:17 -04:00
Scott Kitterman 0ac431a1bb Add new Table config items to nameConversion, not setting defaults 2019-10-19 03:38:56 -04:00
Scott Kitterman d0bc03453f Use self.conf vice milterconfig in dkimMilter to make sure config doesn't change while running 2019-10-19 03:31:29 -04:00
Scott Kitterman 2106e2b1f6 Refactor private key internal storage, it is now part of the milterconfig dict 2019-10-19 02:51:44 -04:00
Scott Kitterman bad89cec2a Clean up __pycache__ directory after test suite run 2019-10-19 00:00:12 -04:00
Scott Kitterman 70d10f9b1a Make error logging more explicit to aid debugging 2019-10-18 23:24:12 -04:00
Scott Kitterman c9f95e4045 Make test scripts executable 2019-10-18 21:46:15 -04:00
Scott Kitterman 68bd86e065 More sysvinit path fixes 2019-10-07 00:44:53 -04:00
Scott Kitterman 0a22747df6 Change README to markdown (README.md) and add as long_desciption to setup.py 2019-10-06 00:59:24 -04:00
Scott Kitterman df575ff80d Update README now that sysv init is tested 2019-10-06 00:15:40 -04:00
Scott Kitterman 4297b5dc68 XFix sysv init so it works (LP: #1839487) 2019-10-05 21:47:55 -04:00
Scott Kitterman 3b3e64c058 More reslience fixes 2019-09-23 11:35:44 -04:00
Scott Kitterman 5652fce7e2 Merge branch 'master' of git+ssh://git.launchpad.net/dkimpy-milter 2019-09-23 11:24:31 -04:00
Scott Kitterman d24b298dce Merge branch 'master' of git+ssh://git.launchpad.net/dkimpy-milter 2019-09-23 11:23:30 -04:00
Scott Kitterman 81a56c300e Merge branch 'master' of git+ssh://git.launchpad.net/dkimpy-milter 2019-09-23 11:01:56 -04:00
Scott Kitterman 47b7e9892f Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
2019-09-23 11:01:32 -04:00
Scott Kitterman 68e61d419b Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
2019-09-23 10:28:43 -04:00
Scott Kitterman 5800d25e0c Fix variable initialization so mailformed mails missing body From do not
cause a traceback (LP: #1844161)
2019-09-16 20:05:11 -04:00
Scott Kitterman 89bb7c71e9 Update TODO for 1.2 feature goals 2019-09-11 15:35:27 -04:00
Scott Kitterman 708e94f266 Add debug logging for content type to assist troubleshooting MIME
conversion issues
2019-09-11 15:31:15 -04:00
Scott Kitterman 39d79ae5a6 Add information on message content conversion to README 2019-09-11 15:25:50 -04:00
Scott Kitterman f0871078ac Add support for SignHeaders feature, thanks to Ralph Seichter for the patch 2019-09-11 13:53:54 -04:00
Scott Kitterman b735d223f5 Add LP bug number for missing i= fix 2019-09-06 00:52:50 -04:00
Scott Kitterman 34d440c7a7 Fixup missing i= processing 2019-09-06 00:27:26 -04:00
Scott Kitterman 5a68cf9e25 - Fix message extraction so that signing in the same pass through the milter
as verifying works correctly
2019-09-05 23:52:07 -04:00
Scott Kitterman 6f75a1a967 - Fix verify processing so missing (optional) i= tag doesn't cause the milter
to fail
2019-08-09 11:28:53 -04:00
Scott Kitterman 787e25325e Fix startup logging so it provides information at a useful time 2019-08-09 08:58:03 -04:00
Scott Kitterman a337e27f0d Permission changes 2019-08-04 16:35:28 -04:00
Scott Kitterman 9cd67c1b25 Clarify usage statement on bad command line run 2019-04-28 03:37:25 -04:00
Scott Kitterman 5ebaf5d848 - Add support for passing PID file name on command line to make it easier to keep system init and daemon configuration in sync. 2019-04-26 20:24:34 -04:00
Scott Kitterman 35745456a2 Ship openrc file in /etc/init.d 2019-04-26 19:57:06 -04:00
Scott Kitterman ec32109a52 Add post-expand output files using default values to install works if expand is not run. 2019-04-26 19:51:50 -04:00
Scott Kitterman 69721af3f8 Add documentation for expand to README 2019-04-26 19:50:19 -04:00
Scott Kitterman 2f74edfc1b Fix -rundir expansion 2019-04-26 19:33:36 -04:00
Scott Kitterman 9b1f3c5e31 Fix default rundir in etc/dkimpy-milter.conf.inwq 2019-04-26 19:31:56 -04:00
Scott Kitterman f73596a67e More expand cleanups 2019-04-26 18:44:17 -04:00
Scott Kitterman c89bfdb9df Split out sysconfdir (/etc) and confdir (/etc/dkimpy-milter) in expand 2019-04-26 18:36:12 -04:00
Scott Kitterman b9435d735d Add system/socket-activation files to expand 2019-04-26 18:16:04 -04:00
Scott Kitterman 0092b10064 More expand in s/s/dkimpy-milter.service.in 2019-04-26 18:14:23 -04:00
Scott Kitterman 5d48b5ea2b Use expand in socket-acitvation files 2019-04-26 18:09:27 -04:00
Scott Kitterman 0ef0f2f509 Add system/dkimpy-milter.service to expand 2019-04-26 18:03:44 -04:00
Scott Kitterman 5ff6ef5c4b Set daemon path and rundir in dkimpy-milter.service using variable expansion 2019-04-26 18:00:14 -04:00
Scott Kitterman cd86159057 fix dkimpy-milter.in permissions 2019-04-26 17:41:45 -04:00
Scott Kitterman 43ea5c1cdf bindir vice sbindir in openrc file 2019-04-26 17:38:49 -04:00
Scott Kitterman d1cfcb7c44 More expand fixes/updates 2019-04-26 17:37:29 -04:00
Scott Kitterman ad505cda6e Use variable expansion in system/dkimpy-milter 2019-04-26 17:33:44 -04:00
Scott Kitterman d291f10a9b Use variable expansion for dkimpy-milter.conf.5 2019-04-26 17:20:37 -04:00
Scott Kitterman 6268032484 Simplify openrc.in - no checkconfig in dkimpy-milter 2019-04-26 17:19:37 -04:00
Scott Kitterman 385271982f Don't need to find the grep command after all 2019-04-26 17:18:58 -04:00
Scott Kitterman f7d4dd2d47 Start CHANGES for 1.2.0 2019-04-26 17:18:36 -04:00
Scott Kitterman 7521e156f8 Belatedly bump version and start CHANGES for 1.2.0 2019-04-25 07:17:07 -04:00
Scott Kitterman e993125514 Use setup.py expand to fill out man/dkimpy-milter.conf.5 2019-04-25 07:15:04 -04:00
Scott Kitterman b8118c604a Merge branch 'master' of git+ssh://git.launchpad.net/dkimpy-milter 2019-04-25 07:05:33 -04:00
Scott Kitterman 518a66d60b Use setup.py expand to fill out etc/dkimpy-milter.conf 2019-04-25 07:05:25 -04:00
Scott Kitterman 6dacbb59df Use setup.py expand to fill out etc/dkimpy-milter.conf 2019-04-25 07:02:15 -04:00
Scott Kitterman ae8b17c0ce First effort at doing make like variable expansion so we don't have to patch when file locations change 2019-04-25 00:50:23 -04:00
Scott Kitterman 0b522ca4d1 Minor README corrections 2019-04-13 08:42:30 -04:00
Scott Kitterman aba9c0aa0c Update TODO 2019-04-12 22:43:18 -04:00
Scott Kitterman 19b6ce7a68 Update CHANGES and man/dkimpy-milter.conf.5 for SubDomains option. 2019-04-12 22:34:59 -04:00
Scott Kitterman 8dc3ac6474 Add option to sign for subdomains - Thanks to Sagi for the patch 2019-04-12 22:31:24 -04:00
Scott Kitterman 290a37b99c Update CHANGES to current 2019-04-12 22:27:23 -04:00
Scott Kitterman 564799402a Catch and log config items missing values 2019-04-12 22:01:56 -04:00
Scott Kitterman 357905bb68 Fix debugLevel for config._readConfigFile 2019-04-12 21:45:31 -04:00
Scott Kitterman 6b851f18df Changes in dkimpy_milter/__init__.py to harmonize logging messages and log text instead of bytestring. 2019-04-12 21:29:09 -04:00
Scott Kitterman 7ab58edb1b Consisently use /run vice /var/run for documentation and init system integration 2019-04-12 20:57:08 -04:00
Scott Kitterman 23b0e8a386 README updates for python3 switch 2019-04-12 20:51:33 -04:00
Scott Kitterman 2e105bd18c Py3DNS vice PyDNS because we're python3 now 2019-04-12 20:34:42 -04:00
Scott Kitterman fb72b9f6e7 Merge 1_0 into master 2019-03-12 22:06:42 -04:00
Scott Kitterman 7eed8995a2 Merge branch 'dkg/socket-activation' into 1_0 2019-03-12 22:01:00 -04:00
Scott Kitterman a16d887ac6 Fix merge conflicts 2019-02-24 07:22:58 -05:00
Scott Kitterman 23d91b2b50 Update CHANGES for merge of dkg/test-suite 2019-02-24 07:19:18 -05:00
Scott Kitterman 481fbdae29 Merge branch 'dkg/test-suite' into 1_0 2019-02-24 07:13:30 -05:00
Scott Kitterman 4b0c39b0c7 Start changes for python3 update 2019-02-24 06:57:47 -05:00
Daniel Kahn Gillmor 7092874729 Enable sd_listen_fds(3)-style socket-activation support
I've added straightforward systemd unit files in
system/socket-activation/ that make use of this approach, and a
README.md in the same location that describes the tradeoffs.
2019-02-21 19:22:11 -05:00
Daniel Kahn Gillmor ea09bab1a8 Convert __init__.py to python3
The main work here is about bytes vs. strings.  This work was
confusing for several reasons:

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

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

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

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

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

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

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

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

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

The changes in this commit are intended to be relatively
uncontroversial, so that the following commit contains the tricky
bits.
2019-02-21 19:22:09 -05:00
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 851f8ff9c9 Update references in man/dkimpy-milter.8 2019-02-08 04:02:34 -05:00
Scott Kitterman cbb6098dd8 Remove obsolete .IX macro from man pages 2019-02-08 01:12:21 -05:00
Scott Kitterman c90d694fff Fix grammar error in README 2019-02-01 15:15:50 -05:00
Scott Kitterman 8d8cd15cba Fix spelling error in README 2019-02-01 15:11:00 -05:00
Scott Kitterman 4f21623f92 Add read_keytable function to util.py 2018-12-24 16:08:50 -05:00
Scott Kitterman bf2548f891 Documentation updates for 1.1.0 2018-10-11 21:45:17 -04:00
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
32 changed files with 2207 additions and 432 deletions
+1
View File
@@ -1,3 +1,4 @@
dist dist
dkimpy_milter.egg-info dkimpy_milter.egg-info
*.pyc *.pyc
*~
+63
View File
@@ -1,3 +1,66 @@
1.2.0 2020-01-03
- Add support for SigningTable, KeyTable, and KeyTableEd25519 (LP: #1797397)
- Add support for specifying MinimumKeyBits for RSA signatures
- Add support for SignHeaders feature, thanks to Ralph Seichter for the patch
- Add support for specifying DNSTimeout (bumps required dkimpy version to 1.0)
- Add information on message content conversion to README
- Add new expand option to setup.py so various file system locations can be
specified at build/install time rather than being hard coded
- Install openrc init file for Gentoo and other openrc users
- Add support for passing PID file name on command line to make it easier to
keep system init and daemon configuration in sync
- Add support for storing DKIM failed mails in a specified
DiagnosticDirectory
- 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
- Add debug logging for content type to assist troubleshooting MIME
conversion issues
- Fix variable initialization so mailformed mails missing body From do not
cause a traceback (LP: #1844161)
- Catch more ascii encoding errors to improve resilience against bad data
(LP: #1844189)
- Fix sysv init so it works (LP: #1839487)
- Make error logging more explicit to aid debugging
- Remove SigningTableEd25519 from documentation - it was never implemented
and a per algorithm signing table turns out not to be needed
- Delete own_socketfile to resolve race condition where the permissions
change fails on a Unix socket because it hasn't been created yet (libmilter
will do this correctly on its own based on umask, the milter doesn't need
to do it) (LP: #1849712)
1.1.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
- 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 0.9.6 2018-03-13
- Fixed typo in package installation section of README - Fixed typo in package installation section of README
- Added more to README about first run with systemd - Added more to README about first run with systemd
-135
View File
@@ -1,135 +0,0 @@
This is a DKIM signing and verification milter. In theory it works with both
Postfix and Sendmail, but the author has zero experience with Sendmail, so
reports of success/failure with Sendmail and patches are welcom.
The configuration file is designed to be compatible with OpenDKIM, but only
a subset of OpenDKIM options are supported. If an unsupported option is
specified, an error will be raised.
This package includes a default configuration file and man pages. For those
to be installed when installing using setup.py, the following incantation is
required because setuptools developers decided not being able to do this by
default is a feature:
python setup.py install --single-version-externally-managed --record=/dev/null
For users of Debian Stable (Debian 9, Codename Squueze), all dependencies are
available in either the main or backports repositories:
[sudo] apt install python-milter python-nacl python-ipaddress python-dnspython
[sudo] apt install -t stretch-backports python-authres python-dkim
The preferred method of installation is from PyPi using pip (if distribution
packages are not available):
[sudo] pip install dkimpy_milter
Using pip will cause required packages to be installed via easy_install if they
have not been previously installed.
The milter will work with either pydns (DNS) or dnspython (dns), preferring
dnspython is both are available. The dkimpy DKIM module also works with
either.
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
used, they will need to be updated. The sysv init file is Debian specific and
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
UserID. During initial setup, this system user needs to be manually created.
As an example, using the default dkimpy-user on Debian, the command would be:
[sudo] adduser --system --no-create-home --quiet --disabled-password \
--disabled-login --shell /bin/false --group \
--home /var/run/dkimpy-milter dkimpy-milter
Since /var/run or /run is sometimes on a tempfs, if the PID file directory is
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
choice (Sendmail or Postfix).
For Sendmail:
Configuration is very similar to opendkim, but needs some adjustment for
dkimpy-milter. Here's an example configuration line to include in your
sendmail.mc:
INPUT_MAIL_FILTER(`dkimpy-milter', `S=local:/var/run/dkimpy-milter/dkimpy-milter.sock')dnl
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
the dkimpy-milter configuration file.
Milter support should be present by default in most versions of sendmail
these days, but if not included in your Sendmail build, see:
http://www.elandsys.com/resources/sendmail/milter.html
For Postfix:
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
two dkimpy-milter instances, one configured for signing and one configured for
verification:
smtp inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8892
...
submission inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8891
...
These need to match the Socket value for each dkimpy-milter instance.
Care is required to segregate outbound mail to be signed and inbound mail to
be verified. The above example uses two instances of dkimpy-milter to do
this. There are many possible ways. Here is another example using milter
macros to keep the mail streams segregated:
Postfix master.cf:
smtp inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8891
-o milter_macro_daemon_name=VERIFYING
...
submission inet n - - - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
...
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_milters=inet:localhost:8891
...
Dkimpy-milter.conf:
...
Mode sv
MacroList dameon_name|ORIGINATING
MacroListVerify daemon_name|VERIFYING
...
The python DKIM library, dkimpy, requires the entire message being signed or
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
signatures sufficient functionality for basic use. The documented
functionality has been implemented and at least partially tested. It is free
of known major defects, but is not fully tested in a variety of environments.
+337
View File
@@ -0,0 +1,337 @@
# OVERVIEW
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
a subset of OpenDKIM options are supported. If an unsupported option is
specified, an error will be raised.
# INSTALLATION
This package includes a default configuration file and man pages. For those
to be installed when installing using setup.py, the following incantation is
required because setuptools developers decided not being able to do this by
default is a feature:
[sudo] python3 setup.py install --single-version-externally-managed --record=/dev/null
For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are
available in either the main or backports repositories:
[sudo] apt install python3-milter python3-nacl python3-dnspython
[sudo] apt install -t stretch-backports python3-authres python3-dkim
It is also available in the Debian package archive:
[sudo] apt install dkimpy-milter [Debian 10 or later]
[sudo] apt install -t stretch-backports dkimpy-milter [Debian 9]
When installing using the Debian package, all dependencies are automatically
installed.
The preferred method of installation is from PyPi using pip (if distribution
packages are not available):
[sudo] pip install dkimpy_milter
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
Python extensions, the system will need appropriate development packages and
an C compiler. Alternately, install these dependencies from distribution/OS
packages and then pip install dkimpy_milter.
The milter will work with either py3dns (DNS) or dnspython (dns), preferring
dnspython if both are available. The dkimpy DKIM module also works with
either.
## NON-STANDARD INSTALLATION PATHS
The package includes a custom setup command called expand. It allows various
file locations in init scripts, man pages, and config files to be over-ridden
at install time.
expand: Expand @@ variables in input files, simlar to make macros.
user_options:
--sysconfigdir=, e: Specify system configuration directory.
--sbindir=, s: Specify system binary directory [not used].
--bindir=, b: Specify binary directory.
--rundir=,r: Specify run state directory.
As an example, to change the run directory to /var/run, one would do:
python3 setup.py expand --rundir=/var/run
[sudo] python3 setup.py install --single-version-externally-managed \
--record=/dev/null
or in a single step (the order matters):
[sudo] python3 setup.py expand --rundir=/var/run install \
--single-version-externally-managed \
--record=/dev/null
# 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.
### COMPLEX SIGNING CONFIGURATIONS
The KeyTable, KeyTableEd25519, and SigningTable are used to define signing
instructions to the filter where use of Domain, Selector and KeyFile together
are insufficient.
First, select the type of database you will use for each. They need not
be the same. The "DATA SETS" portion of the dkimpy-milter(8) man page
describes the possibilities and how they are formatted. Then, construct those
databases.
Let's suppose you want to sign for two domains, example.com and example.net.
Within example.com, you want to sign for user "president" differently than
everyone else. Let's say further that you want to use a flat text file.
You've generated private key files for each of these and stored them
in the directory /usr/local/etc/dkim/keys as files "president", "excom" and
"exnet", with the obvious intents. You want to use selectors "foo", "bar"
and "baz" for those, respectively. The signing domains match the senders
(i.e. the signatures for example.com's stuff will be held by example.com,
and example.net likewise).
First, write the KeyTable. This is a list of the keys you intend to use,
and you just assign arbitrary names to them. So as a flat file, the KeyTable
for the above might look like this:
preskey example.com:foo:/usr/local/etc/dkim/keys/president
comkey example.com:bar:/usr/local/etc/dkim/keys/excom
netkey example.net:baz:/usr/local/etc/dkim/keys/exnet
If also signing with ed25519, specify a KeyTableEd25519 pointing to the keys
needed for ed25519. Both KeyTable and KeyTableEd25519 are evaluated if there
is a SigningTable (see below).
Per the documentation, multi-field data sets that are made of flat files have
the fields separated by colons, but the key and value(s) are separated by
whitespace.
So now we've named each key file, and specified with which selector and domain
each will be used, and then given each of those groupings a name. This
is your KeyTable. Let's say you put it in /usr/local/etc/dkim/keytable.
Next, write the SigningTable. This maps senders (by default, taken from the
From: header field of a message passing through the filter) to which keys
will be used to sign their mail. Wildcards are allowed. So to do what was
described above, we write it as follows:
president@example.com preskey
*@example.com comkey
*@example.net netkey
Since we want to use wildcards, we can't actually use a regular flat file.
Wildcards require a regular expression file, or "refile". The above is
valid format for one of those. Let's say you put this in
/usr/local/etc/dkim/signingtable.
Finally, tell the filter that it should use these files by adding this to
your configuration file:
KeyTable /usr/local/etc/dkim/keytable
SigningTable refile:/usr/local/etc/dkim/signingtable
You could put "file:" in front of the filename for the KeyTable just to be
precise, but "file:" is assumed if the value starts with a "/".
Note: Unlike opendkim, dkimpy-milter will check for "\*" in the signing table
regardless of if refile is specified or not. Use of refile is supported for
compatibility with configurations initially developed for use with opendkim.
## MTA INTEGRATION
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
used, they will need to be updated. The sysv init file uses start-stop-deamon
from Debian. It is not portable to systems without that available.
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.
As an example, using the default dkimpy-user on Debian, the command would be:
[sudo] adduser --system --no-create-home --quiet --disabled-password \
--disabled-login --shell /bin/false --group \
--home /run/dkimpy-milter dkimpy-milter
Since /var/run or /run is sometimes on a tempfs, if the PID file directory is
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
choice (Sendmail or Postfix). When integrating with your MTA, the risk of
signature invalidation due to content conversion of the message body needs to
be considered. See RFC 6376, Section 5.3 for discussion of this issue. As a
practical matter, when signing, configure the milter to follow all others that
might modify the message body. When verifying, configure the milter before
other processes that might modify the message body.
### SENDMAIL
Configuration is very similar to opendkim, but needs some adjustment for
dkimpy-milter. Here's an example configuration line to include in your
sendmail.mc:
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)
and a restart of sendmail. Note that S= needs to match the value of Socket in
the dkimpy-milter configuration file.
Milter support should be present by default in most versions of sendmail
these days, but if not included in your Sendmail build, see:
http://www.elandsys.com/resources/sendmail/milter.html
#### 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
README_FILES/MILTER_README). Here's an example master.cf excerpt that talks
to two dkimpy-milter instances, one configured for signing and one configured
for verification:
smtp inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8892
...
submission inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8891
...
These need to match the Socket value for each dkimpy-milter instance.
Care is required to segregate outbound mail to be signed and inbound mail to
be verified. The above example uses two instances of dkimpy-milter to do
this. There are many possible ways. Here is another example using milter
macros to keep the mail streams segregated:
Postfix master.cf:
smtp inet n - - - - smtpd
...
-o smtpd_milters=inet:localhost:8891
-o milter_macro_daemon_name=VERIFYING
...
submission inet n - - - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
...
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_milters=inet:localhost:8891
...
Dkimpy-milter.conf:
...
Mode sv
MacroList dameon_name|ORIGINATING
MacroListVerify daemon_name|VERIFYING
...
# NOTES
The python DKIM library, dkimpy, requires the entire message being signed or
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.
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.
+20 -12
View File
@@ -39,10 +39,26 @@ 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
Port to Python 3 implemented verified
Subdomain support implemented verified
Test suite implemented verified
1.2.0
DNSTimeout (dkimpy 1.0) implemented verified by inspection
KeyTable implemented verified
KeytableEd25519 implemented verified
MinimumKeyBits implemented verified
SignHeaders implemented verified by inspection
SigningTable implemented verified
TemporaryDirectory implemented verified by inspection
Planned dataset type support (if needed):
mdb: mdb:
Considered for near-term feature release Considered for near-term feature release
@@ -50,9 +66,8 @@ Considered for near-term feature release
AlwaysAddARHeader AlwaysAddARHeader
ChangeRootDirectory ChangeRootDirectory
ClockDrift (requires dkimpy change) ClockDrift (requires dkimpy change)
DNSTimeout (requires dkmpy change)
MilterDebug MilterDebug
MinimumKeyBits OmitHeaders
OversignHeaders (may require dkimpy changes) OversignHeaders (may require dkimpy changes)
PeerList PeerList
SignatureAlgorithm SignatureAlgorithm
@@ -67,8 +82,6 @@ ExternalIgnoreList
FixCRLF FixCRLF
KeepAuthResults KeepAuthResults
KeepTemporaryFiles KeepTemporaryFiles
KeyTable
KeytableEd25519
LogResults LogResults
LogWhy LogWhy
MaximumHeaders MaximumHeaders
@@ -76,7 +89,6 @@ MaximumSignaturesToVerify
MultipleSignatures MultipleSignatures
MustBeSigned MustBeSigned
NoHeaderB NoHeaderB
OmitHeaders
On-BadSignature On-BadSignature
On-Default On-Default
On-DNSError On-DNSError
@@ -91,12 +103,8 @@ RequiredHeaders
RequireSafeKeys RequireSafeKeys
SignatureAlgorithm SignatureAlgorithm
SignatureTTL SignatureTTL
SignHeaders
SigningTable
SoftwareHeader SoftwareHeader
StrictHeaders StrictHeaders
SubDomains
TemporaryDirectory
TestDNSData TestDNSData
TestPublicKeys TestPublicKeys
+251 -119
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,18 +28,18 @@ 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 get_keys
from dkimpy_milter.util import own_socketfile
from dkimpy_milter.util import fold from dkimpy_milter.util import fold
__version__ = "0.9.6" __version__ = "1.2.0"
FWS = re.compile(r'\r?\n[ \t]+') FWS = re.compile(r'\r?\n[ \t]+')
@@ -51,9 +51,9 @@ class dkimMilter(Milter.Base):
self.id = Milter.uniqueID() self.id = Milter.uniqueID()
# we don't want config used to change during a connection # we don't want config used to change during a connection
self.conf = milterconfig self.conf = milterconfig
self.privatersa = privateRSA
self.privateed25519 = privateEd25519
self.fp = None self.fp = None
self.fdomain = ''
self.iequals = None
@Milter.noreply @Milter.noreply
def connect(self, hostname, unused, hostaddr): def connect(self, hostname, unused, hostaddr):
@@ -61,21 +61,23 @@ 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 = self.conf['AuthservID']
except: except:
self.AuthservID = self.receiver self.AuthservID = self.receiver
if hostaddr and len(hostaddr) > 0: if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0] ipaddr = hostaddr[0]
if milterconfig['IntHosts']: if self.conf['IntHosts']:
if milterconfig['IntHosts'].match(ipaddr): if self.conf['IntHosts'].match(ipaddr):
self.internal_connection = True self.internal_connection = True
else: else:
ipaddr = '' ipaddr = ''
self.connectip = ipaddr self.connectip = ipaddr
if milterconfig.get('MacroList') and not self.internal_connection: if self.conf.get('MacroList') and not self.internal_connection:
macrolist = milterconfig.get('MacroList') macrolist = self.conf.get('MacroList')
for macro in macrolist: for macro in macrolist:
macroname = macro.split('|')[0] macroname = macro.split('|')[0]
macroname = '{' + macroname + '}' macroname = '{' + macroname + '}'
@@ -83,8 +85,8 @@ class dkimMilter(Milter.Base):
if ((len(macro.split('|')) == 1 and macroresult) or macroresult if ((len(macro.split('|')) == 1 and macroresult) or macroresult
in macro.split('|')[1:]): in macro.split('|')[1:]):
self.internal_connection = True self.internal_connection = True
if milterconfig.get('MacroListVerify'): if self.conf.get('MacroListVerify'):
macrolist = milterconfig.get('MacroListVerify') macrolist = self.conf.get('MacroListVerify')
for macro in macrolist: for macro in macrolist:
macroname = macro.split('|')[0] macroname = macro.split('|')[0]
macroname = '{' + macroname + '}' macroname = '{' + macroname + '}'
@@ -96,7 +98,7 @@ class dkimMilter(Milter.Base):
connecttype = 'INTERNAL' connecttype = 'INTERNAL'
else: else:
connecttype = 'EXTERNAL' connecttype = 'EXTERNAL'
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 1: if self.conf.get('Syslog') and self.conf.get('debugLevel') >= 1:
syslog.syslog("connect from {0} at {1} {2}" syslog.syslog("connect from {0} at {1} {2}"
.format(hostname, hostaddr, connecttype)) .format(hostname, hostaddr, connecttype))
return Milter.CONTINUE return Milter.CONTINUE
@@ -106,9 +108,9 @@ class dkimMilter(Milter.Base):
# of each message. # of each message.
@Milter.noreply @Milter.noreply
def envfrom(self, f, *str): def envfrom(self, f, *str):
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2: if self.conf.get('Syslog') and self.conf.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:
@@ -123,30 +125,39 @@ class dkimMilter(Milter.Base):
@Milter.noreply @Milter.noreply
def header(self, name, val): def header(self, name, val):
lname = name.lower() lname = name.lower()
if self.conf.get('Syslog') and self.conf.get('debugLevel') >= 4:
if lname == 'content-transfer-encoding':
syslog.syslog("content-transfer-encodeing: {0}".format(val))
if lname == 'content-type':
syslog.syslog("content-type: {0}".format(val))
if lname == 'dkim-signature': if lname == 'dkim-signature':
if (milterconfig.get('Syslog') and if (self.conf.get('Syslog') and
milterconfig.get('debugLevel') >= 1): self.conf.get('debugLevel') >= 1):
syslog.syslog("{0}: {1}".format(name, val)) syslog.syslog("{0}: {1}".format(name, val))
self.has_dkim += 1 self.has_dkim += 1
if lname == 'from': if lname == 'from':
fname, self.author = parseaddr(val) fname, self.author = parseaddr(val)
try: try:
self.fdomain = self.author.split('@')[1] self.fdomain = self.author.split('@')[1].lower()
except IndexError as er: except IndexError as er:
self.fdomain = '' # self.author was not a proper email address pass # self.author was not a proper email address
if (milterconfig.get('Syslog') and if (self.conf.get('Syslog') and
milterconfig.get('debugLevel') >= 1): self.conf.get('debugLevel') >= 1):
syslog.syslog("{0}: {1}".format(name, val)) syslog.syslog("{0}: {1}".format(name, val))
elif lname == 'authentication-results': elif lname == 'authentication-results':
self.arheaders.append(val) self.arheaders.append(val)
if self.fp: if self.fp:
self.fp.write("%s: %s\n" % (name, val)) try:
self.fp.write(b"%s: %s\n" % (codecs.encode(name, 'ascii'), codecs.encode(val, 'ascii')))
except:
# Don't choke on header fields with non-ascii garbage in them.
pass
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @Milter.noreply
def eoh(self): def eoh(self):
if self.fp: if self.fp:
self.fp.write("\n") # terminate headers self.fp.write(b"\n") # terminate headers
self.bodysize = 0 self.bodysize = 0
return Milter.CONTINUE return Milter.CONTINUE
@@ -168,147 +179,260 @@ class dkimMilter(Milter.Base):
.parse_value(FWS.sub('', val))) .parse_value(FWS.sub('', val)))
if ar.authserv_id == self.AuthservID: if ar.authserv_id == self.AuthservID:
self.chgheader('authentication-results', i, '') self.chgheader('authentication-results', i, '')
if (milterconfig.get('Syslog') and if (self.conf.get('Syslog') and
milterconfig.get('debugLevel') >= 1): self.conf.get('debugLevel') >= 1):
syslog.syslog('REMOVE: {0}'.format(val)) syslog.syslog('REMOVE: {0}'.format(val))
except: except:
# Don't error out on unparseable AR header fiels # Don't error out on unparseable AR header fiels
pass pass
# Check or sign DKIM # Check and/or sign DKIM
if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 4):
syslog.syslog('self.conf: {0}'.format(self.conf))
self.fp.seek(0) self.fp.seek(0)
if milterconfig.get('Domain'):
domain = milterconfig.get('Domain')
else:
domain = ''
if ((self.fdomain in domain) and not milterconfig.get('Mode') == 'v'
and not self.external_connection):
txt = self.fp.read() txt = self.fp.read()
self.get_identities_sign()
if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 3):
syslog.syslog('self.domain: {0}, self.fdomain: {1}, self.iequals: {2}'.format(self.domain, self.fdomain, self.iequals))
if ((self.fdomain in self.domain) and not self.conf.get('Mode') == 'v'
and not self.external_connection):
self.sign_dkim(txt) self.sign_dkim(txt)
if ((self.has_dkim) and (not self.internal_connection) and if ((self.has_dkim) and (not self.internal_connection) and
(milterconfig.get('Mode') == 'v' or (self.conf.get('Mode') == 'v' or
milterconfig.get('Mode') == 'sv')): self.conf.get('Mode') == 'sv')):
txt = self.fp.read()
self.check_dkim(txt) self.check_dkim(txt)
if self.arresults: if self.arresults:
h = authres.AuthenticationResultsHeader(authserv_id= h = authres.AuthenticationResultsHeader(authserv_id=
self.AuthservID, self.AuthservID,
results=self.arresults) results=self.arresults)
h = fold(str(h)) h = fold(codecs.encode(str(h), 'ascii'))
if (milterconfig.get('Syslog') and if (self.conf.get('Syslog') and
milterconfig.get('debugLevel') >= 2): self.conf.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
# get parent domain to be signed for if fdomain is a subdomain
def get_parent_domain(self, 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
syslog.syslog('domain: {0}'.format(domain))
return domain
# or return the fdomain itself
return fdomain
def get_identities_sign(self):
"""Determine d= and i= identiies for signature"""
self.domain = []
iequals = None
try:
self.privkeyRSA = self.conf.get('privateRSA')
except:
self.privkeyRSA = ''
try:
self.privkeyEd25519 = self.conf.get('privateEd25519')
except:
self.privkeyEd25519 = ''
try:
self.selectorRSA = self.conf.get('Selector')
except:
self.selectorRSA = ''
try:
self.selectorEd25519 = self.conf.get('SelectorEd25519')
except:
self.selectorEd25519 = ''
if not self.domain and self.conf.get('Domain'):
self.domain = self.conf.get('Domain')
if self.conf.get('SubDomains'):
self.fdomain = self.get_parent_domain(self.fdomain, self.domain)
if self.conf.get('SigningTable'):
match = False
for dictkey, dictvalues in self.conf.get('SigningTable').items():
if dictkey == '%':
self.domain.append(self.fdomain)
match = True
elif len(dictkey.split('*')) == 1:
if dictkey == self.author:
self.domain.append(self.fdomain)
match = True
else:
if len(dictkey.split('*')) == 2:
if dictkey.split('*')[1] == self.author[-len(dictkey.split('*')[1]):]:
self.domain.append(self.fdomain)
match = True
self.domain.append(self.fdomain)
try:
if len(dictvalues) == 2 and match:
if dictvalues[0] =='%':
self.iequals = codecs.encode('@' + self.fdomain)
elif dictvalues[0][1:] == self.fdomain or self.get_parent_domain(dictvalues[0][1:], self.domain) == self.fdomain:
self.iequals = codecs.encode(dictvalues[0])
except IndexError:
pass
if match:
#TODO add KeyTable stuffs here.
keytablekey = dictvalues[-1] # Last value in the SigningTable row.
if self.conf.get('privateRSATable'):
# Table data is a list of [ signing domain, selector, key ]
keytabledata = self.conf.get('privateRSATable')[keytablekey]
try:
self.fdomain = keytabledata[0]
self.selectorRSA = keytabledata[1]
self.privkeyRSA = keytabledata[2]
except:
if (self.conf.get('Syslog')):
syslog.syslog('Error: Invalid KeyTable data {0}'.format(keytabledata))
if self.conf.get('privateEd25519Table'):
# Table data is a list of [ signing domain, selector, key ]
keytabledata = self.conf.get('privateEd25519Table')[keytablekey]
try:
self.fdomain = keytabledata[0]
self.selectorEd25519 = keytabledata[1]
self.privkeyEd25519 = keytabledata[2]
except:
if (self.conf.get('Syslog')):
syslog.syslog('Error: Invalid KeyTable data {0}'.format(keytabledata))
break
def sign_dkim(self, txt): def sign_dkim(self, txt):
canon = milterconfig.get('Canonicalization') canon = codecs.encode(self.conf.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)
if (milterconfig.get('Syslog') and if (self.conf.get('Syslog') and
milterconfig.get('debugLevel') >= 1): self.conf.get('debugLevel') >= 1):
syslog.syslog('canonicalize: {0}'.format(canonicalize)) syslog.syslog('canonicalize: {0}'.format(canonicalize))
sign_headers = self.conf.get('SignHeaders')
if not sign_headers:
# None or empty. DKIM explicitly tests for None.
sign_headers = None
try: try:
if privateRSA: if self.privkeyRSA:
d = dkim.DKIM(txt) d = dkim.DKIM(txt)
h = d.sign(milterconfig.get('Selector'), self.fdomain, h = d.sign(codecs.encode(self.selectorRSA, 'ascii'), codecs.encode(self.fdomain, 'ascii'),
privateRSA, canonicalize=(canonicalize[0], codecs.encode(self.privkeyRSA, 'ascii'),
canonicalize[1])) canonicalize=(canonicalize[0], canonicalize[1]),
name, val = h.split(': ', 1) identity=self.iequals, include_headers=sign_headers)
self.addheader(name, val.strip().replace('\r\n', '\n'), 0) name, val = h.split(b': ', 1)
if (milterconfig.get('Syslog') and self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
(milterconfig.get('SyslogSuccess') if (self.conf.get('Syslog') and
or milterconfig.get('debugLevel') >= 1)): (self.conf.get('SyslogSuccess')
syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} ' or self.conf.get('debugLevel') >= 1)):
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)) d.domain.decode().lower()))
if privateEd25519: if self.privkeyEd25519:
d = dkim.DKIM(txt) d = dkim.DKIM(txt)
h = d.sign(milterconfig.get('SelectorEd25519'), self.fdomain, h = d.sign(codecs.encode(self.selectorEd25519, 'ascii'), codecs.encode(self.fdomain, 'ascii'),
privateEd25519, canonicalize=(canonicalize[0], self.privkeyEd25519,
canonicalize[1]), canonicalize=(canonicalize[0], canonicalize[1]),
signature_algorithm='ed25519-sha256') identity=self.iequals, include_headers=sign_headers,
name, val = h.split(': ', 1) signature_algorithm=b'ed25519-sha256')
self.addheader(name, val.strip().replace('\r\n', '\n'), 0) name, val = h.split(b': ', 1)
if (milterconfig.get('Syslog') and self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
(milterconfig.get('SyslogSuccess') if (self.conf.get('Syslog') and
or milterconfig.get('debugLevel') >= 1)): (self.conf.get('SyslogSuccess')
syslog.syslog('{0}: {1} DKIM-Signature field added (s={2} ' or self.conf.get('debugLevel') >= 1)):
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)) d.domain.decode().lower()))
except dkim.DKIMException as x: except dkim.DKIMException as x:
if milterconfig.get('Syslog'): if self.conf.get('Syslog'):
syslog.syslog('DKIM: {0}'.format(x)) syslog.syslog('DKIM: {0}'.format(x))
except Exception as x: except Exception as x:
if milterconfig.get('Syslog'): if self.conf.get('Syslog'):
syslog.syslog("sign_dkim: {0}".format(x)) syslog.syslog("sign_dkim: {0}".format(x))
raise raise
def check_dkim(self, txt): def check_dkim(self, txt):
res = False res = False
self.header_a = None
for y in range(self.has_dkim): # Verify _ALL_ the signatures for y in range(self.has_dkim): # Verify _ALL_ the signatures
d = dkim.DKIM(txt) d = dkim.DKIM(txt, minkey=self.conf.get('MinimumKeyBits'), timeout=self.conf.get('DNSTimeout'))
try: try:
dnsoverride = self.conf.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 self.conf.get('Syslog'):
syslog.syslog('DKIM: {0}'.format(x)) syslog.syslog('DKIM: {0}'.format(x))
except Exception as x: except Exception as x:
self.dkim_comment = str(x) self.dkim_comment = str(x)
if milterconfig.get('Syslog'): if self.conf.get('Syslog'):
syslog.syslog("check_dkim: {0}".format(x)) syslog.syslog("check_dkim: Internal program fault while verifying: {0}".format(x))
self.header_i = d.signature_fields.get(b'i') try:
self.header_d = d.signature_fields.get(b'd') # i= is optional and dkimpy is fine if it's not provided
self.header_a = d.signature_fields.get(b'a') self.header_i = codecs.decode(d.signature_fields.get(b'i'), 'ascii')
except TypeError as x:
self.header_i = None
try:
self.header_d = codecs.decode(d.signature_fields.get(b'd'), 'ascii')
self.header_a = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
except Exception as x:
self.dkim_comment = str(x)
if self.conf.get('Syslog'):
syslog.syslog("check_dkim: Internal program fault extracting header a or d: {0}".format(x))
self.header_d = None
if not self.header_a:
self.header_a = 'rsa-sha256'
if res: if res:
if (milterconfig.get('Syslog') and if (self.conf.get('Syslog') and
(milterconfig.get('SyslogSuccess') or (self.conf.get('SyslogSuccess') or
milterconfig.get('debugLevel') >= 1)): self.conf.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)) d.domain.decode().lower()))
self.dkim_domain = d.domain self.dkim_domain = d.domain.lower()
else: else:
if milterconfig.get('DiagnosticDirectory'): if self.conf.get('DiagnosticDirectory'):
tempfile.tempdir = self.conf.get('DiagnosticDirectory')
fd, fname = tempfile.mkstemp(".dkim") fd, fname = tempfile.mkstemp(".dkim")
with os.fdopen(fd, "w+b") as fp: with os.fdopen(fd, "w+b") as fp:
fp.write(txt) fp.write(txt)
if milterconfig.get('Syslog'): if self.conf.get('Syslog'):
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)) if self.conf.get('Syslog'):
if d.domain:
syslog.syslog('DKIM: Fail ({0})'
.format(d.domain.lower()))
else:
syslog.syslog('DKIM: Fail, unextractable domain')
if res: if res:
result = 'pass' result = 'pass'
else: else:
result = 'fail' result = 'fail'
res = False res = False
if self.header_d:
self.arresults.append( self.arresults.append(
authres.DKIMAuthenticationResult(result=result, authres.DKIMAuthenticationResult(result=result,
header_i=self.header_i, header_i=self.header_i,
@@ -317,44 +441,52 @@ class dkimMilter(Milter.Base):
result_comment= result_comment=
self.dkim_comment) self.dkim_comment)
) )
self.header_a = None
return return
def main(): def main():
# Ugh, but there's no easy way around this. # Ugh, but there's no easy way around this.
global milterconfig global milterconfig
global privateRSA
global privateEd25519
privateRSA = False
privateEd25519 = False
configFile = '/usr/local/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')) or len(sys.argv) == 3 or \
print('usage: dkimpy-milter [<configfilename>]') (len(sys.argv) == 4 and sys.argv[2] != '-P'):
print('usage: dkimpy-milter [<configfilename> [-P <pidfile>]]')
sys.exit(1) sys.exit(1)
configFile = sys.argv[1] configFile = sys.argv[1]
milterconfig = config._processConfigFile(filename=configFile) milterconfig = config._processConfigFile(filename=configFile)
if len(sys.argv) == 4:
if sys.argv[2] == '-P':
# Command line PID file argument overrides config file
milterconfig['PidFile'] = sys.argv[3]
if milterconfig.get('Syslog'): if milterconfig.get('Syslog'):
facility = eval("syslog.LOG_{0}" facility = eval("syslog.LOG_{0}"
.format(milterconfig.get('SyslogFacility').upper())) .format(milterconfig.get('SyslogFacility').upper()))
syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility) syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility)
setExceptHook() setExceptHook()
pid = write_pid(milterconfig) pid = write_pid(milterconfig)
if milterconfig.get('KeyFile'): milterconfig = get_keys(milterconfig)
privateRSA = read_keyfile(milterconfig, 'RSA')
if milterconfig.get('KeyFileEd25519'):
privateEd25519 = read_keyfile(milterconfig, 'Ed25519')
Milter.factory = dkimMilter Milter.factory = dkimMilter
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 milterconfig.get('Syslog'): if socketname is None:
syslog.syslog('dkimpy-milter started:{0} user:{1}' if int(os.environ.get('LISTEN_PID', '0')) == os.getpid():
.format(pid, milterconfig.get('UserID'))) 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'
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter(miltername, socketname, 240) if milterconfig.get('Syslog'):
own_socketfile(milterconfig) syslog.syslog('dkimpy-milter starting:{0} user:{1}'
.format(pid, milterconfig.get('UserID')))
drop_privileges(milterconfig) drop_privileges(milterconfig)
Milter.runmilter(miltername, socketname, 240)
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()
+63 -36
View File
@@ -31,16 +31,17 @@ 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', 'MinimumKeyBits': 1024,
'PidFile': '/var/run/dkimpy-milter/dkimpy-milter.pid', 'Socket': None,
'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 +49,10 @@ defaultConfigData = {
'DiagnosticDirectory': '', 'DiagnosticDirectory': '',
'MacroList': '', 'MacroList': '',
'MacroListVerify': '', 'MacroListVerify': '',
'DNSOverride': None,
'DNSTimeout': 5,
'SubDomains': False,
'SigningTable': None,
'debugLevel': 0 # Undocumented config item for developer use 'debugLevel': 0 # Undocumented config item for developer use
} }
@@ -84,14 +89,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 +114,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
@@ -154,18 +159,18 @@ class HostsDataset(object):
'''Get validated PTR name of IP address''' '''Get validated PTR name of IP address'''
results = [] results = []
s = Session() s = Session()
ptrnames = s.dns(source.reverse_pointer, 'PTR') ptrnames = s.dns(source.reverse_pointer, 'PTR', timeout=self.conf.get('DNSTimeout'))
for name in ptrnames: for name in ptrnames:
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 +229,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])
@@ -258,10 +263,10 @@ def _make_authserv_id(as_id):
as_id = socket.gethostname() as_id = socket.gethostname()
return as_id return as_id
def _dataset_to_list(dataset): def _dataset_to_list(dataset):
"""Convert a dataset (as defined in dkimpymilter.8) and return a python """Convert a dataset (as defined in dkimpymilter.8) and return a python
list of values.""" list of values. For multiline datasets like KeyTable and SigningTable a
key : values dictionary is returned"""
if not isinstance(dataset, str): if not isinstance(dataset, str):
# If it was a csl with more than one value, it's already a list, we # If it was a csl with more than one value, it's already a list, we
# only need to remove the name from the first value. # only need to remove the name from the first value.
@@ -271,42 +276,41 @@ def _dataset_to_list(dataset):
dataset[dataset.index(item)] = item.strip().strip(',') dataset[dataset.index(item)] = item.strip().strip(',')
return dataset return dataset
elif isinstance(dataset, str): elif isinstance(dataset, str):
if dataset[0] == '/' or dataset[:5] == 'file:': if dataset[0] == '/' or dataset[:5] == 'file:' or dataset[:7] == 'refile:':
# This is a flat file dataset # This is a flat file dataset, which are key value:value stores
ds = [] ds = []
if dataset[0] == '/': dsd = {}
if dataset[0] == '/' or dataset[:2] == './' or dataset[:3] == '../':
dsname = dataset dsname = dataset
if dataset[:5] == 'file:': elif dataset[:5] == 'file:':
dsname = dataset[5:] dsname = dataset[5:]
elif dataset[:7] == 'refile:':
dsname = dataset[7:]
dsf = open(dsname, 'r') dsf = open(dsname, 'r')
for line in dsf.readlines(): for line in dsf.readlines():
if line[0] != '#': if line[0] != '#':
if len(line.split()) == 1:
if len(line.split(':')) == 1: if len(line.split(':')) == 1:
ds.append(line.strip()) ds.append(line.strip())
else: else:
for element in line.split(':'): for element in line.split(':'):
ds.append(element.strip().strip(':')) ds.append(element.strip().strip(':'))
elif len(line.split()) == 2: # key value:value:value
key, values = line.split()
values = values.split(':')
dsd.update({key:values})
dsf.close() dsf.close()
if ds:
return ds return ds
elif dsd:
return dsd
# If it's a str and csl, it has one value and we return a list # If it's a str and csl, it has one value and we return a list
if dataset[:4] == 'csl:': if dataset[:4] == 'csl:':
return [dataset[4:].strip().strip(',')] return [dataset[4:].strip().strip(',')]
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)))
@@ -320,7 +324,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:
@@ -332,27 +338,35 @@ def _readConfigFile(path, configData=None, configGlobal={}):
'SyslogSuccess': 'bool', 'SyslogSuccess': 'bool',
'UMask': 'int', 'UMask': 'int',
'Mode': 'str', 'Mode': 'str',
'MinimumKeyBits': 'int',
'Socket': 'str', 'Socket': 'str',
'PidFile': 'str', 'PidFile': 'str',
'UserID': 'str', 'UserID': 'str',
'Domain': 'dataset', 'Domain': 'dataset',
'SubDomains': 'bool',
'KeyFile': 'str', 'KeyFile': 'str',
'KeyTable': 'dataset',
'KeyFileEd25519': 'str', 'KeyFileEd25519': 'str',
'KeyTableEd25519': 'dataset',
'Selector': 'str', 'Selector': 'str',
'SelectorEd25519': 'str', 'SelectorEd25519': 'str',
'SigningTable': 'dataset',
'Canonicalization': 'str', 'Canonicalization': 'str',
'InternalHosts': 'dataset', 'InternalHosts': 'dataset',
'IntHosts': 'bool', 'IntHosts': 'bool',
'DiagnosticDirectory': 'str', 'DiagnosticDirectory': 'str',
'MacroList': 'dataset', 'MacroList': 'dataset',
'MacroListVerify': 'dataset', 'MacroListVerify': 'dataset',
'debugLevel': 'int' 'DNSOverride': 'str',
'DNSTimeout': 'int',
'debugLevel': 'int',
'SignHeaders': 'dataset'
} }
# 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)
@@ -388,9 +402,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
@@ -400,8 +420,15 @@ 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':
if name == 'MinimumKeyBits':
if int(value) == 0:
# Odd inheritence from OpenDKIM where value of 0 means use default.
value = configData.get(name)
configData[name] = int(value) configData[name] = int(value)
elif conversion == 'dataset': elif conversion == 'dataset':
configData[name] = _dataset_to_list(value) configData[name] = _dataset_to_list(value)
@@ -411,7 +438,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
+11 -11
View File
@@ -84,11 +84,11 @@ 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
def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=30): def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=5):
try: try:
# FIXME: To be thread safe, we create a fresh DnsRequest with # FIXME: To be thread safe, we create a fresh DnsRequest with
# each call. It would be more efficient to reuse # each call. It would be more efficient to reuse
@@ -103,22 +103,22 @@ 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=5):
retVal = [] retVal = []
try: try:
# FIXME: how to disable TCP fallback in dnspython if not tcpfallback? # FIXME: how to disable TCP fallback in dnspython if not tcpfallback?
answers = dns.resolver.query(name, qtype) answers = dns.resolver.query(name, qtype, raise_on_no_answer=False, lifetime=timeout)
for rdata in answers: for rdata in answers:
if qtype == 'A' or qtype == 'AAAA': if qtype == 'A' or qtype == 'AAAA':
retVal.append(((name, qtype), rdata.address)) retVal.append(((name, qtype), rdata.address))
@@ -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))
+46 -23
View File
@@ -115,54 +115,43 @@ 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 read_keyfile(keyfile, milterconfig):
"""If socket is Unix socket, chown to UserID before dropping privileges"""
import os
user, group = user_group(milterconfig.get('UserID'))
if milterconfig.get('Socket')[:1] == '/':
os.chown(milterconfig.get('Socket')[1:], user, group)
if milterconfig.get('Socket')[:6] == "local:":
os.chown(milterconfig.get('Socket')[6:], user, group)
def read_keyfile(milterconfig, keytype):
"""Read private key from file.""" """Read private key from file."""
import syslog import syslog
if keytype == "RSA":
keyfile = milterconfig.get('KeyFile')
if keytype == "Ed25519":
keyfile = milterconfig.get('KeyFileEd25519')
try: try:
f = open(keyfile, 'r') f = open(keyfile, 'r')
keylist = f.readlines() keylist = f.readlines()
@@ -176,3 +165,37 @@ def read_keyfile(milterconfig, keytype):
for line in keylist: for line in keylist:
key += line key += line
return key return key
def read_keytable(tabledict, milterconfig):
"""Read keytables into in memory configuration data so all keys are read
before priviledges are dropped.
When loaded, tabeldict is a dict:
{searchkey: [donamin, selector, key]}
If key is a file (startswith('/'), then the key is returned in its place."""
import dkim
import syslog
for dictkey, values in tabledict.items():
if values[-1][:1] == '/' or values[-1][:2] == './' or values[-1][:3] == '../':
key = read_keyfile(values[-1], milterconfig)
tabledict[dictkey] = [values[0], values[1], key]
return tabledict
def get_keys(milterconfig):
"""Read keys (table or file) into memory before dropping priviledges"""
milterconfig['privateRSA'] = False
milterconfig['privateRSATable'] = False
milterconfig['privateEd25519'] = False
milterconfig['privateEd25519Table'] = False
if milterconfig.get('KeyTable'):
milterconfig['privateRSATable'] = read_keytable(milterconfig.get('KeyTable'),
milterconfig)
elif milterconfig.get('KeyFile'):
milterconfig['privateRSA'] = read_keyfile(milterconfig.get('KeyFile'),
milterconfig)
if milterconfig.get('KeyTableEd25519'):
milterconfig['privateEd25519Table'] = read_keytable(milterconfig.get('KeyTableEd25519'),
milterconfig)
elif milterconfig.get('KeyFileEd25519'):
milterconfig['privateEd25519'] = read_keyfile(milterconfig.get('KeyFileEd25519'),
milterconfig)
return milterconfig
+8 -9
View File
@@ -1,6 +1,5 @@
# This is a basic configuration that can easily be adapted to suit a standard # This is a basic configuration that can easily be adapted to suit a standard
# installation. For more advanced options, see dkimpy-milter.conf(5) and/or # installation. For more advanced options, see dkimpy-milter.conf(5).
# /usr/share/doc/dkimpy-milter/examples/opendkim.conf.sample.
# Log to syslog # Log to syslog
Syslog yes Syslog yes
@@ -9,18 +8,16 @@ Syslog yes
# privileged user (e.g. Postfix) # privileged user (e.g. Postfix)
UMask 007 UMask 007
# Sign for example.com with key in /etc/dkimkeys/dkim.key using # Sign for example.com with key in /usr/local/etc/dkimkeys/dkim.key using
# selector '2007' (e.g. 2007._domainkey.example.com) # selector '2007' (e.g. 2007._domainkey.example.com)
#Domain example.com #Domain example.com
#KeyFile /etc/mail/dkim.key #KeyFile /usr/local/etc/mail/dkim.key
#Selector default #Selector default
# Commonly-used options; the commented-out versions show the defaults. # Commonly-used options; the commented-out versions show the defaults.
#Canonicalization relaxed/simple #Canonicalization relaxed/simple
#Mode sv #Mode sv
# Socket local:/var/run/dkimpy-milter/dkimpy-milter.sock
#
# ## Socket socketspec # ## Socket socketspec
# ## # ##
# ## Names the socket where this filter should listen for milter connections # ## Names the socket where this filter should listen for milter connections
@@ -30,15 +27,17 @@ UMask 007
# ## inet:port to listen on all interfaces # ## inet:port to listen on all interfaces
# ## local:/path/to/socket to listen on a UNIX domain socket # ## local:/path/to/socket to listen on a UNIX domain socket
# #
Socket inet:8892@localhost #Socket local:/run/dkimpy-milter/dkimpy-milter.sock
#
#Socket inet:8892@localhost
## PidFile filename ## PidFile filename
### default /var/run/dkimpy-milter/dkimpy-milter.pid ### default /run/dkimpy-milter/dkimpy-milter.pid
### ###
### 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
+48
View File
@@ -0,0 +1,48 @@
# This is a basic configuration that can easily be adapted to suit a standard
# installation. For more advanced options, see dkimpy-milter.conf(5).
# Log to syslog
Syslog yes
# Required to use local socket with MTAs that access the socket as a non-
# privileged user (e.g. Postfix)
UMask 007
# Sign for example.com with key in @SYSCONFDIR@/dkimkeys/dkim.key using
# selector '2007' (e.g. 2007._domainkey.example.com)
#Domain example.com
#KeyFile @SYSCONFDIR@/mail/dkim.key
#Selector default
# Commonly-used options; the commented-out versions show the defaults.
#Canonicalization relaxed/simple
#Mode sv
# ## Socket socketspec
# ##
# ## Names the socket where this filter should listen for milter connections
# ## from the MTA. Required. Should be in one of these forms:
# ##
# ## inet:port@address to listen on a specific interface
# ## inet:port to listen on all interfaces
# ## local:/path/to/socket to listen on a UNIX domain socket
#
#Socket local:@RUNSTATEDIR@/dkimpy-milter.sock
#
#Socket inet:8892@localhost
## PidFile filename
### default /run/dkimpy-milter/dkimpy-milter.pid
###
### Name of the file where the filter should write its pid before beginning
### normal operations.
#
PidFile @RUNSTATEDIR@/dkimpy-milter.pid
## Userid userid
### default dkimpy-milter
###
### Change to user "userid" before starting normal operation? May include
### a group ID as well, separated from the userid by a colon.
#
UserID dkimpy-milter
+21 -19
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
@@ -171,22 +170,22 @@ are ignored, and the hash ("#") character denotes the start of a comment.
If a value contains multiple entries, the entries should be separated by If a value contains multiple entries, the entries should be separated by
colons. colons.
.TP .TP
.I c) .I b)
If the string begins with "db:" and the program was compiled with If the string begins with "refile:", then the remainder of the string is
Sleepycat DB support, then the remainder of the string is presumed to presumed to specify a file that contains a set of patterns, one per line,
identify a Sleepycat database containing keys and corresponding values. and their associated values. The pattern is taken as the start of the line
These may be used only to test for membership in the data set, or for to the first whitespace, and the portion after that whitespace is taken as
storing keys and corresponding values. If a value contains multiple entries, the value to be used when that pattern is matched. Patterns are simple
the entries should be separated by colons. [Not implemented yet] wildcard patterns, matching all text except that the asterisk ("*") character
.TP is considered a wildcard. If a value contains multiple entries, the entries
.I h) should be separated by colons.
If the string contains none of these prefixes but ends with ".db", it
is presumed to be a Sleepycat DB as described above (if support for same
is compiled in). [Not implemented yet]
.TP .TP
.I i) .I i)
If the string contains none of these prefixes but starts with a slash ("/") If the string contains none of these prefixes but starts with a slash ("/")
character, it is presumed to be a flat file as described above. character, or "./" or "../", it is presumed to be a flat file as described
above. Note: In OpenDKIM "./" and "../" only apply to KeyTable, but for
dkimpy-milter it is generally applicable and KeyTable specification is not
a special case.
.TP .TP
.I j) .I j)
If the string begins with "csl:", the string is treated as a comma-separated If the string begins with "csl:", the string is treated as a comma-separated
@@ -205,8 +204,9 @@ pairs as described above.
.TP .TP
See See
.I dkimpy-milter.conf (5) .I dkimpy-milter.conf (5)
information about available options. Unlike OpenDKIM, dkimpy-milter does not for information about available options. Unlike OpenDKIM, with the exception of
support command line option switches. \-P for the pidfile and specifying the configuration file to use,
dkimpy-milter does not support command line option switches.
When signing a message, a When signing a message, a
.I DKIM-Signature: .I DKIM-Signature:
@@ -269,7 +269,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 +278,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 +292,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)
+124 -27
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 "2019-04-25"
.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" 1\.2\.0
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
@@ -155,23 +152,21 @@ the value is processed. For positive values, the following are accepted:
"F", "f", "N", "n", "0". "F", "f", "N", "n", "0".
The provided setup.py installs this configuration file in /etc or The provided setup.py installs this configuration file in /etc or
/usr/local/etc. /usr/local/etc based on the value of expand sysconfigdir= used when the
package was installed.
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 [/usr/local/etc/dkimpy-milter/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 +176,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)
@@ -218,9 +211,11 @@ header and the second to the body.
.TP .TP
.I DiagnosticDirectory (string) .I DiagnosticDirectory (string)
Directory into which to write diagnostic reports when message verification Directory into which to write diagnostic reports when message verification
fails. If not set (the default), these files are not generated. [Unlike fails. If not set (the default), these files are not generated. The
OpenDKIM, this applies to all messages, not just on messages bearing a "z=" tag directory must exist, dkimpy-milter will not create it and an error will be
because dkimpy does not yet support "z=".] raised if it does not. [Unlike OpenDKIM, this applies to all messages, not
just on messages bearing a "z=" tag because dkimpy does not yet support
"z=" processing.]
.TP .TP
.I Domain (dataset) .I Domain (dataset)
@@ -230,11 +225,13 @@ 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
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)
@@ -252,7 +249,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)
@@ -260,7 +257,19 @@ Gives the location of a Ed25519 private key to be used for Ed25519 signing
all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key
generation (as used in dkimpy). Ignored if a generation (as used in dkimpy). Ignored if a
.I KeyTableEd25519 .I KeyTableEd25519
is defined. [KeyTableEd25519 NOT IMPLEMENTED] is defined.
.TP
.I KeyTable (dataset)
Gives the location of a file mapping key names to RSA signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signatures "d=" value; (b) the name of the selector to use in the signatures "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender.
See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples.
.TP
.I KeyTableEd25519 (dataset)
Gives the location of a file mapping key names to Ed25519 signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signatures "d=" value; (b) the name of the selector to use in the signatures "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender. NOTE: There is a limitation of the current implementation that a private key can't be directly included in the file if it starts with '/', './', or '../'. If you have such a key, you may store it in a file and reference the file in the table.
See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples.
.TP .TP
.I MacroList (dataset) .I MacroList (dataset)
@@ -317,7 +326,43 @@ 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 MinimumKeyBits (integer)
Establishes a minimum key size for acceptable RSA signatures. Signatures with
smaller key sizes, even if they otherwise pass DKIM validation, will me marked
as invalid. The default is 1024, which accepts all signatures. A value of
0 causes the default to be used. Not Applicable to ed25519 signatures.
.TP
.I OmitHeaders (dataset)
Specifies a set of header fields that should be omitted when generating
signatures. If an entry in the list names any header field that is mandated
by the DKIM specification, the entry is ignored. A set of header fields is
listed in the DKIM specification (RFC6376, Section 5.4) as "SHOULD NOT" be
signed; the default list for this parameter contains those fields
(Return-Path, Received, Comments, Keywords, Bcc, Resent-Bcc and
DKIM-Signature). To omit no headers, simply use the string "." (or any
string that will match no header field names).
Specifying a list with this parameter replaces the default entirely, unless
one entry is "*" in which case the list is interpreted as a delta to the
default; for example, "*,+foobar" will use the entire default list plus
the name "foobar", while "*,-Bcc" would use the entire default list except
for the "Bcc" entry. [OmitHeaders NOT IMPLEMENTED - included for reference
only]
.TP
.I DNSOverride (string)
Provide a text string that a verifying milter should use instead of
consulting the DNS on each message. This is useful primarily for
testing purposes in environments where it is awkward to modify the
system DNS resolution. It should not be used in production.
.TP
.I DNSTimeout (integer)
Sets the DNS timeout in seconds. A value of 0 causes no wait (this is
different than opendkim). The default is 5. See also the NOTES section
below.
.TP .TP
.I PeerList (dataset) .I PeerList (dataset)
@@ -339,7 +384,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)
@@ -353,7 +398,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)
@@ -367,7 +412,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 SignHeaders (dataset)
Specifies the set of header fields that should be included when generating
signatures. If the list omits any header field that is mandated by the DKIM
specification, those fields are implicitly added. By default, those fields
listed in the DKIM specification as "SHOULD" be signed (RFC6376, Section 5.4)
will be signed by the filter. See the
.I OmitHeaders
configuration option for more information about the format and interpretation
of this field.
.TP
.I SigningTable (dataset)
Defines a table used to select one or more signing identities 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. Note: These are not true regular expressions. The terminology is inherited from opendkim. Only wildcards ("*") are supported.
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.
See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples.
.TP .TP
.I Socket (string) .I Socket (string)
@@ -392,6 +463,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
@@ -440,12 +517,32 @@ unless an alternate
.I group .I group
is specified. is specified.
.SH NOTES
When using DNS timeouts (see the
.I DNSTimeout
option above), be sure not to use a timeout that is larger than the timeout
being used for interaction between
.I sendmail
and the filter. Otherwise, the MTA could abort a message while waiting for
a reply from the filter, which in turn is still waiting for a DNS reply. This
must take into accout that the timeout is per DNS lookup so the total DNS wait
time may be subustantially loner than the value specified in
.I DNSTimeout
\. There is a DNS lookup for each connection if the
.I InternalHosts
option is in use and one for DKIM public key record lookup for each algorithm
per signature per message (i.e. potentially two lookups per signature).
.SH FILES
.TP
.I /usr/local/etc/dkimpy-milter/dkimpy-milter.conf
Default location of this file.
.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 dkim-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc.
Copyright (c) 2013-2015 Stuart D. Gathman Copyright (c) 2013-2015 Stuart D. Gathman
Copyright (c) 2018 Scott Kitterman <scott@kitterman.com>. Copyright (c) 2018,2019 Scott Kitterman <scott@kitterman.com>.
.PP .PP
This man-page was created by Scott Kitterman <scott@kitterman.com>. This man-page was created by Scott Kitterman <scott@kitterman.com>.
@@ -459,4 +556,4 @@ See LICENSE.
Updated for dkimpy-milter. Updates licensed under the same terms as the rest Updated for dkimpy-milter. Updates licensed under the same terms as the rest
of the package. of the package.
Copyright (c) 2018, Scott Kitterman <scott@kitterman.com> Copyright (c) 2018,2019 Scott Kitterman <scott@kitterman.com>
+559
View File
@@ -0,0 +1,559 @@
\"
.\" Standard preamble:
.\" ========================================================================
.de Sh \" Subsection heading
.br
.if t .Sp
.ne 5
.PP
\fB\\$1\fR
.PP
..
.de Sp \" Vertical space (when we can't use .PP)
.if t .sp .5v
.if n .sp
..
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.\" Set up some character translations and predefined strings. \*(-- will
.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
.\" double quote, and \*(R" will give a right double quote. \*(C+ will
.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
.\" nothing in troff, for use with C<>.
.tr \(*W-
.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
.ie n \{\
. ds -- \(*W-
. ds PI pi
. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
. ds L" ""
. ds R" ""
. ds C` ""
. ds C' ""
'br\}
.el\{\
. ds -- \|\(em\|
. ds PI \(*p
. ds L" ``
. ds R" ''
'br\}
.\"
.\" If the F register is turned on, we'll generate index entries on stderr for
.\" titles (.TH), headers (.SH), subsections (.Sh), items (.Ip), and index
.\" entries marked with X<> in POD. Of course, you'll have to process the
.\" output yourself in some meaningful fashion.
.if \nF \{\
. de IX
. tm Index:\\$1\t\\n%\t"\\$2"
..
. nr % 0
. rr F
.\}
.\"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.hy 0
.if n .na
.\"
.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
.\" Fear. Run. Save yourself. No user-serviceable parts.
. \" fudge factors for nroff and troff
.if n \{\
. ds #H 0
. ds #V .8m
. ds #F .3m
. ds #[ \f1
. ds #] \fP
.\}
.if t \{\
. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
. ds #V .6m
. ds #F 0
. ds #[ \&
. ds #] \&
.\}
. \" simple accents for nroff and troff
.if n \{\
. ds ' \&
. ds ` \&
. ds ^ \&
. ds , \&
. ds ~ ~
. ds /
.\}
.if t \{\
. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
.\}
. \" troff and (daisy-wheel) nroff accents
.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
.ds ae a\h'-(\w'a'u*4/10)'e
.ds Ae A\h'-(\w'A'u*4/10)'E
. \" corrections for vroff
.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
. \" for low resolution devices (crt and lpr)
.if \n(.H>23 .if \n(.V>19 \
\{\
. ds : e
. ds 8 ss
. ds o a
. ds d- d\h'-1'\(ga
. ds D- D\h'-1'\(hy
. ds th \o'bp'
. ds Th \o'LP'
. ds ae ae
. ds Ae AE
.\}
.rm #[ #] #H #V #F C
.\" ========================================================================
.\"
.TH dkimpy-milter.conf 5 "2019-04-25"
.SH "NAME"
dkimpy-milter \- Python milter for DKIM signing and validation
.SH "VERSION"
1\.2\.0
.SH "DESCRIPTION"
.I dkimpy-milter(8)
implements the
.B DKIM
specification for signing and verifying e-mail messages on a per-domain
basis. This file is its configuration file.
Blank lines are ignored. Lines containing a hash ("#") character are
truncated at the hash character to allow for comments in the file.
Other content should be the name of a parameter, followed by white space,
followed by the value of that parameter, each on a separate line.
For parameters that are Boolean in nature, only the first byte of
the value is processed. For positive values, the following are accepted:
"T", "t", "Y", "y", "1". For negative values, the following are accepted:
"F", "f", "N", "n", "0".
The provided setup.py installs this configuration file in /etc or
/usr/local/etc based on the value of expand sysconfigdir= used when the
package was installed.
Command line invocation of parameters as is done by OpenDKIM is not supported.
.SH "USAGE"
Usage:
dkimpy-milter [@CONFDIR@/dkimpy-milter.conf]
.SH "OTHER DOCUMENTATION"
This documentation assumes you have read Postfix's README_FILES/MILTER_README
(or Sendmail equivalent) and are generally familiar with Domain Keys Identified
Mail (DKIM). See RFC 6376 for details.
.SH "SYNOPSIS"
dkimpy-milter operates with a default installed configuration file and
set of default configuration options that are used if the configuration file
cannot be found. These options can be changed by changing the installed
configuration files. For users transitioning from OpenDKIM, OpenDKIM config
files can be used directly. Not all OpenDKIM options are supported. If an
unsupported option from OpenDKIM is specified, an error will be raised.
.SH "DESCRIPTION"
Configuration options are described here and in the configuration file
provided with the package. The provided setup.py installs this configuration
file in /etc or /usr/local/etc.
.SH "OPTIONS"
.TP
.I AuthservID (string)
Sets the "authserv-id" to use when generating the Authentication-Results:
header field after verifying a message. The default is to use the name of
the MTA processing the message. If the string "HOSTNAME" is provided, the
name of the host running the filter (as returned by the
.I gethostname(3)
function) will be used.
.TP
.I Canonicalization (string)
Selects the canonicalization method(s) to be used when signing messages.
When verifying, the message's DKIM-Signature: header field specifies
the canonicalization method. The recognized values are
.I relaxed
and
.I simple
as defined by the DKIM specification. The default is
.I relaxed
/
.I simple.
The value may include two different canonicalizations separated by a
slash ("/") character, in which case the first will be applied to the
header and the second to the body.
.TP
.I DiagnosticDirectory (string)
Directory into which to write diagnostic reports when message verification
fails. If not set (the default), these files are not generated. The
directory must exist, dkimpy-milter will not create it and an error will be
raised if it does not. [Unlike OpenDKIM, this applies to all messages, not
just on messages bearing a "z=" tag because dkimpy does not yet support
"z=" processing.]
.TP
.I Domain (dataset)
A set of domains whose mail should be signed by this filter. Mail from other
domains will be verified rather than being signed.
This parameter is not required if a
.I SigningTable
is in use; in that case, the list of signed domains is implied by the
lines in that file.
This parameter is ignored if a
.I KeyTable
or
.I KeyTableD25119
is defined.
.TP
.I InternalHosts (dataset)
Identifies a set internal hosts whose mail should be signed rather
than verified. Entries in this data set follow the same form as those of
the
.I PeerList
option below. If not specified, the default of "127.0.0.1" is applied.
Naturally, providing a value here overrides the default, so if mail from
127.0.0.1 should be signed, the list provided here should include that
address explicitly. [PeerList NOT IMPLEMENTED]
.TP
.I KeyFile (string)
Gives the location of a PEM-formatted private key to be used for RSA signing
all messages. Ignored if a
.I KeyTable
is defined.
.TP
.I KeyFileEd25519 (string)
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
generation (as used in dkimpy). Ignored if a
.I KeyTableEd25519
is defined.
.TP
.I KeyTable (dataset)
Gives the location of a file mapping key names to RSA signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signatures "d=" value; (b) the name of the selector to use in the signatures "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender.
See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples.
.TP
.I KeyTableEd25519 (dataset)
Gives the location of a file mapping key names to Ed25519 signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signatures "d=" value; (b) the name of the selector to use in the signatures "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender. NOTE: There is a limitation of the current implementation that a private key can't be directly included in the file if it starts with '/', './', or '../'. If you have such a key, you may store it in a file and reference the file in the table.
See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples.
.TP
.I MacroList (dataset)
Defines a set of MTA-provided
.I macros
that should be checked to see if the sender has been determined to be a
local user and therefore whether or not the message should be signed. If
a
.I value
is specified matching a macro name in the data set, the value of the macro
must match a value specified (matching is case-sensitive), otherwise the
macro must be defined but may contain any value. The set is empty by
default, meaning macros are not considered when making the sign-verify
decision. The general format of the value is
.I value1[|value2[|...]];
if one or more value is defined then the macro must be set to one of the
listed values, otherwise the macro must be set but can contain any
value.
In order for the macro and its value to be available to the filter for
checking, the MTA must send it during the protocol exchange. This is either
accomplished via manual configuration of the MTA to send the desired macros
or, for MTA/filter combinations that support the feature, the filter can
request those macros that are of interest. The latter is a feature negotiated
at the time the filter receives a connection from the MTA and its availability
depends upon the version of milter used to compile the filter and the version
of the MTA making the connection.
.TP
.I MacroListVerify (dataset)
Defines a set of MTA-provided
.I macros
that should be checked to see if the sender has been determined to be an
external source and therefore whether or not the message should be signed.
Entries in this data set follow the same form as those of the
.I MacroList
option above. [this option is not inhereted from OpenDKIM]
.TP
.I Mode (string)
Selects operating modes. The string is a concatenation of characters that
indicate which mode(s) of operation are desired. Valid modes are
.I s
(signer) and
.I v
(verifier). The default is
.I sv
except in test mode (see the
.I opendkim(8)
man page)
in which case the default is
.I v.
When signing mode is enabled, one of the following combinations must also
be set:
(a) Domain, KeyFile, Selector, no KeyTable, no SigningTable;
(b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector;
TP
.I MinimumKeyBits (integer)
Establishes a minimum key size for acceptable RSA signatures. Signatures with
smaller key sizes, even if they otherwise pass DKIM validation, will me marked
as invalid. The default is 1024, which accepts all signatures. A value of
0 causes the default to be used. Not Applicable to ed25519 signatures.
.TP
.I OmitHeaders (dataset)
Specifies a set of header fields that should be omitted when generating
signatures. If an entry in the list names any header field that is mandated
by the DKIM specification, the entry is ignored. A set of header fields is
listed in the DKIM specification (RFC6376, Section 5.4) as "SHOULD NOT" be
signed; the default list for this parameter contains those fields
(Return-Path, Received, Comments, Keywords, Bcc, Resent-Bcc and
DKIM-Signature). To omit no headers, simply use the string "." (or any
string that will match no header field names).
Specifying a list with this parameter replaces the default entirely, unless
one entry is "*" in which case the list is interpreted as a delta to the
default; for example, "*,+foobar" will use the entire default list plus
the name "foobar", while "*,-Bcc" would use the entire default list except
for the "Bcc" entry. [OmitHeaders NOT IMPLEMENTED - included for reference
only]
.TP
.I DNSOverride (string)
Provide a text string that a verifying milter should use instead of
consulting the DNS on each message. This is useful primarily for
testing purposes in environments where it is awkward to modify the
system DNS resolution. It should not be used in production.
.TP
.I DNSTimeout (integer)
Sets the DNS timeout in seconds. A value of 0 causes no wait (this is
different than opendkim). The default is 5. See also the NOTES section
below.
.TP
.I PeerList (dataset)
Identifies a set of "peers" that identifies clients whose connections
should be accepted without processing by this filter. The set
should contain on each line a hostname, domain name (e.g. ".example.com"),
IP address, an IPv6 address (including an IPv4 mapped address), or a
CIDR-style IP specification (e.g. "192.168.1.0/24"). An entry beginning
with a bang ("!") character means "not", allowing exclusions of specific
hosts that are otherwise members of larger sets. Host and domain names are
matched first, then the IP or IPv6 address depending on the connection
type. More precise entries are preferred over less precise ones, i.e.
"192.168.1.1" will match before "!192.168.1.0/24". The text form of IPv6
addresses will be forced to lowercase when queried (RFC5952), so the contents
of this data set should also use lowercase. The IP address portion of an
entry may optionally contain square brackets; both forms (with and without)
will be checked. [PeerList NOT IMPLEMENTED - included for reference only]
.TP
.I PidFile (string)
Specifies the path to a file that should be created at process start
containing the process ID. If not specified, no such file will be created.
.TP
.I Selector (string)
Defines the name of the selector to be used when signing messages using RSA.
See the
.B DKIM
specification for details. Used only when signing with a single key;
see the
.I SigningTable
parameter below for more information.
This parameter is ignored if a
.I KeyTable
is defined.
.TP
.I SelectorEd25519 (string)
Defines the name of the selector to be used when signing messages using Ed25519.
See the
.B DKIM
specification for details. Used only when signing with a single key;
see the
.I SigningTable
parameter below for more information.
This parameter is ignored if a
.I KeyTableEd25519
is defined.
.TP
.I SignHeaders (dataset)
Specifies the set of header fields that should be included when generating
signatures. If the list omits any header field that is mandated by the DKIM
specification, those fields are implicitly added. By default, those fields
listed in the DKIM specification as "SHOULD" be signed (RFC6376, Section 5.4)
will be signed by the filter. See the
.I OmitHeaders
configuration option for more information about the format and interpretation
of this field.
.TP
.I SigningTable (dataset)
Defines a table used to select one or more signing identities 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. Note: These are not true regular expressions. The terminology is inherited from opendkim. Only wildcards ("*") are supported.
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.
See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples.
.TP
.I Socket (string)
Specifies the socket that should be established by the filter to receive
connections from
.I postfix(1)
in order to provide service.
.I socketspec
is in one of two forms:
.I local:path,
which creates a UNIX domain socket at the specified
.I path,
or
.I inet:port[@host]
or
.I inet6:port[@host]
which creates a TCP socket on the specified
.I port
and in the specified protocol family. If the
.I host
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
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
.I Syslog (Boolean)
Log via calls to
.I syslog(3)
any interesting activity.
.TP
.I SyslogFacility (string)
Log via calls to
.I syslog(3)
using the named facility. The facility names are the same as the ones
allowed in
.I syslog.conf(5).
The default is "mail".
.TP
.I SyslogSuccess (Boolean)
Log via calls to
.I syslog(3)
additional entries indicating successful signing or verification of
messages.
.TP
.I UMask (integer)
Requests a specific permissions mask to be used for file creation.
This only really applies to creation of the socket when
.I Socket
specifies a UNIX domain socket, and to the
.I PidFile
(if any); temporary files are created by the
.I mkstemp(3)
function that enforces a specific file mode on creation regardless
of the process umask. See
.I umask(2)
for more information.
.TP
.I UserID (string)
Attempts to become the specified userid before starting operations.
The value is of the form
.I userid[:group].
The process will be assigned all of the groups and primary group ID of
the named
.I userid
unless an alternate
.I group
is specified.
.SH NOTES
When using DNS timeouts (see the
.I DNSTimeout
option above), be sure not to use a timeout that is larger than the timeout
being used for interaction between
.I sendmail
and the filter. Otherwise, the MTA could abort a message while waiting for
a reply from the filter, which in turn is still waiting for a DNS reply. This
must take into accout that the timeout is per DNS lookup so the total DNS wait
time may be subustantially loner than the value specified in
.I DNSTimeout
\. There is a DNS lookup for each connection if the
.I InternalHosts
option is in use and one for DKIM public key record lookup for each algorithm
per signature per message (i.e. potentially two lookups per signature).
.SH FILES
.TP
.I @CONFDIR@/dkimpy-milter.conf
Default location of this file.
.SH "AUTHORS"
\ddkimpy-milter\fR was written by Scott Kitterman <scott@kitterman.com>.
It is based on dkim-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc.
Copyright (c) 2013-2015 Stuart D. Gathman
Copyright (c) 2018,2019 Scott Kitterman <scott@kitterman.com>.
.PP
This man-page was created by Scott Kitterman <scott@kitterman.com>.
.SH COPYRIGHT
Configuration items derived from OpenDKIM 2.11.0 opendkim.conf.5.in:
Copyright (c) 2007, 2008, Sendmail, Inc. and its suppliers. All rights
reserved. See LICENSE.Sendmail.
Copyright (c) 2009-2015, The Trusted Domain Project. All rights reserved.
See LICENSE.
Updated for dkimpy-milter. Updates licensed under the same terms as the rest
of the package.
Copyright (c) 2018,2019 Scott Kitterman <scott@kitterman.com>
+71 -10
View File
@@ -1,7 +1,7 @@
#! /usr/bin/python #! /usr/bin/python3
# dkimpy-milter: A DKIM signing/verification Milter application # dkimpy-milter: A DKIM signing/verification Milter application
# Author: Scott Kitterman <scott@kitterman.com> # Author: Scott Kitterman <scott@kitterman.com>
# Copyright 2018 Scott Kitterman # Copyright 2018,2019 Scott Kitterman
""" This program is free software; you can redistribute it and/or modify """ This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or the Free Software Foundation; either version 2 of the License, or
@@ -17,34 +17,91 @@
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.""" 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from setuptools import setup from setuptools import setup
import distutils.cmd
import distutils.log
import sys
import os import os
import dkimpy_milter import subprocess
description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail." description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail."
with open("README.md", "r") as fh:
long_description = fh.read()
class FileMacroExpand(distutils.cmd.Command):
description = "Expand @@ variables in input files, simlar to make macros."
user_options = [
('sysconfigdir=', 'e', 'Specify system configuration directory. [/usr/local/etc]'),
('sbindir=', 's', 'Specify system binary directory. [/usr/local/sbin]'),
('bindir=', 'b', 'Specify binary directory. [/usr/loca/bin]'),
('rundir=', 'r', 'Specify run state directory. [/run]'),
]
def initialize_options(self):
self.sysconfigdir = '/usr/local/etc'
self.sbindir = '/usr/local/sbin'
self.bindir = '/usr/local/bin'
self.rundir = '/run'
def finalize_options(self):
self.configdir = self.sysconfigdir + '/dkimpy-milter'
self.rundir += '/dkimpy-milter'
def run(self):
files = ['etc/dkimpy-milter.conf', 'man/dkimpy-milter.conf.5', \
'system/dkimpy-milter.service', 'system/dkimpy-milter', \
'system/dkimpy-milter.openrc', \
'system/socket-activation/dkimpy-milter.service', \
'system/socket-activation/dkimpy-milter.socket',]
for infile in files:
outfile = ''
filein = open(infile + '.in')
for line in filein:
for function in ["@SYSCONFDIR@", "@CONFDIR@", "@SBINDIR@", "@BINDIR@", "@RUNSTATEDIR@"]:
splitline = line.split(function)
if len(splitline) > 1:
if function == "@SYSCONFDIR@":
line = splitline[0] + self.sysconfigdir + splitline[1]
elif function == "@CONFDIR@":
line = splitline[0] + self.configdir + splitline[1]
elif function == "@SBINDIR@":
line = splitline[0] + self.sbindir + splitline[1]
elif function == "@BINDIR@":
line = splitline[0] + self.bindir + splitline[1]
elif function == "@RUNSTATEDIR@":
line = splitline[0] + self.rundir + splitline[1]
outfile += line
out = open(infile, 'w')
for line in outfile:
out.write(line)
out.close()
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>=1.0', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'dnspython>=1.16.0']
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>=1.0', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'Py3DNS']
setup( setup(
name='dkimpy-milter', name='dkimpy-milter',
version=dkimpy_milter.__version__, version='1.2.0',
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,
long_description=long_description,
long_description_content_type='text/markdown',
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',
@@ -61,7 +118,11 @@ setup(
['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']), (os.path.join('etc', 'init.d'),
['system/dkimpy-milter.openrc'])],
zip_safe = False, zip_safe = False,
cmdclass={
'expand': FileMacroExpand,
},
**kw **kw
) )
+13 -15
View File
@@ -13,32 +13,28 @@
# 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
# Description: Python DKIM Milter for Sendmail and Postfix # Description: Python DKIM Milter for Sendmail and Postfix
### END INIT INFO ### END INIT INFO
prefix="/usr/local" sysconfdir="/usr/local/etc/dkimpy-milter"
exec_prefix=${prefix} bindir="/usr/local/bin"
sysconfdir="/etc/dkimpy-milter" RUNDIR="/run/dkimpy-milter"
bindir="${exec_prefix}/bin/"
RUNDIR="/var/run/dkimpy-milter"
DAEMON=${bindir}/dkimpy-milter DAEMON=${bindir}/dkimpy-milter
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin: PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:
NAME=dkimpy-milter NAME=dkimpy-milter
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
# Include dkimpy-python defaults if available # Include dkimpy-python defaults if available
# Typically not used # Typically not used
if [ -f /etc/default/dkimpy-milter ] ; then if [ -f $sysconfdir/default/dkimpy-milter ] ; then
. /etc/default/dkimpy-milter . $sysconfdir/default/dkimpy-milter
fi fi
set -e set -e
@@ -69,14 +65,14 @@ case "$1" in
fi fi
fi fi
fi fi
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."
;; ;;
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
@@ -89,6 +85,7 @@ case "$1" in
force-reload) force-reload)
echo -n "Force reloading $DESC: " echo -n "Force reloading $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid
#echo $SOCKET #echo $SOCKET
@@ -97,7 +94,7 @@ case "$1" in
fi fi
fi fi
sleep 1 sleep 1
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \ start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME." echo "$NAME."
;; ;;
@@ -105,6 +102,7 @@ case "$1" in
echo "Restarting $DESC: " echo "Restarting $DESC: "
echo -n "Stopping $DESC: " echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid
#echo $SOCKET #echo $SOCKET
@@ -115,12 +113,12 @@ case "$1" in
echo "$NAME." echo "$NAME."
sleep 1 sleep 1
echo -n "Starting $DESC: " echo -n "Starting $DESC: "
start-stop-daemon --start --chuid $USER --background --quiet --pidfile \ start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME." echo "$NAME."
;; ;;
status) status)
status_of_proc -p /var/run/dkimpy-milter/dkimpy-milter.pid /usr/local/bin/dkimpy-milter dkimpy-milter status_of_proc -p $RUNDIR/$NAME.pid $DAEMON dkimpy-milter
;; ;;
*) *)
+131
View File
@@ -0,0 +1,131 @@
#! /bin/sh
#
# skeleton example file to build /etc/init.d/ scripts.
# This file should be used to construct scripts for /etc/init.d.
#
# Written by Miquel van Smoorenburg <miquels@cistron.nl>.
# Modified for Debian
# by Ian Murdock <imurdock@gnu.ai.mit.edu>.
#
# Version: @(#)skeleton 1.9 26-Feb-2001 miquels@cistron.nl
#
### BEGIN INIT INFO
# Provides: dkim-milter dkim-milter-python dkimpy-milter
# Required-Start: $remote_fs $syslog $network $time
# Required-Stop: $remote_fs $syslog $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: dkimpy-milter
# Description: Python DKIM Milter for Sendmail and Postfix
### END INIT INFO
sysconfdir="@CONFDIR@"
bindir="@BINDIR@"
RUNDIR="@RUNSTATEDIR@"
DAEMON=${bindir}/dkimpy-milter
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:
NAME=dkimpy-milter
DESC="Python DKIM Milter"
USER=dkimpy-milter
GROUP=dkimpy-milter
SOCKET=$RUNDIR/dkimpy-milter.sock
test -x $DAEMON || exit 0
# Include dkimpy-python defaults if available
# Typically not used
if [ -f $sysconfdir/default/dkimpy-milter ] ; then
. $sysconfdir/default/dkimpy-milter
fi
set -e
. /lib/lsb/init-functions
case "$1" in
start)
echo -n "Starting $DESC: "
# Create the run directory if it doesn't exist
if [ ! -d $RUNDIR ]; then
install -o $USER -g $GROUP -m 755 -d $RUNDIR || return 2
fi
# Clean up stale sockets
if [ -f $RUNDIR/$NAME.pid ]; then
pid=`cat $RUNDIR/$NAME.pid`
if ! ps -C $DAEMON -s $pid >/dev/null; then
rm $RUNDIR/$NAME.pid
# UNIX sockets may be specified with or without the
# local: prefix; handle both
t=`echo $SOCKET | cut -d: -f1`
s=`echo $SOCKET | cut -d: -f2`
if [ -e $s -a -S $s ]; then
if [ "$t" = "$s" -o "$t" = "local" ]; then
rm $s
fi
fi
fi
fi
start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME."
;;
stop)
echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid
#echo $SOCKET
if [ -e $SOCKET ]; then
rm $SOCKET
fi
fi
echo "$NAME."
;;
force-reload)
echo -n "Force reloading $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid
#echo $SOCKET
if [ -e $SOCKET ]; then
rm $SOCKET
fi
fi
sleep 1
start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME."
;;
restart)
echo "Restarting $DESC: "
echo -n "Stopping $DESC: "
if [ -f $RUNDIR/$NAME.pid ]; then
chown root:root $RUNDIR/$NAME.pid
start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid
rm $RUNDIR/$NAME.pid
#echo $SOCKET
if [ -e $SOCKET ]; then
rm $SOCKET
fi
fi
echo "$NAME."
sleep 1
echo -n "Starting $DESC: "
start-stop-daemon --start --background --quiet --pidfile \
$RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf
echo "$NAME."
;;
status)
status_of_proc -p $RUNDIR/$NAME.pid $DAEMON dkimpy-milter
;;
*)
N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|force-reload|restart|}" >&2
exit 1
;;
esac
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/sbin/openrc-run
# Copyright 1999-2019 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
CONFFILE="/usr/local/etc/dkimpy-milter/${RC_SVCNAME}.conf"
required_files="${CONFFILE}"
command="/usr/local/bin/dkimpy-milter"
pidfile="/run/dkimpy-milter/${RC_SVCNAME}.pid"
command_args="${CONFFILE} -P ${pidfile}"
depend() {
use dns logger net
before mta
}
+15
View File
@@ -0,0 +1,15 @@
#!/sbin/openrc-run
# Copyright 1999-2019 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
CONFFILE="@CONFDIR@/${RC_SVCNAME}.conf"
required_files="${CONFFILE}"
command="@BINDIR@/dkimpy-milter"
pidfile="@RUNSTATEDIR@/${RC_SVCNAME}.pid"
command_args="${CONFFILE} -P ${pidfile}"
depend() {
use dns logger net
before mta
}
+4 -3
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=/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/dkimpy-milter.conf -P /run/dkimpy-milter/dkimpy-milter.pid
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=DKIMpy Milter
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
After=network.target
[Service]
Type=simple
PIDFile=@RUNSTATEDIR@/dkimpy-milter.pid
ExecStart=@BINDIR@/dkimpy-milter @CONFDIR@/dkimpy-milter.conf -P @RUNSTATEDIR@/dkimpy-milter.pid
[Install]
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/local/bin/dkimpy-milter /usr/local/etc/dkimpy-milter/dkimpy-milter.conf
User=dkimpy-milter
[Install]
Also=dkimpy-milter.socket
@@ -0,0 +1,11 @@
[Unit]
Description=DKIMpy Milter
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
Requires=dkimpy-milter.socket
[Service]
ExecStart=@BINDIR@/dkimpy-milter @CONFDIR@/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 @@
[Unit]
Description=DKIMpy Milter socket
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
[Socket]
ListenStream=@RUNSTATEDIR@/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", "ed25519.stable", "rsa.stable", "ed25519.table", "rsa.table"}) 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
+143
View File
@@ -0,0 +1,143 @@
#!/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
elif [ "$keytype" = rsa ]; then
keyfile=KeyFile
selector=Selector
fi
if [ "$keytype" = ed25519 ]; then
keytable=KeyTableEd25519
signingtable=SigningTable
selector=SelectorEd25519
elif [ "$keytype" = rsa ]; then
keytable=KeyTable
signingtable=SigningTable
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)
MinimumKeyBits 2048
UserID $(id --name --user):$(id --name --group)
EOF
cat > "$keytype.stable.conf" <<EOF
$keyfile testkey.$keytype.key
$selector testkey
$signingtable $WORKDIR/signing-table
Socket unix:$keytype.stable.signing.sock
PidFile $keytype.stable.pid
Mode s
UserID $(id --name --user):$(id --name --group)
EOF
cat > "$keytype.stable.verify.conf" <<EOF
Socket unix:$keytype.stable.verify.sock
PidFile $keytype.stable.verify.pid
Mode v
DNSOverride $(cat testkey.$keytype.dns)
UserID $(id --name --user):$(id --name --group)
EOF
cat > "$keytype.table.conf" <<EOF
$keytable $WORKDIR/$keytype-table
$signingtable $WORKDIR/signing-table
Socket unix:$keytype.table.signing.sock
PidFile $keytype.table.pid
Mode s
UserID $(id --name --user):$(id --name --group)
EOF
cat > "$keytype.table.verify.conf" <<EOF
Socket unix:$keytype.table.verify.sock
PidFile $keytype.table.verify.pid
Mode v
DNSOverride $(cat testkey.$keytype.dns)
UserID $(id --name --user):$(id --name --group)
EOF
cat > "$keytype-table" <<EOF
preskey example.org:testkey:$WORKDIR/testkey.$keytype.key
orgkey example.org:testkey:$WORKDIR/testkey.$keytype.key
netkey example.net:testkey:$WORKDIR/testkey.$keytype.key
EOF
cat > "signing-table" <<EOF
president@example.org @special.example.org:preskey
*@example.org orgkey
*@example.net netkey
EOF
done
cleanup() {
echo cleaning up jobs:
jobs
for keytype in "${KEY_TYPES[@]}"; do
for func in signing verify stable stable.verify table table.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 stable stable.verify table table.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 stable stable.verify table table.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
rm -rf "$(dirname $TESTDIR)/dkimpy_milter/__pycache__"