Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abba014c5c | |||
| 807b1391b2 | |||
| dcb781c365 | |||
| 577a971f1a | |||
| 59296be0cf | |||
| 4e1fa3c8ff | |||
| 99899062bb | |||
| 6a1705926f | |||
| e6f8db9f94 | |||
| e63867d517 | |||
| 209ad11661 | |||
| 795a914845 | |||
| be92e5c5b1 | |||
| 6910ff1f9a | |||
| 7953e54ffb | |||
| bc98f9180f | |||
| a144791f2a | |||
| 55e1a6b54e | |||
| 7c3ff1905a | |||
| 7ec97a6001 | |||
| aba9c0aa0c | |||
| 19b6ce7a68 | |||
| 8dc3ac6474 | |||
| 290a37b99c | |||
| 564799402a | |||
| 357905bb68 | |||
| 6b851f18df | |||
| 7ab58edb1b | |||
| 23b0e8a386 | |||
| 2e105bd18c | |||
| fb72b9f6e7 | |||
| 7eed8995a2 | |||
| a16d887ac6 | |||
| 23d91b2b50 | |||
| 481fbdae29 | |||
| 4b0c39b0c7 | |||
| 7092874729 | |||
| ea09bab1a8 | |||
| 25fdd3b81c | |||
| 9d5316ca0e | |||
| 391b5352f3 | |||
| ad8f396db0 | |||
| 479820a07d | |||
| 7bfb87fab7 | |||
| 5c1d5d6e52 | |||
| ae31730593 | |||
| 72ed000ccf | |||
| b3db013754 | |||
| bd1d25d83e | |||
| 1c6030024d | |||
| 71c0c3f20a | |||
| a9a6893c89 | |||
| bb44f36519 | |||
| 9e11b75ec3 | |||
| f60ea12e86 | |||
| e872bd44b0 | |||
| 51c8fdcb6c | |||
| aa4dadc22f | |||
| b1abbf9d61 | |||
| ea2b612e8d | |||
| 5945e818ca | |||
| f38fed3bee | |||
| 06948b3dbc | |||
| e951ab6c5e | |||
| 03c86a2b08 | |||
| 2cda1758c1 | |||
| 851f8ff9c9 | |||
| cbb6098dd8 | |||
| c90d694fff | |||
| 8d8cd15cba | |||
| 4f21623f92 | |||
| bf2548f891 | |||
| a188bd3960 | |||
| c91a12f0d1 | |||
| 286ffbb6c9 | |||
| ec3252c367 | |||
| a2ff03727d | |||
| af4b05e242 | |||
| 6509eaad35 | |||
| f9483fea8c | |||
| 7a3a7bfb43 | |||
| 8a0e1bdd97 | |||
| e3005aa723 | |||
| 45d3ba13ca | |||
| f05309437f | |||
| d4499f6990 | |||
| 7d87309f4b | |||
| 1d8c309da9 | |||
| 4d5961e4d5 | |||
| 59448e8e57 | |||
| 695de0db14 | |||
| dfd6fa68c3 | |||
| 86eb152f93 | |||
| 126966e110 | |||
| 5d8d47cd52 | |||
| 1843ca6244 | |||
| f9358d594c | |||
| a8aa422b03 | |||
| 9836f2c9c2 | |||
| 70606ac58c | |||
| 6348bdcdc7 | |||
| fd39384e78 | |||
| 924c96d555 | |||
| efeabd19d3 | |||
| a9b8a44bfc | |||
| daaa6aada7 | |||
| e795db7c69 | |||
| 977fac5fae | |||
| 36ff60d8d3 | |||
| 4769bde19c | |||
| e6021dd960 | |||
| 9d28ab3567 | |||
| df19aa081e | |||
| 2e9d0f607f | |||
| fb32a8fe0b | |||
| 3e57876361 | |||
| 7683fa7187 | |||
| fc893a62c3 | |||
| c01c04b83f | |||
| fc583a6e3c | |||
| ebfb0b5fc3 | |||
| 48a44916e7 | |||
| 5a81886a5e | |||
| 5e2cff5e5d | |||
| 5886edda42 | |||
| e4a17d7be6 | |||
| 96978c2747 | |||
| 77722a0ffd | |||
| ced16fda72 | |||
| f381986f7a | |||
| 02ad614657 | |||
| 6f6cd77587 | |||
| d8e7e7830e | |||
| f9c8039582 | |||
| f8011611a2 | |||
| 98e5c17858 | |||
| a71d3b5d99 | |||
| 51464bd7f8 | |||
| 76f2a34fe4 | |||
| 162c115fa5 | |||
| 57a626d157 | |||
| 716752e2fb |
@@ -1,3 +1,4 @@
|
|||||||
dist
|
dist
|
||||||
dkimpy_milter.egg-info
|
dkimpy_milter.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*~
|
||||||
|
|||||||
@@ -1,8 +1,126 @@
|
|||||||
|
1.1.4 2019-11-22
|
||||||
|
- Make error logging more explicit to aid debugging
|
||||||
|
- Delete own_socketfile to resolve race condition where the permissions
|
||||||
|
change fails on a Unix socket because it hasn't been created yet (libmilter
|
||||||
|
will do this correctly on its own based on umask, the milter doesn't need
|
||||||
|
to do it) (LP: #1849712)
|
||||||
|
|
||||||
0.9.1 UNRELEASED
|
1.1.3 2019-10-06
|
||||||
|
- Fix sysv init so it works (LP: #1839487)
|
||||||
|
|
||||||
|
1.1.2 2019-09-23
|
||||||
|
- Fix variable initialization so mailformed mails missing body From do not
|
||||||
|
cause a traceback (LP: #1844161)
|
||||||
|
- Catch more ascii encoding errors to improve resilience against bad data
|
||||||
|
(LP: #1844189)
|
||||||
|
|
||||||
|
1.1.1 2019-09-06
|
||||||
|
- Fix startup logging so it provides information at a useful time
|
||||||
|
- Fix verify processing so missing (optional) i= tag doesn't cause the milter
|
||||||
|
to fail (LP: #1842250)
|
||||||
|
- Fix message extraction so that signing in the same pass through the milter
|
||||||
|
as verifying works correctly
|
||||||
|
|
||||||
|
1.1.0 2019-04-12
|
||||||
|
- Add SubDomains option to enable signing for sub-domains (LP: #1811535)
|
||||||
|
- Port to python3 (LP: #1815502)
|
||||||
|
- Add test suite using opendkim miltertest
|
||||||
|
- When Socket is absolute path, do not strip leading /
|
||||||
|
- Handle unix: socket prefix the same as local:
|
||||||
|
- Set up correct AuthservID defaults
|
||||||
|
- config: Reassemble strings sensibly
|
||||||
|
- Consistently prefer dnspython to Py3DNS (LP: #1815558)
|
||||||
|
|
||||||
|
1.0.1 2019-02-11
|
||||||
|
- Reorder milter start and dropping privileges so permissions on Unix socket
|
||||||
|
are correct (LP: 1797720)
|
||||||
|
- Make domain checks case insensitive for determining if signing should be
|
||||||
|
done (LP: #1815311)
|
||||||
|
- Add additional Sendmail configuration information to README from OpenDKIM
|
||||||
|
update based on input from Дилян Палаузов (LP: #1801619)
|
||||||
|
- Add information on Ed25519 key creation to README (LP: #1815313)
|
||||||
|
|
||||||
|
1.0.0 2018-05-11
|
||||||
|
- Minor documentation updates
|
||||||
|
- Deleted reference to obsolete syslog target in unit file
|
||||||
|
|
||||||
|
0.9.7 2018-03-19
|
||||||
|
- Made sysv init executable
|
||||||
|
- Add missing documentation key to system/dkimpy-milter.service
|
||||||
|
- Put version directly in setup.py and do not import dkimpy_milter to ease
|
||||||
|
install via pip
|
||||||
|
- Minor sysv init improvments
|
||||||
|
|
||||||
|
0.9.6 2018-03-13
|
||||||
|
- Fixed typo in package installation section of README
|
||||||
|
- Added more to README about first run with systemd
|
||||||
|
- Fixed typo in path for fallback location of the config file if one is not
|
||||||
|
provided
|
||||||
|
- Added protection for malformed From addresses. If the From does not at
|
||||||
|
least have an '@' in the address, then the signing domain is not extracted
|
||||||
|
and the message will not be signed
|
||||||
|
|
||||||
|
0.9.5.1 2018-03-10
|
||||||
|
- Add conf file location to systemd unit file
|
||||||
|
- Fix setup.py install locations so they are installed correctly
|
||||||
|
|
||||||
|
0.9.5 2018-03-10
|
||||||
|
- Beta 1 (updated Alpha -> Beta warning in README and trove classifiers)
|
||||||
|
- Added support for MacroList option
|
||||||
|
- Added support for MacroListVerify option
|
||||||
|
- Added example in README to show use of MacroList* to separate inbound and
|
||||||
|
outbound mail streams
|
||||||
|
- Added support for SyslogSuccess option (both signing and verifying)
|
||||||
|
- Rationalized logging to be much less verbose unless SyslogSuccess or
|
||||||
|
debugLevel are set - default is generally start/stop/errors only
|
||||||
|
- Fixed install_requires so either dnspython (preferred if neither is
|
||||||
|
installed) or PyDNS satisfies the install requirements
|
||||||
|
- Updated Authentication Results result comment not to mention key size for
|
||||||
|
ed25519 signatures, since it's irrelevant
|
||||||
|
- Enhanced signature verification logging to provide more useful information
|
||||||
|
|
||||||
|
0.9.4 2018-03-09
|
||||||
|
- Create PID directory if it is missing
|
||||||
|
- Fix crash when verifying if domain for signing was not set
|
||||||
|
- Fix header folding to use \n only to align with milter protocol
|
||||||
|
requirements
|
||||||
|
- Added information about creating a dedicated user and PID file directory
|
||||||
|
creation to README
|
||||||
|
- Fixed a bug where dkim fail might be reported as pass when verifying
|
||||||
|
multiple signatures and a previous signature had passed
|
||||||
|
- Make RSA signatures in dkimpy-milter optional, so dkimpy-milter can be
|
||||||
|
added after an existing DKIM signing application to add an Ed25519
|
||||||
|
signature (Thanks to A. Schulze for the patch)
|
||||||
|
- Added support for AuthservID option
|
||||||
|
- Added support for InternalHosts option (ipaddress and either dns (dnspython)
|
||||||
|
or pydns (DNS) modules are now required)
|
||||||
|
- Added support for DiagnosticDirectory and updated dkimpy-milter specifics in
|
||||||
|
dkimpy-milter.conf.5
|
||||||
|
|
||||||
|
0.9.3 2018-03-02
|
||||||
|
- Fixup csl dataset processing for single item lists
|
||||||
|
- file: dataset support
|
||||||
|
- Bump minimum authres version to 1.1.0 due to known issues with 1.0.2
|
||||||
|
- Ignore errors parsing broken authres header fields
|
||||||
|
- Fold added authres header fields
|
||||||
|
- Fix pidfile permissions
|
||||||
|
- Fix socket setup sequence so Unix sockets work
|
||||||
|
|
||||||
|
0.9.2 2018-02-19
|
||||||
|
- Improved package requirements definition
|
||||||
|
- Added systemd unit file and (untested) sysv init file
|
||||||
|
- Added dkim-milter.8 (based on opendim.8)
|
||||||
|
- Implemented support for Canonicalization option
|
||||||
|
- Implemented support for SyslogFacility option
|
||||||
|
- Initial dataset support: csl
|
||||||
|
- Only sign if mail from from a domain in Domain and only if Mode is not
|
||||||
|
verfication only
|
||||||
|
|
||||||
|
0.9.1 2018-02-17
|
||||||
- DKIM signing and verification using both RSA and Ed25519
|
- DKIM signing and verification using both RSA and Ed25519
|
||||||
- The following configuration options are supported (same definition as
|
- The following configuration options are supported (same definition as
|
||||||
OpenDKIM): Domain, KeyFile, KeyFileEd25519, Mode, PidFile, Selector,
|
OpenDKIM): Domain, KeyFile, KeyFileEd25519, Mode, PidFile, Selector,
|
||||||
Socket, Syslog, UMask, and UserID (see dkimpy-milter.conf.5)
|
Socket, Syslog, UMask, and UserID (see dkimpy-milter.conf.5)
|
||||||
- This is an Alpha grade release and while the implemented features work, it
|
- This is an Alpha grade release and while the implemented features work, it
|
||||||
is nowhere near being a complete package
|
is nowhere near being a complete package
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
include etc/*
|
include etc/*
|
||||||
include man/*
|
include man/*
|
||||||
|
include system/*
|
||||||
include Authors.conf
|
include Authors.conf
|
||||||
include TODO
|
include TODO
|
||||||
include README
|
include README
|
||||||
|
|||||||
@@ -1,18 +1,242 @@
|
|||||||
This is a DKIM signing and verification milter. In theory it works with both
|
OVERVIEW
|
||||||
Postfix and Sendmail, but the author has zero experience with Sendmail, so
|
========
|
||||||
reports of success/failure with Sendmail and patches are welcom.
|
|
||||||
|
This is a DKIM signing and verification milter. It has been tested with both
|
||||||
|
Postfix and Sendmail.
|
||||||
|
|
||||||
The configuration file is designed to be compatible with OpenDKIM, but only
|
The configuration file is designed to be compatible with OpenDKIM, but only
|
||||||
a subset of OpenDKIM options are supported. If an unsupported option is
|
a subset of OpenDKIM options are supported. If an unsupported option is
|
||||||
specified, an error will be raised.
|
specified, an error will be raised.
|
||||||
|
|
||||||
|
|
||||||
|
INSTALLATION
|
||||||
|
===========
|
||||||
|
|
||||||
This package includes a default configuration file and man pages. For those
|
This package includes a default configuration file and man pages. For those
|
||||||
to be installed when installing using setup.py, the following incantation is
|
to be installed when installing using setup.py, the following incantation is
|
||||||
required because setuptools developers decided not being able to do this by
|
required because setuptools developers decided not being able to do this by
|
||||||
default is a feature:
|
default is a feature:
|
||||||
|
|
||||||
python setup.py install --single-version-externally-managed --record=/dev/null
|
python3 setup.py install --single-version-externally-managed --record=/dev/null
|
||||||
|
|
||||||
WARNING: This is an alpha grade release to support interoperability testing with
|
For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are
|
||||||
Ed25519 signatures and basic functionality. It is known to be incomplete and
|
available in either the main or backports repositories:
|
||||||
not suitable for general use.
|
|
||||||
|
[sudo] apt install python3-milter python3-nacl python3-dnspython
|
||||||
|
[sudo] apt install -t stretch-backports python3-authres python3-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. 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.
|
||||||
|
|
||||||
|
|
||||||
|
SETUP
|
||||||
|
====
|
||||||
|
|
||||||
|
SIGNING KEYS
|
||||||
|
============
|
||||||
|
|
||||||
|
In order to create DKIM signatures, a private key must be available. Signing
|
||||||
|
keys should be protected (owned by root:root with permissions 600 in a
|
||||||
|
directory that is not world readable). Different keys are required for RSA
|
||||||
|
and (if used) Ed25519.
|
||||||
|
|
||||||
|
RSA
|
||||||
|
===
|
||||||
|
|
||||||
|
Both public and private keys for RSA have standard formats and there are many
|
||||||
|
tools available to create them. Keys must (RFC 8302) have a minimum size of
|
||||||
|
1024 bits and should have a size of at least 2048 bits. The dknewkey script
|
||||||
|
that is provided with dkimpy is one such tool:
|
||||||
|
|
||||||
|
dknewkey exampleprivkey
|
||||||
|
|
||||||
|
will produce both the private key file (.key suffix) and a file with the DKIM
|
||||||
|
public key record to be published DNS (.dns suffix). RSA is the default key
|
||||||
|
type. 2048 bits is the default key size.
|
||||||
|
|
||||||
|
ED25519
|
||||||
|
=======
|
||||||
|
|
||||||
|
There is no standardized non-binary representation for Ed25519 private keys,
|
||||||
|
so in order to generate Ed25519 keys for dkimpy-milter, dkimpy specific tools
|
||||||
|
must be used to be compatible. The same dknewkey script support Ed25519:
|
||||||
|
|
||||||
|
dknewkey --ktype ed25519 anothernewkey
|
||||||
|
|
||||||
|
will provide both the private key file (.key suffix) and a file with the DKIM
|
||||||
|
public key record to be published DNS (.dns suffix). Ed25519 keys do not have
|
||||||
|
variable bit lengths.
|
||||||
|
|
||||||
|
MTA INTEGRATION
|
||||||
|
==============
|
||||||
|
|
||||||
|
Both a systemd unit file and a sysv init file are provided. Both make
|
||||||
|
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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -15,44 +15,69 @@ UMask implemented
|
|||||||
UserID implemented verified
|
UserID implemented verified
|
||||||
DKIM 'a' in AR implemented verified
|
DKIM 'a' in AR implemented verified
|
||||||
|
|
||||||
|
0.9.2 (Alpha)
|
||||||
|
dkimpy-milter.service implemented verified
|
||||||
|
sysv init implemented lightly tested
|
||||||
|
remove PidFile on stop implemented verified
|
||||||
|
dkimpy-milter.8 provided needs work
|
||||||
|
Basic dataset (csl) implemented verified
|
||||||
|
Sign based on Domain implemented verified
|
||||||
|
Canonicalization implemented verified
|
||||||
|
SyslogFacility implemented verified
|
||||||
|
|
||||||
|
0.9.3 (Alpha)
|
||||||
|
File dataset implemented verified
|
||||||
|
|
||||||
|
0.9.4 (Alpha)
|
||||||
|
AuthservID implemented verified
|
||||||
|
DiagnosticDirectory implemented verified
|
||||||
|
InternalHosts implemented verified
|
||||||
|
|
||||||
0.9.5 (Beta)
|
0.9.5 (Beta)
|
||||||
dkimpy-milter.8
|
MacroList implemented verified
|
||||||
dkimpy-milter.service
|
MacroListVerify implemented verified
|
||||||
remove PidFile on stop
|
SyslogSuccess implemented verified
|
||||||
AuthservID
|
|
||||||
Canonicalization
|
|
||||||
Diagnostics
|
|
||||||
DiagnosticDirectory
|
|
||||||
InternalHosts
|
|
||||||
SyslogFacility
|
|
||||||
SyslogSuccess
|
|
||||||
|
|
||||||
1.0.0
|
1.0.0
|
||||||
No additional features planned
|
No additional features
|
||||||
|
|
||||||
|
1.0.1
|
||||||
|
Bug fix only, improved documentation
|
||||||
|
|
||||||
|
1.1.0
|
||||||
|
Port to Python 3 implemented verified
|
||||||
|
Subdomain support implemented verified
|
||||||
|
Test suite implemented verified
|
||||||
|
|
||||||
|
Planned dataset type support (if needed):
|
||||||
|
mdb:
|
||||||
|
|
||||||
Considered for near-term feature release
|
Considered for near-term feature release
|
||||||
|
|
||||||
|
KeyTable
|
||||||
|
KeytableEd25519
|
||||||
|
SigningTable
|
||||||
|
SigningTableEd25519
|
||||||
AlwaysAddARHeader
|
AlwaysAddARHeader
|
||||||
ChangeRootDirectory
|
ChangeRootDirectory
|
||||||
ClockDrift (requires dkimpy change)
|
ClockDrift (requires dkimpy change)
|
||||||
DNSTimeout (requires dkmpy change)
|
DNSTimeout (requires dkimpy change)
|
||||||
MacroList
|
|
||||||
MilterDebug
|
MilterDebug
|
||||||
MinimumKeyBits
|
MinimumKeyBits
|
||||||
|
OversignHeaders (may require dkimpy changes)
|
||||||
PeerList
|
PeerList
|
||||||
SignatureAlgorithm
|
SignatureAlgorithm
|
||||||
|
|
||||||
Later
|
Later
|
||||||
|
|
||||||
BaseDirectory
|
BaseDirectory
|
||||||
|
Diagnostics (requires dkimpy changes)
|
||||||
DontSignMailTo
|
DontSignMailTo
|
||||||
ExemptDomains
|
ExemptDomains
|
||||||
ExternalIgnoreList
|
ExternalIgnoreList
|
||||||
FixCRLF
|
FixCRLF
|
||||||
KeepAuthResults
|
KeepAuthResults
|
||||||
KeepTemporaryFiles
|
KeepTemporaryFiles
|
||||||
KeyTable
|
|
||||||
KeytableEd25519
|
|
||||||
LogResults
|
LogResults
|
||||||
LogWhy
|
LogWhy
|
||||||
MaximumHeaders
|
MaximumHeaders
|
||||||
@@ -68,7 +93,6 @@ On-InternalError
|
|||||||
On-KeyNotFound
|
On-KeyNotFound
|
||||||
On-NoSignature
|
On-NoSignature
|
||||||
On-SignatureError
|
On-SignatureError
|
||||||
OversignHeaders
|
|
||||||
RemoveARAll
|
RemoveARAll
|
||||||
RemoveARFrom
|
RemoveARFrom
|
||||||
RemoveOldSignatures
|
RemoveOldSignatures
|
||||||
@@ -77,7 +101,6 @@ RequireSafeKeys
|
|||||||
SignatureAlgorithm
|
SignatureAlgorithm
|
||||||
SignatureTTL
|
SignatureTTL
|
||||||
SignHeaders
|
SignHeaders
|
||||||
SigningTable
|
|
||||||
SoftwareHeader
|
SoftwareHeader
|
||||||
StrictHeaders
|
StrictHeaders
|
||||||
SubDomains
|
SubDomains
|
||||||
|
|||||||
+220
-81
@@ -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.
|
||||||
@@ -25,23 +25,24 @@ import sys
|
|||||||
import syslog
|
import syslog
|
||||||
import Milter
|
import Milter
|
||||||
import dkim
|
import dkim
|
||||||
from dkim.dnsplug import get_txt
|
|
||||||
from dkim.util import parse_tag_value
|
|
||||||
import authres
|
import authres
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import StringIO
|
import io
|
||||||
import re
|
import re
|
||||||
from Milter.config import MilterConfigParser
|
import codecs
|
||||||
from Milter.utils import iniplist,parse_addr,parseaddr
|
from Milter.utils import parse_addr, parseaddr
|
||||||
import dkimpy_milter.config as config
|
import dkimpy_milter.config as config
|
||||||
from dkimpy_milter.util import drop_privileges
|
from dkimpy_milter.util import drop_privileges
|
||||||
from dkimpy_milter.util import setExceptHook
|
from dkimpy_milter.util import setExceptHook
|
||||||
from dkimpy_milter.util import write_pid
|
from dkimpy_milter.util import write_pid
|
||||||
from dkimpy_milter.util import read_keyfile
|
from dkimpy_milter.util import read_keyfile
|
||||||
|
from dkimpy_milter.util import fold
|
||||||
|
|
||||||
|
__version__ = "1.0.1"
|
||||||
FWS = re.compile(r'\r?\n[ \t]+')
|
FWS = re.compile(r'\r?\n[ \t]+')
|
||||||
|
|
||||||
|
|
||||||
class dkimMilter(Milter.Base):
|
class dkimMilter(Milter.Base):
|
||||||
"Milter to check and sign DKIM. Each connection gets its own instance."
|
"Milter to check and sign DKIM. Each connection gets its own instance."
|
||||||
|
|
||||||
@@ -53,25 +54,54 @@ class dkimMilter(Milter.Base):
|
|||||||
self.privatersa = privateRSA
|
self.privatersa = privateRSA
|
||||||
self.privateed25519 = privateEd25519
|
self.privateed25519 = privateEd25519
|
||||||
self.fp = None
|
self.fp = None
|
||||||
|
self.fdomain = ''
|
||||||
|
|
||||||
@Milter.noreply
|
@Milter.noreply
|
||||||
def connect(self, hostname, unused, hostaddr):
|
def connect(self, hostname, unused, hostaddr):
|
||||||
self.internal_connection = False
|
self.internal_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:
|
||||||
|
self.AuthservID = milterconfig['AuthservID']
|
||||||
|
except:
|
||||||
|
self.AuthservID = self.receiver
|
||||||
if hostaddr and len(hostaddr) > 0:
|
if hostaddr and len(hostaddr) > 0:
|
||||||
ipaddr = hostaddr[0]
|
ipaddr = hostaddr[0]
|
||||||
"""if iniplist(ipaddr,self.conf.internal_connect): FIXME
|
if milterconfig['IntHosts']:
|
||||||
self.internal_connection = True"""
|
if milterconfig['IntHosts'].match(ipaddr):
|
||||||
else: ipaddr = ''
|
self.internal_connection = True
|
||||||
|
else:
|
||||||
|
ipaddr = ''
|
||||||
self.connectip = ipaddr
|
self.connectip = ipaddr
|
||||||
|
if milterconfig.get('MacroList') and not self.internal_connection:
|
||||||
|
macrolist = milterconfig.get('MacroList')
|
||||||
|
for macro in macrolist:
|
||||||
|
macroname = macro.split('|')[0]
|
||||||
|
macroname = '{' + macroname + '}'
|
||||||
|
macroresult = self.getsymval(macroname)
|
||||||
|
if ((len(macro.split('|')) == 1 and macroresult) or macroresult
|
||||||
|
in macro.split('|')[1:]):
|
||||||
|
self.internal_connection = True
|
||||||
|
if milterconfig.get('MacroListVerify'):
|
||||||
|
macrolist = milterconfig.get('MacroListVerify')
|
||||||
|
for macro in macrolist:
|
||||||
|
macroname = macro.split('|')[0]
|
||||||
|
macroname = '{' + macroname + '}'
|
||||||
|
macroresult = self.getsymval(macroname)
|
||||||
|
if ((len(macro.split('|')) == 1 and macroresult) or macroresult
|
||||||
|
in macro.split('|')[1:]):
|
||||||
|
self.external_connection = True
|
||||||
if self.internal_connection:
|
if self.internal_connection:
|
||||||
connecttype = 'INTERNAL'
|
connecttype = 'INTERNAL'
|
||||||
else:
|
else:
|
||||||
connecttype = 'EXTERNAL'
|
connecttype = 'EXTERNAL'
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 1:
|
||||||
syslog.syslog("connect from {0} at {1} {2}".format(hostname,hostaddr,connecttype))
|
syslog.syslog("connect from {0} at {1} {2}"
|
||||||
|
.format(hostname, hostaddr, connecttype))
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
# multiple messages can be received on a single connection
|
# multiple messages can be received on a single connection
|
||||||
@@ -79,57 +109,51 @@ 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'):
|
if milterconfig.get('Syslog') and milterconfig.get('debugLevel') >= 2:
|
||||||
syslog.syslog("mail from: {0} {1}".format(f, str))
|
syslog.syslog("mail from: {0} {1}".format(f, str))
|
||||||
self.fp = StringIO.StringIO()
|
self.fp = io.BytesIO()
|
||||||
self.mailfrom = f
|
self.mailfrom = f
|
||||||
t = parse_addr(f)
|
t = parse_addr(f)
|
||||||
if len(t) == 2: t[1] = t[1].lower()
|
if len(t) == 2:
|
||||||
|
t[1] = t[1].lower()
|
||||||
self.canon_from = '@'.join(t)
|
self.canon_from = '@'.join(t)
|
||||||
self.user = self.getsymval('{auth_authen}')
|
|
||||||
self.has_dkim = 0
|
self.has_dkim = 0
|
||||||
self.author = None
|
self.author = None
|
||||||
self.arheaders = []
|
self.arheaders = []
|
||||||
self.arresults = []
|
self.arresults = []
|
||||||
'''if self.user:
|
|
||||||
# Very simple SMTP AUTH policy by default:
|
|
||||||
# any successful authentication is considered INTERNAL
|
|
||||||
self.internal_connection = True
|
|
||||||
auth_type = self.getsymval('{auth_type}')
|
|
||||||
ssl_bits = self.getsymval('{cipher_bits}')
|
|
||||||
if milterconfig.get('Syslog'):
|
|
||||||
syslog.syslog(
|
|
||||||
"SMTP AUTH:",self.user,"sslbits =",ssl_bits, auth_type,
|
|
||||||
"ssf =",self.getsymval('{auth_ssf}'), "INTERNAL"
|
|
||||||
)
|
|
||||||
# Detailed authorization policy is configured in the access file below.
|
|
||||||
self.arresults.append(
|
|
||||||
authres.SMTPAUTHAuthenticationResult(result = 'pass',
|
|
||||||
result_comment = auth_type+' sslbits='+ssl_bits, smtp_auth = self.user)
|
|
||||||
)'''
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
@Milter.noreply
|
@Milter.noreply
|
||||||
def header(self, name, val):
|
def header(self, name, val):
|
||||||
lname = name.lower()
|
lname = name.lower()
|
||||||
if lname == 'dkim-signature':
|
if lname == 'dkim-signature':
|
||||||
if milterconfig.get('Syslog'):
|
if (milterconfig.get('Syslog') and
|
||||||
|
milterconfig.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)
|
||||||
if milterconfig.get('Syslog'):
|
try:
|
||||||
|
self.fdomain = self.author.split('@')[1].lower()
|
||||||
|
except IndexError as er:
|
||||||
|
pass # self.author was not a proper email address
|
||||||
|
if (milterconfig.get('Syslog') and
|
||||||
|
milterconfig.get('debugLevel') >= 1):
|
||||||
syslog.syslog("{0}: {1}".format(name, val))
|
syslog.syslog("{0}: {1}".format(name, val))
|
||||||
elif lname == 'authentication-results':
|
elif lname == 'authentication-results':
|
||||||
self.arheaders.append(val)
|
self.arheaders.append(val)
|
||||||
if self.fp:
|
if self.fp:
|
||||||
self.fp.write("%s: %s\n" % (name,val))
|
try:
|
||||||
|
self.fp.write(b"%s: %s\n" % (codecs.encode(name, 'ascii'), codecs.encode(val, 'ascii')))
|
||||||
|
except:
|
||||||
|
# Don't choke on header fields with non-ascii garbage in them.
|
||||||
|
pass
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
@Milter.noreply
|
@Milter.noreply
|
||||||
def eoh(self):
|
def eoh(self):
|
||||||
if self.fp:
|
if self.fp:
|
||||||
self.fp.write("\n") # terminate headers
|
self.fp.write(b"\n") # terminate headers
|
||||||
self.bodysize = 0
|
self.bodysize = 0
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
@@ -146,45 +170,90 @@ class dkimMilter(Milter.Base):
|
|||||||
# Remove existing Authentication-Results headers for our authserv_id
|
# Remove existing Authentication-Results headers for our authserv_id
|
||||||
for i, val in enumerate(self.arheaders, 1):
|
for i, val in enumerate(self.arheaders, 1):
|
||||||
# FIXME: don't delete A-R headers from trusted MTAs
|
# FIXME: don't delete A-R headers from trusted MTAs
|
||||||
ar = authres.AuthenticationResultsHeader.parse_value(FWS.sub('',val))
|
try:
|
||||||
if ar.authserv_id == self.receiver:
|
ar = (authres.AuthenticationResultsHeader
|
||||||
|
.parse_value(FWS.sub('', val)))
|
||||||
|
if ar.authserv_id == self.AuthservID:
|
||||||
self.chgheader('authentication-results', i, '')
|
self.chgheader('authentication-results', i, '')
|
||||||
if milterconfig.get('Syslog'):
|
if (milterconfig.get('Syslog') and
|
||||||
|
milterconfig.get('debugLevel') >= 1):
|
||||||
syslog.syslog('REMOVE: {0}'.format(val))
|
syslog.syslog('REMOVE: {0}'.format(val))
|
||||||
# Check or sign DKIM
|
except:
|
||||||
|
# Don't error out on unparseable AR header fiels
|
||||||
|
pass
|
||||||
|
# Check and/or sign DKIM
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
if self.internal_connection or milterconfig.get('Mode') == 's' or milterconfig.get('Mode') == 'sv':
|
|
||||||
txt = self.fp.read()
|
txt = self.fp.read()
|
||||||
self.sign_dkim(txt)
|
if milterconfig.get('Domain'):
|
||||||
result = None
|
domain = milterconfig.get('Domain')
|
||||||
if self.has_dkim and (milterconfig.get('Mode') == 'v' or milterconfig.get('Mode') == 'sv'):
|
|
||||||
txt = self.fp.read()
|
|
||||||
self.check_dkim(txt)
|
|
||||||
else:
|
else:
|
||||||
result = 'none'
|
domain = ''
|
||||||
|
if milterconfig.get('SubDomains'):
|
||||||
|
self.fdomain = _get_parent_domain(self.fdomain, domain)
|
||||||
|
if ((self.fdomain in domain) and not milterconfig.get('Mode') == 'v'
|
||||||
|
and not self.external_connection):
|
||||||
|
self.sign_dkim(txt)
|
||||||
|
if ((self.has_dkim) and (not self.internal_connection) and
|
||||||
|
(milterconfig.get('Mode') == 'v' or
|
||||||
|
milterconfig.get('Mode') == 'sv')):
|
||||||
|
self.check_dkim(txt)
|
||||||
if self.arresults:
|
if self.arresults:
|
||||||
h = authres.AuthenticationResultsHeader(authserv_id = self.receiver,
|
h = authres.AuthenticationResultsHeader(authserv_id=
|
||||||
|
self.AuthservID,
|
||||||
results=self.arresults)
|
results=self.arresults)
|
||||||
if milterconfig.get('Syslog'):
|
h = fold(codecs.encode(str(h), 'ascii'))
|
||||||
syslog.syslog(str(h))
|
if (milterconfig.get('Syslog') and
|
||||||
name,val = str(h).split(': ',1)
|
milterconfig.get('debugLevel') >= 2):
|
||||||
|
syslog.syslog(codecs.decode(h, 'ascii'))
|
||||||
|
name, val = codecs.decode(h, 'ascii').split(': ', 1)
|
||||||
self.addheader(name, val, 0)
|
self.addheader(name, val, 0)
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def sign_dkim(self, txt):
|
def sign_dkim(self, txt):
|
||||||
conf = self.conf
|
canon = codecs.encode(milterconfig.get('Canonicalization'), 'ascii')
|
||||||
|
canonicalize = []
|
||||||
|
if len(canon.split(b'/')) == 2:
|
||||||
|
canonicalize.append(canon.split(b'/')[0])
|
||||||
|
canonicalize.append(canon.split(b'/')[1])
|
||||||
|
else:
|
||||||
|
canonicalize.append(canon)
|
||||||
|
canonicalize.append(canon)
|
||||||
|
if (milterconfig.get('Syslog') and
|
||||||
|
milterconfig.get('debugLevel') >= 1):
|
||||||
|
syslog.syslog('canonicalize: {0}'.format(canonicalize))
|
||||||
try:
|
try:
|
||||||
|
if privateRSA:
|
||||||
d = dkim.DKIM(txt)
|
d = dkim.DKIM(txt)
|
||||||
h = d.sign(milterconfig.get('Selector'),milterconfig.get('Domain'), privateRSA,
|
h = d.sign(codecs.encode(milterconfig.get('Selector'), 'ascii'), codecs.encode(self.fdomain, 'ascii'),
|
||||||
canonicalize=('relaxed','simple'))
|
codecs.encode(privateRSA, 'ascii'),
|
||||||
name,val = h.split(': ',1)
|
canonicalize=(canonicalize[0],
|
||||||
self.addheader(name,val.strip().replace('\r\n','\n'),0)
|
canonicalize[1]))
|
||||||
|
name, val = h.split(b': ', 1)
|
||||||
|
self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
|
||||||
|
if (milterconfig.get('Syslog') and
|
||||||
|
(milterconfig.get('SyslogSuccess')
|
||||||
|
or milterconfig.get('debugLevel') >= 1)):
|
||||||
|
syslog.syslog('{0}: {1} DKIM signature added (s={2} '
|
||||||
|
'd={3})'.format(self.getsymval('i'),
|
||||||
|
d.signature_fields.get(b'a').decode(),
|
||||||
|
d.signature_fields.get(b's').decode(),
|
||||||
|
d.domain.decode().lower()))
|
||||||
if privateEd25519:
|
if privateEd25519:
|
||||||
d = dkim.DKIM(txt)
|
d = dkim.DKIM(txt)
|
||||||
h = d.sign(milterconfig.get('SelectorEd25519'),milterconfig.get('Domain'), privateEd25519,
|
h = d.sign(codecs.encode(milterconfig.get('SelectorEd25519'), 'ascii'), codecs.encode(self.fdomain, 'ascii'),
|
||||||
canonicalize=('relaxed','simple'), signature_algorithm='ed25519-sha256')
|
privateEd25519, canonicalize=(canonicalize[0],
|
||||||
name,val = h.split(': ',1)
|
canonicalize[1]),
|
||||||
self.addheader(name,val.strip().replace('\r\n','\n'),0)
|
signature_algorithm=b'ed25519-sha256')
|
||||||
|
name, val = h.split(b': ', 1)
|
||||||
|
self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0)
|
||||||
|
if (milterconfig.get('Syslog') and
|
||||||
|
(milterconfig.get('SyslogSuccess')
|
||||||
|
or milterconfig.get('debugLevel') >= 1)):
|
||||||
|
syslog.syslog('{0}: {1} DKIM signature added (s={2} '
|
||||||
|
'd={3})'.format(self.getsymval('i'),
|
||||||
|
d.signature_fields.get(b'a').decode(),
|
||||||
|
d.signature_fields.get(b's').decode(),
|
||||||
|
d.domain.decode().lower()))
|
||||||
except dkim.DKIMException as x:
|
except dkim.DKIMException as x:
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog('DKIM: {0}'.format(x))
|
syslog.syslog('DKIM: {0}'.format(x))
|
||||||
@@ -195,15 +264,27 @@ class dkimMilter(Milter.Base):
|
|||||||
|
|
||||||
def check_dkim(self, txt):
|
def check_dkim(self, txt):
|
||||||
res = False
|
res = False
|
||||||
conf = self.conf
|
self.header_a = None
|
||||||
for y in range(self.has_dkim): # Verify _ALL_ the signatures
|
for y in range(self.has_dkim): # Verify _ALL_ the signatures
|
||||||
d = dkim.DKIM(txt)
|
d = dkim.DKIM(txt)
|
||||||
try:
|
try:
|
||||||
res = d.verify(idx=y)
|
dnsoverride = milterconfig.get('DNSOverride')
|
||||||
if res:
|
if isinstance(dnsoverride, str):
|
||||||
self.dkim_comment = 'Good {0} bit {1} signature.'.format(d.keysize, d.signature_fields.get(b'a'))
|
syslog.syslog("DNSOverride: {0}".format(dnsoverride))
|
||||||
|
res = d.verify(idx=y, dnsfunc=lambda _x: dnsoverride)
|
||||||
else:
|
else:
|
||||||
self.dkim_comment = 'Bad {0} bit {1} signature.'.format(d.keysize, d.signature_fields.get(b'a'))
|
res = d.verify(idx=y)
|
||||||
|
algo = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
|
||||||
|
if res:
|
||||||
|
if algo == 'ed25519-sha256':
|
||||||
|
self.dkim_comment = ('Good {0} signature'
|
||||||
|
.format(algo))
|
||||||
|
else:
|
||||||
|
self.dkim_comment = ('Good {0} bit {1} signature'
|
||||||
|
.format(d.keysize, algo))
|
||||||
|
else:
|
||||||
|
self.dkim_comment = ('Bad {0} bit {1} signature.'
|
||||||
|
.format(d.keysize, algo))
|
||||||
except dkim.DKIMException as x:
|
except dkim.DKIMException as x:
|
||||||
self.dkim_comment = str(x)
|
self.dkim_comment = str(x)
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
@@ -211,31 +292,75 @@ class dkimMilter(Milter.Base):
|
|||||||
except Exception as x:
|
except Exception as x:
|
||||||
self.dkim_comment = str(x)
|
self.dkim_comment = str(x)
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog("check_dkim: {0}".format(x))
|
syslog.syslog("check_dkim: Internal program fault while verifying: {0}".format(x))
|
||||||
self.header_i = d.signature_fields.get(b'i')
|
try:
|
||||||
self.header_d = d.signature_fields.get(b'd')
|
# i= is optional and dkimpy is fine if it's not provided
|
||||||
self.header_a = d.signature_fields.get(b'a')
|
self.header_i = codecs.decode(d.signature_fields.get(b'i'), 'ascii')
|
||||||
if res:
|
except TypeError as x:
|
||||||
|
self.header_i = None
|
||||||
|
try:
|
||||||
|
self.header_d = codecs.decode(d.signature_fields.get(b'd'), 'ascii')
|
||||||
|
self.header_a = codecs.decode(d.signature_fields.get(b'a'), 'ascii')
|
||||||
|
except Exception as x:
|
||||||
|
self.dkim_comment = str(x)
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog('DKIM: Pass ({0})'.format(d.domain))
|
syslog.syslog("check_dkim: Internal proram fuault extracting header a or d: {0}".format(x))
|
||||||
self.dkim_domain = d.domain
|
self.header_d = None
|
||||||
|
if not self.header_a:
|
||||||
|
self.header_a = 'rsa-sha256'
|
||||||
|
if res:
|
||||||
|
if (milterconfig.get('Syslog') and
|
||||||
|
(milterconfig.get('SyslogSuccess') or
|
||||||
|
milterconfig.get('debugLevel') >= 1)):
|
||||||
|
syslog.syslog('{0}: {1} DKIM signature verified (s={2} '
|
||||||
|
'd={3})'.format(self.getsymval('i'),
|
||||||
|
d.signature_fields.get(b'a').decode(),
|
||||||
|
d.signature_fields.get(b's').decode(),
|
||||||
|
d.domain.decode().lower()))
|
||||||
|
self.dkim_domain = d.domain.lower()
|
||||||
else:
|
else:
|
||||||
|
if milterconfig.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 milterconfig.get('Syslog'):
|
||||||
syslog.syslog('DKIM: Fail (saved as {0})'.format(fname))
|
syslog.syslog('DKIM: Fail (saved as {0})'
|
||||||
|
.format(fname))
|
||||||
|
else:
|
||||||
|
if milterconfig.get('Syslog'):
|
||||||
|
if d.domain:
|
||||||
|
syslog.syslog('DKIM: Fail ({0})'
|
||||||
|
.format(d.domain.lower()))
|
||||||
|
else:
|
||||||
|
syslog.syslog('DKIM: Fail, unextractable domain')
|
||||||
if res:
|
if res:
|
||||||
result = 'pass'
|
result = 'pass'
|
||||||
else:
|
else:
|
||||||
result = 'fail'
|
result = 'fail'
|
||||||
|
res = False
|
||||||
|
if self.header_d:
|
||||||
self.arresults.append(
|
self.arresults.append(
|
||||||
authres.DKIMAuthenticationResult(result=result,
|
authres.DKIMAuthenticationResult(result=result,
|
||||||
header_i = self.header_i, header_d = self.header_d, header_a = self.header_a,
|
header_i=self.header_i,
|
||||||
result_comment = self.dkim_comment)
|
header_d=self.header_d,
|
||||||
|
header_a=self.header_a,
|
||||||
|
result_comment=
|
||||||
|
self.dkim_comment)
|
||||||
)
|
)
|
||||||
|
self.header_a = None
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# get parent domain to be signed for if fdomain is a subdomain
|
||||||
|
def _get_parent_domain(fdomain, domains):
|
||||||
|
for domain in domains:
|
||||||
|
rhs = '.'+domain
|
||||||
|
# compare right hand side of fdomain against .domain
|
||||||
|
if fdomain[-len(rhs):] == rhs:
|
||||||
|
# return parent domain on match
|
||||||
|
return domain
|
||||||
|
# or return the fdomain itself
|
||||||
|
return fdomain
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Ugh, but there's no easy way around this.
|
# Ugh, but there's no easy way around this.
|
||||||
global milterconfig
|
global milterconfig
|
||||||
@@ -243,7 +368,7 @@ def main():
|
|||||||
global privateEd25519
|
global privateEd25519
|
||||||
privateRSA = False
|
privateRSA = False
|
||||||
privateEd25519 = False
|
privateEd25519 = False
|
||||||
configFile = '/etc/dkimpy-milter.conf'
|
configFile = '/usr/local/etc/dkimpy-milter.conf'
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
if sys.argv[1] in ('-?', '--help', '-h'):
|
if sys.argv[1] in ('-?', '--help', '-h'):
|
||||||
print('usage: dkimpy-milter [<configfilename>]')
|
print('usage: dkimpy-milter [<configfilename>]')
|
||||||
@@ -251,21 +376,35 @@ def main():
|
|||||||
configFile = sys.argv[1]
|
configFile = sys.argv[1]
|
||||||
milterconfig = config._processConfigFile(filename=configFile)
|
milterconfig = config._processConfigFile(filename=configFile)
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, syslog.LOG_MAIL)
|
facility = eval("syslog.LOG_{0}"
|
||||||
|
.format(milterconfig.get('SyslogFacility').upper()))
|
||||||
|
syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility)
|
||||||
setExceptHook()
|
setExceptHook()
|
||||||
write_pid(milterconfig)
|
pid = write_pid(milterconfig)
|
||||||
if milterconfig.get('KeyFile'):
|
if milterconfig.get('KeyFile'):
|
||||||
privateRSA = read_keyfile(milterconfig, 'RSA')
|
privateRSA = read_keyfile(milterconfig, 'RSA')
|
||||||
if milterconfig.get('KeyFileEd25519'):
|
if milterconfig.get('KeyFileEd25519'):
|
||||||
privateEd25519 = read_keyfile(milterconfig, 'Ed25519')
|
privateEd25519 = read_keyfile(milterconfig, 'Ed25519')
|
||||||
drop_privileges(milterconfig)
|
|
||||||
if milterconfig.get('Syslog'):
|
|
||||||
syslog.syslog('dkimpy-milter started. user: {0}'.format(milterconfig.get('UserID')))
|
|
||||||
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 socketname is None:
|
||||||
|
if int(os.environ.get('LISTEN_PID', '0')) == os.getpid():
|
||||||
|
lfds = os.environ.get('LISTEN_FDS')
|
||||||
|
if lfds is not None:
|
||||||
|
if lfds != '1':
|
||||||
|
syslog.syslog('LISTEN_FDS is set to "{0}", but we only know how to deal with "1", ignoring it'.
|
||||||
|
format(lfds))
|
||||||
|
else:
|
||||||
|
socketname = 'fd:3'
|
||||||
|
if socketname is None:
|
||||||
|
socketname = 'local:/var/run/dkimpy-milter/dkimpy-milter.sock'
|
||||||
|
drop_privileges(milterconfig)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
if milterconfig.get('Syslog'):
|
||||||
|
syslog.syslog('dkimpy-milter starting:{0} user:{1}'
|
||||||
|
.format(pid, milterconfig.get('UserID')))
|
||||||
Milter.runmilter(miltername, socketname, 240)
|
Milter.runmilter(miltername, socketname, 240)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from dkimpy_milter import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+290
-35
@@ -27,42 +27,212 @@
|
|||||||
import syslog
|
import syslog
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
|
||||||
import urllib
|
|
||||||
import stat
|
import stat
|
||||||
import dkim
|
import dkim
|
||||||
|
import socket
|
||||||
|
import ipaddress
|
||||||
|
from .dnsplug import Session
|
||||||
|
|
||||||
# default values
|
# default values
|
||||||
defaultConfigData = {
|
defaultConfigData = {
|
||||||
'Syslog': 'yes',
|
'Syslog': 'yes',
|
||||||
'SyslogFacility': 'mail',
|
'SyslogFacility': 'mail',
|
||||||
'UMask' : 007,
|
'UMask': 0o07,
|
||||||
'Mode': 'sv',
|
'Mode': 'sv',
|
||||||
'Socket' : 'local:/var/run/dkimpy-milter/dkimpy-milter.sock',
|
'Socket': None,
|
||||||
'PidFile' : '/var/run/dkimpy-milter/dkimpy-milter.pid',
|
'PidFile': None,
|
||||||
'UserID': 'dkimpy-milter',
|
'UserID': 'dkimpy-milter',
|
||||||
'Canonicalization' : 'simple'
|
'Canonicalization': 'relaxed/simple',
|
||||||
|
'InternalHosts': '127.0.0.1',
|
||||||
|
'IntHosts': False,
|
||||||
|
'DiagnosticDirectory': '',
|
||||||
|
'MacroList': '',
|
||||||
|
'MacroListVerify': '',
|
||||||
|
'DNSOverride': None,
|
||||||
|
'SubDomains': False,
|
||||||
|
'debugLevel': 0 # Undocumented config item for developer use
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#################################
|
|
||||||
class ConfigException(Exception):
|
class ConfigException(Exception):
|
||||||
'''Exception raised when there's a configuration file error.'''
|
'''Exception raised when there's a configuration file error.'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
####################################################################
|
|
||||||
|
class HostsDataset(object):
|
||||||
|
'''Hold a group of host related dataset objects'''
|
||||||
|
|
||||||
|
def __init__(self, dataset):
|
||||||
|
self.dataset = []
|
||||||
|
# Self.dataset will end up being a list of DataSetItem(s).
|
||||||
|
for item in dataset:
|
||||||
|
item = item.rstrip(']')
|
||||||
|
item = item.lstrip('[')
|
||||||
|
self.dataset.append(self.DatasetItem(item))
|
||||||
|
|
||||||
|
class DatasetItem(object):
|
||||||
|
'''Individual dataset item'''
|
||||||
|
|
||||||
|
def __init__(self, item):
|
||||||
|
self.item = item
|
||||||
|
self.isipv4 = False
|
||||||
|
self.isipv4cidr = False
|
||||||
|
self.isipv6 = False
|
||||||
|
self.isipv6cidr = False
|
||||||
|
self.ishostname = False
|
||||||
|
self.isdomain = False
|
||||||
|
self.negative = False
|
||||||
|
if self.item[0] == '!':
|
||||||
|
self.item = item[1:]
|
||||||
|
self.negative = True
|
||||||
|
try:
|
||||||
|
self.item = ipaddress.ip_address(str(self.item, "utf-8"))
|
||||||
|
if isinstance(self.item, ipaddress.IPv4Address):
|
||||||
|
self.isipv4 = True
|
||||||
|
elif isinstance(self.item, ipaddress.IPv6Address):
|
||||||
|
self.isipv6 = True
|
||||||
|
except ValueError as e:
|
||||||
|
try:
|
||||||
|
self.item = ipaddress.ip_network(str
|
||||||
|
(self.item, "utf-8"),
|
||||||
|
strict=False)
|
||||||
|
if isinstance(self.item, ipaddress.IPv4Network):
|
||||||
|
self.isipv4cidr = True
|
||||||
|
elif isinstance(self.item, ipaddress.IPv6Network):
|
||||||
|
self.isipv6cidr = True
|
||||||
|
except ValueError as e2:
|
||||||
|
if self.item[0] == '.' and len(self.item.split('.')) > 2:
|
||||||
|
self.isdomain = True
|
||||||
|
elif len(self.item.split('.')) > 1: # It has a '.' in it
|
||||||
|
self.ishostname = True
|
||||||
|
else:
|
||||||
|
raise ConfigException('Unknown dataset item: {0}'
|
||||||
|
.format(item))
|
||||||
|
|
||||||
|
def match(self, connectip):
|
||||||
|
'''Check if the connect IP is part of the dataset'''
|
||||||
|
source = ipaddress.ip_address(str(connectip, "utf-8"))
|
||||||
|
for item in self.dataset:
|
||||||
|
if item.isdomain or item.ishostname:
|
||||||
|
result = self.matchname(source) # Match host/domains first
|
||||||
|
if result:
|
||||||
|
return(result)
|
||||||
|
elif item.isipv4 or item.isipv4cidr: # Then IPv4/6 addresses or
|
||||||
|
if isinstance(source, ipaddress.IPv4Address): # networks
|
||||||
|
return(self.match4(source)) # depending on the item type
|
||||||
|
elif item.isipv6 or item.isipv6cidr: # and connect type
|
||||||
|
if isinstance(source, ipaddress.IPv6Address):
|
||||||
|
return(self.match6(source))
|
||||||
|
|
||||||
|
def matchname(self, source):
|
||||||
|
'''Does source IP address relate to a domain/hostname in the dataset'''
|
||||||
|
match = False
|
||||||
|
matchone = False
|
||||||
|
negativeone = False
|
||||||
|
matchdomain = False
|
||||||
|
negativedomain = False
|
||||||
|
ptrlist = self.getptr(source)
|
||||||
|
for item in self.dataset:
|
||||||
|
if item.isdomain:
|
||||||
|
for ptr in ptrlist:
|
||||||
|
# Strip the leading '.' off the domain name for exact match
|
||||||
|
if item.item[1:] == ptr[-len(item.item)+1:]:
|
||||||
|
matchdomain = True
|
||||||
|
negativedomain = item.negative
|
||||||
|
elif item.ishostname:
|
||||||
|
for ptr in ptrlist:
|
||||||
|
if item.item == ptr:
|
||||||
|
matchone = True
|
||||||
|
negativeone = item.negative
|
||||||
|
if matchdomain and not negativedomain:
|
||||||
|
match = True
|
||||||
|
if matchone and not negativeone:
|
||||||
|
return True
|
||||||
|
if matchone and negativeone:
|
||||||
|
match = False
|
||||||
|
return(match)
|
||||||
|
|
||||||
|
def getptr(self, source):
|
||||||
|
'''Get validated PTR name of IP address'''
|
||||||
|
results = []
|
||||||
|
s = Session()
|
||||||
|
ptrnames = s.dns(source.reverse_pointer, 'PTR')
|
||||||
|
for name in ptrnames:
|
||||||
|
if isinstance(source, ipaddress.IPv4Address):
|
||||||
|
ips = s.dns(name, 'A')
|
||||||
|
for ip in ips:
|
||||||
|
ip = ipaddress.IPv4Address(str(ip, 'UTF-8'))
|
||||||
|
if ip == source:
|
||||||
|
results.append(name)
|
||||||
|
if isinstance(source, ipaddress.IPv6Address):
|
||||||
|
ips = s.dns(name, 'AAAA')
|
||||||
|
for ip in ips:
|
||||||
|
ip = ipaddress.IPv6Address(str(ip, 'UTF-8'))
|
||||||
|
if ip == source:
|
||||||
|
results.append(name)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def match4(self, source):
|
||||||
|
'''Is the source IP related to a IPv4 address/network in the dataset'''
|
||||||
|
match = False
|
||||||
|
matchone = False
|
||||||
|
negativeone = False
|
||||||
|
matchcidr = False
|
||||||
|
negativecidr = False
|
||||||
|
for item in self.dataset:
|
||||||
|
if item.isipv4:
|
||||||
|
if source == item.item:
|
||||||
|
matchone = True
|
||||||
|
negativeone = item.negative
|
||||||
|
elif item.isipv4cidr:
|
||||||
|
if source in item.item:
|
||||||
|
matchcidr = True
|
||||||
|
negativecidr = item.negative
|
||||||
|
if matchcidr and not negativecidr:
|
||||||
|
match = True
|
||||||
|
if matchone and not negativeone:
|
||||||
|
return True
|
||||||
|
if matchone and negativeone:
|
||||||
|
match = False
|
||||||
|
return(match)
|
||||||
|
|
||||||
|
def match6(self, source):
|
||||||
|
'''Is the source IP realted to a IPv6 address/network in the dataset'''
|
||||||
|
match = False
|
||||||
|
matchone = False
|
||||||
|
negativeone = False
|
||||||
|
matchcidr = False
|
||||||
|
negativecidr = False
|
||||||
|
for item in self.dataset:
|
||||||
|
if item.isipv6:
|
||||||
|
if source == item.item:
|
||||||
|
matchone = True
|
||||||
|
negativeone = item.negative
|
||||||
|
elif item.isipv6cidr:
|
||||||
|
if source in item.item:
|
||||||
|
matchcidr = True
|
||||||
|
negativecidr = item.negative
|
||||||
|
if matchcidr and not negativecidr:
|
||||||
|
match = True
|
||||||
|
if matchone and not negativeone:
|
||||||
|
return True
|
||||||
|
if matchone and negativeone:
|
||||||
|
match = False
|
||||||
|
return(match)
|
||||||
|
|
||||||
|
|
||||||
def _processConfigFile(filename=None, configdata=None, useSyslog=1,
|
def _processConfigFile(filename=None, configdata=None, useSyslog=1,
|
||||||
useStderr=0):
|
useStderr=0):
|
||||||
'''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 == None: configdata = config.defaultConfigData
|
if configdata is None:
|
||||||
if filename != None:
|
configdata = config.defaultConfigData
|
||||||
|
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])
|
||||||
@@ -71,7 +241,7 @@ def _processConfigFile(filename = None, configdata = None, useSyslog = 1,
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return(configdata)
|
return(configdata)
|
||||||
|
|
||||||
####################
|
|
||||||
def _find_boolean(item):
|
def _find_boolean(item):
|
||||||
if type(item) == int:
|
if type(item) == int:
|
||||||
item = str(item)
|
item = str(item)
|
||||||
@@ -84,17 +254,69 @@ def _find_boolean(item):
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
###############################################################
|
def _make_authserv_id(as_id):
|
||||||
commentRx = re.compile(r'^(.*)#.*$')
|
"""Determine AuthservID if needed"""
|
||||||
|
if as_id == 'HOSTNAME':
|
||||||
|
as_id = socket.gethostname()
|
||||||
|
return as_id
|
||||||
|
|
||||||
|
|
||||||
|
def _dataset_to_list(dataset):
|
||||||
|
"""Convert a dataset (as defined in dkimpymilter.8) and return a python
|
||||||
|
list of values."""
|
||||||
|
if not isinstance(dataset, str):
|
||||||
|
# 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.
|
||||||
|
if dataset[0][:4] == 'csl:':
|
||||||
|
dataset[0] = dataset[0][4:]
|
||||||
|
for item in dataset:
|
||||||
|
dataset[dataset.index(item)] = item.strip().strip(',')
|
||||||
|
return dataset
|
||||||
|
elif isinstance(dataset, str):
|
||||||
|
if dataset[0] == '/' or dataset[:5] == 'file:':
|
||||||
|
# This is a flat file dataset
|
||||||
|
ds = []
|
||||||
|
if dataset[0] == '/':
|
||||||
|
dsname = dataset
|
||||||
|
if dataset[:5] == 'file:':
|
||||||
|
dsname = dataset[5:]
|
||||||
|
dsf = open(dsname, 'r')
|
||||||
|
for line in dsf.readlines():
|
||||||
|
if line[0] != '#':
|
||||||
|
if len(line.split(':')) == 1:
|
||||||
|
ds.append(line.strip())
|
||||||
|
else:
|
||||||
|
for element in line.split(':'):
|
||||||
|
ds.append(element.strip().strip(':'))
|
||||||
|
dsf.close()
|
||||||
|
return ds
|
||||||
|
# If it's a str and csl, it has one value and we return a list
|
||||||
|
if dataset[:4] == 'csl:':
|
||||||
|
return [dataset[4:].strip().strip(',')]
|
||||||
|
else:
|
||||||
|
return [dataset.strip().strip(',')]
|
||||||
|
if dataset[-3:] == '.db' or dataset[:3] == 'db:':
|
||||||
|
# This is a Sleepycat (Oracle) DB dataset, which we dont support
|
||||||
|
raise dkim.ParameterError('Unsupported dataset db datase: {0}'
|
||||||
|
.format(type(dataset)))
|
||||||
|
|
||||||
|
raise dkim.ParameterError('Unimplmented dataset type: {0}'
|
||||||
|
.format(type(dataset)))
|
||||||
|
|
||||||
|
|
||||||
def _readConfigFile(path, configData=None, configGlobal={}):
|
def _readConfigFile(path, configData=None, configGlobal={}):
|
||||||
'''Reads a configuration file from the specified path, merging it
|
'''Reads a configuration file from the specified path, merging it
|
||||||
with the configuration data specified in configData. Returns a
|
with the configuration data specified in configData. Returns a
|
||||||
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
|
||||||
if debugLevel >= 5: syslog.syslog('readConfigFile: Loading "%s"' % path)
|
# the value here.
|
||||||
if configData == None: configData = {}
|
debugLevel = 0
|
||||||
|
if debugLevel >= 5:
|
||||||
|
syslog.syslog('readConfigFile: Loading "%s"' % path)
|
||||||
|
if configData is None:
|
||||||
|
configData = {}
|
||||||
nameConversion = {
|
nameConversion = {
|
||||||
'AuthservID': 'str',
|
'AuthservID': 'str',
|
||||||
'Syslog': 'bool',
|
'Syslog': 'bool',
|
||||||
@@ -105,63 +327,96 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
|
|||||||
'Socket': 'str',
|
'Socket': 'str',
|
||||||
'PidFile': 'str',
|
'PidFile': 'str',
|
||||||
'UserID': 'str',
|
'UserID': 'str',
|
||||||
'Domain' : 'str',
|
'Domain': 'dataset',
|
||||||
|
'SubDomains': 'bool',
|
||||||
'KeyFile': 'str',
|
'KeyFile': 'str',
|
||||||
'KeyFileEd25519': 'str',
|
'KeyFileEd25519': 'str',
|
||||||
'Selector': 'str',
|
'Selector': 'str',
|
||||||
'SelectorEd25519': 'str',
|
'SelectorEd25519': 'str',
|
||||||
'Canonicalization': 'str',
|
'Canonicalization': 'str',
|
||||||
'CanonicalizationEd25519' : 'str'
|
'InternalHosts': 'dataset',
|
||||||
|
'IntHosts': 'bool',
|
||||||
|
'DiagnosticDirectory': 'str',
|
||||||
|
'MacroList': 'dataset',
|
||||||
|
'MacroListVerify': 'dataset',
|
||||||
|
'DNSOverride': 'str',
|
||||||
|
'debugLevel': 'int'
|
||||||
}
|
}
|
||||||
|
|
||||||
# check to see if it's a file
|
# check to see if it's a file
|
||||||
try:
|
try:
|
||||||
mode = os.stat(path)[0]
|
mode = os.stat(path)[0]
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
syslog.syslog(syslog.LOG_ERR,'ERROR stating "%s": %s' % ( path, e.strerror ))
|
syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s'
|
||||||
|
% (path, e.strerror))
|
||||||
return(configData)
|
return(configData)
|
||||||
if not stat.S_ISREG(mode):
|
if not stat.S_ISREG(mode):
|
||||||
syslog.syslog(syslog.LOG_ERR,'ERROR: is not a file: "%s", mode=%s' % ( path, oct(mode) ))
|
syslog.syslog(syslog.LOG_ERR, 'ERROR: is not a file: "%s", mode=%s'
|
||||||
|
% (path, oct(mode)))
|
||||||
return(configData)
|
return(configData)
|
||||||
|
|
||||||
# load file
|
# load file
|
||||||
fp = open(path, 'r')
|
fp = open(path, 'r')
|
||||||
while 1:
|
while 1:
|
||||||
line = fp.readline()
|
line = fp.readline()
|
||||||
if not line: break
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
# parse line
|
# parse line
|
||||||
line = line.split('#', 1)[0].strip()
|
line = line.split('#', 1)[0].strip()
|
||||||
if not line: continue
|
if not line:
|
||||||
|
continue
|
||||||
data = line.split()
|
data = line.split()
|
||||||
if len(data) != 2:
|
if len(data) != 2:
|
||||||
if len(data) == 1:
|
if len(data) == 1:
|
||||||
if debugLevel >= 1:
|
if debugLevel >= 1:
|
||||||
syslog.syslog('Configuration item "%s" not defined in file "%s"'
|
syslog.syslog('Config item "%s" not defined in file "%s"'
|
||||||
% (line, path))
|
% (line, path))
|
||||||
else:
|
if len(data) == 1:
|
||||||
syslog.syslog('ERROR parsing line "%s" from file "%s"'
|
name = data
|
||||||
% ( line, path ))
|
value = ''
|
||||||
continue
|
if len(data) == 2:
|
||||||
name, value = data
|
name, value = data
|
||||||
|
if len(data) >= 3:
|
||||||
|
name = data[0]
|
||||||
|
value = data[1:]
|
||||||
|
|
||||||
# check validity of name
|
# check validity of name
|
||||||
|
try:
|
||||||
conversion = nameConversion.get(name)
|
conversion = nameConversion.get(name)
|
||||||
if conversion == None:
|
except TypeError:
|
||||||
syslog.syslog('ERROR: Unknown name "%s" in file "%s"' % ( name, path ))
|
name = name[0]
|
||||||
|
syslog.syslog('Config item "%s" does not provide a value in file "%s"'
|
||||||
|
% (name, path))
|
||||||
|
conversion = None
|
||||||
|
if conversion is None:
|
||||||
|
syslog.syslog('ERROR: Unknown name or name missing value "%s" in file "%s"'
|
||||||
|
% (name, path))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if debugLevel >= 5: syslog.syslog('readConfigFile: Found entry "%s=%s"'
|
if debugLevel >= 5:
|
||||||
|
syslog.syslog('readConfigFile: Found entry "%s=%s"'
|
||||||
% (name, value))
|
% (name, value))
|
||||||
if conversion == 'bool':
|
if conversion == 'bool':
|
||||||
configData[name] = _find_boolean(value)
|
configData[name] = _find_boolean(value)
|
||||||
elif conversion == 'str':
|
elif conversion == 'str':
|
||||||
|
if isinstance(value, list):
|
||||||
|
configData[name] = line.split(None, 1)[1]
|
||||||
|
else:
|
||||||
configData[name] = str(value)
|
configData[name] = str(value)
|
||||||
elif conversion == 'int':
|
elif conversion == 'int':
|
||||||
configData[name] = int(value)
|
configData[name] = int(value)
|
||||||
|
elif conversion == 'dataset':
|
||||||
|
configData[name] = _dataset_to_list(value)
|
||||||
else:
|
else:
|
||||||
syslog.syslog(str('name: ' + name + ' value: ' + value + ' conversion: ' + conversion))
|
syslog.syslog(str('name: ' + name + ' value: ' + value +
|
||||||
|
' conversion: ' + conversion))
|
||||||
configData[name] = conversion(value)
|
configData[name] = conversion(value)
|
||||||
fp.close()
|
fp.close()
|
||||||
|
try:
|
||||||
|
configData['AuthservID'] = _make_authserv_id(configData.get('AuthservID', 'HOSTNAME'))
|
||||||
|
configData['IntHosts'] = HostsDataset(configData['InternalHosts'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return(configData)
|
return(configData)
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
## @package dnsplug
|
||||||
|
# Provide a higher level interface to pydns or dnspython (or other provider).
|
||||||
|
# NOT RELEASED: this is a proposed API and implementation.
|
||||||
|
# Goals - work with both pydns and dnspython (and possibly other libraries)
|
||||||
|
# at a simplied level.
|
||||||
|
# TODO:
|
||||||
|
# 1. map exceptions to common dnsplug.DNSError exception (with
|
||||||
|
# original exception saved as a member).
|
||||||
|
# 2. include dict based implementation (handy for test suites)
|
||||||
|
# 3. move implementations to subpackages to enable autoselect on first call.
|
||||||
|
|
||||||
|
## Maximum number of CNAME records to follow
|
||||||
|
MAX_CNAME = 10
|
||||||
|
|
||||||
|
## Lookup DNS records by label and RR type.
|
||||||
|
# The response can include records of other types that the DNS
|
||||||
|
# server thinks we might need. FIXME: empty result
|
||||||
|
# could mean NXDOMAIN or NOANSWER.
|
||||||
|
# @param name the DNS label to lookup
|
||||||
|
# @param qtype the name of the DNS RR type to lookup
|
||||||
|
# @param tcpfallback if False, raise exception instead of TCP fallback
|
||||||
|
# @return a list of ((name,type),data) tuples
|
||||||
|
def DNSLookup(name, qtype, tcpfallback=True, timeout=30):
|
||||||
|
raise NotImplementedError('No supported dns library found')
|
||||||
|
|
||||||
|
class Session(object):
|
||||||
|
"""A Session object has a simple cache with no TTL that is valid
|
||||||
|
for a single "session", for example an SMTP conversation."""
|
||||||
|
def __init__(self):
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
## Additional DNS RRs we can safely cache.
|
||||||
|
# We have to be careful which additional DNS RRs we cache. For
|
||||||
|
# instance, PTR records are controlled by the connecting IP, and they
|
||||||
|
# could poison our local cache with bogus A and MX records.
|
||||||
|
# Each entry is a tuple of (query_type,rr_type). So for instance,
|
||||||
|
# the entry ('MX','A') says it is safe (for milter purposes) to cache
|
||||||
|
# any 'A' RRs found in an 'MX' query.
|
||||||
|
SAFE2CACHE = frozenset((
|
||||||
|
('MX','MX'), ('MX','A'),
|
||||||
|
('CNAME','CNAME'), ('CNAME','A'),
|
||||||
|
('A','A'),
|
||||||
|
('AAAA','AAAA'),
|
||||||
|
('PTR','PTR'),
|
||||||
|
('NS','NS'), ('NS','A'),
|
||||||
|
('TXT','TXT'),
|
||||||
|
('SPF','SPF')
|
||||||
|
))
|
||||||
|
|
||||||
|
## Cached DNS lookup.
|
||||||
|
# @param name the DNS label to query
|
||||||
|
# @param qtype the query type, e.g. 'A'
|
||||||
|
# @param cnames tracks CNAMES already followed in recursive calls
|
||||||
|
def dns(self, name, qtype, cnames=None):
|
||||||
|
"""DNS query.
|
||||||
|
|
||||||
|
If the result is in cache, return that. Otherwise pull the
|
||||||
|
result from DNS, and cache ALL answers, so additional info
|
||||||
|
is available for further queries later.
|
||||||
|
|
||||||
|
CNAMEs are followed.
|
||||||
|
|
||||||
|
If there is no data, [] is returned.
|
||||||
|
|
||||||
|
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
|
||||||
|
post: isinstance(__return__, types.ListType)
|
||||||
|
"""
|
||||||
|
result = self.cache.get( (name, qtype) )
|
||||||
|
cname = None
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
safe2cache = Session.SAFE2CACHE
|
||||||
|
for k, v in DNSLookup(name, qtype):
|
||||||
|
if k == (name, 'CNAME'):
|
||||||
|
cname = v
|
||||||
|
if (qtype,k[1]) in safe2cache:
|
||||||
|
self.cache.setdefault(k, []).append(v)
|
||||||
|
result = self.cache.get( (name, qtype), [])
|
||||||
|
if not result and cname:
|
||||||
|
if not cnames:
|
||||||
|
cnames = {}
|
||||||
|
elif len(cnames) >= MAX_CNAME:
|
||||||
|
#return result # if too many == NX_DOMAIN
|
||||||
|
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
||||||
|
cnames[name] = cname
|
||||||
|
if cname in cnames:
|
||||||
|
raise DNSError('CNAME loop')
|
||||||
|
result = self.dns(cname, qtype, cnames=cnames)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=30):
|
||||||
|
try:
|
||||||
|
# FIXME: To be thread safe, we create a fresh DnsRequest with
|
||||||
|
# each call. It would be more efficient to reuse
|
||||||
|
# a req object stored in a Session.
|
||||||
|
req = DNS.DnsRequest(name, qtype=qtype, timeout=timeout)
|
||||||
|
resp = req.req()
|
||||||
|
#resp.show()
|
||||||
|
# key k: ('wayforward.net', 'A'), value v
|
||||||
|
# FIXME: pydns returns AAAA RR as 16 byte binary string, but
|
||||||
|
# A RR as dotted quad. For consistency, this driver should
|
||||||
|
# return both as binary string.
|
||||||
|
#
|
||||||
|
if resp.header['tc'] == True:
|
||||||
|
if not tcpfallback:
|
||||||
|
raise DNS.DNSError('DNS: Truncated UDP Reply, SPF records should fit in a UDP packet')
|
||||||
|
try:
|
||||||
|
req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp',
|
||||||
|
timeout=timeout)
|
||||||
|
resp = req.req()
|
||||||
|
except DNS.DNSError as x:
|
||||||
|
raise DNS.DNSError('TCP Fallback error: ' + str(x))
|
||||||
|
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
||||||
|
except IOError as x:
|
||||||
|
raise DNS.DNSError('DNS: ' + str(x))
|
||||||
|
|
||||||
|
def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30):
|
||||||
|
retVal = []
|
||||||
|
try:
|
||||||
|
# FIXME: how to disable TCP fallback in dnspython if not tcpfallback?
|
||||||
|
answers = dns.resolver.query(name, qtype)
|
||||||
|
for rdata in answers:
|
||||||
|
if qtype == 'A' or qtype == 'AAAA':
|
||||||
|
retVal.append(((name, qtype), rdata.address))
|
||||||
|
elif qtype == 'MX':
|
||||||
|
retVal.append(((name, qtype), (rdata.preference, rdata.exchange)))
|
||||||
|
elif qtype == 'PTR':
|
||||||
|
retVal.append(((name, qtype), rdata.target.to_text(True)))
|
||||||
|
elif qtype == 'TXT' or qtype == 'SPF':
|
||||||
|
retVal.append(((name, qtype), rdata.strings))
|
||||||
|
except dns.resolver.NoAnswer:
|
||||||
|
pass
|
||||||
|
except dns.resolver.NXDOMAIN:
|
||||||
|
pass
|
||||||
|
return retVal
|
||||||
|
|
||||||
|
try:
|
||||||
|
# prefer dnspython (the more complete library)
|
||||||
|
import dns
|
||||||
|
import dns.resolver # http://www.dnspython.org
|
||||||
|
import dns.exception
|
||||||
|
|
||||||
|
if not hasattr(dns.rdatatype,'SPF'):
|
||||||
|
# patch in type99 support
|
||||||
|
dns.rdatatype.SPF = 99
|
||||||
|
dns.rdatatype._by_text['SPF'] = dns.rdatatype.SPF
|
||||||
|
|
||||||
|
DNSLookup = DNSLookup_dnspython
|
||||||
|
except:
|
||||||
|
import DNS # http://pydns.sourceforge.net
|
||||||
|
|
||||||
|
if not hasattr(DNS.Type, 'SPF'):
|
||||||
|
# patch in type99 support
|
||||||
|
DNS.Type.SPF = 99
|
||||||
|
DNS.Type.typemap[99] = 'SPF'
|
||||||
|
DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata
|
||||||
|
|
||||||
|
# Fails on Mac OS X? Add domain to /etc/resolv.conf
|
||||||
|
DNS.DiscoverNameServers()
|
||||||
|
|
||||||
|
DNSLookup = DNSLookup_pydns
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
s = Session()
|
||||||
|
for n,t in zip(*[iter(sys.argv[1:])]*2):
|
||||||
|
print(n,t)
|
||||||
|
print(s.dns(n,t))
|
||||||
+105
-26
@@ -16,41 +16,79 @@
|
|||||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
def drop_privileges(milterconfig):
|
|
||||||
import os
|
def fold(header):
|
||||||
|
"""Fold a header line into multiple crlf-separated lines at column 72.
|
||||||
|
Borrowed from dkimpy and updated to only add \n instead of \r\n because
|
||||||
|
that's what the milter protocol wants.
|
||||||
|
|
||||||
|
>>> text(fold(b'foo'))
|
||||||
|
'foo'
|
||||||
|
>>> text(fold(b'foo '+b'foo'*24).splitlines()[0])
|
||||||
|
'foo '
|
||||||
|
>>> text(fold(b'foo'*25).splitlines()[-1])
|
||||||
|
' foo'
|
||||||
|
>>> len(fold(b'foo'*25).splitlines()[0])
|
||||||
|
72
|
||||||
|
"""
|
||||||
|
i = header.rfind(b"\r\n ")
|
||||||
|
if i == -1:
|
||||||
|
pre = b""
|
||||||
|
else:
|
||||||
|
i += 3
|
||||||
|
pre = header[:i]
|
||||||
|
header = header[i:]
|
||||||
|
maxleng = 72
|
||||||
|
while len(header) > maxleng:
|
||||||
|
i = header[:maxleng].rfind(b" ")
|
||||||
|
if i == -1:
|
||||||
|
j = maxleng
|
||||||
|
else:
|
||||||
|
j = i + 1
|
||||||
|
pre += header[:j] + b"\n "
|
||||||
|
header = header[j:]
|
||||||
|
return pre + header
|
||||||
|
|
||||||
|
|
||||||
|
def user_group(userid):
|
||||||
|
"""Return user and group from UserID"""
|
||||||
import grp
|
import grp
|
||||||
import pwd
|
import pwd
|
||||||
import syslog
|
|
||||||
|
|
||||||
if os.getuid() != 0:
|
userlist = userid.split(':')
|
||||||
if milterconfig.get('Syslog'):
|
|
||||||
syslog.syslog('drop_privileges: Not running as root. Cannot drop permissions.')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Figure out if user and group are specified
|
|
||||||
userstr = milterconfig.get('UserID')
|
|
||||||
userlist = userstr.split(':')
|
|
||||||
if len(userlist) == 1:
|
if len(userlist) == 1:
|
||||||
gidname = userlist[0]
|
gidname = userlist[0]
|
||||||
else:
|
else:
|
||||||
gidname = userlist[1]
|
gidname = userlist[1]
|
||||||
uidname = userlist[0]
|
|
||||||
|
|
||||||
# Get the uid/gid from the name
|
# Get the uid/gid from the name
|
||||||
running_uid = pwd.getpwnam(uidname).pw_uid
|
running_uid = pwd.getpwnam(userlist[0]).pw_uid
|
||||||
running_gid = grp.getgrnam(gidname).gr_gid
|
running_gid = grp.getgrnam(gidname).gr_gid
|
||||||
|
return running_uid, running_gid
|
||||||
|
|
||||||
|
|
||||||
|
def drop_privileges(milterconfig):
|
||||||
|
import os
|
||||||
|
import syslog
|
||||||
|
|
||||||
|
if os.getuid() != 0:
|
||||||
|
if milterconfig.get('Syslog'):
|
||||||
|
syslog.syslog('drop_privileges: Not root. No action taken.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get user and group
|
||||||
|
uid, gid = user_group(milterconfig.get('UserID'))
|
||||||
|
|
||||||
# Remove group privileges
|
# Remove group privileges
|
||||||
os.setgroups([])
|
os.setgroups([])
|
||||||
|
|
||||||
# Try setting the new uid/gid
|
# Try setting the new uid/gid
|
||||||
os.setgid(running_gid)
|
os.setgid(gid)
|
||||||
os.setuid(running_uid)
|
os.setuid(uid)
|
||||||
|
|
||||||
# Set umask
|
# Set umask
|
||||||
old_umask = os.umask(milterconfig.get('UMask'))
|
old_umask = os.umask(milterconfig.get('UMask'))
|
||||||
|
|
||||||
#################
|
|
||||||
class ExceptHook:
|
class ExceptHook:
|
||||||
def __init__(self, useSyslog=1, useStderr=0):
|
def __init__(self, useSyslog=1, useStderr=0):
|
||||||
self.useSyslog = useSyslog
|
self.useSyslog = useSyslog
|
||||||
@@ -68,32 +106,49 @@ class ExceptHook:
|
|||||||
sys.stderr.write(line)
|
sys.stderr.write(line)
|
||||||
|
|
||||||
|
|
||||||
####################
|
|
||||||
def setExceptHook():
|
def setExceptHook():
|
||||||
import sys
|
import sys
|
||||||
sys.excepthook = ExceptHook(useSyslog=1, useStderr=1)
|
sys.excepthook = ExceptHook(useSyslog=1, useStderr=1)
|
||||||
|
|
||||||
####################
|
|
||||||
def write_pid(milterconfig):
|
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':
|
||||||
|
piddir = pidfile.rsplit('/', 1)[0]
|
||||||
|
os.mkdir(piddir)
|
||||||
|
user, group = user_group(milterconfig.get('UserID'))
|
||||||
|
os.chown(piddir, user, group)
|
||||||
|
f = open(pidfile, 'w')
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog('Unable to write pidfle {0}. IOError: {1}'.format(milterconfig.get('PidFile'), e))
|
syslog.syslog('PID dir created: {0}'.format(piddir))
|
||||||
|
else:
|
||||||
|
if milterconfig.get('Syslog'):
|
||||||
|
syslog.syslog('Unable to write pidfle {0}. IOError: {1}'
|
||||||
|
.format(pidfile, e))
|
||||||
raise
|
raise
|
||||||
f.write(pid)
|
f.write(pid)
|
||||||
f.close()
|
f.close()
|
||||||
|
user, group = user_group(milterconfig.get('UserID'))
|
||||||
|
os.chown(pidfile, user, group)
|
||||||
else:
|
else:
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog('Unable to write pidfle {0}. File exists.'.format(milterconfig.get('PidFile')))
|
syslog.syslog('Unable to write pidfle {0}. File exists.'
|
||||||
raise RuntimeError('Unable to write pidfle {0}. File exists.'.format(milterconfig.get('PidFile')))
|
.format(pidfile))
|
||||||
|
raise RuntimeError('Unable to write pidfle {0}. File exists.'
|
||||||
|
.format(pidfile))
|
||||||
|
return pid
|
||||||
|
|
||||||
|
|
||||||
####################
|
|
||||||
def read_keyfile(milterconfig, keytype):
|
def read_keyfile(milterconfig, keytype):
|
||||||
"""Read private key from file."""
|
"""Read private key from file."""
|
||||||
import syslog
|
import syslog
|
||||||
@@ -106,10 +161,34 @@ def read_keyfile(milterconfig, keytype):
|
|||||||
keylist = f.readlines()
|
keylist = f.readlines()
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
if milterconfig.get('Syslog'):
|
if milterconfig.get('Syslog'):
|
||||||
syslog.syslog('Unable to read keyfile {0}. IOError: {1}'.format(keyfile, e))
|
syslog.syslog('Unable to read keyfile {0}. IOError: {1}'
|
||||||
|
.format(keyfile, e))
|
||||||
raise
|
raise
|
||||||
f.close()
|
f.close()
|
||||||
key = ''
|
key = ''
|
||||||
for line in keylist:
|
for line in keylist:
|
||||||
key += line
|
key += line
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
def read_keytable(milterconfig, tabletype):
|
||||||
|
"""Read keytables into in memory configuration data so all keys are read
|
||||||
|
before priviledges are dropped."""
|
||||||
|
import syslog
|
||||||
|
if tabletype == "RSA":
|
||||||
|
tablefile = milterconfig.get('KeyTable')
|
||||||
|
if tabletype == "Ed25519":
|
||||||
|
tablefile = milterconfig.get('KeyTableEd25519')
|
||||||
|
if milterconfig.get(tablefile):
|
||||||
|
keytabledata = []
|
||||||
|
try:
|
||||||
|
f = open(milterconfig.get(tablefile))
|
||||||
|
for row in f:
|
||||||
|
keytabledata.append(row)
|
||||||
|
f.close()
|
||||||
|
except IOError as e:
|
||||||
|
if milterconfig.get('Syslog'):
|
||||||
|
syslog.syslog('Unable to read keytable {0}. IOError: {1}'
|
||||||
|
.format(tablefile, e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
return keytabledata
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ UMask 007
|
|||||||
|
|
||||||
# Sign for example.com with key in /etc/dkimkeys/dkim.key using
|
# Sign for example.com with key in /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 /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 simple
|
#Canonicalization relaxed/simple
|
||||||
#Mode sv
|
#Mode sv
|
||||||
|
|
||||||
# Socket local:/var/run/dkimpy-milter/dkimpy-milter.sock
|
# Socket local:/var/run/dkimpy-milter/dkimpy-milter.sock
|
||||||
@@ -38,7 +38,7 @@ Socket inet:8892@localhost
|
|||||||
### Name of the file where the filter should write its pid before beginning
|
### Name of the file where the filter should write its pid before beginning
|
||||||
### normal operations.
|
### normal operations.
|
||||||
#
|
#
|
||||||
PidFile /var/run/dkimpy-milter/dkimpy-milter.pid
|
PidFile /run/dkimpy-milter/dkimpy-milter.pid
|
||||||
|
|
||||||
## Userid userid
|
## Userid userid
|
||||||
### default dkimpy-milter
|
### default dkimpy-milter
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
\"
|
||||||
|
.\" 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 dkimpyy-milter 8
|
||||||
|
.SH NAME
|
||||||
|
.B dkimpy
|
||||||
|
\- DKIM signing and verifying filter for MTAs
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B dkimpy-milter [configfile]
|
||||||
|
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B dkimpy-milter
|
||||||
|
implements the
|
||||||
|
.B DKIM
|
||||||
|
standard for signing and verifying e-mail messages on a per-domain basis.
|
||||||
|
|
||||||
|
.B dkimpy-milter
|
||||||
|
uses the
|
||||||
|
.I milter
|
||||||
|
interface, originally distributed as part of version 8.11 of
|
||||||
|
.B sendmail(8),
|
||||||
|
to provide DKIM signing and/or verifying service for mail transiting
|
||||||
|
a milter-aware MTA.
|
||||||
|
|
||||||
|
.SH DATA SETS
|
||||||
|
Many of the configuration file parameters will refer to a "dataset" as their
|
||||||
|
values. This refers to a string that either contains the list of desirable
|
||||||
|
values, or to a file that contains them, or a database containing the data.
|
||||||
|
|
||||||
|
Some data sets require that the value contain more than one entry. How this
|
||||||
|
is done depends on which data set type is used. Not all these datasets are
|
||||||
|
currently used by dkimpy-milter. See
|
||||||
|
.B dkimpy-milter.conf(5)
|
||||||
|
for details about specific options and which dataset types they use.
|
||||||
|
|
||||||
|
In particular:
|
||||||
|
.TP
|
||||||
|
.I a)
|
||||||
|
If the string begins with "file:", then the remainder of the string is
|
||||||
|
presumed to refer to a flat file that contains elements of the data set,
|
||||||
|
one per line. If a line contains whitespace-separated values, then the
|
||||||
|
line is presumed to define a key and its corresponding value. Blank lines
|
||||||
|
are ignored, and the hash ("#") character denotes the start of a comment.
|
||||||
|
If a value contains multiple entries, the entries should be separated by
|
||||||
|
colons.
|
||||||
|
.TP
|
||||||
|
.I c)
|
||||||
|
If the string begins with "db:" and the program was compiled with
|
||||||
|
Sleepycat DB support, then the remainder of the string is presumed to
|
||||||
|
identify a Sleepycat database containing keys and corresponding values.
|
||||||
|
These may be used only to test for membership in the data set, or for
|
||||||
|
storing keys and corresponding values. If a value contains multiple entries,
|
||||||
|
the entries should be separated by colons. [Not implemented yet]
|
||||||
|
.TP
|
||||||
|
.I h)
|
||||||
|
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
|
||||||
|
.I i)
|
||||||
|
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.
|
||||||
|
.TP
|
||||||
|
.I j)
|
||||||
|
If the string begins with "csl:", the string is treated as a comma-separated
|
||||||
|
list as described in m) below.
|
||||||
|
.TP
|
||||||
|
.I l)
|
||||||
|
If the string begins with "mdb:", it refers to a directory that contains
|
||||||
|
a memory database, as provided by libmdb from OpenLDAP. [Not implemented yet]
|
||||||
|
.TP
|
||||||
|
.I m)
|
||||||
|
In any other case, the string is presumed to be a comma-separated list.
|
||||||
|
Elements in the list are either simple data elements that are part of the
|
||||||
|
set or, in the case of an entry of the form "x=y", are stored as key-value
|
||||||
|
pairs as described above.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
See
|
||||||
|
.I dkimpy-milter.conf(5)
|
||||||
|
information about available options. Unlike OpenDKIM, dkimpy-milter does not
|
||||||
|
support command line option switches.
|
||||||
|
|
||||||
|
When signing a message, a
|
||||||
|
.I DKIM-Signature:
|
||||||
|
header will be prepended to the message. The signature is computed using
|
||||||
|
the private key provided. You must be running a version of
|
||||||
|
.I sendmail(8)
|
||||||
|
recent enough to be able to do header prepend operations (8.13.0 or later).
|
||||||
|
|
||||||
|
When verifying a message, an
|
||||||
|
.I Authentication-Results:
|
||||||
|
header will be prepended to indicate the presence of a signature and whether
|
||||||
|
or not it could be validated against the body of the message using the
|
||||||
|
public key advertised by the sender's nameserver. The value of this header
|
||||||
|
can be used by mail user agents to sort or discard messages that were not
|
||||||
|
signed or could not be verified.
|
||||||
|
|
||||||
|
.SH FILE PERMISSIONS
|
||||||
|
When the filter is started as the superuser and the UserID setting is
|
||||||
|
used, the filter gives up its root privileges by changing to the specified
|
||||||
|
user after the following steps are taken: (1) the configuration file (if any)
|
||||||
|
is loaded; (2) if the KeyFile or KeyFileEd25519 settings are used, the keys are
|
||||||
|
loaded into memory; (3) all data sets in the configuration file are opened, and
|
||||||
|
those that are based on flat files are also read into memory; and (4) if
|
||||||
|
ChangeRootDirectory is set, the process root is changed to that directory.
|
||||||
|
This means on configuration reload, the filter will not be accessing these
|
||||||
|
files or the configuration file as the superuser (and possibly from a
|
||||||
|
different root), and any key files referenced by the KeyTable will also be
|
||||||
|
accessed by the new user.
|
||||||
|
|
||||||
|
Thus, keys referenced by the KeyTable must always be accessible for read by
|
||||||
|
the unprivileged user. Also, run-time reloads are not possible if any of the
|
||||||
|
other files will not be readable by the unprivileged user.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
The following environment variable(s) can be used to adjust the behaviour
|
||||||
|
of this filter:
|
||||||
|
.TP
|
||||||
|
.I DKIM_TMPDIR
|
||||||
|
The directory to use when creating temporary files. The default is
|
||||||
|
.I /tmp.
|
||||||
|
.SH NOTES
|
||||||
|
When using DNS timeouts 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.
|
||||||
|
|
||||||
|
Features that involve specification of IPv4 addresses or CIDR blocks
|
||||||
|
will use the
|
||||||
|
.I inet_addr(3)
|
||||||
|
function to parse that information. Users should be familiar with the
|
||||||
|
way that function handles the non-trivial cases (for example, "192.0.2/24"
|
||||||
|
and "192.0.2.0/24" are not the same thing).
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Filter exit status codes are selected according to
|
||||||
|
.I sysexits(3).
|
||||||
|
.SH HISTORY
|
||||||
|
DKIM is an amalgam of Yahoo!'s
|
||||||
|
.B DomainKeys
|
||||||
|
proposal, and Cisco's
|
||||||
|
.B Internet Identified Mail
|
||||||
|
(IIM) proposal.
|
||||||
|
.SH VERSION
|
||||||
|
This man page covers version 1.1.0 of
|
||||||
|
.I dkimpy-milter.
|
||||||
|
.SH COPYRIGHT
|
||||||
|
Copyright (c) 2005-2008, Sendmail, Inc. and its suppliers. All rights
|
||||||
|
reserved.
|
||||||
|
|
||||||
|
Copyright (c) 2009-2013, 2015, The Trusted Domain Project.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Copyright (c) 2018, 2019 Scott Kitterman <scott@kitterman.com>
|
||||||
|
.SH SEE ALSO
|
||||||
|
.I dkimpy-milter.conf(5), sendmail(8)
|
||||||
|
.P
|
||||||
|
Sendmail Operations Guide
|
||||||
|
.P
|
||||||
|
RFC5321 - Simple Mail Transfer Protocol
|
||||||
|
.P
|
||||||
|
RFC5322 - Internet Messages
|
||||||
|
.P
|
||||||
|
RFC6376 - DomainKeys Identified Mail
|
||||||
|
.P
|
||||||
|
RFC7601 - Message Header Field for Indicating Message Authentication Status
|
||||||
|
.P
|
||||||
|
RFC8301 - Cryptographic Algorithm and Key Usage Update to DomainKeys Identified Mail (DKIM)
|
||||||
|
.P
|
||||||
|
RFC8463 - A New Cryptographic Signature Method for DomainKeys Identified Mail (DKIM)
|
||||||
+105
-29
@@ -127,16 +127,13 @@
|
|||||||
.rm #[ #] #H #V #F C
|
.rm #[ #] #H #V #F C
|
||||||
.\" ========================================================================
|
.\" ========================================================================
|
||||||
.\"
|
.\"
|
||||||
.IX Title "dkimpy-milter.conf 5"
|
|
||||||
.TH dkimpy-milter.conf 5 "2018-02-12"
|
.TH dkimpy-milter.conf 5 "2018-02-12"
|
||||||
.SH "NAME"
|
.SH "NAME"
|
||||||
dkimpy-milter \- Python milter for DKIM signing and validation
|
dkimpy-milter \- Python milter for DKIM signing and validation
|
||||||
.SH "VERSION"
|
.SH "VERSION"
|
||||||
.IX Header "VERSION"
|
1\.1\.0
|
||||||
0\.9\.1
|
|
||||||
|
|
||||||
.SH "DESCRIPTION"
|
.SH "DESCRIPTION"
|
||||||
.IX Header "DESCRIPTION"
|
|
||||||
.I dkimpy-milter(8)
|
.I dkimpy-milter(8)
|
||||||
implements the
|
implements the
|
||||||
.B DKIM
|
.B DKIM
|
||||||
@@ -160,18 +157,15 @@ The provided setup.py installs this configuration file in /etc or
|
|||||||
Command line invocation of parameters as is done by OpenDKIM is not supported.
|
Command line invocation of parameters as is done by OpenDKIM is not supported.
|
||||||
|
|
||||||
.SH "USAGE"
|
.SH "USAGE"
|
||||||
.IX Header "USAGE"
|
|
||||||
Usage:
|
Usage:
|
||||||
dkimpy-milter [/etc/dkimpy-milter.conf]
|
dkimpy-milter [/etc/dkimpy-milter.conf]
|
||||||
|
|
||||||
.SH "OTHER DOCUMENTATION"
|
.SH "OTHER DOCUMENTATION"
|
||||||
.IX Header "OTHER DOCUMENTATION"
|
|
||||||
This documentation assumes you have read Postfix's README_FILES/MILTER_README
|
This documentation assumes you have read Postfix's README_FILES/MILTER_README
|
||||||
(or Sendmail equivalent) and are generally familiar with Domain Keys Identified
|
(or Sendmail equivalent) and are generally familiar with Domain Keys Identified
|
||||||
Mail (DKIM). See RFC 6376 for details.
|
Mail (DKIM). See RFC 6376 for details.
|
||||||
|
|
||||||
.SH "SYNOPSIS"
|
.SH "SYNOPSIS"
|
||||||
.IX Header "SYNOPSIS"
|
|
||||||
|
|
||||||
dkimpy-milter operates with a default installed configuration file and
|
dkimpy-milter operates with a default installed configuration file and
|
||||||
set of default configuration options that are used if the configuration file
|
set of default configuration options that are used if the configuration file
|
||||||
@@ -181,14 +175,12 @@ files can be used directly. Not all OpenDKIM options are supported. If an
|
|||||||
unsupported option from OpenDKIM is specified, an error will be raised.
|
unsupported option from OpenDKIM is specified, an error will be raised.
|
||||||
|
|
||||||
.SH "DESCRIPTION"
|
.SH "DESCRIPTION"
|
||||||
.IX Header "DESCRIPTION"
|
|
||||||
|
|
||||||
Configuration options are described here and in the configuration file
|
Configuration options are described here and in the configuration file
|
||||||
provided with the package. The provided setup.py installs this configuration
|
provided with the package. The provided setup.py installs this configuration
|
||||||
file in /etc or /usr/local/etc.
|
file in /etc or /usr/local/etc.
|
||||||
|
|
||||||
.SH "OPTIONS"
|
.SH "OPTIONS"
|
||||||
.IX Header "OPTIONS"
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I AuthservID (string)
|
.I AuthservID (string)
|
||||||
@@ -208,23 +200,19 @@ the canonicalization method. The recognized values are
|
|||||||
and
|
and
|
||||||
.I simple
|
.I simple
|
||||||
as defined by the DKIM specification. The default is
|
as defined by the DKIM specification. The default is
|
||||||
|
.I relaxed
|
||||||
|
/
|
||||||
.I simple.
|
.I simple.
|
||||||
The value may include two different canonicalizations separated by a
|
The value may include two different canonicalizations separated by a
|
||||||
slash ("/") character, in which case the first will be applied to the
|
slash ("/") character, in which case the first will be applied to the
|
||||||
header and the second to the body.
|
header and the second to the body.
|
||||||
|
|
||||||
.TP
|
|
||||||
.I Diagnostics (Boolean)
|
|
||||||
Requests the inclusion of "z=" tags in signatures, which encode the
|
|
||||||
original header field set for use by verifiers when diagnosing verification
|
|
||||||
failures. Not recommended for normal operation. [dkimpy-milter specific: also
|
|
||||||
increases the verbosity of Syslog logging if enabled.]
|
|
||||||
|
|
||||||
.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 on a message bearing a "z=" tag. If not set (the default), these files
|
fails. If not set (the default), these files are not generated. [Unlike
|
||||||
are not generated.
|
OpenDKIM, this applies to all messages, not just on messages bearing a "z=" tag
|
||||||
|
because dkimpy does not yet support "z=".]
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I Domain (dataset)
|
.I Domain (dataset)
|
||||||
@@ -233,12 +221,16 @@ domains will be verified rather than being signed.
|
|||||||
|
|
||||||
This parameter is not required if a
|
This parameter is not required if a
|
||||||
.I SigningTable
|
.I SigningTable
|
||||||
|
or
|
||||||
|
.I SigningTableEd25519
|
||||||
is in use; in that case, the list of signed domains is implied by the
|
is in use; in that case, the list of signed domains is implied by the
|
||||||
lines in that file. [NOT IMPLEMENTED]
|
lines in that file.
|
||||||
|
|
||||||
This parameter is ignored if a
|
This parameter is ignored if a
|
||||||
.I KeyTable
|
.I KeyTable
|
||||||
is defined. [NOT IMPLEMENTED]
|
or
|
||||||
|
.I KeyTableD25119
|
||||||
|
is defined.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I InternalHosts (dataset)
|
.I InternalHosts (dataset)
|
||||||
@@ -256,15 +248,62 @@ 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 KeyFileEd25119 (string)
|
.I KeyFileEd25519 (string)
|
||||||
Gives the location of a Ed25519 private key to be used for Ed25519 signing
|
Gives the location of a Ed25519 private key to be used for Ed25519 signing
|
||||||
all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key
|
all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key
|
||||||
generation (as used in dkimpy). Ignored if a
|
generation (as used in dkimpy). Ignored if a
|
||||||
.I KeyTableEd25519
|
.I KeyTableEd25519
|
||||||
is defined. [KeyTableEd25519 NOT IMPLEMENTED]
|
is defined.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.I KeyTable (dataset)
|
||||||
|
Gives the location of a file mapping key names to RSA signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signature’s "d=" value; (b) the name of the selector to use in the signature’s "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender. NOTE: direct specification of keys in the table as is done by OpenDKIM is not supported.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.I KeyTableEd25519 (dataset)
|
||||||
|
Gives the location of a file mapping key names to Ed25519 signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the signature’s "d=" value; (b) the name of the selector to use in the signature’s "s=" value; and (c) the path to a file containing a private key. If the first value consists solely of a percent sign ("%") character, it will be replaced by the apparent domain of the sender when generating a signature. The third value must start with a slash ("/") character, or "./" or "../" to indicate it refers to a file from which the private key should be read. The SigningTable (see below) is used to select records from this table to be used to add signatures based on the message sender. NOTE: direct specification of keys in the table as is done by OpenDKIM is not support
|
||||||
|
ed.
|
||||||
|
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.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
|
.TP
|
||||||
.I Mode (string)
|
.I Mode (string)
|
||||||
@@ -284,7 +323,13 @@ When signing mode is enabled, one of the following combinations must also
|
|||||||
be set:
|
be set:
|
||||||
(a) Domain, KeyFile, Selector, no KeyTable, no SigningTable;
|
(a) Domain, KeyFile, Selector, no KeyTable, no SigningTable;
|
||||||
(b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector;
|
(b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector;
|
||||||
[fooTable options NOT IMPLEMENTED]
|
|
||||||
|
.TP
|
||||||
|
.I DNSOverride (string)
|
||||||
|
Provide a text string that a verifying milter should use instead of
|
||||||
|
consulting the DNS on each message. This is useful primarily for
|
||||||
|
testing purposes in environments where it is awkward to modify the
|
||||||
|
system DNS resolution. It should not be used in production.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I PeerList (dataset)
|
.I PeerList (dataset)
|
||||||
@@ -306,7 +351,7 @@ will be checked. [PeerList NOT IMPLEMENTED - included for reference only]
|
|||||||
.TP
|
.TP
|
||||||
.I PidFile (string)
|
.I PidFile (string)
|
||||||
Specifies the path to a file that should be created at process start
|
Specifies the path to a file that should be created at process start
|
||||||
containing the process ID.
|
containing the process ID. If not specified, no such file will be created.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I Selector (string)
|
.I Selector (string)
|
||||||
@@ -320,7 +365,7 @@ parameter below for more information.
|
|||||||
|
|
||||||
This parameter is ignored if a
|
This parameter is ignored if a
|
||||||
.I KeyTable
|
.I KeyTable
|
||||||
is defined. [KeyTable NOT IMPLEMENTED]
|
is defined.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I SelectorEd25519 (string)
|
.I SelectorEd25519 (string)
|
||||||
@@ -334,7 +379,33 @@ parameter below for more information.
|
|||||||
|
|
||||||
This parameter is ignored if a
|
This parameter is ignored if a
|
||||||
.I KeyTableEd25519
|
.I KeyTableEd25519
|
||||||
is defined. [KeyTable NOT IMPLEMENTED]
|
is defined.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.I SigningTable (dataset)
|
||||||
|
|
||||||
|
Defines a table used to select one or more signatures to apply to a message based on the address found in the From: header field. Keys in this table vary depending on the type of table used; values in this data set should include one field that contains a name found in the KeyTable (see above) that identifies which key should be used in generating the signature, and an optional second field naming the signer of the message that will be included in the "i=" tag in the generated signature. Note that the "i=" value will not be included in the signature if it conflicts with the signing domain (the "d=" value).
|
||||||
|
|
||||||
|
If the first field contains only a "%" character, it will be replaced by the domain found in the From: header field. Similarly, within the optional second field, any "%" character will be replaced by the domain found in the From: header field.
|
||||||
|
|
||||||
|
If this table specifies a regular expression file ("refile"), then the keys are wildcard patterns that are matched against the address found in the From: header field. Entries are checked in the order in which they appear in the file. ["refile support not implemented"].
|
||||||
|
|
||||||
|
For all other database types, the full user@host is checked first, then simply host, then user@.domain (with all superdomains checked in sequence, so "foo.example.com" would first check "user@foo.example.com", then "user@.example.com", then "user@.com"), then .domain, then user@*, and finally *.
|
||||||
|
|
||||||
|
In any case, only the first match is applied.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.I SigningTableEd25519 (dataset)
|
||||||
|
|
||||||
|
Defines a table used to select one or more signatures to apply to a message based on the address found in the From: header field. Keys in this table vary depending on the type of table used; values in this data set should include one field that contains a name found in the KeyTable (see above) that identifies which key should be used in generating the signature, and an optional second field naming the signer of the message that will be included in the "i=" tag in the generated signature. Note that the "i=" value will not be included in the signature if it conflicts with the signing domain (the "d=" value).
|
||||||
|
|
||||||
|
If the first field contains only a "%" character, it will be replaced by the domain found in the From: header field. Similarly, within the optional second field, any "%" character will be replaced by the domain found in the From: header field.
|
||||||
|
|
||||||
|
If this table specifies a regular expression file ("refile"), then the keys are wildcard patterns that are matched against the address found in the From: header field. Entries are checked in the order in which they appear in the file. ["refile support not implemented"].
|
||||||
|
|
||||||
|
For all other database types, the full user@host is checked first, then simply host, then user@.domain (with all superdomains checked in sequence, so "foo.example.com" would first check "user@foo.example.com", then "user@.example.com", then "user@.com"), then .domain, then user@*, and finally *.
|
||||||
|
|
||||||
|
In any case, only the first match is applied.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I Socket (string)
|
.I Socket (string)
|
||||||
@@ -359,6 +430,12 @@ is not given as either a hostname or an IP address, the socket will be
|
|||||||
listening on all interfaces. A literal IP address must be enclosed in
|
listening on all interfaces. A literal IP address must be enclosed in
|
||||||
square brackets. This option is mandatory in the configuration file.
|
square brackets. This option is mandatory in the configuration file.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.I SubDomains (Boolean)
|
||||||
|
Sign subdomains of those listed by the
|
||||||
|
.I Domain
|
||||||
|
parameter as well as the actual domains.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I Syslog (Boolean)
|
.I Syslog (Boolean)
|
||||||
Log via calls to
|
Log via calls to
|
||||||
@@ -372,7 +449,7 @@ Log via calls to
|
|||||||
using the named facility. The facility names are the same as the ones
|
using the named facility. The facility names are the same as the ones
|
||||||
allowed in
|
allowed in
|
||||||
.I syslog.conf(5).
|
.I syslog.conf(5).
|
||||||
The default is "mail". [Hardcoded to default for now]
|
The default is "mail".
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.I SyslogSuccess (Boolean)
|
.I SyslogSuccess (Boolean)
|
||||||
@@ -408,7 +485,6 @@ unless an alternate
|
|||||||
is specified.
|
is specified.
|
||||||
|
|
||||||
.SH "AUTHORS"
|
.SH "AUTHORS"
|
||||||
.IX Header "AUTHORS"
|
|
||||||
\ddkimpy-milter\fR was written by Scott Kitterman <scott@kitterman.com>.
|
\ddkimpy-milter\fR was written by Scott Kitterman <scott@kitterman.com>.
|
||||||
It is based on dkimpy-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc.
|
It is based on dkimpy-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc.
|
||||||
Copyright (c) 2013-2015 Stuart D. Gathman
|
Copyright (c) 2013-2015 Stuart D. Gathman
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -21,22 +21,29 @@ import os
|
|||||||
|
|
||||||
description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail."
|
description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail."
|
||||||
|
|
||||||
|
kw = {} # Work-around for lack of 'or' requires in setuptools.
|
||||||
|
try:
|
||||||
|
import dns
|
||||||
|
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'dnspython']
|
||||||
|
except ImportError: # If PyDNS is not installed, prefer dnspython
|
||||||
|
kw['install_requires'] = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'Py3DNS']
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='dkimpy-milter',
|
name='dkimpy-milter',
|
||||||
version='0.9.1',
|
version='1.1.4',
|
||||||
author='Scott Kitterman',
|
author='Scott Kitterman',
|
||||||
author_email='scott@kitterman.com',
|
author_email='scott@kitterman.com',
|
||||||
url='https://launchpad.net/dkimpy-milter',
|
url='https://launchpad.net/dkimpy-milter',
|
||||||
description=description,
|
description=description,
|
||||||
download_url = "https://pypi.python.org/pypi/dkimpy-milter",
|
download_url = "https://pypi.python.org/pypi/dkimpy-milter",
|
||||||
classifiers= [
|
classifiers= [
|
||||||
'Development Status :: 3 - Alpha',
|
'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',
|
||||||
@@ -49,8 +56,11 @@ setup(
|
|||||||
},
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
data_files=[(os.path.join('share', 'man', 'man5'),
|
data_files=[(os.path.join('share', 'man', 'man5'),
|
||||||
['man/dkimpy-milter.conf.5']), ('etc', ['etc/dkimpy-milter.conf'])],
|
['man/dkimpy-milter.conf.5']), (os.path.join('share', 'man', 'man8'),
|
||||||
|
['man/dkimpy-milter.8']), ('etc', ['etc/dkimpy-milter.conf']),
|
||||||
install_requires = ['dkimpy', 'pymilter', 'authres>=1.0.2'],
|
(os.path.join('lib', 'systemd', 'system'),
|
||||||
|
['system/dkimpy-milter.service']),(os.path.join('etc', 'init.d'),
|
||||||
|
['system/dkimpy-milter'])],
|
||||||
zip_safe = False,
|
zip_safe = False,
|
||||||
|
**kw
|
||||||
)
|
)
|
||||||
|
|||||||
Executable
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#! /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
|
||||||
|
prefix="/usr/local"
|
||||||
|
exec_prefix=${prefix}
|
||||||
|
sysconfdir="/usr/local/etc"
|
||||||
|
bindir="${exec_prefix}/bin/"
|
||||||
|
RUNDIR="/run/dkimpy-milter"
|
||||||
|
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 /etc/default/dkimpy-milter ] ; then
|
||||||
|
. /etc/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 /var/run/dkimpy-milter/dkimpy-milter.pid /usr/local/bin/dkimpy-milter dkimpy-milter
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
N=/etc/init.d/$NAME
|
||||||
|
echo "Usage: $N {start|stop|force-reload|restart|}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -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=/run/dkimpy-milter/dkimpy-milter.pid
|
||||||
|
ExecStart=/usr/local/bin/dkimpy-milter /usr/local/etc/dkimpy-milter.conf
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
This directory contains example systemd unit files for running a
|
||||||
|
supervised, socket-activated instance of dkimpy-milter.
|
||||||
|
|
||||||
|
There are several advantages of using socket activation:
|
||||||
|
|
||||||
|
- dkimpy-milter never runs with elevated privileges, they are dropped
|
||||||
|
before any dkimpy-milter code is executed.
|
||||||
|
|
||||||
|
- The socket is opened before dkimpy-milter runs. This means that
|
||||||
|
clients can connect() to the socket immediately. So even if there
|
||||||
|
is a delay in dkimpy-milter startup, or in libmilter itself, the
|
||||||
|
connection will not fail.
|
||||||
|
|
||||||
|
- You can set the privileges of a listening Unix-domain socket by an
|
||||||
|
override of ListenGroup= in dkimpy-milter.socket (see
|
||||||
|
systemd.unit(5) for how to override). This lets you control who has
|
||||||
|
access to the daemon with finer granularity than is available with
|
||||||
|
dkimpy-milter on its own.
|
||||||
|
|
||||||
|
- dkimpy-milter will not consume system resources if it is not used.
|
||||||
|
|
||||||
|
- A fully-supervised dkimpy-milter needs no PIDFile, UMask, UserID, or
|
||||||
|
Socket configuation. This eliminates common race conditions and
|
||||||
|
startup failures, and simplifies the resulting configuration file.
|
||||||
|
|
||||||
|
There is one downside to using socket activation:
|
||||||
|
|
||||||
|
- it will only work on systems where libmilter can support connection
|
||||||
|
strings like "fd:3". This has been supported on Debian and derived
|
||||||
|
systems since sendmail 8.14.4-6 (before Debian Jessie, in early
|
||||||
|
2014), see for example:
|
||||||
|
https://sources.debian.org/src/sendmail/8.15.2-8/debian/patches/socket_activation.patch/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=DKIMpy Milter
|
||||||
|
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
|
||||||
|
Requires=dkimpy-milter.socket
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/dkimpy-milter /etc/dkimpy-milter.conf
|
||||||
|
User=dkimpy-milter
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
Also=dkimpy-milter.socket
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=DKIMpy Milter socket
|
||||||
|
Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5)
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=/run/dkimpy-milter/dkimpy-milter.sock
|
||||||
|
SocketMode=0660
|
||||||
|
# override SocketGroup to grant access to members of another system group:
|
||||||
|
SocketGroup=dkimpy-milter
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- -*- lua -*-
|
||||||
|
for _, keytype in ipairs({"ed25519", "rsa"}) do
|
||||||
|
for _, func in ipairs({"signing", "verify"}) do
|
||||||
|
mt.echo("testing "..keytype.." "..func)
|
||||||
|
conn = mt.connect("unix:"..keytype.."."..func..".sock")
|
||||||
|
if conn == nil then
|
||||||
|
error("mt.connect() failed "..keytype.." "..func)
|
||||||
|
end
|
||||||
|
mt.disconnect(conn)
|
||||||
|
mt.echo(keytype.." "..func.." complete")
|
||||||
|
end
|
||||||
|
end
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
-- -*- lua -*-
|
||||||
|
for _, keytype in ipairs({"ed25519", "rsa"}) do
|
||||||
|
for _, func in ipairs({"signing", "verify"}) do
|
||||||
|
mt.echo("testing "..keytype.." "..func)
|
||||||
|
conn = mt.connect("unix:"..keytype.."."..func..".sock")
|
||||||
|
if conn == nil then
|
||||||
|
error("mt.connect() failed "..keytype.." "..func)
|
||||||
|
end
|
||||||
|
if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then
|
||||||
|
error("mt.conninfo() failed "..keytype.." "..func)
|
||||||
|
end
|
||||||
|
if mt.getreply(conn) ~= SMFIR_CONTINUE then
|
||||||
|
error("mt.conninfo() unexpected reply "..keytype.." "..func)
|
||||||
|
end
|
||||||
|
|
||||||
|
if mt.test_action(conn, SMFIF_ADDHDRS) then
|
||||||
|
print("could add headers "..keytype.." "..func)
|
||||||
|
else
|
||||||
|
error("mt.test_action() says could not add headers "..keytype.." "..func)
|
||||||
|
end
|
||||||
|
|
||||||
|
if mt.test_action(conn, SMFIF_CHGHDRS) then
|
||||||
|
print("could change headers "..keytype.." "..func)
|
||||||
|
else
|
||||||
|
error("mt.test_action() says could not change headers "..keytype.." "..func)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -- FIXME: this part of the test fails, as apparently the
|
||||||
|
-- -- dkimpy-milter claims the right to change the body of a message,
|
||||||
|
-- -- even though it shouldn't. How can we fix the negotiation?
|
||||||
|
-- if mt.test_action(conn, SMFIF_CHGBODY) then
|
||||||
|
-- error("mt.test_action() says could change body "..keytype.." "..func)
|
||||||
|
-- else
|
||||||
|
-- print("could not change body "..keytype.." "..func)
|
||||||
|
-- end
|
||||||
|
|
||||||
|
mt.disconnect(conn)
|
||||||
|
mt.echo(keytype.." "..func.." test complete")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
-- -*- lua -*-
|
||||||
|
|
||||||
|
msg = {
|
||||||
|
['headers'] = {
|
||||||
|
['From'] = 'Alice <alice@example.net>',
|
||||||
|
['Message-Id'] = '<dkimpy-milter-test-02@example.net>',
|
||||||
|
['To'] = 'Bob <bob@example.biz>',
|
||||||
|
['Date'] = 'Mon, 18 Feb 2019 08:32:50 -0500',
|
||||||
|
['Subject'] = 'Signing test',
|
||||||
|
['Content-Type'] = 'text/plain',
|
||||||
|
},
|
||||||
|
['body'] = "This is a test!\r\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- returns miltertest connection object
|
||||||
|
function connect_and_send (sockname, headers, body)
|
||||||
|
conn = mt.connect(sockname)
|
||||||
|
if conn == nil then
|
||||||
|
error "mt.connect() failed"
|
||||||
|
end
|
||||||
|
if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then
|
||||||
|
error "mt.conninfo() failed"
|
||||||
|
end
|
||||||
|
if mt.getreply(conn) ~= SMFIR_CONTINUE then
|
||||||
|
error "mt.conninfo() unexpected reply"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- mt.macro(conn, SMFIC_MAIL, "i", "simple-message")
|
||||||
|
if mt.mailfrom(conn, "<alice@example.net>") ~= nil then
|
||||||
|
error "mt.mailfrom() failed"
|
||||||
|
end
|
||||||
|
if mt.getreply(conn) ~= SMFIR_CONTINUE then
|
||||||
|
error "mt.mailfrom() unexpected reply"
|
||||||
|
end
|
||||||
|
-- mt.rcptto() is called implicitly
|
||||||
|
|
||||||
|
-- send headers
|
||||||
|
for key,value in pairs(headers) do
|
||||||
|
if mt.header(conn, key, value) ~= nil then
|
||||||
|
error("mt.header(" .. key .. ") failed")
|
||||||
|
end
|
||||||
|
if mt.getreply(conn) ~= SMFIR_CONTINUE then
|
||||||
|
error("mt.header(" .. key .. ") unexpected reply")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- send EOH
|
||||||
|
if mt.eoh(conn) ~= nil then
|
||||||
|
error "mt.eoh() failed"
|
||||||
|
end
|
||||||
|
if mt.getreply(conn) ~= SMFIR_CONTINUE then
|
||||||
|
error "mt.eoh() unexpected reply"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- send body
|
||||||
|
if mt.bodystring(conn, body) ~= nil then
|
||||||
|
error "mt.bodystring() failed"
|
||||||
|
end
|
||||||
|
if mt.getreply(conn) ~= SMFIR_CONTINUE then
|
||||||
|
error "mt.bodystring() unexpected reply"
|
||||||
|
end
|
||||||
|
-- end of message; let the filter react
|
||||||
|
if mt.eom(conn) ~= nil then
|
||||||
|
error "mt.eom() failed"
|
||||||
|
end
|
||||||
|
reply = mt.getreply(conn)
|
||||||
|
if reply ~= SMFIR_CONTINUE then
|
||||||
|
error ("mt.eom() unexpected reply: " .. reply)
|
||||||
|
end
|
||||||
|
return conn
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, keytype in ipairs({"ed25519", "rsa"}) do
|
||||||
|
mt.echo("testing "..keytype)
|
||||||
|
signing = connect_and_send("unix:"..keytype..".signing.sock", msg.headers, msg.body)
|
||||||
|
-- verify that a test header field got added
|
||||||
|
if not mt.eom_check(signing, MT_HDRINSERT) then
|
||||||
|
error "no header added by signer"
|
||||||
|
end
|
||||||
|
|
||||||
|
signature = mt.getheader(signing, "DKIM-Signature", 0)
|
||||||
|
|
||||||
|
mt.disconnect(signing)
|
||||||
|
|
||||||
|
mt.echo("DKIM-Signature: " .. signature)
|
||||||
|
|
||||||
|
msg.headers['DKIM-Signature'] = signature
|
||||||
|
|
||||||
|
verify = connect_and_send("unix:"..keytype..".verify.sock", msg.headers, msg.body)
|
||||||
|
|
||||||
|
if not mt.eom_check(verify, MT_HDRINSERT) then
|
||||||
|
error "no header added in verify"
|
||||||
|
end
|
||||||
|
|
||||||
|
authres = mt.getheader(verify, "Authentication-Results", 0)
|
||||||
|
mt.echo("Authentication-Results: "..authres)
|
||||||
|
|
||||||
|
mt.disconnect(verify)
|
||||||
|
|
||||||
|
mt.echo(keytype.." complete")
|
||||||
|
end
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
python3 -m dkimpy_milter "$@"
|
||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
WORKDIR=$(mktemp -d)
|
||||||
|
TESTDIR=$(realpath "$(dirname "$0")")
|
||||||
|
DKIMPY_MILTER=${DKIMPY_MILTER:-"$TESTDIR/dkimpy-milter"}
|
||||||
|
KEY_TYPES=(ed25519 rsa)
|
||||||
|
|
||||||
|
cd "$WORKDIR"
|
||||||
|
|
||||||
|
printf "Testing %s from directory %s\n" "$DKIMPY_MILTER" "$WORKDIR"
|
||||||
|
|
||||||
|
for keytype in "${KEY_TYPES[@]}"; do
|
||||||
|
dknewkey --ktype "$keytype" "testkey.$keytype"
|
||||||
|
if [ "$keytype" = ed25519 ]; then
|
||||||
|
keyfile=KeyFileEd25519
|
||||||
|
selector=SelectorEd25519
|
||||||
|
else
|
||||||
|
keyfile=KeyFile
|
||||||
|
selector=Selector
|
||||||
|
fi
|
||||||
|
cat > "$keytype.signing.conf" <<EOF
|
||||||
|
Domain example.net
|
||||||
|
$keyfile testkey.$keytype.key
|
||||||
|
$selector testkey
|
||||||
|
Socket unix:$keytype.signing.sock
|
||||||
|
PidFile $keytype.signing.pid
|
||||||
|
Mode s
|
||||||
|
UserID $(id --name --user):$(id --name --group)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$keytype.verify.conf" <<EOF
|
||||||
|
Socket unix:$keytype.verify.sock
|
||||||
|
PidFile $keytype.verify.pid
|
||||||
|
Mode v
|
||||||
|
DNSOverride $(cat testkey.$keytype.dns)
|
||||||
|
UserID $(id --name --user):$(id --name --group)
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo cleaning up jobs:
|
||||||
|
jobs
|
||||||
|
for keytype in "${KEY_TYPES[@]}"; do
|
||||||
|
for func in signing verify; do
|
||||||
|
if [ -s "$keytype.$func.pid" ] && kill -0 "$(cat "$keytype.$func.pid")"; then
|
||||||
|
kill "$(cat $keytype.$func.pid)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
for keytype in "${KEY_TYPES[@]}"; do
|
||||||
|
for func in signing verify; do
|
||||||
|
errdata="$keytype.$func.stderr"
|
||||||
|
if [ -s "$errdata" ]; then
|
||||||
|
printf -- "-> %s:\n" "$errdata"
|
||||||
|
cat "$errdata"
|
||||||
|
printf -- "-> end %s\n" "$errdata"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
rm -rf "$WORKDIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
for keytype in "${KEY_TYPES[@]}"; do
|
||||||
|
for func in signing verify; do
|
||||||
|
PYTHONPATH="$(dirname "$TESTDIR")" "$DKIMPY_MILTER" "$keytype.$func.conf" 2>"$keytype.$func.stderr" &
|
||||||
|
done
|
||||||
|
done
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# ugly ugly (how are we supposed to know that the milters are all ready?):
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# uses miltertest from opendkim:
|
||||||
|
for x in ${TESTS:-"$TESTDIR"/*.miltertest}; do
|
||||||
|
if ! [ -e "$x" ]; then
|
||||||
|
if [ -e "$TESTDIR/$x" ]; then
|
||||||
|
x="$TESTDIR/$x"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
printf -- "-> running %s...\n" "$x"
|
||||||
|
miltertest -s "$x"
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user