././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677460454.4488747 dkimpy-milter-1.2.3/0000755000175100017510000000000000000000000014531 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/Authors.conf0000644000175100017510000001557300000000000017040 0ustar00kittermakittermaOption values and definitions derived from OpenDKIM 2.11.0 opendkim.conf.5.in: Copyright (c) 2007, 2008, Sendmail, Inc. and its suppliers. All rights reserved. See LICENSE.Sendmail. Copyright (c) 2009-2015, The Trusted Domain Project. All rights reserved. See LICENSE. Updates Copyright (c) 2018 Scott Kitterman Updates and the collective work distributed under the GPL, see COPYING. SENDMAIL OPEN SOURCE LICENSE The following license terms and conditions apply to this open source software ("Software"), unless a different license is obtained directly from Sendmail, Inc. ("Sendmail") located at 6475 Christie Ave, Suite 350, Emeryville, CA 94608, USA. Use, modification and redistribution (including distribution of any modified or derived work) of the Software in source and binary forms is permitted only if each of the following conditions of 1-6 are met: 1. Redistributions of the Software qualify as "freeware" or "open source software" under one of the following terms: (a) Redistributions are made at no charge beyond the reasonable cost of materials and delivery; or (b) Redistributions are accompanied by a copy of the modified Source Code (on an acceptable machine-readable medium) or by an irrevocable offer to provide a copy of the modified Source Code (on an acceptable machine-readable medium) for up to three years at the cost of materials and delivery. Such redistributions must allow further use, modification, and redistribution of the Source Code under substantially the same terms as this license. For the purposes of redistribution "Source Code" means the complete human-readable, compilable, linkable, and operational source code of the redistributed module(s) including all modifications. 2. Redistributions of the Software Source Code must retain the copyright notices as they appear in each Source Code file, these license terms and conditions, and the disclaimer/limitation of liability set forth in paragraph 6 below. Redistributions of the Software Source Code must also comply with the copyright notices and/or license terms and conditions imposed by contributors on embedded code. The contributors' license terms and conditions and/or copyright notices are contained in the Source Code distribution. 3. Redistributions of the Software in binary form must reproduce the Copyright Notice described below, these license terms and conditions, and the disclaimer/limitation of liability set forth in paragraph 6 below, in the documentation and/or other materials provided with the binary distribution. For the purposes of binary distribution, "Copyright Notice" refers to the following language: "Copyright (c) 1998-2009 Sendmail, Inc. All rights reserved." 4. Neither the name, trademark or logo of Sendmail, Inc. (including without limitation its subsidiaries or affiliates) or its contributors may be used to endorse or promote products, or software or services derived from this Software without specific prior written permission. The name "sendmail" is a registered trademark and service mark of Sendmail, Inc. 5. We reserve the right to cancel this license if you do not comply with the terms. This license is governed by California law and both of us agree that for any dispute arising out of or relating to this Software, that jurisdiction and venue is proper in San Francisco or Alameda counties. These license terms and conditions reflect the complete agreement for the license of the Software (which means this supercedes prior or contemporaneous agreements or representations). If any term or condition under this license is found to be invalid, the remaining terms and conditions still apply. 6. Disclaimer/Limitation of Liability: THIS SOFTWARE IS PROVIDED BY SENDMAIL AND ITS CONTRIBUTORS "AS IS" WITHOUT WARRANTY OF ANY KIND AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE ARE EXPRESSLY DISCLAIMED. IN NO EVENT SHALL SENDMAIL OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING WITHOUT LIMITATION NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. $Revision: 1.1 $ $Date: 2009/07/16 18:43:18 $ Copyright (c) 2009, 2010, 2012, 2013, The Trusted Domain Project. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of The Trusted Domain Project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. Portions of this project are also covered by the Sendmail Open Source License, available in this distribution in the file "LICENSE.Sendmail". See the copyright notice(s) in each file to determine whether it is covered by either or both of the licenses. For example: Copyright (c) Sendmail, Inc. and its suppliers. All rights reserved. Files bearing the banner above are covered under the Sendmail Open Source License (see LICENSE.Sendmail). Copyright (c) , The Trusted Domain Project. All rights reserved. Files bearing the banner above are covered under the Trusted Domain Project License (above). Files bearing both banners are covered under both sets of license terms. THIS SOFTWARE IS PROVIDED BY THE TRUSTED DOMAIN PROJECT ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE TRUSTED DOMAIN PROJECT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460389.0 dkimpy-milter-1.2.3/CHANGES0000644000175100017510000001753500000000000015537 0ustar00kittermakitterma1.2.3 2023-02-26 - Improve support for non-ASCII email messages. Anything UTF-8 should work (including correct signing/verification). For messages that contain header fields with non-ASCII or UTF-8 content, signatures are likely fail verification, but the milter should continue to run. (Thanks to Casper Bruun for help with this) - Set minimum pymilter and dkimpy versions in setup.py to those that will work reliably with non-ASCII content. - Fixed support for percent in KeyTable - Thanks to Mika Tiainen - Fix formatting for MinimumKeyBits in dkimpy-milter.conf(5) (Closes: #995335) - Reset the i= signature identity in get_identities_sign() (Closes: #981157) - Improve documentation of inter-relationship between Mode, InternalHosts, MacroList, and MacroListVerify options in dkimpy-milter.conf.5 (Closes: #969215) - Fix subdomain signing with top-level organizational domain (LP: #1999434) - Thanks to Matthias Hunstock for the report and the fix - Fix comma separated list processing in dkimpy_milter/config.py (LP: #1901445) 1.2.2 2020-08-09 - Improve README.md formating for markdown display on pypi - Improve documentation in dkimpy-milter.conf (5) and README.md for signing for multiple domains (Thanks to Stefano Rivera) - Minimal fix for dnspython 2.0.0 compatibility (still works with 1.16.0) 1.2.1 2020-01-04 - Fix expand option not to fail if files are missing since socket activation service files are not shipped in the sdist - Correct dkimpy-milter.conf file install location to match expand locations 1.2.0 2020-01-03 - Add support for SigningTable, KeyTable, and KeyTableEd25519 (LP: #1797397) - Add support for specifying MinimumKeyBits for RSA signatures - Add support for SignHeaders feature, thanks to Ralph Seichter for the patch - Add support for specifying DNSTimeout (bumps required dkimpy version to 1.0) - Add information on message content conversion to README - Add new expand option to setup.py so various file system locations can be specified at build/install time rather than being hard coded - Install openrc init file for Gentoo and other openrc users - Add support for passing PID file name on command line to make it easier to keep system init and daemon configuration in sync - Add support for storing DKIM failed mails in a specified DiagnosticDirectory - Fix startup logging so it provides information at a useful time - Fix verify processing so missing (optional) i= tag doesn't cause the milter to fail (LP: #1842250) - Fix message extraction so that signing in the same pass through the milter as verifying works correctly - Add debug logging for content type to assist troubleshooting MIME conversion issues - Fix variable initialization so mailformed mails missing body From do not cause a traceback (LP: #1844161) - Catch more ascii encoding errors to improve resilience against bad data (LP: #1844189) - Fix sysv init so it works (LP: #1839487) - Make error logging more explicit to aid debugging - Remove SigningTableEd25519 from documentation - it was never implemented and a per algorithm signing table turns out not to be needed - Delete own_socketfile to resolve race condition where the permissions change fails on a Unix socket because it hasn't been created yet (libmilter will do this correctly on its own based on umask, the milter doesn't need to do it) (LP: #1849712) 1.1.0 2019-04-12 - Add SubDomains option to enable signing for sub-domains (LP: #1811535) - Port to python3 (LP: #1815502) - Add test suite using opendkim miltertest - When Socket is absolute path, do not strip leading / - Handle unix: socket prefix the same as local: - Set up correct AuthservID defaults - config: Reassemble strings sensibly - Consistently prefer dnspython to Py3DNS (LP: #1815558) 1.0.1 2019-02-11 - Reorder milter start and dropping privileges so permissions on Unix socket are correct (LP: 1797720) - Make domain checks case insensitive for determining if signing should be done (LP: #1815311) - Add additional Sendmail configuration information to README from OpenDKIM update based on input from Дилян Палаузов (LP: #1801619) - Add information on Ed25519 key creation to README (LP: #1815313) 1.0.0 2018-05-11 - Minor documentation updates - Deleted reference to obsolete syslog target in unit file 0.9.7 2018-03-19 - Made sysv init executable - Add missing documentation key to system/dkimpy-milter.service - Put version directly in setup.py and do not import dkimpy_milter to ease install via pip - Minor sysv init improvments 0.9.6 2018-03-13 - 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 - The following configuration options are supported (same definition as OpenDKIM): Domain, KeyFile, KeyFileEd25519, Mode, PidFile, Selector, Socket, Syslog, UMask, and UserID (see dkimpy-milter.conf.5) - This is an Alpha grade release and while the implemented features work, it is nowhere near being a complete package ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/COPYING0000644000175100017510000004325400000000000015574 0ustar00kittermakitterma GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/MANIFEST.in0000644000175100017510000000022200000000000016263 0ustar00kittermakittermainclude etc/* include man/* include system/* include Authors.conf include TODO include README include COPYING include CHANGES include MANIFEST.in ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677460454.4488747 dkimpy-milter-1.2.3/PKG-INFO0000644000175100017510000004317200000000000015635 0ustar00kittermakittermaMetadata-Version: 2.1 Name: dkimpy-milter Version: 1.2.3 Summary: Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail. Home-page: https://launchpad.net/dkimpy-milter Author: Scott Kitterman Author-email: scott@kitterman.com License: UNKNOWN Download-URL: https://pypi.python.org/pypi/dkimpy-milter Description: # OVERVIEW This is a DKIM signing and verification milter. It has been tested with both Postfix and Sendmail. The configuration file is designed to be compatible with OpenDKIM, but only a subset of OpenDKIM options are supported. If an unsupported option is specified, an error will be raised. # INSTALLATION This package includes a default configuration file and man pages. For those to be installed when installing using setup.py, the following incantation is required because setuptools developers decided not being able to do this by default is a feature: [sudo] python3 setup.py install --single-version-externally-managed --record=/dev/null For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are available in either the main or backports repositories: [sudo] apt install python3-milter python3-nacl python3-dnspython [sudo] apt install -t stretch-backports python3-authres python3-dkim It is also available in the Debian package archive: [sudo] apt install dkimpy-milter [Debian 10 or later] [sudo] apt install -t stretch-backports dkimpy-milter [Debian 9] When installing using the Debian package, all dependencies are automatically installed. The preferred method of installation is from PyPi using pip (if distribution packages are not available): [sudo] pip install dkimpy_milter Using pip will cause required packages to be installed via easy_install if they have not been previously installed. Because pymilter and PyNaCl are compiled Python extensions, the system will need appropriate development packages and an C compiler. Alternately, install these dependencies from distribution/OS packages and then pip install dkimpy_milter. The milter will work with either py3dns (DNS) or dnspython (dns), preferring dnspython if both are available. The dkimpy DKIM module also works with either. ## NON-STANDARD INSTALLATION PATHS The package includes a custom setup command called expand. It allows various file locations in init scripts, man pages, and config files to be over-ridden at install time. expand: Expand @@ variables in input files, simlar to make macros. user_options: --sysconfigdir=, e: Specify system configuration directory. --sbindir=, s: Specify system binary directory [not used]. --bindir=, b: Specify binary directory. --rundir=,r: Specify run state directory. As an example, to change the run directory to /var/run, one would do: python3 setup.py expand --rundir=/var/run [sudo] python3 setup.py install --single-version-externally-managed \ --record=/dev/null or in a single step (the order matters): [sudo] python3 setup.py expand --rundir=/var/run install \ --single-version-externally-managed \ --record=/dev/null # SETUP ## SIGNING KEYS In order to create DKIM signatures, a private key must be available. Signing keys should be protected (owned by root:root with permissions 600 in a directory that is not world readable). Different keys are required for RSA and (if used) Ed25519. ### RSA Both public and private keys for RSA have standard formats and there are many tools available to create them. Keys must (RFC 8302) have a minimum size of 1024 bits and should have a size of at least 2048 bits. The dknewkey script that is provided with dkimpy is one such tool: dknewkey exampleprivkey will produce both the private key file (.key suffix) and a file with the DKIM public key record to be published DNS (.dns suffix). RSA is the default key type. 2048 bits is the default key size. ### ED25519 There is no standardized non-binary representation for Ed25519 private keys, so in order to generate Ed25519 keys for dkimpy-milter, dkimpy specific tools must be used to be compatible. The same dknewkey script support Ed25519: dknewkey --ktype ed25519 anothernewkey will provide both the private key file (.key suffix) and a file with the DKIM public key record to be published DNS (.dns suffix). Ed25519 keys do not have variable bit lengths. ### COMPLEX SIGNING CONFIGURATIONS The KeyTable, KeyTableEd25519, and SigningTable are used to define signing instructions to the filter where use of Domain, Selector and KeyFile together are insufficient. First, select the type of database you will use for each. They need not be the same. The "DATA SETS" portion of the dkimpy-milter(8) man page describes the possibilities and how they are formatted. Then, construct those databases. Let's suppose you want to sign for two domains, example.com and example.net. Within example.com, you want to sign for user "president" differently than everyone else. Let's say further that you want to use a flat text file. You've generated private key files for each of these and stored them in the directory /usr/local/etc/dkim/keys as files "president", "excom" and "exnet", with the obvious intents. You want to use selectors "foo", "bar" and "baz" for those, respectively. The signing domains match the senders (i.e. the signatures for example.com's stuff will be held by example.com, and example.net likewise). First, write the KeyTable. This is a list of the keys you intend to use, and you just assign arbitrary names to them. So as a flat file, the KeyTable for the above might look like this: preskey example.com:foo:/usr/local/etc/dkim/keys/president comkey example.com:bar:/usr/local/etc/dkim/keys/excom netkey example.net:baz:/usr/local/etc/dkim/keys/exnet If also signing with ed25519, specify a KeyTableEd25519, with the same names, pointing to the keys needed for ed25519. Both KeyTable and KeyTableEd25519 are evaluated if there is a SigningTable (see below). Per the documentation, multi-field data sets that are made of flat files have the fields separated by colons, but the key and value(s) are separated by whitespace. So now we've named each key file, and specified with which selector and domain each will be used, and then given each of those groupings a name. This is your KeyTable. Let's say you put it in /usr/local/etc/dkim/keytable. Next, write the SigningTable. This maps senders (by default, taken from the From: header field of a message passing through the filter) to which keys will be used to sign their mail. Wildcards are allowed. So to do what was described above, we write it as follows: president@example.com preskey *@example.com comkey *@example.net netkey Since we want to use wildcards, we can't actually use a regular flat file. Wildcards require a regular expression file, or "refile". The above is valid format for one of those. Let's say you put this in /usr/local/etc/dkim/signingtable. Finally, tell the filter that it should use these files by adding this to your configuration file: KeyTable /usr/local/etc/dkim/keytable SigningTable refile:/usr/local/etc/dkim/signingtable You could put "file:" in front of the filename for the KeyTable just to be precise, but "file:" is assumed if the value starts with a "/". Note: Unlike opendkim, dkimpy-milter will check for "\*" in the signing table regardless of if refile is specified or not. Use of refile is supported for compatibility with configurations initially developed for use with opendkim. ## MTA INTEGRATION Both a systemd unit file and a sysv init file are provided. Both make assumptions about defaults being used, e.g. if a non-standard pidfile name is used, they will need to be updated. The sysv init file uses start-stop-deamon from Debian. It is not portable to systems without that available. The dkimpy-milter drops priviledges after setup to the user/group specified in UserID. During initial setup, this system user needs to be manually created. As an example, using the default dkimpy-user on Debian, the command would be: [sudo] adduser --system --no-create-home --quiet --disabled-password \ --disabled-login --shell /bin/false --group \ --home /run/dkimpy-milter dkimpy-milter Since /var/run or /run is sometimes on a tempfs, if the PID file directory is missing, the milter will create it on startup. To start dkimpy-milter with systemd for the first time, you will need to take the following steps: [sudo] systemctl daemon-reload [sudo] systemctl enable dkimpy-milter [sudo] systemctl start dkimpy-milter [sudo] systemctl status dkimpy-milter (to verify it started correctly) As with all milters, dkimpy-milter needs to be integrated with your MTA of choice (Sendmail or Postfix). When integrating with your MTA, the risk of signature invalidation due to content conversion of the message body needs to be considered. See RFC 6376, Section 5.3 for discussion of this issue. As a practical matter, when signing, configure the milter to follow all others that might modify the message body. When verifying, configure the milter before other processes that might modify the message body. ### SENDMAIL Configuration is very similar to opendkim, but needs some adjustment for dkimpy-milter. Here's an example configuration line to include in your sendmail.mc: INPUT_MAIL_FILTER(`dkimpy-milter', `S=local:/run/dkimpy-milter/dkimpy-milter.sock')dnl Changing the sendmail.mc file requires a Make (to compile it into sendmail.cf) and a restart of sendmail. Note that S= needs to match the value of Socket in the dkimpy-milter configuration file. Milter support should be present by default in most versions of sendmail these days, but if not included in your Sendmail build, see: http://www.elandsys.com/resources/sendmail/milter.html #### ISSUES USING SENDMAIL TO SIGN AND VERIFY When using the sendmail MTA in both signing and verifying mode, there are a few issues of which to be aware that might cause operational problems and deserve consideration. (a) When the MTA will be used for relaying emails, e.g. delivering to other hosts using the aliases mechanism, it is important not to break signatures inserted by the original sender. This is particularly sensitive particular when the sending domain has published a "reject" DMARC policy. By default, sendmail quotes to address header fields when there are no quotes and the display part of the address contains a period or an apostrophe. However, dkimpy-milter 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 ,anotherloo...ong name b ...might be rewritten as: To: very long name , anotherloo...ong name b This rewrite is also done after dkimpy-milter has seen the message, meaning the signature dkimpy-milter 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 dkimpy-milter 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. Support for non-ASCII email messages: Anything UTF-8 should work (including correct signing/verification). For messages that contain header fields with non-ASCII or UTF-8 content, signatures are likely fail verification, but the milter should continue to run. RFC 8616 is not supported. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Communications :: Email :: Mail Transport Agents Classifier: Topic :: Communications :: Email :: Filters Classifier: Topic :: Security Description-Content-Type: text/markdown ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677459926.0 dkimpy-milter-1.2.3/README.md0000644000175100017510000003404600000000000016017 0ustar00kittermakitterma# OVERVIEW This is a DKIM signing and verification milter. It has been tested with both Postfix and Sendmail. The configuration file is designed to be compatible with OpenDKIM, but only a subset of OpenDKIM options are supported. If an unsupported option is specified, an error will be raised. # INSTALLATION This package includes a default configuration file and man pages. For those to be installed when installing using setup.py, the following incantation is required because setuptools developers decided not being able to do this by default is a feature: [sudo] python3 setup.py install --single-version-externally-managed --record=/dev/null For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are available in either the main or backports repositories: [sudo] apt install python3-milter python3-nacl python3-dnspython [sudo] apt install -t stretch-backports python3-authres python3-dkim It is also available in the Debian package archive: [sudo] apt install dkimpy-milter [Debian 10 or later] [sudo] apt install -t stretch-backports dkimpy-milter [Debian 9] When installing using the Debian package, all dependencies are automatically installed. The preferred method of installation is from PyPi using pip (if distribution packages are not available): [sudo] pip install dkimpy_milter Using pip will cause required packages to be installed via easy_install if they have not been previously installed. Because pymilter and PyNaCl are compiled Python extensions, the system will need appropriate development packages and an C compiler. Alternately, install these dependencies from distribution/OS packages and then pip install dkimpy_milter. The milter will work with either py3dns (DNS) or dnspython (dns), preferring dnspython if both are available. The dkimpy DKIM module also works with either. ## NON-STANDARD INSTALLATION PATHS The package includes a custom setup command called expand. It allows various file locations in init scripts, man pages, and config files to be over-ridden at install time. expand: Expand @@ variables in input files, simlar to make macros. user_options: --sysconfigdir=, e: Specify system configuration directory. --sbindir=, s: Specify system binary directory [not used]. --bindir=, b: Specify binary directory. --rundir=,r: Specify run state directory. As an example, to change the run directory to /var/run, one would do: python3 setup.py expand --rundir=/var/run [sudo] python3 setup.py install --single-version-externally-managed \ --record=/dev/null or in a single step (the order matters): [sudo] python3 setup.py expand --rundir=/var/run install \ --single-version-externally-managed \ --record=/dev/null # SETUP ## SIGNING KEYS In order to create DKIM signatures, a private key must be available. Signing keys should be protected (owned by root:root with permissions 600 in a directory that is not world readable). Different keys are required for RSA and (if used) Ed25519. ### RSA Both public and private keys for RSA have standard formats and there are many tools available to create them. Keys must (RFC 8302) have a minimum size of 1024 bits and should have a size of at least 2048 bits. The dknewkey script that is provided with dkimpy is one such tool: dknewkey exampleprivkey will produce both the private key file (.key suffix) and a file with the DKIM public key record to be published DNS (.dns suffix). RSA is the default key type. 2048 bits is the default key size. ### ED25519 There is no standardized non-binary representation for Ed25519 private keys, so in order to generate Ed25519 keys for dkimpy-milter, dkimpy specific tools must be used to be compatible. The same dknewkey script support Ed25519: dknewkey --ktype ed25519 anothernewkey will provide both the private key file (.key suffix) and a file with the DKIM public key record to be published DNS (.dns suffix). Ed25519 keys do not have variable bit lengths. ### COMPLEX SIGNING CONFIGURATIONS The KeyTable, KeyTableEd25519, and SigningTable are used to define signing instructions to the filter where use of Domain, Selector and KeyFile together are insufficient. First, select the type of database you will use for each. They need not be the same. The "DATA SETS" portion of the dkimpy-milter(8) man page describes the possibilities and how they are formatted. Then, construct those databases. Let's suppose you want to sign for two domains, example.com and example.net. Within example.com, you want to sign for user "president" differently than everyone else. Let's say further that you want to use a flat text file. You've generated private key files for each of these and stored them in the directory /usr/local/etc/dkim/keys as files "president", "excom" and "exnet", with the obvious intents. You want to use selectors "foo", "bar" and "baz" for those, respectively. The signing domains match the senders (i.e. the signatures for example.com's stuff will be held by example.com, and example.net likewise). First, write the KeyTable. This is a list of the keys you intend to use, and you just assign arbitrary names to them. So as a flat file, the KeyTable for the above might look like this: preskey example.com:foo:/usr/local/etc/dkim/keys/president comkey example.com:bar:/usr/local/etc/dkim/keys/excom netkey example.net:baz:/usr/local/etc/dkim/keys/exnet If also signing with ed25519, specify a KeyTableEd25519, with the same names, pointing to the keys needed for ed25519. Both KeyTable and KeyTableEd25519 are evaluated if there is a SigningTable (see below). Per the documentation, multi-field data sets that are made of flat files have the fields separated by colons, but the key and value(s) are separated by whitespace. So now we've named each key file, and specified with which selector and domain each will be used, and then given each of those groupings a name. This is your KeyTable. Let's say you put it in /usr/local/etc/dkim/keytable. Next, write the SigningTable. This maps senders (by default, taken from the From: header field of a message passing through the filter) to which keys will be used to sign their mail. Wildcards are allowed. So to do what was described above, we write it as follows: president@example.com preskey *@example.com comkey *@example.net netkey Since we want to use wildcards, we can't actually use a regular flat file. Wildcards require a regular expression file, or "refile". The above is valid format for one of those. Let's say you put this in /usr/local/etc/dkim/signingtable. Finally, tell the filter that it should use these files by adding this to your configuration file: KeyTable /usr/local/etc/dkim/keytable SigningTable refile:/usr/local/etc/dkim/signingtable You could put "file:" in front of the filename for the KeyTable just to be precise, but "file:" is assumed if the value starts with a "/". Note: Unlike opendkim, dkimpy-milter will check for "\*" in the signing table regardless of if refile is specified or not. Use of refile is supported for compatibility with configurations initially developed for use with opendkim. ## MTA INTEGRATION Both a systemd unit file and a sysv init file are provided. Both make assumptions about defaults being used, e.g. if a non-standard pidfile name is used, they will need to be updated. The sysv init file uses start-stop-deamon from Debian. It is not portable to systems without that available. The dkimpy-milter drops priviledges after setup to the user/group specified in UserID. During initial setup, this system user needs to be manually created. As an example, using the default dkimpy-user on Debian, the command would be: [sudo] adduser --system --no-create-home --quiet --disabled-password \ --disabled-login --shell /bin/false --group \ --home /run/dkimpy-milter dkimpy-milter Since /var/run or /run is sometimes on a tempfs, if the PID file directory is missing, the milter will create it on startup. To start dkimpy-milter with systemd for the first time, you will need to take the following steps: [sudo] systemctl daemon-reload [sudo] systemctl enable dkimpy-milter [sudo] systemctl start dkimpy-milter [sudo] systemctl status dkimpy-milter (to verify it started correctly) As with all milters, dkimpy-milter needs to be integrated with your MTA of choice (Sendmail or Postfix). When integrating with your MTA, the risk of signature invalidation due to content conversion of the message body needs to be considered. See RFC 6376, Section 5.3 for discussion of this issue. As a practical matter, when signing, configure the milter to follow all others that might modify the message body. When verifying, configure the milter before other processes that might modify the message body. ### SENDMAIL Configuration is very similar to opendkim, but needs some adjustment for dkimpy-milter. Here's an example configuration line to include in your sendmail.mc: INPUT_MAIL_FILTER(`dkimpy-milter', `S=local:/run/dkimpy-milter/dkimpy-milter.sock')dnl Changing the sendmail.mc file requires a Make (to compile it into sendmail.cf) and a restart of sendmail. Note that S= needs to match the value of Socket in the dkimpy-milter configuration file. Milter support should be present by default in most versions of sendmail these days, but if not included in your Sendmail build, see: http://www.elandsys.com/resources/sendmail/milter.html #### ISSUES USING SENDMAIL TO SIGN AND VERIFY When using the sendmail MTA in both signing and verifying mode, there are a few issues of which to be aware that might cause operational problems and deserve consideration. (a) When the MTA will be used for relaying emails, e.g. delivering to other hosts using the aliases mechanism, it is important not to break signatures inserted by the original sender. This is particularly sensitive particular when the sending domain has published a "reject" DMARC policy. By default, sendmail quotes to address header fields when there are no quotes and the display part of the address contains a period or an apostrophe. However, dkimpy-milter 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 ,anotherloo...ong name b ...might be rewritten as: To: very long name , anotherloo...ong name b This rewrite is also done after dkimpy-milter has seen the message, meaning the signature dkimpy-milter 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 dkimpy-milter 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. Support for non-ASCII email messages: Anything UTF-8 should work (including correct signing/verification). For messages that contain header fields with non-ASCII or UTF-8 content, signatures are likely fail verification, but the milter should continue to run. RFC 8616 is not supported. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/TODO0000644000175100017510000000522000000000000015220 0ustar00kittermakittermaTODO 0.9.1 (Alpha) Sign rsa/ed25519 implemented verified Verify rsa/ed25519 implemented verified Domain implemented verified KeyFile implemented verified KeyFileEd25519 implemented verified Mode implemented verified PidFile implemented verified Selector implemented verified Socket implemented verified Syslog implemented verified UMask implemented UserID 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) MacroList implemented verified MacroListVerify implemented verified SyslogSuccess implemented verified 1.0.0 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 1.2.0 DNSTimeout (dkimpy 1.0) implemented verified by inspection KeyTable implemented verified KeytableEd25519 implemented verified MinimumKeyBits implemented verified SignHeaders implemented verified by inspection SigningTable implemented verified TemporaryDirectory implemented verified by inspection Planned dataset type support (if needed): mdb: Considered for near-term feature release AlwaysAddARHeader ChangeRootDirectory ClockDrift (requires dkimpy change) MilterDebug OmitHeaders OversignHeaders (may require dkimpy changes) PeerList SignatureAlgorithm Later BaseDirectory Diagnostics (requires dkimpy changes) DontSignMailTo ExemptDomains ExternalIgnoreList FixCRLF KeepAuthResults KeepTemporaryFiles LogResults LogWhy MaximumHeaders MaximumSignaturesToVerify MultipleSignatures MustBeSigned NoHeaderB On-BadSignature On-Default On-DNSError On-InternalError On-KeyNotFound On-NoSignature On-SignatureError RemoveARAll RemoveARFrom RemoveOldSignatures RequiredHeaders RequireSafeKeys SignatureAlgorithm SignatureTTL SoftwareHeader StrictHeaders TestDNSData TestPublicKeys Other OpenDKIM options not contemplated ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677460454.4488747 dkimpy-milter-1.2.3/dkimpy_milter/0000755000175100017510000000000000000000000017402 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460132.0 dkimpy-milter-1.2.3/dkimpy_milter/__init__.py0000644000175100017510000005776100000000000021533 0ustar00kittermakitterma#! /usr/bin/python3 # Original dkim-milter.py code: # Author: Stuart D. Gathman # Copyright 2007 Business Management Systems, Inc. # This code is under GPL. See COPYING for details. # dkimpy-milter: A DKIM signing/verification Milter application # Author: Scott Kitterman # Copyright 2018 Scott Kitterman """ 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.""" import sys import syslog import Milter import dkim import authres import os import tempfile import io import re import codecs from Milter.utils import parse_addr, parseaddr import dkimpy_milter.config as config from dkimpy_milter.util import drop_privileges from dkimpy_milter.util import setExceptHook from dkimpy_milter.util import write_pid from dkimpy_milter.util import get_keys from dkimpy_milter.util import fold __version__ = "1.2.3" FWS = re.compile(r'\r?\n[ \t]+') class dkimMilter(Milter.Base): "Milter to check and sign DKIM. Each connection gets its own instance." def __init__(self): self.mailfrom = None self.id = Milter.uniqueID() # we don't want config used to change during a connection self.conf = milterconfig self.fp = None self.fdomain = '' self.iequals = None @Milter.noreply def connect(self, hostname, unused, hostaddr): self.internal_connection = False self.external_connection = False self.hello_name = None # sometimes people put extra space in sendmail config, so we strip self.receiver = self.getsymval('j') if self.receiver is not None: self.receiver = self.receiver.strip() try: self.AuthservID = self.conf['AuthservID'] except: self.AuthservID = self.receiver if hostaddr and len(hostaddr) > 0: ipaddr = hostaddr[0] if self.conf['IntHosts']: if self.conf['IntHosts'].match(ipaddr): self.internal_connection = True else: ipaddr = '' self.connectip = ipaddr if self.conf.get('MacroList') and not self.internal_connection: macrolist = self.conf.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 self.conf.get('MacroListVerify'): macrolist = self.conf.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: connecttype = 'INTERNAL' else: connecttype = 'EXTERNAL' if self.conf.get('Syslog') and self.conf.get('debugLevel') >= 1: syslog.syslog("connect from {0} at {1} {2}" .format(hostname, hostaddr, connecttype)) if self.conf.get('Syslog') and self.conf.get('debugLevel') >= 3: syslog.syslog("internal_conn: {0}, external_conn: {1}" .format(self.internal_connection, self.external_connection)) return Milter.CONTINUE # multiple messages can be received on a single connection # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start # of each message. @Milter.noreply def envfrom(self, f, *moredata): try: f = str(codecs.encode(f, 'UTF-8', 'replace'), 'UTF-8', 'ignore') except TypeError: f = codecs.encode(f, 'UTF-8', 'replace').decode() try: moredata = str(codecs.encode(str(moredata), 'UTF-8', 'replace'), 'UTF-8', 'ignore') except TypeError: moredata = codecs.encode(str(moredata), 'UTF-8', 'replace').decode() if self.conf.get('Syslog') and self.conf.get('debugLevel') >= 2: syslog.syslog("mail from: {0} {1}".format(f, moredata)) self.fp = io.BytesIO() self.mailfrom = f t = parse_addr(f) if len(t) == 2: t[1] = t[1].lower() self.canon_from = '@'.join(t) self.has_dkim = 0 self.author = None self.arheaders = [] self.arresults = [] return Milter.CONTINUE @Milter.noreply def header(self, name, val): lname = name.lower() if self.conf.get('Syslog') and self.conf.get('debugLevel') >= 4: if lname == 'content-transfer-encoding': syslog.syslog("content-transfer-encodeing: {0}".format(val)) if lname == 'content-type': syslog.syslog("content-type: {0}".format(val)) if lname == 'dkim-signature': if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 1): syslog.syslog("{0}: {1}".format(name, val)) self.has_dkim += 1 if lname == 'from': fname, self.author = parseaddr(val) try: self.fdomain = self.author.split('@')[1].lower() except IndexError as er: pass # self.author was not a proper email address # This keeps non-ascii characters out of the From domain try: self.fdomain = str(codecs.encode(self.fdomain, 'ascii', 'replace'), 'ascii', 'ignore') except TypeError: self.fdomain = codecs.encode(self.fdomain, 'ascii', 'replace').decode('ascii','ignore') if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 1): syslog.syslog("{0}: {1}".format(name, val)) elif lname == 'authentication-results': self.arheaders.append(val) if self.fp: try: if lname == 'from': # Non-ascii in email address localpart is legal, so this is a special case self.fp.write(b"%s: %s\n" % (codecs.encode(name, 'ascii'), codecs.encode(val, 'UTF-8', 'replace'))) else: 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 @Milter.noreply def eoh(self): if self.fp: self.fp.write(b"\n") # terminate headers self.bodysize = 0 return Milter.CONTINUE @Milter.noreply def body(self, chunk): # copy body to temp file if self.fp: self.fp.write(chunk) # IOError causes TEMPFAIL in milter self.bodysize += len(chunk) return Milter.CONTINUE def eom(self): if not self.fp: return Milter.ACCEPT # no message collected - so no eom processing # Remove existing Authentication-Results headers for our authserv_id for i, val in enumerate(self.arheaders, 1): # FIXME: don't delete A-R headers from trusted MTAs try: ar = (authres.AuthenticationResultsHeader .parse_value(FWS.sub('', val))) if ar.authserv_id == self.AuthservID: self.chgheader('authentication-results', i, '') if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 1): syslog.syslog('REMOVE: {0}'.format(val)) except: # Don't error out on unparseable AR header fiels pass # Check and/or sign DKIM if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 4): syslog.syslog('self.conf: {0}'.format(self.conf)) self.fp.seek(0) txt = self.fp.read() self.get_identities_sign() if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 3): syslog.syslog('self.domain: {0}, self.fdomain: {1}, self.iequals: {2}'.format(self.domain, self.fdomain, self.iequals)) if ((self.fdomain in self.domain) and not self.conf.get('Mode') == 'v' and not self.external_connection): if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 3): syslog.syslog("Signing DKIM") self.sign_dkim(txt) if ((self.has_dkim) and (not self.internal_connection) and (self.conf.get('Mode') == 'v' or self.conf.get('Mode') == 'sv')): self.check_dkim(txt) if self.arresults: h = authres.AuthenticationResultsHeader(authserv_id= self.AuthservID, results=self.arresults) h = fold(codecs.encode(str(h), 'ascii')) if (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 2): syslog.syslog(codecs.decode(h, 'ascii')) name, val = codecs.decode(h, 'ascii').split(': ', 1) self.addheader(name, val, 0) return Milter.CONTINUE # get parent domain to be signed for if fdomain is a subdomain def get_parent_domain(self, fdomain, domains): for domain in domains: rhs = '.'+domain # compare right hand side of fdomain against .domain if fdomain[-len(rhs):] == rhs: # return parent domain on match syslog.syslog('domain: {0}'.format(domain)) return domain # or return the fdomain itself return fdomain def get_identities_sign(self): """Determine d= and i= identiies for signature""" self.domain = [] self.iequals = None try: self.privkeyRSA = self.conf.get('privateRSA') except: self.privkeyRSA = '' try: self.privkeyEd25519 = self.conf.get('privateEd25519') except: self.privkeyEd25519 = '' try: self.selectorRSA = self.conf.get('Selector') except: self.selectorRSA = '' try: self.selectorEd25519 = self.conf.get('SelectorEd25519') except: self.selectorEd25519 = '' if not self.domain and self.conf.get('Domain'): self.domain = self.conf.get('Domain') if self.conf.get('SubDomains'): self.fdomain = self.get_parent_domain(self.fdomain, self.domain) if self.conf.get('SigningTable'): match = False for dictkey, dictvalues in self.conf.get('SigningTable').items(): if dictkey == '%': self.domain.append(self.fdomain) match = True elif len(dictkey.split('*')) == 1: if dictkey == self.author: self.domain.append(self.fdomain) match = True else: if len(dictkey.split('*')) == 2: if dictkey.split('*')[1] == self.author[-len(dictkey.split('*')[1]):]: self.domain.append(self.fdomain) match = True self.domain.append(self.fdomain) try: if len(dictvalues) == 2 and match: if dictvalues[0] =='%': self.iequals = codecs.encode('@' + self.fdomain) elif dictvalues[0][1:] == self.fdomain or self.get_parent_domain(dictvalues[0][1:], self.domain) == self.fdomain: self.iequals = codecs.encode(dictvalues[0]) except IndexError: pass if match: #TODO add KeyTable stuffs here. keytablekey = dictvalues[-1] # Last value in the SigningTable row. if self.conf.get('privateRSATable'): # Table data is a list of [ signing domain, selector, key ] keytabledata = self.conf.get('privateRSATable')[keytablekey] try: self.fdomain = keytabledata[0] self.domain.append(self.fdomain) self.selectorRSA = keytabledata[1] self.privkeyRSA = keytabledata[2] except: if (self.conf.get('Syslog')): syslog.syslog('Error: Invalid KeyTable data {0}'.format(keytabledata)) if self.conf.get('privateEd25519Table'): # Table data is a list of [ signing domain, selector, key ] keytabledata = self.conf.get('privateEd25519Table')[keytablekey] try: self.fdomain = keytabledata[0] self.domain.append(self.fdomain) self.selectorEd25519 = keytabledata[1] self.privkeyEd25519 = keytabledata[2] except: if (self.conf.get('Syslog')): syslog.syslog('Error: Invalid KeyTable data {0}'.format(keytabledata)) if (self.fdomain == '%'): self.fdomain = self.author.split('@')[1].lower() break def sign_dkim(self, txt): canon = codecs.encode(self.conf.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 (self.conf.get('Syslog') and self.conf.get('debugLevel') >= 1): syslog.syslog('canonicalize: {0}'.format(canonicalize)) sign_headers = self.conf.get('SignHeaders') if not sign_headers: # None or empty. DKIM explicitly tests for None. sign_headers = None try: if self.privkeyRSA: d = dkim.DKIM(txt) h = d.sign(codecs.encode(self.selectorRSA, 'ascii'), codecs.encode(self.fdomain, 'ascii'), codecs.encode(self.privkeyRSA, 'ascii'), canonicalize=(canonicalize[0], canonicalize[1]), identity=self.iequals, include_headers=sign_headers) name, val = h.split(b': ', 1) self.addheader(codecs.decode(name, 'ascii'), codecs.decode(val, 'ascii').strip().replace('\r\n', '\n'), 0) if (self.conf.get('Syslog') and (self.conf.get('SyslogSuccess') or self.conf.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 self.privkeyEd25519: d = dkim.DKIM(txt) h = d.sign(codecs.encode(self.selectorEd25519, 'ascii'), codecs.encode(self.fdomain, 'ascii'), self.privkeyEd25519, canonicalize=(canonicalize[0], canonicalize[1]), identity=self.iequals, include_headers=sign_headers, 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 (self.conf.get('Syslog') and (self.conf.get('SyslogSuccess') or self.conf.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: if self.conf.get('Syslog'): syslog.syslog('DKIM: {0}'.format(x)) except Exception as x: if self.conf.get('Syslog'): syslog.syslog("sign_dkim: {0}".format(x)) raise def check_dkim(self, txt): res = False self.header_a = None for y in range(self.has_dkim): # Verify _ALL_ the signatures d = dkim.DKIM(txt, minkey=self.conf.get('MinimumKeyBits'), timeout=self.conf.get('DNSTimeout')) try: dnsoverride = self.conf.get('DNSOverride') if isinstance(dnsoverride, str): timeout = 5 domain = self.fdomain def dnsfunc(domain, timeout=timeout, dnsoverride=dnsoverride): return dnsoverride syslog.syslog("DNSOverride: {0}".format(dnsoverride)) res = d.verify(idx=y, dnsfunc=dnsfunc) else: 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: self.dkim_comment = str(x) if self.conf.get('Syslog'): syslog.syslog('DKIM: {0}'.format(x)) except Exception as x: self.dkim_comment = str(x) if self.conf.get('Syslog'): syslog.syslog("check_dkim: Internal program fault while verifying: {0}".format(x)) try: # i= is optional and dkimpy is fine if it's not provided self.header_i = codecs.decode(d.signature_fields.get(b'i'), 'ascii') except TypeError as x: self.header_i = None try: self.header_d = codecs.decode(d.signature_fields.get(b'd'), 'ascii') self.header_a = codecs.decode(d.signature_fields.get(b'a'), 'ascii') except Exception as x: self.dkim_comment = str(x) if self.conf.get('Syslog'): syslog.syslog("check_dkim: Internal program fault extracting header a or d: {0}".format(x)) self.header_d = None if not self.header_a: self.header_a = 'rsa-sha256' if res: if (self.conf.get('Syslog') and (self.conf.get('SyslogSuccess') or self.conf.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: if self.conf.get('DiagnosticDirectory'): tempfile.tempdir = self.conf.get('DiagnosticDirectory') fd, fname = tempfile.mkstemp(".dkim") with os.fdopen(fd, "w+b") as fp: fp.write(txt) if self.conf.get('Syslog'): syslog.syslog('DKIM: Fail (saved as {0})' .format(fname)) else: if self.conf.get('Syslog'): if d.domain: syslog.syslog('DKIM: Fail ({0})' .format(d.domain.lower())) else: syslog.syslog('DKIM: Fail, unextractable domain') if res: result = 'pass' else: result = 'fail' res = False if self.header_d: self.arresults.append( authres.DKIMAuthenticationResult(result=result, header_i=self.header_i, header_d=self.header_d, header_a=self.header_a, result_comment= self.dkim_comment) ) self.header_a = None return def main(): # Ugh, but there's no easy way around this. global milterconfig configFile = '/usr/local/etc/dkimpy-milter.conf' if len(sys.argv) > 1: if (sys.argv[1] in ('-?', '--help', '-h')) or len(sys.argv) == 3 or \ (len(sys.argv) == 4 and sys.argv[2] != '-P'): print('usage: dkimpy-milter [ [-P ]]') sys.exit(1) configFile = sys.argv[1] milterconfig = config._processConfigFile(filename=configFile) if len(sys.argv) == 4: if sys.argv[2] == '-P': # Command line PID file argument overrides config file milterconfig['PidFile'] = sys.argv[3] if milterconfig.get('Syslog'): facility = eval("syslog.LOG_{0}" .format(milterconfig.get('SyslogFacility').upper())) syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PID, facility) setExceptHook() pid = write_pid(milterconfig) milterconfig = get_keys(milterconfig) Milter.factory = dkimMilter Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS) miltername = 'dkimpy-filter' 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' sys.stdout.flush() if milterconfig.get('Syslog'): syslog.syslog('dkimpy-milter starting:{0} user:{1}' .format(pid, milterconfig.get('UserID'))) drop_privileges(milterconfig) Milter.runmilter(miltername, socketname, 240) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/dkimpy_milter/__main__.py0000644000175100017510000000013200000000000021470 0ustar00kittermakitterma#!/usr/bin/python3 from dkimpy_milter import main if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677459926.0 dkimpy-milter-1.2.3/dkimpy_milter/config.py0000644000175100017510000004156400000000000021233 0ustar00kittermakitterma# -*- coding: utf-8 -*- # # Tumgreyspf # Copyright © 2004-2005, Sean Reifschneider, tummy.com, ltd. # # pypolicyd-spf changes # Copyright © 2007,2008,2009,2010 Scott Kitterman # # dkimpy-milter changes # Copyright © 2018 Scott Kitterman # Note: Derived from pypolicydspfsupp.py version before relicensing to Apache # 2.0 license - 100% GPL ''' This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.''' import syslog import os import sys import stat import dkim import socket import ipaddress from .dnsplug import Session # default values defaultConfigData = { 'Syslog': 'yes', 'SyslogFacility': 'mail', 'UMask': 0o07, 'Mode': 'sv', 'MinimumKeyBits': 1024, 'Socket': None, 'PidFile': None, 'UserID': 'dkimpy-milter', 'Canonicalization': 'relaxed/simple', 'InternalHosts': '127.0.0.1', 'IntHosts': False, 'DiagnosticDirectory': '', 'MacroList': '', 'MacroListVerify': '', 'DNSOverride': None, 'DNSTimeout': 5, 'SubDomains': False, 'SigningTable': None, 'debugLevel': 0 # Undocumented config item for developer use } class ConfigException(Exception): '''Exception raised when there's a configuration file error.''' 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: try: self.item = ipaddress.ip_address(str(self.item, "utf-8")) except TypeError: self.item = ipaddress.ip_address(self.item) if isinstance(self.item, ipaddress.IPv4Address): self.isipv4 = True elif isinstance(self.item, ipaddress.IPv6Address): self.isipv6 = True except ValueError as e: try: try: self.item = ipaddress.ip_network(str (self.item, "utf-8"), strict=False) except TypeError: self.item = ipaddress.ip_network(self.item, 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''' try: source = ipaddress.ip_address(str(connectip, "utf-8")) except TypeError: source = ipaddress.ip_address(connectip) 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', timeout=self.conf.get('DNSTimeout')) for name in ptrnames: if isinstance(source, ipaddress.IPv4Address): ips = s.dns(name, 'A') for ip in ips: try: ip = ipaddress.IPv4Address(str(ip, 'UTF-8')) except TypeError: ip = ipaddress.IPv4Address(ip) if ip == source: results.append(name) if isinstance(source, ipaddress.IPv6Address): ips = s.dns(name, 'AAAA') for ip in ips: try: ip = ipaddress.IPv6Address(str(ip, 'UTF-8')) except TypeError: ip = ipaddress.IPv6Address(ip) 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, useStderr=0): '''Load the specified config file, exit and log errors if it fails, otherwise return a config dictionary.''' from . import config if configdata is None: configdata = config.defaultConfigData if filename is not None: try: _readConfigFile(filename, configdata) except Exception as e: raise if useSyslog: syslog.syslog(e.args[0]) if useStderr: sys.stderr.write('%s\n' % e.args[0]) sys.exit(1) return(configdata) def _find_boolean(item): if type(item) == int: item = str(item) if item[0] in ["T", "t", "Y", "y", "1"]: item = True elif item[0] in ["F", "f", "N", "n", "0"]: item = False else: raise dkim.ParameterError() return item def _make_authserv_id(as_id): """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. For multiline datasets like KeyTable and SigningTable a key : values dictionary is returned""" 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:' or dataset[:7] == 'refile:': # This is a flat file dataset, which are key value:value stores ds = [] dsd = {} if dataset[0] == '/' or dataset[:2] == './' or dataset[:3] == '../': dsname = dataset elif dataset[:5] == 'file:': dsname = dataset[5:] elif dataset[:7] == 'refile:': dsname = dataset[7:] dsf = open(dsname, 'r') for line in dsf.readlines(): if line[0] != '#': if len(line.split()) == 1: if len(line.split(':')) == 1: ds.append(line.strip()) else: for element in line.split(':'): ds.append(element.strip().strip(':')) elif len(line.split()) == 2: # key value:value:value key, values = line.split() values = values.split(':') dsd.update({key:values}) dsf.close() if ds: return ds elif dsd: return dsd # If it's a str and csl, it has one value and we return a list if dataset[:4] == 'csl:': datalist = dataset[4:].split(',') for item in datalist: datalist[datalist.index(item)] = item.strip().strip(',') return datalist else: datalist = dataset.split(',') for item in datalist: datalist[datalist.index(item)] = item.strip().strip(',') return datalist 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={}): '''Reads a configuration file from the specified path, merging it with the configuration data specified in configData. Returns a dictionary of name/value pairs based on configData and the values read from path.''' # No config file data is available yet, so to debug _readConfigFile, set # the value here. debugLevel = 0 if debugLevel >= 5: syslog.syslog('readConfigFile: Loading "%s"' % path) if configData is None: configData = {} nameConversion = { 'AuthservID': 'str', 'Syslog': 'bool', 'SyslogFacility': 'str', 'SyslogSuccess': 'bool', 'UMask': 'int', 'Mode': 'str', 'MinimumKeyBits': 'int', 'Socket': 'str', 'PidFile': 'str', 'UserID': 'str', 'Domain': 'dataset', 'SubDomains': 'bool', 'KeyFile': 'str', 'KeyTable': 'dataset', 'KeyFileEd25519': 'str', 'KeyTableEd25519': 'dataset', 'Selector': 'str', 'SelectorEd25519': 'str', 'SigningTable': 'dataset', 'Canonicalization': 'str', 'InternalHosts': 'dataset', 'IntHosts': 'bool', 'DiagnosticDirectory': 'str', 'MacroList': 'dataset', 'MacroListVerify': 'dataset', 'DNSOverride': 'str', 'DNSTimeout': 'int', 'debugLevel': 'int', 'SignHeaders': 'dataset' } # check to see if it's a file try: mode = os.stat(path)[0] except OSError as e: syslog.syslog(syslog.LOG_ERR, 'ERROR stating "%s": %s' % (path, e.strerror)) return(configData) if not stat.S_ISREG(mode): syslog.syslog(syslog.LOG_ERR, 'ERROR: is not a file: "%s", mode=%s' % (path, oct(mode))) return(configData) # load file fp = open(path, 'r') while 1: line = fp.readline() if not line: break # parse line line = line.split('#', 1)[0].strip() if not line: continue data = line.split() if len(data) != 2: if len(data) == 1: if debugLevel >= 1: syslog.syslog('Config item "%s" not defined in file "%s"' % (line, path)) if len(data) == 1: name = data value = '' if len(data) == 2: name, value = data if len(data) >= 3: name = data[0] value = data[1:] # check validity of name try: conversion = nameConversion.get(name) except TypeError: name = name[0] syslog.syslog('Config item "%s" does not provide a value in file "%s"' % (name, path)) conversion = None if conversion is None: syslog.syslog('ERROR: Unknown name or name missing value "%s" in file "%s"' % (name, path)) continue if debugLevel >= 5: syslog.syslog('readConfigFile: Found entry "%s=%s"' % (name, value)) if conversion == 'bool': configData[name] = _find_boolean(value) elif conversion == 'str': if isinstance(value, list): configData[name] = line.split(None, 1)[1] else: configData[name] = str(value) elif conversion == 'int': if name == 'MinimumKeyBits': if int(value) == 0: # Odd inheritence from OpenDKIM where value of 0 means use default. value = configData.get(name) configData[name] = int(value) elif conversion == 'dataset': configData[name] = _dataset_to_list(value) else: syslog.syslog(str('name: ' + name + ' value: ' + value + ' conversion: ' + conversion)) configData[name] = conversion(value) fp.close() try: configData['AuthservID'] = _make_authserv_id(configData.get('AuthservID', 'HOSTNAME')) except Exception as e: syslog.syslog("Could not make AuthservID: {}".format(e)) pass try: configData['IntHosts'] = HostsDataset(configData['InternalHosts']) except Exception as e: syslog.syslog("Could not make HostDataset from InternalHosts: {}".format(e)) pass return(configData) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/dkimpy_milter/dnsplug.py0000644000175100017510000001374300000000000021440 0ustar00kittermakitterma## @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=5): 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=5): retVal = [] try: # FIXME: how to disable TCP fallback in dnspython if not tcpfallback? answers = dns.resolver.query(name, qtype, raise_on_no_answer=False, lifetime=timeout) 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), list(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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/dkimpy_milter/util.py0000644000175100017510000001515300000000000020736 0ustar00kittermakitterma# drop_priviledges (from https://github.com/nigelb/Static-UPnP) # Copyright (C) 2016 NigelB # Copyright (C) 2018 Scott Kitterman # # 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 # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 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 pwd userlist = userid.split(':') if len(userlist) == 1: gidname = userlist[0] else: gidname = userlist[1] # Get the uid/gid from the name running_uid = pwd.getpwnam(userlist[0]).pw_uid 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 os.setgroups([]) # Try setting the new uid/gid os.setgid(gid) os.setuid(uid) # Set umask old_umask = os.umask(milterconfig.get('UMask')) class ExceptHook: def __init__(self, useSyslog=1, useStderr=0): self.useSyslog = useSyslog self.useStderr = useStderr def __call__(self, etype, evalue, etb): import traceback import sys tb = traceback.format_exception(*(etype, evalue, etb)) for line in tb: if self.useSyslog: import syslog syslog.syslog(line) if self.useStderr: sys.stderr.write(line) def setExceptHook(): import sys sys.excepthook = ExceptHook(useSyslog=1, useStderr=1) def write_pid(milterconfig): """Write PID in pidfile. Will not overwrite an existing file.""" import os import syslog pidfile = milterconfig.get('PidFile') if pidfile is None: return if not os.path.isfile(pidfile): pid = str(os.getpid()) try: f = open(pidfile, 'w') 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'): 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 f.write(pid) f.close() user, group = user_group(milterconfig.get('UserID')) os.chown(pidfile, user, group) else: if milterconfig.get('Syslog'): syslog.syslog('Unable to write pidfle {0}. File exists.' .format(pidfile)) raise RuntimeError('Unable to write pidfle {0}. File exists.' .format(pidfile)) return pid def read_keyfile(keyfile, milterconfig): """Read private key from file.""" import syslog try: f = open(keyfile, 'r') keylist = f.readlines() except IOError as e: if milterconfig.get('Syslog'): syslog.syslog('Unable to read keyfile {0}. IOError: {1}' .format(keyfile, e)) raise f.close() key = '' for line in keylist: key += line return key def read_keytable(tabledict, milterconfig): """Read keytables into in memory configuration data so all keys are read before priviledges are dropped. When loaded, tabeldict is a dict: {searchkey: [donamin, selector, key]} If key is a file (startswith('/'), then the key is returned in its place.""" import dkim import syslog for dictkey, values in tabledict.items(): if values[-1][:1] == '/' or values[-1][:2] == './' or values[-1][:3] == '../': key = read_keyfile(values[-1], milterconfig) tabledict[dictkey] = [values[0], values[1], key] return tabledict def get_keys(milterconfig): """Read keys (table or file) into memory before dropping priviledges""" milterconfig['privateRSA'] = False milterconfig['privateRSATable'] = False milterconfig['privateEd25519'] = False milterconfig['privateEd25519Table'] = False if milterconfig.get('KeyTable'): milterconfig['privateRSATable'] = read_keytable(milterconfig.get('KeyTable'), milterconfig) elif milterconfig.get('KeyFile'): milterconfig['privateRSA'] = read_keyfile(milterconfig.get('KeyFile'), milterconfig) if milterconfig.get('KeyTableEd25519'): milterconfig['privateEd25519Table'] = read_keytable(milterconfig.get('KeyTableEd25519'), milterconfig) elif milterconfig.get('KeyFileEd25519'): milterconfig['privateEd25519'] = read_keyfile(milterconfig.get('KeyFileEd25519'), milterconfig) return milterconfig ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677460454.4488747 dkimpy-milter-1.2.3/dkimpy_milter.egg-info/0000755000175100017510000000000000000000000021074 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460454.0 dkimpy-milter-1.2.3/dkimpy_milter.egg-info/PKG-INFO0000644000175100017510000004317200000000000022200 0ustar00kittermakittermaMetadata-Version: 2.1 Name: dkimpy-milter Version: 1.2.3 Summary: Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail. Home-page: https://launchpad.net/dkimpy-milter Author: Scott Kitterman Author-email: scott@kitterman.com License: UNKNOWN Download-URL: https://pypi.python.org/pypi/dkimpy-milter Description: # OVERVIEW This is a DKIM signing and verification milter. It has been tested with both Postfix and Sendmail. The configuration file is designed to be compatible with OpenDKIM, but only a subset of OpenDKIM options are supported. If an unsupported option is specified, an error will be raised. # INSTALLATION This package includes a default configuration file and man pages. For those to be installed when installing using setup.py, the following incantation is required because setuptools developers decided not being able to do this by default is a feature: [sudo] python3 setup.py install --single-version-externally-managed --record=/dev/null For users of Debian Stable (Debian 9, Codename Squeeze), all dependencies are available in either the main or backports repositories: [sudo] apt install python3-milter python3-nacl python3-dnspython [sudo] apt install -t stretch-backports python3-authres python3-dkim It is also available in the Debian package archive: [sudo] apt install dkimpy-milter [Debian 10 or later] [sudo] apt install -t stretch-backports dkimpy-milter [Debian 9] When installing using the Debian package, all dependencies are automatically installed. The preferred method of installation is from PyPi using pip (if distribution packages are not available): [sudo] pip install dkimpy_milter Using pip will cause required packages to be installed via easy_install if they have not been previously installed. Because pymilter and PyNaCl are compiled Python extensions, the system will need appropriate development packages and an C compiler. Alternately, install these dependencies from distribution/OS packages and then pip install dkimpy_milter. The milter will work with either py3dns (DNS) or dnspython (dns), preferring dnspython if both are available. The dkimpy DKIM module also works with either. ## NON-STANDARD INSTALLATION PATHS The package includes a custom setup command called expand. It allows various file locations in init scripts, man pages, and config files to be over-ridden at install time. expand: Expand @@ variables in input files, simlar to make macros. user_options: --sysconfigdir=, e: Specify system configuration directory. --sbindir=, s: Specify system binary directory [not used]. --bindir=, b: Specify binary directory. --rundir=,r: Specify run state directory. As an example, to change the run directory to /var/run, one would do: python3 setup.py expand --rundir=/var/run [sudo] python3 setup.py install --single-version-externally-managed \ --record=/dev/null or in a single step (the order matters): [sudo] python3 setup.py expand --rundir=/var/run install \ --single-version-externally-managed \ --record=/dev/null # SETUP ## SIGNING KEYS In order to create DKIM signatures, a private key must be available. Signing keys should be protected (owned by root:root with permissions 600 in a directory that is not world readable). Different keys are required for RSA and (if used) Ed25519. ### RSA Both public and private keys for RSA have standard formats and there are many tools available to create them. Keys must (RFC 8302) have a minimum size of 1024 bits and should have a size of at least 2048 bits. The dknewkey script that is provided with dkimpy is one such tool: dknewkey exampleprivkey will produce both the private key file (.key suffix) and a file with the DKIM public key record to be published DNS (.dns suffix). RSA is the default key type. 2048 bits is the default key size. ### ED25519 There is no standardized non-binary representation for Ed25519 private keys, so in order to generate Ed25519 keys for dkimpy-milter, dkimpy specific tools must be used to be compatible. The same dknewkey script support Ed25519: dknewkey --ktype ed25519 anothernewkey will provide both the private key file (.key suffix) and a file with the DKIM public key record to be published DNS (.dns suffix). Ed25519 keys do not have variable bit lengths. ### COMPLEX SIGNING CONFIGURATIONS The KeyTable, KeyTableEd25519, and SigningTable are used to define signing instructions to the filter where use of Domain, Selector and KeyFile together are insufficient. First, select the type of database you will use for each. They need not be the same. The "DATA SETS" portion of the dkimpy-milter(8) man page describes the possibilities and how they are formatted. Then, construct those databases. Let's suppose you want to sign for two domains, example.com and example.net. Within example.com, you want to sign for user "president" differently than everyone else. Let's say further that you want to use a flat text file. You've generated private key files for each of these and stored them in the directory /usr/local/etc/dkim/keys as files "president", "excom" and "exnet", with the obvious intents. You want to use selectors "foo", "bar" and "baz" for those, respectively. The signing domains match the senders (i.e. the signatures for example.com's stuff will be held by example.com, and example.net likewise). First, write the KeyTable. This is a list of the keys you intend to use, and you just assign arbitrary names to them. So as a flat file, the KeyTable for the above might look like this: preskey example.com:foo:/usr/local/etc/dkim/keys/president comkey example.com:bar:/usr/local/etc/dkim/keys/excom netkey example.net:baz:/usr/local/etc/dkim/keys/exnet If also signing with ed25519, specify a KeyTableEd25519, with the same names, pointing to the keys needed for ed25519. Both KeyTable and KeyTableEd25519 are evaluated if there is a SigningTable (see below). Per the documentation, multi-field data sets that are made of flat files have the fields separated by colons, but the key and value(s) are separated by whitespace. So now we've named each key file, and specified with which selector and domain each will be used, and then given each of those groupings a name. This is your KeyTable. Let's say you put it in /usr/local/etc/dkim/keytable. Next, write the SigningTable. This maps senders (by default, taken from the From: header field of a message passing through the filter) to which keys will be used to sign their mail. Wildcards are allowed. So to do what was described above, we write it as follows: president@example.com preskey *@example.com comkey *@example.net netkey Since we want to use wildcards, we can't actually use a regular flat file. Wildcards require a regular expression file, or "refile". The above is valid format for one of those. Let's say you put this in /usr/local/etc/dkim/signingtable. Finally, tell the filter that it should use these files by adding this to your configuration file: KeyTable /usr/local/etc/dkim/keytable SigningTable refile:/usr/local/etc/dkim/signingtable You could put "file:" in front of the filename for the KeyTable just to be precise, but "file:" is assumed if the value starts with a "/". Note: Unlike opendkim, dkimpy-milter will check for "\*" in the signing table regardless of if refile is specified or not. Use of refile is supported for compatibility with configurations initially developed for use with opendkim. ## MTA INTEGRATION Both a systemd unit file and a sysv init file are provided. Both make assumptions about defaults being used, e.g. if a non-standard pidfile name is used, they will need to be updated. The sysv init file uses start-stop-deamon from Debian. It is not portable to systems without that available. The dkimpy-milter drops priviledges after setup to the user/group specified in UserID. During initial setup, this system user needs to be manually created. As an example, using the default dkimpy-user on Debian, the command would be: [sudo] adduser --system --no-create-home --quiet --disabled-password \ --disabled-login --shell /bin/false --group \ --home /run/dkimpy-milter dkimpy-milter Since /var/run or /run is sometimes on a tempfs, if the PID file directory is missing, the milter will create it on startup. To start dkimpy-milter with systemd for the first time, you will need to take the following steps: [sudo] systemctl daemon-reload [sudo] systemctl enable dkimpy-milter [sudo] systemctl start dkimpy-milter [sudo] systemctl status dkimpy-milter (to verify it started correctly) As with all milters, dkimpy-milter needs to be integrated with your MTA of choice (Sendmail or Postfix). When integrating with your MTA, the risk of signature invalidation due to content conversion of the message body needs to be considered. See RFC 6376, Section 5.3 for discussion of this issue. As a practical matter, when signing, configure the milter to follow all others that might modify the message body. When verifying, configure the milter before other processes that might modify the message body. ### SENDMAIL Configuration is very similar to opendkim, but needs some adjustment for dkimpy-milter. Here's an example configuration line to include in your sendmail.mc: INPUT_MAIL_FILTER(`dkimpy-milter', `S=local:/run/dkimpy-milter/dkimpy-milter.sock')dnl Changing the sendmail.mc file requires a Make (to compile it into sendmail.cf) and a restart of sendmail. Note that S= needs to match the value of Socket in the dkimpy-milter configuration file. Milter support should be present by default in most versions of sendmail these days, but if not included in your Sendmail build, see: http://www.elandsys.com/resources/sendmail/milter.html #### ISSUES USING SENDMAIL TO SIGN AND VERIFY When using the sendmail MTA in both signing and verifying mode, there are a few issues of which to be aware that might cause operational problems and deserve consideration. (a) When the MTA will be used for relaying emails, e.g. delivering to other hosts using the aliases mechanism, it is important not to break signatures inserted by the original sender. This is particularly sensitive particular when the sending domain has published a "reject" DMARC policy. By default, sendmail quotes to address header fields when there are no quotes and the display part of the address contains a period or an apostrophe. However, dkimpy-milter 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 ,anotherloo...ong name b ...might be rewritten as: To: very long name , anotherloo...ong name b This rewrite is also done after dkimpy-milter has seen the message, meaning the signature dkimpy-milter 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 dkimpy-milter 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. Support for non-ASCII email messages: Anything UTF-8 should work (including correct signing/verification). For messages that contain header fields with non-ASCII or UTF-8 content, signatures are likely fail verification, but the milter should continue to run. RFC 8616 is not supported. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Communications :: Email :: Mail Transport Agents Classifier: Topic :: Communications :: Email :: Filters Classifier: Topic :: Security Description-Content-Type: text/markdown ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460454.0 dkimpy-milter-1.2.3/dkimpy_milter.egg-info/SOURCES.txt0000644000175100017510000000133600000000000022763 0ustar00kittermakittermaAuthors.conf CHANGES COPYING MANIFEST.in README.md TODO setup.py dkimpy_milter/__init__.py dkimpy_milter/__main__.py dkimpy_milter/config.py dkimpy_milter/dnsplug.py dkimpy_milter/util.py dkimpy_milter.egg-info/PKG-INFO dkimpy_milter.egg-info/SOURCES.txt dkimpy_milter.egg-info/dependency_links.txt dkimpy_milter.egg-info/entry_points.txt dkimpy_milter.egg-info/not-zip-safe dkimpy_milter.egg-info/requires.txt dkimpy_milter.egg-info/top_level.txt etc/dkimpy-milter.conf etc/dkimpy-milter.conf.in man/dkimpy-milter.8 man/dkimpy-milter.conf.5 man/dkimpy-milter.conf.5.in system/dkimpy-milter system/dkimpy-milter.in system/dkimpy-milter.openrc system/dkimpy-milter.openrc.in system/dkimpy-milter.service system/dkimpy-milter.service.in././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460454.0 dkimpy-milter-1.2.3/dkimpy_milter.egg-info/dependency_links.txt0000644000175100017510000000000100000000000025142 0ustar00kittermakitterma ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460454.0 dkimpy-milter-1.2.3/dkimpy_milter.egg-info/entry_points.txt0000644000175100017510000000007700000000000024376 0ustar00kittermakitterma[console_scripts] dkimpy-milter = dkimpy_milter.__init__:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460454.0 dkimpy-milter-1.2.3/dkimpy_milter.egg-info/not-zip-safe0000644000175100017510000000000100000000000023322 0ustar00kittermakitterma ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460454.0 dkimpy-milter-1.2.3/dkimpy_milter.egg-info/requires.txt0000644000175100017510000000007300000000000023474 0ustar00kittermakittermaPy3DNS PyNaCl authres>=1.1.0 dkimpy>=1.1.0 pymilter>=1.0.5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460454.0 dkimpy-milter-1.2.3/dkimpy_milter.egg-info/top_level.txt0000644000175100017510000000001600000000000023623 0ustar00kittermakittermadkimpy_milter ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677460454.4488747 dkimpy-milter-1.2.3/etc/0000755000175100017510000000000000000000000015304 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/etc/dkimpy-milter.conf0000644000175100017510000000277400000000000020754 0ustar00kittermakitterma# This is a basic configuration that can easily be adapted to suit a standard # installation. For more advanced options, see dkimpy-milter.conf(5). # Log to syslog Syslog yes # Required to use local socket with MTAs that access the socket as a non- # privileged user (e.g. Postfix) UMask 007 # Sign for example.com with key in /usr/local/etc/dkimkeys/dkim.key using # selector '2007' (e.g. 2007._domainkey.example.com) #Domain example.com #KeyFile /usr/local/etc/mail/dkim.key #Selector default # Commonly-used options; the commented-out versions show the defaults. #Canonicalization relaxed/simple #Mode sv # ## Socket socketspec # ## # ## Names the socket where this filter should listen for milter connections # ## from the MTA. Required. Should be in one of these forms: # ## # ## inet:port@address to listen on a specific interface # ## inet:port to listen on all interfaces # ## local:/path/to/socket to listen on a UNIX domain socket # #Socket local:/run/dkimpy-milter/dkimpy-milter.sock # #Socket inet:8892@localhost ## PidFile filename ### default /run/dkimpy-milter/dkimpy-milter.pid ### ### Name of the file where the filter should write its pid before beginning ### normal operations. # PidFile /run/dkimpy-milter/dkimpy-milter.pid ## Userid userid ### default dkimpy-milter ### ### Change to user "userid" before starting normal operation? May include ### a group ID as well, separated from the userid by a colon. # UserID dkimpy-milter ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/etc/dkimpy-milter.conf.in0000644000175100017510000000275600000000000021361 0ustar00kittermakitterma# This is a basic configuration that can easily be adapted to suit a standard # installation. For more advanced options, see dkimpy-milter.conf(5). # Log to syslog Syslog yes # Required to use local socket with MTAs that access the socket as a non- # privileged user (e.g. Postfix) UMask 007 # Sign for example.com with key in @SYSCONFDIR@/dkimkeys/dkim.key using # selector '2007' (e.g. 2007._domainkey.example.com) #Domain example.com #KeyFile @SYSCONFDIR@/mail/dkim.key #Selector default # Commonly-used options; the commented-out versions show the defaults. #Canonicalization relaxed/simple #Mode sv # ## Socket socketspec # ## # ## Names the socket where this filter should listen for milter connections # ## from the MTA. Required. Should be in one of these forms: # ## # ## inet:port@address to listen on a specific interface # ## inet:port to listen on all interfaces # ## local:/path/to/socket to listen on a UNIX domain socket # #Socket local:@RUNSTATEDIR@/dkimpy-milter.sock # #Socket inet:8892@localhost ## PidFile filename ### default /run/dkimpy-milter/dkimpy-milter.pid ### ### Name of the file where the filter should write its pid before beginning ### normal operations. # PidFile @RUNSTATEDIR@/dkimpy-milter.pid ## Userid userid ### default dkimpy-milter ### ### Change to user "userid" before starting normal operation? May include ### a group ID as well, separated from the userid by a colon. # UserID dkimpy-milter ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677460454.4488747 dkimpy-milter-1.2.3/man/0000755000175100017510000000000000000000000015304 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/man/dkimpy-milter.80000644000175100017510000002361400000000000020172 0ustar00kittermakitterma\" .\" 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 b) If the string begins with "refile:", then the remainder of the string is presumed to specify a file that contains a set of patterns, one per line, and their associated values. The pattern is taken as the start of the line to the first whitespace, and the portion after that whitespace is taken as the value to be used when that pattern is matched. Patterns are simple wildcard patterns, matching all text except that the asterisk ("*") character is considered a wildcard. If a value contains multiple entries, the entries should be separated by colons. .TP .I i) If the string contains none of these prefixes but starts with a slash ("/") character, or "./" or "../", it is presumed to be a flat file as described above. Note: In OpenDKIM "./" and "../" only apply to KeyTable, but for dkimpy-milter it is generally applicable and KeyTable specification is not a special case. .TP .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) for information about available options. Unlike OpenDKIM, with the exception of \-P for the pidfile and specifying the configuration file to use, dkimpy-milter does not support command line option switches. When signing a message, a .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 .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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677459926.0 dkimpy-milter-1.2.3/man/dkimpy-milter.conf.50000644000175100017510000005363700000000000021123 0ustar00kittermakitterma\" .\" Standard preamble: .\" ======================================================================== .de Sh \" Subsection heading .br .if t .Sp .ne 5 .PP \fB\\$1\fR .PP .. .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' 'br\} .\" .\" If the F register is turned on, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.Sh), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .if \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . nr % 0 . rr F .\} .\" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .hy 0 .if n .na .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .TH dkimpy-milter.conf 5 "2019-04-25" .SH "NAME" dkimpy-milter \- Python milter for DKIM signing and validation .SH "VERSION" 1\.2\.0 .SH "DESCRIPTION" .I dkimpy-milter(8) implements the .B DKIM specification for signing and verifying e-mail messages on a per-domain basis. This file is its configuration file. Blank lines are ignored. Lines containing a hash ("#") character are truncated at the hash character to allow for comments in the file. Other content should be the name of a parameter, followed by white space, followed by the value of that parameter, each on a separate line. For parameters that are Boolean in nature, only the first byte of the value is processed. For positive values, the following are accepted: "T", "t", "Y", "y", "1". For negative values, the following are accepted: "F", "f", "N", "n", "0". The provided setup.py installs this configuration file in /etc or /usr/local/etc based on the value of expand sysconfigdir= used when the package was installed. Command line invocation of parameters as is done by OpenDKIM is not supported. .SH "USAGE" Usage: dkimpy-milter [/usr/local/etc/dkimpy-milter/dkimpy-milter.conf] .SH "OTHER DOCUMENTATION" This documentation assumes you have read Postfix's README_FILES/MILTER_README (or Sendmail equivalent) and are generally familiar with Domain Keys Identified Mail (DKIM). See RFC 6376 for details. .SH "SYNOPSIS" dkimpy-milter operates with a default installed configuration file and set of default configuration options that are used if the configuration file cannot be found. These options can be changed by changing the installed configuration files. For users transitioning from OpenDKIM, OpenDKIM config files can be used directly. Not all OpenDKIM options are supported. If an unsupported option from OpenDKIM is specified, an error will be raised. .SH "DESCRIPTION" Configuration options are described here and in the configuration file provided with the package. The provided setup.py installs this configuration file in /etc or /usr/local/etc. .SH "OPTIONS" .TP .I AuthservID (string) Sets the "authserv-id" to use when generating the Authentication-Results: header field after verifying a message. The default is to use the name of the MTA processing the message. If the string "HOSTNAME" is provided, the name of the host running the filter (as returned by the .I gethostname(3) function) will be used. .TP .I Canonicalization (string) Selects the canonicalization method(s) to be used when signing messages. When verifying, the message's DKIM-Signature: header field specifies the canonicalization method. The recognized values are .I relaxed and .I simple as defined by the DKIM specification. The default is .I relaxed / .I simple. The value may include two different canonicalizations separated by a slash ("/") character, in which case the first will be applied to the header and the second to the body. .TP .I DiagnosticDirectory (string) Directory into which to write diagnostic reports when message verification fails. If not set (the default), these files are not generated. The directory must exist, dkimpy-milter will not create it and an error will be raised if it does not. [Unlike OpenDKIM, this applies to all messages, not just on messages bearing a "z=" tag because dkimpy does not yet support "z=" processing.] .TP .I Domain (dataset) A set of domains whose mail should be signed by this filter. Mail from other domains will be verified rather than being signed. This parameter is not required if a .I SigningTable is in use; in that case, the list of signed domains is implied by the lines in that file. This parameter is ignored if a .I KeyTable or .I KeyTableD25119 is defined. .TP .I InternalHosts (dataset) Identifies a set internal hosts whose mail should be signed rather than verified. Entries in this data set follow the same form as those of the .I PeerList option below. If not specified, the default of "127.0.0.1" is applied. Naturally, providing a value here overrides the default, so if mail from 127.0.0.1 should be signed, the list provided here should include that address explicitly. [PeerList NOT IMPLEMENTED] Mail sent via connections from InternalHosts will not have any existing DKIM signatures verified. This is not overridden by MacroList or Mode. If the Mode is 'v', then no actions will be performed. .TP .I KeyFile (string) Gives the location of a PEM-formatted private key to be used for RSA signing all messages. Ignored if a .I KeyTable is defined. .TP .I KeyFileEd25519 (string) Gives the location of a Ed25519 private key to be used for Ed25519 signing all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key generation (as used in dkimpy). Ignored if a .I KeyTableEd25519 is defined. .TP .I KeyTable (dataset) Gives the location of a file mapping key names to RSA signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the 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. See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples. .TP .I KeyTableEd25519 (dataset) Gives the location of a file mapping key names to Ed25519 signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the 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: There is a limitation of the current implementation that a private key can't be directly included in the file if it starts with '/', './', or '../'. If you have such a key, you may store it in a file and reference the file in the table. See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples. .TP .I MacroList (dataset) Defines a set of MTA-provided .I macros that should be checked to see if the sender has been determined to be a local user and therefore whether or not the message should be signed. If a .I value is specified matching a macro name in the data set, the value of the macro must match a value specified (matching is case-sensitive), otherwise the macro must be defined but may contain any value. The set is empty by default, meaning macros are not considered when making the sign-verify decision. The general format of the value is .I value1[|value2[|...]]; if one or more value is defined then the macro must be set to one of the listed values, otherwise the macro must be set but can contain any value. In order for the macro and its value to be available to the filter for checking, the MTA must send it during the protocol exchange. This is either accomplished via manual configuration of the MTA to send the desired macros or, for MTA/filter combinations that support the feature, the filter can request those macros that are of interest. The latter is a feature negotiated at the time the filter receives a connection from the MTA and its availability depends upon the version of milter used to compile the filter and the version of the MTA making the connection. Mail sent via connections where macros that are in MacroList are provided will not have any existing DKIM signatures verified. If the Mode is 'v', then no actions will be performed. .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] Mail sent via connections where macros that are in MacroListVerify are provided will be not DKIM signed. If the Mode is 's', then no actions will be performed. .TP .I Mode (string) Selects operating modes. The string is a concatenation of characters that indicate which mode(s) of operation are desired. Valid modes are .I s (signer) and .I v (verifier). The default is .I sv except in test mode (see the .I opendkim(8) man page) in which case the default is .I v. When signing mode is enabled, one of the following combinations must also be set: (a) Domain, KeyFile, Selector, no KeyTable, no SigningTable; (b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector; The action to sign or verify is also affected by the InternalHosts, MacroList, and MacroListVerify options. Those options may preclude signing or verification in some cases, but will not enable signing or verifying if not allowed by Mode. .TP .I MinimumKeyBits (integer) Establishes a minimum key size for acceptable RSA signatures. Signatures with smaller key sizes, even if they otherwise pass DKIM validation, will me marked as invalid. The default is 1024, which accepts all signatures. A value of 0 causes the default to be used. Not Applicable to ed25519 signatures. .TP .I OmitHeaders (dataset) Specifies a set of header fields that should be omitted when generating signatures. If an entry in the list names any header field that is mandated by the DKIM specification, the entry is ignored. A set of header fields is listed in the DKIM specification (RFC6376, Section 5.4) as "SHOULD NOT" be signed; the default list for this parameter contains those fields (Return-Path, Received, Comments, Keywords, Bcc, Resent-Bcc and DKIM-Signature). To omit no headers, simply use the string "." (or any string that will match no header field names). Specifying a list with this parameter replaces the default entirely, unless one entry is "*" in which case the list is interpreted as a delta to the default; for example, "*,+foobar" will use the entire default list plus the name "foobar", while "*,-Bcc" would use the entire default list except for the "Bcc" entry. [OmitHeaders NOT IMPLEMENTED - included for reference only] .TP .I DNSOverride (string) Provide a text string that a verifying milter should use instead of consulting the DNS on each message. This is useful primarily for testing purposes in environments where it is awkward to modify the system DNS resolution. It should not be used in production. .TP .I DNSTimeout (integer) Sets the DNS timeout in seconds. A value of 0 causes no wait (this is different than opendkim). The default is 5. See also the NOTES section below. .TP .I PeerList (dataset) Identifies a set of "peers" that identifies clients whose connections should be accepted without processing by this filter. The set should contain on each line a hostname, domain name (e.g. ".example.com"), IP address, an IPv6 address (including an IPv4 mapped address), or a CIDR-style IP specification (e.g. "192.168.1.0/24"). An entry beginning with a bang ("!") character means "not", allowing exclusions of specific hosts that are otherwise members of larger sets. Host and domain names are matched first, then the IP or IPv6 address depending on the connection type. More precise entries are preferred over less precise ones, i.e. "192.168.1.1" will match before "!192.168.1.0/24". The text form of IPv6 addresses will be forced to lowercase when queried (RFC5952), so the contents of this data set should also use lowercase. The IP address portion of an entry may optionally contain square brackets; both forms (with and without) will be checked. [PeerList NOT IMPLEMENTED - included for reference only] .TP .I PidFile (string) Specifies the path to a file that should be created at process start containing the process ID. If not specified, no such file will be created. .TP .I Selector (string) Defines the name of the selector to be used when signing messages using RSA. See the .B DKIM specification for details. Used only when signing with a single key; see the .I SigningTable parameter below for more information. This parameter is ignored if a .I KeyTable is defined. .TP .I SelectorEd25519 (string) Defines the name of the selector to be used when signing messages using Ed25519. See the .B DKIM specification for details. Used only when signing with a single key; see the .I SigningTable parameter below for more information. This parameter is ignored if a .I KeyTableEd25519 is defined. .TP .I SignHeaders (dataset) Specifies the set of header fields that should be included when generating signatures. If the list omits any header field that is mandated by the DKIM specification, those fields are implicitly added. By default, those fields listed in the DKIM specification as "SHOULD" be signed (RFC6376, Section 5.4) will be signed by the filter. See the .I OmitHeaders configuration option for more information about the format and interpretation of this field. .TP .I SigningTable (dataset) Defines a table used to select one or more signing identities to apply to a message based on the address found in the From: header field. Keys in this table vary depending on the type of table used; values in this data set should include one field that contains a name found in the KeyTable (see above) that identifies which key should be used in generating the signature, and an optional second field naming the signer of the message that will be included in the "i=" tag in the generated signature. Note that the "i=" value will not be included in the signature if it conflicts with the signing domain (the "d=" value). If the first field contains only a "%" character, it will be replaced by the domain found in the From: header field. Similarly, within the optional second field, any "%" character will be replaced by the domain found in the From: header field. If this table specifies a regular expression file ("refile"), then the keys are wildcard patterns that are matched against the address found in the From: header field. Entries are checked in the order in which they appear in the file. Note: These are not true regular expressions. The terminology is inherited from opendkim. Only wildcards ("*") are supported. For all other database types, the full user@host is checked first, then simply host, then user@.domain (with all superdomains checked in sequence, so "foo.example.com" would first check "user@foo.example.com", then "user@.example.com", then "user@.com"), then .domain, then user@*, and finally *. In any case, only the first match is applied. See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples. .TP .I Socket (string) Specifies the socket that should be established by the filter to receive connections from .I postfix(1) in order to provide service. .I socketspec is in one of two forms: .I local:path, which creates a UNIX domain socket at the specified .I path, or .I inet:port[@host] or .I inet6:port[@host] which creates a TCP socket on the specified .I port and in the specified protocol family. If the .I host is not given as either a hostname or an IP address, the socket will be listening on all interfaces. A literal IP address must be enclosed in square brackets. This option is mandatory in the configuration file. .TP .I SubDomains (Boolean) Sign subdomains of those listed by the .I Domain parameter as well as the actual domains. .TP .I Syslog (Boolean) Log via calls to .I syslog(3) any interesting activity. .TP .I SyslogFacility (string) Log via calls to .I syslog(3) using the named facility. The facility names are the same as the ones allowed in .I syslog.conf(5). The default is "mail". .TP .I SyslogSuccess (Boolean) Log via calls to .I syslog(3) additional entries indicating successful signing or verification of messages. .TP .I UMask (integer) Requests a specific permissions mask to be used for file creation. This only really applies to creation of the socket when .I Socket specifies a UNIX domain socket, and to the .I PidFile (if any); temporary files are created by the .I mkstemp(3) function that enforces a specific file mode on creation regardless of the process umask. See .I umask(2) for more information. .TP .I UserID (string) Attempts to become the specified userid before starting operations. The value is of the form .I userid[:group]. The process will be assigned all of the groups and primary group ID of the named .I userid unless an alternate .I group is specified. .SH NOTES When using DNS timeouts (see the .I DNSTimeout option above), be sure not to use a timeout that is larger than the timeout being used for interaction between .I sendmail and the filter. Otherwise, the MTA could abort a message while waiting for a reply from the filter, which in turn is still waiting for a DNS reply. This must take into accout that the timeout is per DNS lookup so the total DNS wait time may be subustantially loner than the value specified in .I DNSTimeout \. There is a DNS lookup for each connection if the .I InternalHosts option is in use and one for DKIM public key record lookup for each algorithm per signature per message (i.e. potentially two lookups per signature). .SH FILES .TP .I /usr/local/etc/dkimpy-milter/dkimpy-milter.conf Default location of this file. .SH "AUTHORS" \ddkimpy-milter\fR was written by Scott Kitterman . It is based on dkim-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc. Copyright (c) 2013-2015 Stuart D. Gathman Copyright (c) 2018,2019 Scott Kitterman . .PP This man-page was created by Scott Kitterman . .SH COPYRIGHT Configuration items derived from OpenDKIM 2.11.0 opendkim.conf.5.in: Copyright (c) 2007, 2008, Sendmail, Inc. and its suppliers. All rights reserved. See LICENSE.Sendmail. Copyright (c) 2009-2015, The Trusted Domain Project. All rights reserved. See LICENSE. Updated for dkimpy-milter. Updates licensed under the same terms as the rest of the package. Copyright (c) 2018,2019 Scott Kitterman ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677459926.0 dkimpy-milter-1.2.3/man/dkimpy-milter.conf.5.in0000644000175100017510000005355500000000000021527 0ustar00kittermakitterma\" .\" Standard preamble: .\" ======================================================================== .de Sh \" Subsection heading .br .if t .Sp .ne 5 .PP \fB\\$1\fR .PP .. .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' 'br\} .\" .\" If the F register is turned on, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.Sh), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .if \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . nr % 0 . rr F .\} .\" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .hy 0 .if n .na .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .TH dkimpy-milter.conf 5 "2019-04-25" .SH "NAME" dkimpy-milter \- Python milter for DKIM signing and validation .SH "VERSION" 1\.2\.0 .SH "DESCRIPTION" .I dkimpy-milter(8) implements the .B DKIM specification for signing and verifying e-mail messages on a per-domain basis. This file is its configuration file. Blank lines are ignored. Lines containing a hash ("#") character are truncated at the hash character to allow for comments in the file. Other content should be the name of a parameter, followed by white space, followed by the value of that parameter, each on a separate line. For parameters that are Boolean in nature, only the first byte of the value is processed. For positive values, the following are accepted: "T", "t", "Y", "y", "1". For negative values, the following are accepted: "F", "f", "N", "n", "0". The provided setup.py installs this configuration file in /etc or /usr/local/etc based on the value of expand sysconfigdir= used when the package was installed. Command line invocation of parameters as is done by OpenDKIM is not supported. .SH "USAGE" Usage: dkimpy-milter [@CONFDIR@/dkimpy-milter.conf] .SH "OTHER DOCUMENTATION" This documentation assumes you have read Postfix's README_FILES/MILTER_README (or Sendmail equivalent) and are generally familiar with Domain Keys Identified Mail (DKIM). See RFC 6376 for details. .SH "SYNOPSIS" dkimpy-milter operates with a default installed configuration file and set of default configuration options that are used if the configuration file cannot be found. These options can be changed by changing the installed configuration files. For users transitioning from OpenDKIM, OpenDKIM config files can be used directly. Not all OpenDKIM options are supported. If an unsupported option from OpenDKIM is specified, an error will be raised. .SH "DESCRIPTION" Configuration options are described here and in the configuration file provided with the package. The provided setup.py installs this configuration file in /etc or /usr/local/etc. .SH "OPTIONS" .TP .I AuthservID (string) Sets the "authserv-id" to use when generating the Authentication-Results: header field after verifying a message. The default is to use the name of the MTA processing the message. If the string "HOSTNAME" is provided, the name of the host running the filter (as returned by the .I gethostname(3) function) will be used. .TP .I Canonicalization (string) Selects the canonicalization method(s) to be used when signing messages. When verifying, the message's DKIM-Signature: header field specifies the canonicalization method. The recognized values are .I relaxed and .I simple as defined by the DKIM specification. The default is .I relaxed / .I simple. The value may include two different canonicalizations separated by a slash ("/") character, in which case the first will be applied to the header and the second to the body. .TP .I DiagnosticDirectory (string) Directory into which to write diagnostic reports when message verification fails. If not set (the default), these files are not generated. The directory must exist, dkimpy-milter will not create it and an error will be raised if it does not. [Unlike OpenDKIM, this applies to all messages, not just on messages bearing a "z=" tag because dkimpy does not yet support "z=" processing.] .TP .I Domain (dataset) A set of domains whose mail should be signed by this filter. Mail from other domains will be verified rather than being signed. This parameter is not required if a .I SigningTable is in use; in that case, the list of signed domains is implied by the lines in that file. This parameter is ignored if a .I KeyTable or .I KeyTableD25119 is defined. .TP .I InternalHosts (dataset) Identifies a set internal hosts whose mail should be signed rather than verified. Entries in this data set follow the same form as those of the .I PeerList option below. If not specified, the default of "127.0.0.1" is applied. Naturally, providing a value here overrides the default, so if mail from 127.0.0.1 should be signed, the list provided here should include that address explicitly. [PeerList NOT IMPLEMENTED] Mail sent via connections from InternalHosts will not have any existing DKIM signatures verified. This is not overridden by MacroList or Mode. If the Mode is 'v', then no actions will be performed. .TP .I KeyFile (string) Gives the location of a PEM-formatted private key to be used for RSA signing all messages. Ignored if a .I KeyTable is defined. .TP .I KeyFileEd25519 (string) Gives the location of a Ed25519 private key to be used for Ed25519 signing all messages. File is the Base64 encoded output of RFC 8032 Ed25519 private Key generation (as used in dkimpy). Ignored if a .I KeyTableEd25519 is defined. .TP .I KeyTable (dataset) Gives the location of a file mapping key names to RSA signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the 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. See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples. .TP .I KeyTableEd25519 (dataset) Gives the location of a file mapping key names to Ed25519 signing keys. If present, overrides any KeyFile setting in the configuration file. The data set named here maps each key name to three values: (a) the name of the domain to use in the 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: There is a limitation of the current implementation that a private key can't be directly included in the file if it starts with '/', './', or '../'. If you have such a key, you may store it in a file and reference the file in the table. See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples. .TP .I MacroList (dataset) Defines a set of MTA-provided .I macros that should be checked to see if the sender has been determined to be a local user and therefore whether or not the message should be signed. If a .I value is specified matching a macro name in the data set, the value of the macro must match a value specified (matching is case-sensitive), otherwise the macro must be defined but may contain any value. The set is empty by default, meaning macros are not considered when making the sign-verify decision. The general format of the value is .I value1[|value2[|...]]; if one or more value is defined then the macro must be set to one of the listed values, otherwise the macro must be set but can contain any value. In order for the macro and its value to be available to the filter for checking, the MTA must send it during the protocol exchange. This is either accomplished via manual configuration of the MTA to send the desired macros or, for MTA/filter combinations that support the feature, the filter can request those macros that are of interest. The latter is a feature negotiated at the time the filter receives a connection from the MTA and its availability depends upon the version of milter used to compile the filter and the version of the MTA making the connection. Mail sent via connections where macros that are in MacroList are provided will not have any existing DKIM signatures verified. If the Mode is 'v', then no actions will be performed. .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] Mail sent via connections where macros that are in MacroListVerify are provided will be not DKIM signed. If the Mode is 's', then no actions will be performed. .TP .I Mode (string) Selects operating modes. The string is a concatenation of characters that indicate which mode(s) of operation are desired. Valid modes are .I s (signer) and .I v (verifier). The default is .I sv except in test mode (see the .I opendkim(8) man page) in which case the default is .I v. When signing mode is enabled, one of the following combinations must also be set: (a) Domain, KeyFile, Selector, no KeyTable, no SigningTable; (b) KeyTable, SigningTable, no Domain, no KeyFile, no Selector; The action to sign or verify is also affected by the InternalHosts, MacroList, and MacroListVerify options. Those options may preclude signing or verification in some cases, but will not enable signing or verifying if not allowed by Mode. .TP .I MinimumKeyBits (integer) Establishes a minimum key size for acceptable RSA signatures. Signatures with smaller key sizes, even if they otherwise pass DKIM validation, will me marked as invalid. The default is 1024, which accepts all signatures. A value of 0 causes the default to be used. Not Applicable to ed25519 signatures. .TP .I OmitHeaders (dataset) Specifies a set of header fields that should be omitted when generating signatures. If an entry in the list names any header field that is mandated by the DKIM specification, the entry is ignored. A set of header fields is listed in the DKIM specification (RFC6376, Section 5.4) as "SHOULD NOT" be signed; the default list for this parameter contains those fields (Return-Path, Received, Comments, Keywords, Bcc, Resent-Bcc and DKIM-Signature). To omit no headers, simply use the string "." (or any string that will match no header field names). Specifying a list with this parameter replaces the default entirely, unless one entry is "*" in which case the list is interpreted as a delta to the default; for example, "*,+foobar" will use the entire default list plus the name "foobar", while "*,-Bcc" would use the entire default list except for the "Bcc" entry. [OmitHeaders NOT IMPLEMENTED - included for reference only] .TP .I DNSOverride (string) Provide a text string that a verifying milter should use instead of consulting the DNS on each message. This is useful primarily for testing purposes in environments where it is awkward to modify the system DNS resolution. It should not be used in production. .TP .I DNSTimeout (integer) Sets the DNS timeout in seconds. A value of 0 causes no wait (this is different than opendkim). The default is 5. See also the NOTES section below. .TP .I PeerList (dataset) Identifies a set of "peers" that identifies clients whose connections should be accepted without processing by this filter. The set should contain on each line a hostname, domain name (e.g. ".example.com"), IP address, an IPv6 address (including an IPv4 mapped address), or a CIDR-style IP specification (e.g. "192.168.1.0/24"). An entry beginning with a bang ("!") character means "not", allowing exclusions of specific hosts that are otherwise members of larger sets. Host and domain names are matched first, then the IP or IPv6 address depending on the connection type. More precise entries are preferred over less precise ones, i.e. "192.168.1.1" will match before "!192.168.1.0/24". The text form of IPv6 addresses will be forced to lowercase when queried (RFC5952), so the contents of this data set should also use lowercase. The IP address portion of an entry may optionally contain square brackets; both forms (with and without) will be checked. [PeerList NOT IMPLEMENTED - included for reference only] .TP .I PidFile (string) Specifies the path to a file that should be created at process start containing the process ID. If not specified, no such file will be created. .TP .I Selector (string) Defines the name of the selector to be used when signing messages using RSA. See the .B DKIM specification for details. Used only when signing with a single key; see the .I SigningTable parameter below for more information. This parameter is ignored if a .I KeyTable is defined. .TP .I SelectorEd25519 (string) Defines the name of the selector to be used when signing messages using Ed25519. See the .B DKIM specification for details. Used only when signing with a single key; see the .I SigningTable parameter below for more information. This parameter is ignored if a .I KeyTableEd25519 is defined. .TP .I SignHeaders (dataset) Specifies the set of header fields that should be included when generating signatures. If the list omits any header field that is mandated by the DKIM specification, those fields are implicitly added. By default, those fields listed in the DKIM specification as "SHOULD" be signed (RFC6376, Section 5.4) will be signed by the filter. See the .I OmitHeaders configuration option for more information about the format and interpretation of this field. .TP .I SigningTable (dataset) Defines a table used to select a signing identity to apply to a message based on the address found in the From: header field. Keys in this table vary depending on the type of table used; values in this data set should include one field that contains a name found in the KeyTable (see above) that identifies which key should be used in generating the signature, and an optional second field naming the signer of the message that will be included in the "i=" tag in the generated signature. Note that the "i=" value will not be included in the signature if it conflicts with the signing domain (the "d=" value). If the first field contains only a "%" character, it will be replaced by the domain found in the From: header field. Similarly, within the optional second field, any "%" character will be replaced by the domain found in the From: header field. If this table specifies a regular expression file ("refile"), then the keys are wildcard patterns that are matched against the address found in the From: header field. Entries are checked in the order in which they appear in the file. Note: These are not true regular expressions. The terminology is inherited from opendkim. Only wildcards ("*") are supported. For all other database types, the full user@host is checked first, then simply host, then user@.domain (with all superdomains checked in sequence, so "foo.example.com" would first check "user@foo.example.com", then "user@.example.com", then "user@.com"), then .domain, then user@*, and finally *. In any case, only the first match is applied. See the COMPLEX SIGNING CONFIGURATIONS section of README.md for examples. .TP .I Socket (string) Specifies the socket that should be established by the filter to receive connections from .I postfix(1) in order to provide service. .I socketspec is in one of two forms: .I local:path, which creates a UNIX domain socket at the specified .I path, or .I inet:port[@host] or .I inet6:port[@host] which creates a TCP socket on the specified .I port and in the specified protocol family. If the .I host is not given as either a hostname or an IP address, the socket will be listening on all interfaces. A literal IP address must be enclosed in square brackets. This option is mandatory in the configuration file. .TP .I SubDomains (Boolean) Sign subdomains of those listed by the .I Domain parameter as well as the actual domains. .TP .I Syslog (Boolean) Log via calls to .I syslog(3) any interesting activity. .TP .I SyslogFacility (string) Log via calls to .I syslog(3) using the named facility. The facility names are the same as the ones allowed in .I syslog.conf(5). The default is "mail". .TP .I SyslogSuccess (Boolean) Log via calls to .I syslog(3) additional entries indicating successful signing or verification of messages. .TP .I UMask (integer) Requests a specific permissions mask to be used for file creation. This only really applies to creation of the socket when .I Socket specifies a UNIX domain socket, and to the .I PidFile (if any); temporary files are created by the .I mkstemp(3) function that enforces a specific file mode on creation regardless of the process umask. See .I umask(2) for more information. .TP .I UserID (string) Attempts to become the specified userid before starting operations. The value is of the form .I userid[:group]. The process will be assigned all of the groups and primary group ID of the named .I userid unless an alternate .I group is specified. .SH NOTES When using DNS timeouts (see the .I DNSTimeout option above), be sure not to use a timeout that is larger than the timeout being used for interaction between .I sendmail and the filter. Otherwise, the MTA could abort a message while waiting for a reply from the filter, which in turn is still waiting for a DNS reply. This must take into accout that the timeout is per DNS lookup so the total DNS wait time may be subustantially loner than the value specified in .I DNSTimeout \. There is a DNS lookup for each connection if the .I InternalHosts option is in use and one for DKIM public key record lookup for each algorithm per signature per message (i.e. potentially two lookups per signature). .SH FILES .TP .I @CONFDIR@/dkimpy-milter.conf Default location of this file. .SH "AUTHORS" \ddkimpy-milter\fR was written by Scott Kitterman . It is based on dkim-milter.py Copyright (c) 2001-2013 Business Management Systems, Inc. Copyright (c) 2013-2015 Stuart D. Gathman Copyright (c) 2018,2019 Scott Kitterman . .PP This man-page was created by Scott Kitterman . .SH COPYRIGHT Configuration items derived from OpenDKIM 2.11.0 opendkim.conf.5.in: Copyright (c) 2007, 2008, Sendmail, Inc. and its suppliers. All rights reserved. See LICENSE.Sendmail. Copyright (c) 2009-2015, The Trusted Domain Project. All rights reserved. See LICENSE. Updated for dkimpy-milter. Updates licensed under the same terms as the rest of the package. Copyright (c) 2018,2019 Scott Kitterman ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677460454.4488747 dkimpy-milter-1.2.3/setup.cfg0000644000175100017510000000004600000000000016352 0ustar00kittermakitterma[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677460037.0 dkimpy-milter-1.2.3/setup.py0000644000175100017510000001303000000000000016240 0ustar00kittermakitterma#! /usr/bin/python3 # dkimpy-milter: A DKIM signing/verification Milter application # Author: Scott Kitterman # Copyright 2018,2019 Scott Kitterman """ 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.""" from setuptools import setup import distutils.cmd import distutils.log import sys import os import subprocess description = "Domain Keys Identified Mail (DKIM) signing/verifying milter for Postfix/Sendmail." with open("README.md", "r") as fh: long_description = fh.read() class FileMacroExpand(distutils.cmd.Command): description = "Expand @@ variables in input files, simlar to make macros." user_options = [ ('sysconfigdir=', 'e', 'Specify system configuration directory. [/usr/local/etc]'), ('sbindir=', 's', 'Specify system binary directory. [/usr/local/sbin]'), ('bindir=', 'b', 'Specify binary directory. [/usr/loca/bin]'), ('rundir=', 'r', 'Specify run state directory. [/run]'), ] def initialize_options(self): self.sysconfigdir = '/usr/local/etc' self.sbindir = '/usr/local/sbin' self.bindir = '/usr/local/bin' self.rundir = '/run' def finalize_options(self): self.configdir = self.sysconfigdir + '/dkimpy-milter' self.rundir += '/dkimpy-milter' def run(self): files = ['etc/dkimpy-milter.conf', 'man/dkimpy-milter.conf.5', \ 'system/dkimpy-milter.service', 'system/dkimpy-milter', \ 'system/dkimpy-milter.openrc', \ 'system/socket-activation/dkimpy-milter.service', \ 'system/socket-activation/dkimpy-milter.socket', ] for infile in files: outfile = '' try: filein = open(infile + '.in') for line in filein: for function in ["@SYSCONFDIR@", "@CONFDIR@", "@SBINDIR@", "@BINDIR@", "@RUNSTATEDIR@"]: splitline = line.split(function) if len(splitline) > 1: if function == "@SYSCONFDIR@": line = splitline[0] + self.sysconfigdir + splitline[1] elif function == "@CONFDIR@": line = splitline[0] + self.configdir + splitline[1] elif function == "@SBINDIR@": line = splitline[0] + self.sbindir + splitline[1] elif function == "@BINDIR@": line = splitline[0] + self.bindir + splitline[1] elif function == "@RUNSTATEDIR@": line = splitline[0] + self.rundir + splitline[1] outfile += line out = open(infile, 'w') for line in outfile: out.write(line) out.close() except FileNotFoundError as x: pass kw = {} # Work-around for lack of 'or' requires in setuptools. try: import dns kw['install_requires'] = ['dkimpy>=1.1.0', 'pymilter>=1.0.5', 'authres>=1.1.0', 'PyNaCl', 'dnspython>=1.16.0'] except ImportError: # If PyDNS is not installed, prefer dnspython kw['install_requires'] = ['dkimpy>=1.1.0', 'pymilter>=1.0.5', 'authres>=1.1.0', 'PyNaCl', 'Py3DNS'] setup( name='dkimpy-milter', version='1.2.3', author='Scott Kitterman', author_email='scott@kitterman.com', url='https://launchpad.net/dkimpy-milter', description=description, long_description=long_description, long_description_content_type='text/markdown', download_url = "https://pypi.python.org/pypi/dkimpy-milter", classifiers= [ 'Development Status :: 5 - Production/Stable', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Natural Language :: English', 'Operating System :: POSIX', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Communications :: Email :: Mail Transport Agents', 'Topic :: Communications :: Email :: Filters', 'Topic :: Security', ], packages=['dkimpy_milter'], entry_points = { 'console_scripts' : [ 'dkimpy-milter = dkimpy_milter.__init__:main', ], }, include_package_data=True, data_files=[(os.path.join('share', 'man', 'man5'), ['man/dkimpy-milter.conf.5']), (os.path.join('share', 'man', 'man8'), ['man/dkimpy-milter.8']), (os.path.join('etc', 'dkimpy-milter'), ['etc/dkimpy-milter.conf']), (os.path.join('lib', 'systemd', 'system'), ['system/dkimpy-milter.service']),(os.path.join('etc', 'init.d'), ['system/dkimpy-milter']), (os.path.join('etc', 'init.d'), ['system/dkimpy-milter.openrc'])], zip_safe = False, cmdclass={ 'expand': FileMacroExpand, }, **kw ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677460454.4488747 dkimpy-milter-1.2.3/system/0000755000175100017510000000000000000000000016055 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/system/dkimpy-milter0000644000175100017510000000713100000000000020571 0ustar00kittermakitterma#! /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 . # Modified for Debian # by Ian Murdock . # # Version: @(#)skeleton 1.9 26-Feb-2001 miquels@cistron.nl # ### BEGIN INIT INFO # Provides: dkim-milter dkim-milter-python dkimpy-milter # Required-Start: $remote_fs $syslog $network $time # Required-Stop: $remote_fs $syslog $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: dkimpy-milter # Description: Python DKIM Milter for Sendmail and Postfix ### END INIT INFO sysconfdir="/usr/local/etc/dkimpy-milter" bindir="/usr/local/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 $sysconfdir/default/dkimpy-milter ] ; then . $sysconfdir/default/dkimpy-milter fi set -e . /lib/lsb/init-functions case "$1" in start) echo -n "Starting $DESC: " # Create the run directory if it doesn't exist if [ ! -d $RUNDIR ]; then install -o $USER -g $GROUP -m 755 -d $RUNDIR || return 2 fi # Clean up stale sockets if [ -f $RUNDIR/$NAME.pid ]; then pid=`cat $RUNDIR/$NAME.pid` if ! ps -C $DAEMON -s $pid >/dev/null; then rm $RUNDIR/$NAME.pid # UNIX sockets may be specified with or without the # local: prefix; handle both t=`echo $SOCKET | cut -d: -f1` s=`echo $SOCKET | cut -d: -f2` if [ -e $s -a -S $s ]; then if [ "$t" = "$s" -o "$t" = "local" ]; then rm $s fi fi fi fi start-stop-daemon --start --background --quiet --pidfile \ $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf echo "$NAME." ;; stop) echo -n "Stopping $DESC: " if [ -f $RUNDIR/$NAME.pid ]; then chown root:root $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid #echo $SOCKET if [ -e $SOCKET ]; then rm $SOCKET fi fi echo "$NAME." ;; force-reload) echo -n "Force reloading $DESC: " if [ -f $RUNDIR/$NAME.pid ]; then chown root:root $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid #echo $SOCKET if [ -e $SOCKET ]; then rm $SOCKET fi fi sleep 1 start-stop-daemon --start --background --quiet --pidfile \ $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf echo "$NAME." ;; restart) echo "Restarting $DESC: " echo -n "Stopping $DESC: " if [ -f $RUNDIR/$NAME.pid ]; then chown root:root $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid #echo $SOCKET if [ -e $SOCKET ]; then rm $SOCKET fi fi echo "$NAME." sleep 1 echo -n "Starting $DESC: " start-stop-daemon --start --background --quiet --pidfile \ $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf echo "$NAME." ;; status) status_of_proc -p $RUNDIR/$NAME.pid $DAEMON dkimpy-milter ;; *) N=/etc/init.d/$NAME echo "Usage: $N {start|stop|force-reload|restart|}" >&2 exit 1 ;; esac exit 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/system/dkimpy-milter.in0000644000175100017510000000707300000000000021203 0ustar00kittermakitterma#! /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 . # Modified for Debian # by Ian Murdock . # # Version: @(#)skeleton 1.9 26-Feb-2001 miquels@cistron.nl # ### BEGIN INIT INFO # Provides: dkim-milter dkim-milter-python dkimpy-milter # Required-Start: $remote_fs $syslog $network $time # Required-Stop: $remote_fs $syslog $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: dkimpy-milter # Description: Python DKIM Milter for Sendmail and Postfix ### END INIT INFO sysconfdir="@CONFDIR@" bindir="@BINDIR@" RUNDIR="@RUNSTATEDIR@" DAEMON=${bindir}/dkimpy-milter PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin: NAME=dkimpy-milter DESC="Python DKIM Milter" USER=dkimpy-milter GROUP=dkimpy-milter SOCKET=$RUNDIR/dkimpy-milter.sock test -x $DAEMON || exit 0 # Include dkimpy-python defaults if available # Typically not used if [ -f $sysconfdir/default/dkimpy-milter ] ; then . $sysconfdir/default/dkimpy-milter fi set -e . /lib/lsb/init-functions case "$1" in start) echo -n "Starting $DESC: " # Create the run directory if it doesn't exist if [ ! -d $RUNDIR ]; then install -o $USER -g $GROUP -m 755 -d $RUNDIR || return 2 fi # Clean up stale sockets if [ -f $RUNDIR/$NAME.pid ]; then pid=`cat $RUNDIR/$NAME.pid` if ! ps -C $DAEMON -s $pid >/dev/null; then rm $RUNDIR/$NAME.pid # UNIX sockets may be specified with or without the # local: prefix; handle both t=`echo $SOCKET | cut -d: -f1` s=`echo $SOCKET | cut -d: -f2` if [ -e $s -a -S $s ]; then if [ "$t" = "$s" -o "$t" = "local" ]; then rm $s fi fi fi fi start-stop-daemon --start --background --quiet --pidfile \ $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf echo "$NAME." ;; stop) echo -n "Stopping $DESC: " if [ -f $RUNDIR/$NAME.pid ]; then chown root:root $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid #echo $SOCKET if [ -e $SOCKET ]; then rm $SOCKET fi fi echo "$NAME." ;; force-reload) echo -n "Force reloading $DESC: " if [ -f $RUNDIR/$NAME.pid ]; then chown root:root $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid #echo $SOCKET if [ -e $SOCKET ]; then rm $SOCKET fi fi sleep 1 start-stop-daemon --start --background --quiet --pidfile \ $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf echo "$NAME." ;; restart) echo "Restarting $DESC: " echo -n "Stopping $DESC: " if [ -f $RUNDIR/$NAME.pid ]; then chown root:root $RUNDIR/$NAME.pid start-stop-daemon --stop --pidfile $RUNDIR/$NAME.pid rm $RUNDIR/$NAME.pid #echo $SOCKET if [ -e $SOCKET ]; then rm $SOCKET fi fi echo "$NAME." sleep 1 echo -n "Starting $DESC: " start-stop-daemon --start --background --quiet --pidfile \ $RUNDIR/$NAME.pid --exec $DAEMON $sysconfdir/$NAME.conf echo "$NAME." ;; status) status_of_proc -p $RUNDIR/$NAME.pid $DAEMON dkimpy-milter ;; *) N=/etc/init.d/$NAME echo "Usage: $N {start|stop|force-reload|restart|}" >&2 exit 1 ;; esac exit 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/system/dkimpy-milter.openrc0000644000175100017510000000060200000000000022052 0ustar00kittermakitterma#!/sbin/openrc-run # Copyright 1999-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 CONFFILE="/usr/local/etc/dkimpy-milter/${RC_SVCNAME}.conf" required_files="${CONFFILE}" command="/usr/local/bin/dkimpy-milter" pidfile="/run/dkimpy-milter/${RC_SVCNAME}.pid" command_args="${CONFFILE} -P ${pidfile}" depend() { use dns logger net before mta } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/system/dkimpy-milter.openrc.in0000644000175100017510000000054400000000000022464 0ustar00kittermakitterma#!/sbin/openrc-run # Copyright 1999-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 CONFFILE="@CONFDIR@/${RC_SVCNAME}.conf" required_files="${CONFFILE}" command="@BINDIR@/dkimpy-milter" pidfile="@RUNSTATEDIR@/${RC_SVCNAME}.pid" command_args="${CONFFILE} -P ${pidfile}" depend() { use dns logger net before mta } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/system/dkimpy-milter.service0000644000175100017510000000053500000000000022231 0ustar00kittermakitterma[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/dkimpy-milter.conf -P /run/dkimpy-milter/dkimpy-milter.pid [Install] WantedBy=multi-user.target ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670288665.0 dkimpy-milter-1.2.3/system/dkimpy-milter.service.in0000644000175100017510000000047200000000000022636 0ustar00kittermakitterma[Unit] Description=DKIMpy Milter Documentation=man:dkimpy-milter(8) man:dkimpy-milter.conf(5) After=network.target [Service] Type=simple PIDFile=@RUNSTATEDIR@/dkimpy-milter.pid ExecStart=@BINDIR@/dkimpy-milter @CONFDIR@/dkimpy-milter.conf -P @RUNSTATEDIR@/dkimpy-milter.pid [Install] WantedBy=multi-user.target