././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1713123138.070522 dkimpy-1.1.6/0000755000175100017510000000000014607027502013303 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713123039.0 dkimpy-1.1.6/ChangeLog0000644000175100017510000003423114607027337015066 0ustar00kittermakitterma2024-04-14 Version 1.1.6 - Use raw byte string for regex; fixes SyntaxWarning in Python 3.12 due to invalid escape sequence (LP: #2049518) - Thanks to Simon Chopin for the fix 2023-07-28 Version 1.1.5 - Use dns.resolver.resolve instead of dns.resolver.query due to deprecation (LP: 2028783) - Thanks to Pedro Vicente for the report and the fix 2023-05-12 Version 1.1.4 - Treat dns.resolver.NoNameservers like NXDOMAIN (not an error) (Thanks to David for the patch and the report) - Confine errors from dnspython to dnsplug and use dkim errors, since dkim.__init__.py doesn't import dns and needs dkim errors (LP: #2018646) 2023-04-30 Version 1.1.3 - Catch nacl.exceptions.ValueError and raise KeyFormatError, similar to how RSA key errors are treated (LP: #2018021) - Create ed25519 key files with secure permissions to avoid risk of insecure chmode call/race condition (Thanks to Hanno Böck for the report and the suggested fix) (LP: #2017430) - Properly cleanup temporary directories in tests 2023-04-09 Version 1.1.2 - Verify correct AMS header is used for ARC seal verification (André Cruz) 2023-03-09 Version 1.1.1 - Document dropping of Python 2 support (dropped as of 1.1.0) (LP: #20086738) - Fix traceback when attempting to verify an unsigned message using async verify (Thanks to Nikita Sychev for the report and a suggested fix) (LP: #2008723) 2023-02-25 Version 1.1.0 - Add domain validity check for ascii domains (no specials) - Add option to specify index number of signature to verify to dkimverify (Thanks to Nick Baugh for the change) - Correct signature indexing error introduced in 1.0.0 that prevents verification of multiple signatures in a single message - Correct dkim.verify processing to avoid errors when verifying messages with no DKIM signatures - Update dnsplug for DNS Python (dns) 2.0 compatibility (LP: #1888583) - Fix @param srv_id typos (LP: #1890532) - Provide more specific error message when ed25519 private key is invalid (See LP 1901569 for background) - Add support for PKCS#8 for private keys, openssl 3 default (LP: #1978835) - Thanks to Adrien (spitap) for the change - Add limitations section to README to document current IDN status - Add USE_ASYNC flag to allow async to be disabled when aiodns is installed (LP: #1954331) - see README.md for details - Add new dkim.DnsTimeoutError class to report queried domain and selector along with timeout error from dnspython (LP: #1873449) - Invalid Authentication-Results header fields are ignored for ARC signing (LP: #1884044) - Correct base64 validation regexp so that valid signature with == split between two lines are not incorrectly evaluated as invalid (LP: #2002295) - Thanks to for the report and the proposed fix 2019-12-31 Version 1.0.2 - dknewkey: On posix operating systems set file permissions to 600 for ed25519 private key files (as is already done for RSA) (LP: #1857827) - Update documentation URL in README.md - Set minimum dnspython version to 1.16 because previous versions can not support the timeout parameter (LP: #1856546) 2019-12-15 Version 1.0.1 - Follow CNAMES when looking up key records when using DNS (pydns) (LP: #1856421) - Provide specialized error message when signing or verifying ed25519 signatures and pynacl is not installed (LP: #1854475) 2019-12-09 Version 1.0.0 - Add support for RFC 8460 tlsrpt DKIM signature processing (LP: #1847020) - Add async support with aiodns for DKIM verification (ARC not supported) (LP: #1847002) - Add new timeout parameter to enable DNS lookup timeouts to be adjusted - Add new DKIM.present function to allow applications to test if a DKIM signature is present without doing validation (LP: #1851141) - Support signature verification with RSAPublicKey formatted keys since, although rare, they are RFC 6376 specified (LP: #1851862) - Drop usage of pymilter Milter.dns in dnsplug since it doesn't support havine a timeout passed to it - Catch binascii related key format errors (LP: #1854477) 2019-10-07 Version 0.9.5 - Ignore unknown service types in key records (LP: #1847020) - This is required by RFC 6376 and predecessors. It becomes important now that RFC 8460, which defines a new DKIM service type exists. This change is required to avoid processing tlsrpt keys like regular email keys, which is incorrect, they have different requirements. 2019-09-25 Verstion 0.9.4 - Add LICENSE to MANIFEST.in so it is included in the tarball (LP: #1845318) 2019-08-09 Version 0.9.3 - Fix linesep setting in arcsign script (LP: #1838262) (Thanks to Gowtham Gopalakrishnan for the report and the patch) - Fix default canonicalization for DKIM signature verification to be simple/simple per RFC 6376 (LP: #1839299) (Thanks to Cyril Nicodème for the report and a suggested fix) 2019-04-14 Version 0.9.2 - Fix the arcsign script so it works with the current API (Note: the new srv_id option is the authserv_id to use in the ARC signatures - Only AR fields with an authserv-id that matches srv_id will be considered for ARC signing) - Fix cv=none processing for initial signature in chain - Add additional text documenting use of srv_id for ARC signing to docstrings and man 1 arcsign (LP: #1808301) - Use same line seperator for output as input in dkimsign/arcsign (LP: #1808686) - Refactor canonicalization.py strip_trailing_lines to avoid using re for more consistent processing across python versions (Thanks to Jonathan Bastien-Filiatrault for the change) - Refactor header folding for more consistent results, including reduced stray whitespace (Also Jonathan Bastien-Filiatrault) - Don't log message headers and body unless explicitely requested. This should also reduce memory usage on large messages. (Jonathan Bastien-Filiatrault) - Clarify the crlf does not count towards line length in fold - Adjust fold maxlen to one shorter for lines after the first, since they already have a leading space (LP: #1823008) 2018-12-09 Version 0.9.1 - Fixed ARC verification to fail if h= tag is present in Arc-Seal and added tests - Refactored dknewkey so that it correctly writes out text instead of bytes - Added missing v=DKIM1; to dns record producedby dknewkey 2018-10-30 Version 0.9.0 - Update oversigned (frozen) header field list to reduce signature fragility (removes 'date' and 'subject' fields from being oversigned by default - see usage section of README for information on how to restore the previous behavior) - Added new add_should_not for DKIM/ARC classes to prevent additional header fields from being signed - Added 'from' to should sign list (to prevent it from not being signed at all in the unusual event that 'from' is locally removed from the frozen header field set (LP: #1525048) - Updates for experimental ARC support: - Limit to rsa-sha256, rsa-sha1 not used by ARC and multi-signature design TBD - Raise error when ARC signing if i= instance limit value of 50 is exceeded - Specified that for ARC, Authentication-Results should not be signed - Added missing documentation for timestamp function dkim.arc_sign (LP: #1800314) - Fix DNS lookups to be compatible with EAI addresses in domains and selectors (John Levine) - Add type Hinting for sign and verify functions (LP: #1782596) (Thomas Ward) - PEP8 Blank Lines Style Issues (LP: #1782596) (Thomas Ward) - Python 3.7 compatibility fixup for dkim.canonicalization. strip_trailing_lines due to changed RE.sub() processing (LP: #1800313) 2018-06-16 Version 0.8.1 - Correctly fold lines at or near the maximum line length (fix folding zero length lines and adding a blank line and adding an exra cr/lf for lines near max length (LP: #1717576) - Thanks to Christian Jørgensen and John Levine for reporting the issue - Add testing extras option to setup.py (Daniel Hahler) - Fix deprecation warnings in test asserts (Daniel Hahler) - Correctly limit try/except for imports to import errors (Daniel Hahler) - Don't error out in Python 3 if include headers is string (LP: #1776775) - Correct requires invocation for py3dns 2018-05-18 Version 0.8.0 - Change from distutils to setuptools with entry points because it's the future - Use install_requires and extras_requires to document external dependencies for dkimpy (LP: #1227526) - Fix typo in dknewkey(1) for k= tag (Thanks to Andreas Schulze for reporting) - Detect incorrect version in DKIM public key record (LP: #1763815) - Detect unknown algorithm in k= tag and raise an appropriate error, vice failing with a traceback - Indicate that ed25519-sha256 is no longer experimental 2018-02-17 Version 0.7.1 - Update ed25519 tests, including using sample keys from RFC 8032 Section 7.1 and the sample message from RFC 6376 - Return an empty list (as expected) when no AR headers are found or no AR arc stamps are found and the chain terminated (LP: #1748146) - Use correct h= tag in dknewnkey.py generated DKIM records for RSA 2018-02-07 Version 0.7.0 - Initial ed25519 implementation based on draft-ietf-dcrup-dkim-crypto experimental - IETF draft, design not finalized, See README for details - Port dkimsign.py to use argparse; now gives standard usage message and is more extensible - Add command line options to dkimsign.py to select header and body canonicalization algorithmns (LP: #1272724) - Add command line option to dkimsign.py to select signing algorithm - For dknewkey.py make default to include h=sha256 in the DNS record to exclude usage with sha1. Can be overriden - Update ARC processing to current draft - Fix arcverify tag requirements (LP: #1710312) - Fix empty body canonicalization for relaxed canonicalization (LP: #1727319) * Thanks to Matthew Palmer for the report and the proposed fix - Add new test, test_implicit_k, to verify that RSA processing is still correct when the optional k= tag is not present in the DKIM public key record - Fix -v verbose reporting in dkimverify.py - Fix unbound local variable error when processing signatures with an x tag, but no t tag (LP: #1739637) 2017-05-30 Version 0.6.2 - Fixed problem with header folding that caused the first line to be folded too long (Updated test test_add_body_length since l= tag is no longer at the beginning of a line) - Fixed python3.4 string interpolation issue - Fix some byte casting issues & typos - Add test case for verification when should headers are signed - Check and update references: * Replace RFC 4871 withRFC 6376 * Replaace RFC 3447 with RFC 8017 * Add mention of DCRUP working group addressing key length issues 2017-01-27 Version 0.6.1 - Fixed python3 dns lookup issue - Fixed arcverify.py issue 2017-01-23 Version 0.6.0 - Add capability to sign and verify ARC signatures - Added new script, dknewkey.py, to generate DKIM keys 2015-12-07 Version 0.5.6 - Brown paper bag release, 0.5.5 tarball inadvertently included pyc files and other artifacts from development 2015-12-07 Version 0.5.5 - Fix and test case for case insensitive subdomain matching. - Python3 compatibility fixes and test cases thanks to Diane Trout 2013-06-10 Version 0.5.4 - Fixed error in FWS regular expression that cause some valid signatures to fail verification (Thanks to Peter Palfrader (weasel) for the patch) http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=711751 - Change dkimsign.py to sign the default (recommended) set of headers instead of all headers 2012-10-27 Version 0.5.3 - Make key parsing error messages more specific to help troubleshooting based on user feedback 2012-06-13 Version 0.5.2 - Change canonicalization defaults to work around issues with different verification implementations - Fully fold DKIM-Signature on sign, and ignore FWS in b= value on verify - Fix hashing problem while signing using sha1 - Trap NXDOMAIN exception with dnspython - Other minor bug fixes 2012-02-03 Version 0.5.1 - Rename tarball to dkimpy to avoid confusion with original project - Apply performance patch from - save parsed signatures in DKIM object - do not require DNS/dnspython for signing 2011-10-26 Version 0.5 - Add test case and fix for - Add test case and fix for - Fix dkim.fold() - raise KeyFormatError when missing required key parts in DNS - do not sign all headers by default - option to verify signatures other than first 2011-06-16 Version 0.4.2 - Fix use of dns namespace so dnspython works 2011-06-15 Version 0.4.1 - Fix some except clauses for python3 - Correct Changelog and release versions - Add test case for - add back dkim.Relaxed and dkim.Simple constants 2011-06-14 Version 0.4 - new API: class DKIM to retrieve domain and other info after verify - Add support for python3 - pydns driver tested and fixed - when producing Relaxed mode signatures, the partial DKIM-Signature header must be canonicalized before hashing (Martin Pool) - other bug fixes 2008-06-25 Version 0.3 - length parameter to sign() is now a boolean - sign() now folds the DKIM-Signature line - validation of all inputs - general code cleanup 2008-02-19 Version 0.2 - handle "rsa-sha1" algorithm properly - handle multiple DKIM-Signature lines - handle FWS around = in DKIM-Signature lines - handle case of single canonicalization algorithm - handle l= signature property 2008-02-18 Version 0.1 - initial release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/LICENSE0000644000175100017510000000172313642525265014322 0ustar00kittermakittermaThis software is provided 'as-is', without any express or implied warranty. In no event will the author be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Copyright (c) 2008 Greg Hewgill http://hewgill.com See individual files for information about modification to these files and additional copyright information. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/MANIFEST.in0000644000175100017510000000031013642527701015037 0ustar00kittermakittermainclude dkim/* include dkim/tests/* include dkim/tests/data/* include LICENSE include README.md include ChangeLog include MANIFEST.in include man/* include test.py exclude dkim/*.pyc dkim/tests/*.pyc ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1713123138.070522 dkimpy-1.1.6/PKG-INFO0000644000175100017510000002170014607027502014400 0ustar00kittermakittermaMetadata-Version: 2.1 Name: dkimpy Version: 1.1.6 Summary: DKIM (DomainKeys Identified Mail), ARC (Authenticated Receive Chain), and TLSRPT (TLS Report) email signing and verification Home-page: https://launchpad.net/dkimpy Author: Scott Kitterman Author-email: scott@kitterman.com License: BSD-like Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: Developers Classifier: License :: DFSG approved Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Communications :: Email :: Mail Transport Agents Classifier: Topic :: Communications :: Email :: Filters Classifier: Topic :: Internet :: Name Service (DNS) Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/markdown Provides-Extra: ARC Provides-Extra: asyncio Provides-Extra: ed25519 Provides-Extra: testing License-File: LICENSE dkimpy - DKIM (DomainKeys Identified Mail) https://launchpad.net/dkimpy/ Friendly fork of: http://hewgill.com/pydkim/ # INTRODUCTION dkimpy is a library that implements DKIM (DomainKeys Identified Mail) email signing and verification. Basic DKIM requirements are defined in RFC 6376: https://tools.ietf.org/html/rfc6376 # VERSION This is dkimpy 1.1.6. # REQUIREMENTS Dependencies will be automatically included for normal DKIM usage. The extras_requires feature 'ed25519' will add the dependencies needed for signing and verifying using the new DCRUP ed25519-sha256 algorithm. The extras_requires feature 'ARC' will add the extra dependencies needed for ARC. Similarly, extras_requires feature 'asyncio' will add the extra dependencies needed for asyncio. - Python 3.x >= 3.5. Recent versions have not been on python3 < 3.4, but may still work on earlier python3 versions. - dnspython or py3dns. dnspython is preferred if both are present and installed to satisfy the DNS module requirement if neither are installed. - authres. Needed for ARC. - PyNaCl. Needed for use of ed25519 capability. - aiodns. Needed for asycnio (Requires python3.5 or later) # INSTALLATION This package includes a scripts 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: ```python3 setup.py install --single-version-externally-managed --record=/dev/null``` # DOCUMENTATION An online version of the package documentation for the most recent release can be found at: https://pymilter.org/pydkim/ # TESTING To run dkimpy's test suite: ```PYTHONPATH=. python3 dkim``` or ```python3 test.py``` or ```PYTHONPATH=. python3 -m unittest dkim.tests.test_suite``` Alternatively, if you have testrepository installed: ```testr init``` ```testr run``` You should install all optional dependencies required for the test suite, e.g. by creating a virtualenv and using: ```pip install -e '.[testing]'``` The included ARC tests are very limited. The primary testing method for ARC is using the ARC test suite: https://github.com/ValiMail/arc_test_suite As of 0.6.0, all tests pass for both python2.7 and python3. The test suite ships with test runners for dkimpy. After downloading the test suite, you can run the signing and validation tests like this: ```python3 ./testarc.py sign runners/arcsigntest.py``` ```python3 ./testarc.py validate runners/arcverifytest.py``` As ov version 1.1.0, python2.7 is no longer supported. # USAGE The dkimpy library offers one module called dkim. The sign() function takes an RFC822 formatted message, along with some signing options, and returns a DKIM-Signature header line that can be prepended to the message. The verify() function takes an RFC822 formatted message, and returns True or False depending on whether the signature verifies correctly. There is also a DKIM class which can be used to perform these functions in a more modern way. In version 0.9.0, the default set of header fields that are oversigned was changed from 'from', 'subject', 'date' to 'from' to reduce fragility of signatures. To restore the previous behavior, you can add them back after instantiating your DKIM class using the add_frozen function as shown in the following example: ```python >>> dkim = DKIM() >>> dkim.add_frozen((b'date',b'subject')) >>> [text(x) for x in sorted(dkim.frozen_sign)] ['date', 'from', 'subject'] ``` ## DKIM RSA MODERNIZATION (RFC 8301) RFC8301 updated DKIM requirements in two ways: 1. It set the minimum valid RSA key size to 1024 bits. 2. It removed use of rsa-sha1. As of version 0.7, the dkimpy defaults largely support these requirements. It is possible to override the minimum key size to a lower value, but this is strongly discouraged. As of 2018, keys much smaller than the minimum are not difficult to factor. The code for rsa-sha1 signing and verification is retained, but not used for signing by default. Future releases will raise warnings and then errors when verifying rsa-sha1 signatures. There are still some significant users of rsa-sha1 signatures, so operationally it's premature to disable verification of rsa-sha1. ## ED25519 (RFC 8463) SUPPORT As of version 0.7, experimental signing and verifying of DKIM Ed25519 signatures is supported as described in draft-ietf-dcrup-dkim-crypto: https://datatracker.ietf.org/doc/draft-ietf-dcrup-dkim-crypto/ The RFC that documents ed25519 DKIM signatures, RFC 8463, has been released and dkimpy 0.7 and later are aligned to its requirements. As of 0.8, ed25519 need not be considered experimental. The dkimpy implementation has successfully interoperated with three other implementations and the technical parameters for ed25519-sha256 are defined and stable. To install from pypi with the required optional depenencies, use the ed25519 option: ```pip install -e '.[ed25519]'``` ## DKIM SCRIPTS Three helper programs are also supplied: dknewkey, dkimsign and dkimverify dknewkey is s script that produces private and public key pairs suitable for use with DKIM. Note that the private key file format used for ed25519 is not standardized (there is no standard) and is unique to dkimpy. Creation of keys should be done in a secure environment. If an unauthorized entity gains access to current private keys they can generate signed email that will pass DKIM checkes and will be difficult to repudiate. dkimsign is a filter that reads an RFC822 message on standard input, and writes the same message on standard output with a DKIM-Signature line prepended. The signing options are specified on the command line: dkimsign selector domain privatekeyfile [identity] The identity is optional and defaults to "@domain". dkimverify reads an RFC822 message on standard input, and returns with exit code 0 if the signature verifies successfully. Otherwise, it returns with exit code 1. ## ARC (Authenticated Receive Chain) As of version 0.6.0, dkimpy provides experimental support for ARC (Authenticated Received Chain). See RFC 8617 for the current version of ARC: https://tools.ietf.org/html/rfc8617 In addition to arcsign and arcverify, the dkim module now provides arc_sign and arc_verify functions as well as an ARC class. If an invalid authentication results header field is included in the set for ARC, it is ignored and no error is raised. Both DKIM ed25519 and ARC are now considered stable (no longer experimantal). ## ASYNC SUPPORT As of version 1.0, an alternative to dkim.verify for use in an async environment is provied. It requires aiodns, https://pypi.org/project/aiodns/. Here is a simple example of dkim.verify_async usage: ```python >>> sys.stdin = sys.stdin.detach() >>> >>> async def main(): >>> res = await dkim.verify_async(message) >>> return res >>> >>> if __name__ == "__main__": >>> res = asyncio.run(main()) ``` This feature requires python3.5 or newer. If aiodns is available, the async functions will be used. To avoide async when aiodns is availale, set dkim.USE_ASYNC = False. ## TLSRPT (TLS Report) As of version 1.0, the RFC 8460 tlsrpt service type is supported: https://tools.ietf.org/html/rfc8460 A non-tlsrpt signed with a key record with s=tlsrpt won't verify. Since the service type (s=) is optional in the DKIM public key record, it is not required by RFC 8460. When checking for a tlsrpt signature, set the tlsrpt= flag when verifying the signature: ```python >>> res = dkim.verify(smessage, tlsrpt='strict') ``` If tlsrpt='strict', only public key records with s=tlsrpt will be considered valid. If set to tlsrpt=True, the service type is not required, but other RFC 8460 requirements are applied. # LIMITATIONS Dkimpy will correctly sign/verify messages with ASCII or UTF-8 content. Messages that contain other types of content will not verify correctly. It does not yet implement RFC 8616, Email Authentication for Internationalized Mail. # FEEDBACK Bug reports may be submitted to the bug tracker for the dkimpy project on launchpad. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713122758.0 dkimpy-1.1.6/README.md0000644000175100017510000001767214607026706014604 0ustar00kittermakittermadkimpy - DKIM (DomainKeys Identified Mail) https://launchpad.net/dkimpy/ Friendly fork of: http://hewgill.com/pydkim/ # INTRODUCTION dkimpy is a library that implements DKIM (DomainKeys Identified Mail) email signing and verification. Basic DKIM requirements are defined in RFC 6376: https://tools.ietf.org/html/rfc6376 # VERSION This is dkimpy 1.1.6. # REQUIREMENTS Dependencies will be automatically included for normal DKIM usage. The extras_requires feature 'ed25519' will add the dependencies needed for signing and verifying using the new DCRUP ed25519-sha256 algorithm. The extras_requires feature 'ARC' will add the extra dependencies needed for ARC. Similarly, extras_requires feature 'asyncio' will add the extra dependencies needed for asyncio. - Python 3.x >= 3.5. Recent versions have not been on python3 < 3.4, but may still work on earlier python3 versions. - dnspython or py3dns. dnspython is preferred if both are present and installed to satisfy the DNS module requirement if neither are installed. - authres. Needed for ARC. - PyNaCl. Needed for use of ed25519 capability. - aiodns. Needed for asycnio (Requires python3.5 or later) # INSTALLATION This package includes a scripts 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: ```python3 setup.py install --single-version-externally-managed --record=/dev/null``` # DOCUMENTATION An online version of the package documentation for the most recent release can be found at: https://pymilter.org/pydkim/ # TESTING To run dkimpy's test suite: ```PYTHONPATH=. python3 dkim``` or ```python3 test.py``` or ```PYTHONPATH=. python3 -m unittest dkim.tests.test_suite``` Alternatively, if you have testrepository installed: ```testr init``` ```testr run``` You should install all optional dependencies required for the test suite, e.g. by creating a virtualenv and using: ```pip install -e '.[testing]'``` The included ARC tests are very limited. The primary testing method for ARC is using the ARC test suite: https://github.com/ValiMail/arc_test_suite As of 0.6.0, all tests pass for both python2.7 and python3. The test suite ships with test runners for dkimpy. After downloading the test suite, you can run the signing and validation tests like this: ```python3 ./testarc.py sign runners/arcsigntest.py``` ```python3 ./testarc.py validate runners/arcverifytest.py``` As ov version 1.1.0, python2.7 is no longer supported. # USAGE The dkimpy library offers one module called dkim. The sign() function takes an RFC822 formatted message, along with some signing options, and returns a DKIM-Signature header line that can be prepended to the message. The verify() function takes an RFC822 formatted message, and returns True or False depending on whether the signature verifies correctly. There is also a DKIM class which can be used to perform these functions in a more modern way. In version 0.9.0, the default set of header fields that are oversigned was changed from 'from', 'subject', 'date' to 'from' to reduce fragility of signatures. To restore the previous behavior, you can add them back after instantiating your DKIM class using the add_frozen function as shown in the following example: ```python >>> dkim = DKIM() >>> dkim.add_frozen((b'date',b'subject')) >>> [text(x) for x in sorted(dkim.frozen_sign)] ['date', 'from', 'subject'] ``` ## DKIM RSA MODERNIZATION (RFC 8301) RFC8301 updated DKIM requirements in two ways: 1. It set the minimum valid RSA key size to 1024 bits. 2. It removed use of rsa-sha1. As of version 0.7, the dkimpy defaults largely support these requirements. It is possible to override the minimum key size to a lower value, but this is strongly discouraged. As of 2018, keys much smaller than the minimum are not difficult to factor. The code for rsa-sha1 signing and verification is retained, but not used for signing by default. Future releases will raise warnings and then errors when verifying rsa-sha1 signatures. There are still some significant users of rsa-sha1 signatures, so operationally it's premature to disable verification of rsa-sha1. ## ED25519 (RFC 8463) SUPPORT As of version 0.7, experimental signing and verifying of DKIM Ed25519 signatures is supported as described in draft-ietf-dcrup-dkim-crypto: https://datatracker.ietf.org/doc/draft-ietf-dcrup-dkim-crypto/ The RFC that documents ed25519 DKIM signatures, RFC 8463, has been released and dkimpy 0.7 and later are aligned to its requirements. As of 0.8, ed25519 need not be considered experimental. The dkimpy implementation has successfully interoperated with three other implementations and the technical parameters for ed25519-sha256 are defined and stable. To install from pypi with the required optional depenencies, use the ed25519 option: ```pip install -e '.[ed25519]'``` ## DKIM SCRIPTS Three helper programs are also supplied: dknewkey, dkimsign and dkimverify dknewkey is s script that produces private and public key pairs suitable for use with DKIM. Note that the private key file format used for ed25519 is not standardized (there is no standard) and is unique to dkimpy. Creation of keys should be done in a secure environment. If an unauthorized entity gains access to current private keys they can generate signed email that will pass DKIM checkes and will be difficult to repudiate. dkimsign is a filter that reads an RFC822 message on standard input, and writes the same message on standard output with a DKIM-Signature line prepended. The signing options are specified on the command line: dkimsign selector domain privatekeyfile [identity] The identity is optional and defaults to "@domain". dkimverify reads an RFC822 message on standard input, and returns with exit code 0 if the signature verifies successfully. Otherwise, it returns with exit code 1. ## ARC (Authenticated Receive Chain) As of version 0.6.0, dkimpy provides experimental support for ARC (Authenticated Received Chain). See RFC 8617 for the current version of ARC: https://tools.ietf.org/html/rfc8617 In addition to arcsign and arcverify, the dkim module now provides arc_sign and arc_verify functions as well as an ARC class. If an invalid authentication results header field is included in the set for ARC, it is ignored and no error is raised. Both DKIM ed25519 and ARC are now considered stable (no longer experimantal). ## ASYNC SUPPORT As of version 1.0, an alternative to dkim.verify for use in an async environment is provied. It requires aiodns, https://pypi.org/project/aiodns/. Here is a simple example of dkim.verify_async usage: ```python >>> sys.stdin = sys.stdin.detach() >>> >>> async def main(): >>> res = await dkim.verify_async(message) >>> return res >>> >>> if __name__ == "__main__": >>> res = asyncio.run(main()) ``` This feature requires python3.5 or newer. If aiodns is available, the async functions will be used. To avoide async when aiodns is availale, set dkim.USE_ASYNC = False. ## TLSRPT (TLS Report) As of version 1.0, the RFC 8460 tlsrpt service type is supported: https://tools.ietf.org/html/rfc8460 A non-tlsrpt signed with a key record with s=tlsrpt won't verify. Since the service type (s=) is optional in the DKIM public key record, it is not required by RFC 8460. When checking for a tlsrpt signature, set the tlsrpt= flag when verifying the signature: ```python >>> res = dkim.verify(smessage, tlsrpt='strict') ``` If tlsrpt='strict', only public key records with s=tlsrpt will be considered valid. If set to tlsrpt=True, the service type is not required, but other RFC 8460 requirements are applied. # LIMITATIONS Dkimpy will correctly sign/verify messages with ASCII or UTF-8 content. Messages that contain other types of content will not verify correctly. It does not yet implement RFC 8616, Email Authentication for Internationalized Mail. # FEEDBACK Bug reports may be submitted to the bug tracker for the dkimpy project on launchpad. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713123138.0665221 dkimpy-1.1.6/dkim/0000755000175100017510000000000014607027502014227 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713118610.0 dkimpy-1.1.6/dkim/__init__.py0000644000175100017510000016122014607016622016343 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant # # This has been modified from the original software. # Copyright (c) 2016 Google, Inc. # Contact: Brandon Long # # This has been modified from the original software. # Copyright (c) 2016, 2017, 2018, 2019 Scott Kitterman # # This has been modified from the original software. # Copyright (c) 2017 Valimail Inc # Contact: Gene Shuman # import base64 import hashlib import logging import re import sys import time import binascii # Set to False to not use async functions even though aiodns is installed. USE_ASYNC = True # only needed for arc try: import authres except ImportError: pass # only needed for ed25519 signing/verification try: import nacl.signing import nacl.encoding except ImportError: pass from dkim.canonicalization import ( CanonicalizationPolicy, InvalidCanonicalizationPolicyError, ) from dkim.canonicalization import Relaxed as RelaxedCanonicalization from dkim.crypto import ( DigestTooLargeError, HASH_ALGORITHMS, ARC_HASH_ALGORITHMS, parse_pem_private_key, parse_public_key, RSASSA_PKCS1_v1_5_sign, RSASSA_PKCS1_v1_5_verify, UnparsableKeyError, ) try: from dkim.dnsplug import get_txt except ImportError: if USE_ASYNC: try: import aiodns from dkim.asyncsupport import get_txt_async as get_txt except: # Only true if not using async def get_txt(s,timeout=5): raise RuntimeError("DKIM.verify requires DNS or dnspython module") else: raise RuntimeError("DKIM.verify requires DNS or dnspython module") from dkim.util import ( get_default_logger, InvalidTagValueList, parse_tag_value, ) __all__ = [ "DKIMException", "InternalError", "KeyFormatError", "MessageFormatError", "ParameterError", "ValidationError", "AuthresNotFoundError", "NaClNotFoundError", "DnsTimeoutError", "USE_ASYNC", "CV_Pass", "CV_Fail", "CV_None", "Relaxed", "Simple", "DKIM", "ARC", "sign", "verify", "dkim_sign", "dkim_verify", "arc_sign", "arc_verify", ] Relaxed = b'relaxed' # for clients passing dkim.Relaxed Simple = b'simple' # for clients passing dkim.Simple # for ARC CV_Pass = b'pass' CV_Fail = b'fail' CV_None = b'none' class HashThrough(object): def __init__(self, hasher, debug=False): self.data = [] self.hasher = hasher self.name = hasher.name self.debug = debug def update(self, data): if self.debug: self.data.append(data) return self.hasher.update(data) def digest(self): return self.hasher.digest() def hexdigest(self): return self.hasher.hexdigest() def hashed(self): return b''.join(self.data) def bitsize(x): """Return size of long in bits.""" return len(bin(x)) - 2 class DKIMException(Exception): """Base class for DKIM errors.""" pass class InternalError(DKIMException): """Internal error in dkim module. Should never happen.""" pass class KeyFormatError(DKIMException): """Key format error while parsing an RSA public or private key.""" pass class MessageFormatError(DKIMException): """RFC822 message format error.""" pass class ParameterError(DKIMException): """Input parameter error.""" pass class ValidationError(DKIMException): """Validation error.""" pass class AuthresNotFoundError(DKIMException): """ Authres Package not installed, needed for ARC """ pass class NaClNotFoundError(DKIMException): """ Nacl package not installed, needed for ed25119 signatures """ pass class UnknownKeyTypeError(DKIMException): """ Key type (k tag) is not known (rsa/ed25519) """ class DnsTimeoutError(DKIMException): """ DNS query for public key timed out """ def select_headers(headers, include_headers): """Select message header fields to be signed/verified. >>> h = [('from','biz'),('foo','bar'),('from','baz'),('subject','boring')] >>> i = ['from','subject','to','from'] >>> select_headers(h,i) [('from', 'baz'), ('subject', 'boring'), ('from', 'biz')] >>> h = [('From','biz'),('Foo','bar'),('Subject','Boring')] >>> i = ['from','subject','to','from'] >>> select_headers(h,i) [('From', 'biz'), ('Subject', 'Boring')] """ sign_headers = [] lastindex = {} for h in include_headers: assert h == h.lower() i = lastindex.get(h, len(headers)) while i > 0: i -= 1 if h == headers[i][0].lower(): sign_headers.append(headers[i]) break lastindex[h] = i return sign_headers # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space [RFC5322] FWS = br'(?:(?:\s*\r?\n)?\s+)?' RE_BTAG = re.compile(br'([;\s]b'+FWS+br'=)(?:'+FWS+br'[a-zA-Z0-9+/=])*(?:\r?\n\Z)?') def hash_headers(hasher, canonicalize_headers, headers, include_headers, sigheader, sig): """Update hash for signed message header fields.""" sign_headers = select_headers(headers,include_headers) # The call to _remove() assumes that the signature b= only appears # once in the signature header cheaders = canonicalize_headers.canonicalize_headers( [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))]) # the dkim sig is hashed with no trailing crlf, even if the # canonicalization algorithm would add one. for x,y in sign_headers + [(x, y.rstrip()) for x,y in cheaders]: hasher.update(x) hasher.update(b":") hasher.update(y) return sign_headers def hash_headers_ed25519(pk, canonicalize_headers, headers, include_headers, sigheader, sig): """Update hash for signed message header fields.""" hash_header = '' sign_headers = select_headers(headers,include_headers) # The call to _remove() assumes that the signature b= only appears # once in the signature header cheaders = canonicalize_headers.canonicalize_headers( [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))]) # the dkim sig is hashed with no trailing crlf, even if the # canonicalization algorithm would add one. for x,y in sign_headers + [(x, y.rstrip()) for x,y in cheaders]: hash_header += x + y return sign_headers, hash_header def validate_signature_fields(sig, mandatory_fields=[b'v', b'a', b'b', b'bh', b'd', b'h', b's'], arc=False): """Validate DKIM or ARC Signature fields. Basic checks for presence and correct formatting of mandatory fields. Raises a ValidationError if checks fail, otherwise returns None. @param sig: A dict mapping field keys to values. @param mandatory_fields: A list of non-optional fields @param arc: flag to differentiate between dkim & arc """ if arc: hashes = ARC_HASH_ALGORITHMS else: hashes = HASH_ALGORITHMS for field in mandatory_fields: if field not in sig: raise ValidationError("missing %s=" % field) if b'a' in sig and not sig[b'a'] in hashes: raise ValidationError("unknown signature algorithm: %s" % sig[b'a']) if b'b' in sig: if re.match(br"[\s0-9A-Za-z+/]+[\s=]*$", sig[b'b']) is None: raise ValidationError("b= value is not valid base64 (%s)" % sig[b'b']) if len(re.sub(br"\s+", b"", sig[b'b'])) % 4 != 0: raise ValidationError("b= value is not valid base64 (%s)" % sig[b'b']) if b'bh' in sig: if re.match(br"[\s0-9A-Za-z+/]+[\s=]*$", sig[b'b']) is None: raise ValidationError("bh= value is not valid base64 (%s)" % sig[b'bh']) if len(re.sub(br"\s+", b"", sig[b'bh'])) % 4 != 0: raise ValidationError("bh= value is not valid base64 (%s)" % sig[b'bh']) if b'cv' in sig and sig[b'cv'] not in (CV_Pass, CV_Fail, CV_None): raise ValidationError("cv= value is not valid (%s)" % sig[b'cv']) # Limit domain validation to ASCII domains because too hard try: str(sig[b'd'], 'ascii') # No specials, which is close enough if re.findall(rb"[\(\)<>\[\]:;@\\,]", sig[b'd']): raise ValidationError("d= value is not valid (%s)" % sig[b'd']) except UnicodeDecodeError as e: # Not an ASCII domain pass # Nasty hack to support both str and bytes... check for both the # character and integer values. if not arc and b'i' in sig and ( not sig[b'i'].lower().endswith(sig[b'd'].lower()) or sig[b'i'][-len(sig[b'd'])-1] not in ('@', '.', 64, 46)): raise ValidationError( "i= domain is not a subdomain of d= (i=%s d=%s)" % (sig[b'i'], sig[b'd'])) if b'l' in sig and re.match(br"\d{,76}$", sig[b'l']) is None: raise ValidationError( "l= value is not a decimal integer (%s)" % sig[b'l']) if b'q' in sig and sig[b'q'] != b"dns/txt": raise ValidationError("q= value is not dns/txt (%s)" % sig[b'q']) if b't' in sig: if re.match(br"\d+$", sig[b't']) is None: raise ValidationError( "t= value is not a decimal integer (%s)" % sig[b't']) now = int(time.time()) slop = 36000 # 10H leeway for mailers with inaccurate clocks t_sign = int(sig[b't']) if t_sign > now + slop: raise ValidationError("t= value is in the future (%s)" % sig[b't']) if b'v' in sig and sig[b'v'] != b"1": raise ValidationError("v= value is not 1 (%s)" % sig[b'v']) if b'x' in sig: if re.match(br"\d+$", sig[b'x']) is None: raise ValidationError( "x= value is not a decimal integer (%s)" % sig[b'x']) x_sign = int(sig[b'x']) now = int(time.time()) slop = 36000 # 10H leeway for mailers with inaccurate clocks if x_sign < now - slop: raise ValidationError( "x= value is past (%s)" % sig[b'x']) if x_sign < t_sign: raise ValidationError( "x= value is less than t= value (x=%s t=%s)" % (sig[b'x'], sig[b't'])) def rfc822_parse(message): """Parse a message in RFC822 format. @param message: The message in RFC822 format. Either CRLF or LF is an accepted line separator. @return: Returns a tuple of (headers, body) where headers is a list of (name, value) pairs. The body is a CRLF-separated string. """ headers = [] lines = re.split(b"\r?\n", message) i = 0 while i < len(lines): if len(lines[i]) == 0: # End of headers, return what we have plus the body, excluding the blank line. i += 1 break if lines[i][0] in ("\x09", "\x20", 0x09, 0x20): headers[-1][1] += lines[i]+b"\r\n" else: m = re.match(br"([\x21-\x7e]+?):", lines[i]) if m is not None: headers.append([m.group(1), lines[i][m.end(0):]+b"\r\n"]) elif lines[i].startswith(b"From "): pass else: raise MessageFormatError("Unexpected characters in RFC822 header: %s" % lines[i]) i += 1 return (headers, b"\r\n".join(lines[i:])) def text(s): """Normalize bytes/str to str for python 2/3 compatible doctests. >>> text(b'foo') 'foo' >>> text(u'foo') 'foo' >>> text('foo') 'foo' """ if type(s) is str: return s s = s.decode('ascii') if type(s) is str: return s return s.encode('ascii') def fold(header, namelen=0, linesep=b'\r\n'): """Fold a header line into multiple crlf-separated lines of text at column 72. The crlf does not count for line length. >>> 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 >>> text(fold(b'x')) 'x' >>> text(fold(b'xyz'*24)) 'xyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyz' >>> len(fold(b'xyz'*48)) 150 """ # 72 is the max line length we actually want, but the header field name # has to fit in the first line too (See Debian Bug #863690). maxleng = 72 - namelen if len(header) <= maxleng: return header if len(header) - header.rfind(b"\r\n") == 2 and len(header) <= maxleng +2: return header i = header.rfind(b"\r\n ") if i == -1: pre = b"" else: i += 3 pre = header[:i] header = header[i:] while len(header) > maxleng: i = header[:maxleng].rfind(b" ") if i == -1: j = maxleng pre += header[:j] + linesep + b" " else: j = i + 1 pre += header[:i] + linesep + b" " header = header[j:] maxleng = 71 if len(header) > 2: return pre + header else: if pre[0] == b' ': return pre[:-1] else: return pre + header def evaluate_pk(name, s): if not s: raise KeyFormatError("missing public key: %s"%name) try: if type(s) is str: s = s.encode('ascii') pub = parse_tag_value(s) except InvalidTagValueList as e: raise KeyFormatError(e) try: if pub[b'v'] != b'DKIM1': raise KeyFormatError("bad version") except KeyError as e: # Version not required in key record: RFC 6376 3.6.1 pass try: if pub[b'k'] == b'ed25519': try: pk = nacl.signing.VerifyKey(pub[b'p'], encoder=nacl.encoding.Base64Encoder) except NameError: raise NaClNotFoundError('pynacl module required for ed25519 signing, see README.md') except nacl.exceptions.ValueError as e: raise KeyFormatError("could not parse ed25519 public key (%s): %s" % (pub[b'p'],e)) keysize = 256 ktag = b'ed25519' except KeyError: pub[b'k'] = b'rsa' if pub[b'k'] == b'rsa': try: pk = parse_public_key(base64.b64decode(pub[b'p'])) keysize = bitsize(pk['modulus']) except KeyError: raise KeyFormatError("incomplete RSA public key: %s" % s) except (TypeError,UnparsableKeyError) as e: raise KeyFormatError("could not parse RSA public key (%s): %s" % (pub[b'p'],e)) ktag = b'rsa' if pub[b'k'] != b'rsa' and pub[b'k'] != b'ed25519': raise KeyFormatError('unknown algorithm in k= tag: {0}'.format(pub[b'k'])) seqtlsrpt = False try: # Ignore unknown service types, RFC 6376 3.6.1 if pub[b's'] != b'*' and pub[b's'] != b'email' and pub[b's'] != b'tlsrpt': pk = None keysize = None ktag = None raise KeyFormatError('unknown service type in s= tag: {0}'.format(pub[b's'])) elif pub[b's'] == b'tlsrpt': seqtlsrpt = True except: # Default is '*' - all service types, so no error if missing from key record pass return pk, keysize, ktag, seqtlsrpt def load_pk_from_dns(name, dnsfunc=get_txt, timeout=5): s = dnsfunc(name, timeout=timeout) pk, keysize, ktag, seqtlsrpt = evaluate_pk(name, s) return pk, keysize, ktag, seqtlsrpt #: Abstract base class for holding messages and options during DKIM/ARC signing and verification. class DomainSigner(object): # NOTE - the first 2 indentation levels are 2 instead of 4 # to minimize changed lines from the function only version. #: @param message: an RFC822 formatted message to be signed or verified #: (with either \\n or \\r\\n line endings) #: @param logger: a logger to which debug info will be written (default None) #: @param signature_algorithm: the signing algorithm to use when signing #: @param debug_content: log headers and body after canonicalization (default False) #: @param linesep: use this line seperator for folding the headers #: @param timeout: number of seconds for DNS lookup timeout (default = 5) #: @param tlsrpt: message is an RFC 8460 TLS report (default False) #: False: Not a tlsrpt, True: Is a tlsrpt, 'strict': tlsrpt, invalid if #: service type is missing. For signing, if True, length is never used. def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256', minkey=1024, linesep=b'\r\n', debug_content=False, timeout=5, tlsrpt=False): self.set_message(message) if logger is None: logger = get_default_logger() self.logger = logger self.debug_content = debug_content and logger.isEnabledFor(logging.DEBUG) if signature_algorithm not in HASH_ALGORITHMS: raise ParameterError( "Unsupported signature algorithm: "+signature_algorithm) self.signature_algorithm = signature_algorithm #: Header fields which should be signed. Default as suggested by RFC6376 self.should_sign = set(DKIM.SHOULD) #: Header fields which should not be signed. The default is from RFC6376. #: Attempting to sign these headers results in an exception. #: If it is necessary to sign one of these, it must be removed #: from this list first. self.should_not_sign = set(DKIM.SHOULD_NOT) #: Header fields to sign an extra time to prevent additions. self.frozen_sign = set(DKIM.FROZEN) #: Minimum public key size. Shorter keys raise KeyFormatError. The #: default is 1024 self.minkey = minkey # use this line seperator for output self.linesep = linesep self.timeout = timeout self.tlsrpt = tlsrpt # Service type in DKIM record is s=tlsrpt self.seqtlsrpt = False #: Header fields to protect from additions by default. #: #: The short list below is the result more of instinct than logic. #: @since: 0.5 FROZEN = (b'from',) #: The rfc6376 recommended header fields to sign #: @since: 0.5 SHOULD = ( b'from', b'sender', b'reply-to', b'subject', b'date', b'message-id', b'to', b'cc', b'mime-version', b'content-type', b'content-transfer-encoding', b'content-id', b'content-description', b'resent-date', b'resent-from', b'resent-sender', b'resent-to', b'resent-cc', b'resent-message-id', b'in-reply-to', b'references', b'list-id', b'list-help', b'list-unsubscribe', b'list-subscribe', b'list-post', b'list-owner', b'list-archive' ) #: The rfc6376 recommended header fields not to sign. #: @since: 0.5 SHOULD_NOT = ( b'return-path',b'received',b'comments',b'keywords',b'bcc',b'resent-bcc', b'dkim-signature' ) # Doesn't seem to be used (GS) #: The U{RFC5322} #: complete list of singleton headers (which should #: appear at most once). This can be used for a "paranoid" or #: "strict" signing mode. #: Bcc in this list is in the SHOULD NOT sign list, the rest could #: be in the default FROZEN list, but that could also make signatures #: more fragile than necessary. #: @since: 0.5 RFC5322_SINGLETON = (b'date',b'from',b'sender',b'reply-to',b'to',b'cc',b'bcc', b'message-id',b'in-reply-to',b'references') def add_frozen(self,s): """ Add headers not in should_not_sign to frozen_sign. @param s: list of headers to add to frozen_sign @since: 0.5 >>> dkim = DKIM() >>> dkim.add_frozen(DKIM.RFC5322_SINGLETON) >>> [text(x) for x in sorted(dkim.frozen_sign)] ['cc', 'date', 'from', 'in-reply-to', 'message-id', 'references', 'reply-to', 'sender', 'to'] >>> dkim2 = DKIM() >>> dkim2.add_frozen((b'date',b'subject')) >>> [text(x) for x in sorted(dkim2.frozen_sign)] ['date', 'from', 'subject'] """ self.frozen_sign.update(x.lower() for x in s if x.lower() not in self.should_not_sign) def add_should_not(self,s): """ Add headers not in should_not_sign to frozen_sign. @param s: list of headers to add to frozen_sign @since: 0.9 >>> dkim = DKIM() >>> dkim.add_should_not(DKIM.RFC5322_SINGLETON) >>> [text(x) for x in sorted(dkim.should_not_sign)] ['bcc', 'cc', 'comments', 'date', 'dkim-signature', 'in-reply-to', 'keywords', 'message-id', 'received', 'references', 'reply-to', 'resent-bcc', 'return-path', 'sender', 'to'] """ self.should_not_sign.update(x.lower() for x in s if x.lower() not in self.frozen_sign) #: Load a new message to be signed or verified. #: @param message: an RFC822 formatted message to be signed or verified #: (with either \\n or \\r\\n line endings) #: @since: 0.5 def set_message(self,message): if message: self.headers, self.body = rfc822_parse(message) else: self.headers, self.body = [],'' #: The DKIM signing domain last signed or verified. self.domain = None #: The DKIM key selector last signed or verified. self.selector = 'default' #: Signature parameters of last sign or verify. To parse #: a DKIM-Signature header field that you have in hand, #: use L{dkim.util.parse_tag_value}. self.signature_fields = {} #: The list of headers last signed or verified. Each header #: is a name,value tuple. FIXME: The headers are canonicalized. #: This could be more useful as original headers. self.signed_headers = [] #: The public key size last verified. self.keysize = 0 def default_sign_headers(self): """Return the default list of headers to sign: those in should_sign or frozen_sign, with those in frozen_sign signed an extra time to prevent additions. @since: 0.5""" hset = self.should_sign | self.frozen_sign include_headers = [ x for x,y in self.headers if x.lower() in hset ] return include_headers + [ x for x in include_headers if x.lower() in self.frozen_sign] def all_sign_headers(self): """Return header list of all existing headers not in should_not_sign. @since: 0.5""" return [x for x,y in self.headers if x.lower() not in self.should_not_sign] # Abstract helper method to generate a tag=value header from a list of fields #: @param fields: A list of key value tuples to be included in the header #: @param include_headers: A list message headers to include in the b= signature computation #: @param canon_policy: A canonicialization policy for b= & bh= #: @param header_name: The name of the generated header #: @param pk: The private key used for signature generation #: @param standardize: Flag to enable 'standard' header syntax def gen_header(self, fields, include_headers, canon_policy, header_name, pk, standardize=False): if standardize: lower = [(x,y.lower().replace(b' ', b'')) for (x,y) in fields if x != b'bh'] reg = [(x,y.replace(b' ', b'')) for (x,y) in fields if x == b'bh'] fields = lower + reg fields = sorted(fields, key=(lambda x: x[0])) header_value = b"; ".join(b"=".join(x) for x in fields) if not standardize: header_value = fold(header_value, namelen=len(header_name), linesep=b'\r\n') header_value = RE_BTAG.sub(b'\\1',header_value) header = (header_name, b' ' + header_value) h = HashThrough(self.hasher(), self.debug_content) sig = dict(fields) headers = canon_policy.canonicalize_headers(self.headers) self.signed_headers = hash_headers( h, canon_policy, headers, include_headers, header, sig) if self.debug_content: self.logger.debug("sign %s headers: %r" % (header_name, h.hashed())) if self.signature_algorithm == b'rsa-sha256' or self.signature_algorithm == b'rsa-sha1': try: sig2 = RSASSA_PKCS1_v1_5_sign(h, pk) except DigestTooLargeError: raise ParameterError("digest too large for modulus") elif self.signature_algorithm == b'ed25519-sha256': sigobj = pk.sign(h.digest()) sig2 = sigobj.signature # Folding b= is explicity allowed, but yahoo and live.com are broken #header_value += base64.b64encode(bytes(sig2)) # Instead of leaving unfolded (which lets an MTA fold it later and still # breaks yahoo and live.com), we change the default signing mode to # relaxed/simple (for broken receivers), and fold now. idx = [i for i in range(len(fields)) if fields[i][0] == b'b'][0] fields[idx] = (b'b', base64.b64encode(bytes(sig2))) header_value = b"; ".join(b"=".join(x) for x in fields) + self.linesep if not standardize: header_value = fold(header_value, namelen=len(header_name), linesep=self.linesep) return header_value def verify_sig_process(self, sig, include_headers, sig_header, dnsfunc): """Non-async sensitive verify_sig elements. Separated to avoid async code duplication.""" # RFC 8460 MAY ignore signatures without tlsrpt Service Type if self.tlsrpt == 'strict' and not self.seqtlsrpt: raise ValidationError("Message is tlsrpt and Service Type is not tlsrpt") # Inferred requirement from both RFC 8460 and RFC 6376 if not self.tlsrpt and self.seqtlsrpt: raise ValidationError("Message is not tlsrpt and Service Type is tlsrpt") try: canon_policy = CanonicalizationPolicy.from_c_value(sig.get(b'c', b'simple/simple')) except InvalidCanonicalizationPolicyError as e: raise MessageFormatError("invalid c= value: %s" % e.args[0]) hasher = HASH_ALGORITHMS[sig[b'a']] # validate body if present if b'bh' in sig: h = HashThrough(hasher(), self.debug_content) body = canon_policy.canonicalize_body(self.body) if b'l' in sig and not self.tlsrpt: body = body[:int(sig[b'l'])] h.update(body) if self.debug_content: self.logger.debug("body hashed: %r" % h.hashed()) bodyhash = h.digest() self.logger.debug("bh: %s" % base64.b64encode(bodyhash)) try: bh = base64.b64decode(re.sub(br"\s+", b"", sig[b'bh'])) except TypeError as e: raise MessageFormatError(str(e)) if bodyhash != bh: raise ValidationError( "body hash mismatch (got %s, expected %s)" % (base64.b64encode(bodyhash), sig[b'bh'])) # address bug#644046 by including any additional From header # fields when verifying. Since there should be only one From header, # this shouldn't break any legitimate messages. This could be # generalized to check for extras of other singleton headers. if b'from' in include_headers: include_headers.append(b'from') h = HashThrough(hasher(), self.debug_content) headers = canon_policy.canonicalize_headers(self.headers) self.signed_headers = hash_headers( h, canon_policy, headers, include_headers, sig_header, sig) if self.debug_content: self.logger.debug("signed for %s: %r" % (sig_header[0], h.hashed())) signature = base64.b64decode(re.sub(br"\s+", b"", sig[b'b'])) if self.ktag == b'rsa': try: res = RSASSA_PKCS1_v1_5_verify(h, signature, self.pk) self.logger.debug("%s valid: %s" % (sig_header[0], res)) if res and self.keysize < self.minkey: raise KeyFormatError("public key too small: %d" % self.keysize) return res except (TypeError,DigestTooLargeError) as e: raise KeyFormatError("digest too large for modulus: %s"%e) elif self.ktag == b'ed25519': try: self.pk.verify(h.digest(), signature) self.logger.debug("%s valid" % (sig_header[0])) return True except (nacl.exceptions.BadSignatureError) as e: return False else: raise UnknownKeyTypeError(self.ktag) # Abstract helper method to verify a signed header #: @param sig: List of (key, value) tuples containing tag=values of the header #: @param include_headers: headers to validate b= signature against #: @param sig_header: (header_name, header_value) #: @param dnsfunc: interface to dns def verify_sig(self, sig, include_headers, sig_header, dnsfunc): name = sig[b's'] + b"._domainkey." + sig[b'd'] + b"." try: self.pk, self.keysize, self.ktag, self.seqtlsrpt = load_pk_from_dns(name, dnsfunc, timeout=self.timeout) except KeyFormatError as e: self.logger.error("%s" % e) return False except binascii.Error as e: self.logger.error('KeyFormatError: {0}'.format(e)) return False except DnsTimeoutError as e: self.logger.error('DnsTimeoutError: Domain: {0} Selector: {1} Error message: {2}'.format( sig[b'd'], sig[b's'], e)) return False return self.verify_sig_process(sig, include_headers, sig_header, dnsfunc) #: Hold messages and options during DKIM signing and verification. class DKIM(DomainSigner): #: Sign an RFC822 message and return the DKIM-Signature header line. #: #: The include_headers option gives full control over which header fields #: are signed. Note that signing a header field that doesn't exist prevents #: that field from being added without breaking the signature. Repeated #: fields (such as Received) can be signed multiple times. Instances #: of the field are signed from bottom to top. Signing a header field more #: times than are currently present prevents additional instances #: from being added without breaking the signature. #: #: The length option allows the message body to be appended to by MTAs #: enroute (e.g. mailing lists that append unsubscribe information) #: without breaking the signature. #: #: The default include_headers for this method differs from the backward #: compatible sign function, which signs all headers not #: in should_not_sign. The default list for this method can be modified #: by tweaking should_sign and frozen_sign (or even should_not_sign). #: It is only necessary to pass an include_headers list when precise control #: is needed. #: #: @param selector: the DKIM selector value for the signature #: @param domain: the DKIM domain value for the signature #: @param privkey: a PKCS#1 private key in base64-encoded text form #: @param identity: the DKIM identity value for the signature #: (default "@"+domain) #: @param canonicalize: the canonicalization algorithms to use #: (default (Simple, Simple)) #: @param include_headers: a list of strings indicating which headers #: are to be signed (default rfc4871 recommended headers) #: @param length: true if the l= tag should be included to indicate #: body length signed (default False). #: @return: DKIM-Signature header field terminated by '\r\n' #: @raise DKIMException: when the message, include_headers, or key are badly #: formed. def sign(self, selector, domain, privkey, signature_algorithm=None, identity=None, canonicalize=(b'relaxed',b'simple'), include_headers=None, length=False): if signature_algorithm: self.signature_algorithm = signature_algorithm if self.signature_algorithm == b'rsa-sha256' or self.signature_algorithm == b'rsa-sha1': try: pk = parse_pem_private_key(privkey) except UnparsableKeyError as e: raise KeyFormatError(str(e)) elif self.signature_algorithm == b'ed25519-sha256': try: pk = nacl.signing.SigningKey(privkey, encoder=nacl.encoding.Base64Encoder) except NameError: raise NaClNotFoundError('pynacl module required for ed25519 signing, see README.md') except nacl.exceptions.ValueError: raise KeyFormatError('invalid ed25519 private key or format') if identity is not None and not identity.endswith(domain): raise ParameterError("identity must end with domain") canon_policy = CanonicalizationPolicy.from_c_value(b'/'.join(canonicalize)) if include_headers is None: include_headers = self.default_sign_headers() try: include_headers = [bytes(x, 'utf-8') for x in include_headers] except TypeError: # TypeError means it's already bytes and we're good or we're in # Python 2 and we don't care. See LP: #1776775. pass include_headers = tuple([x.lower() for x in include_headers]) # record what verify should extract self.include_headers = include_headers if self.tlsrpt: # RFC 8460 MUST NOT length = False # rfc4871 says FROM is required if b'from' not in include_headers: raise ParameterError("The From header field MUST be signed") # raise exception for any SHOULD_NOT headers, call can modify # SHOULD_NOT if really needed. for x in set(include_headers).intersection(self.should_not_sign): raise ParameterError("The %s header field SHOULD NOT be signed"%x) body = canon_policy.canonicalize_body(self.body) self.hasher = HASH_ALGORITHMS[self.signature_algorithm] h = self.hasher() h.update(body) bodyhash = base64.b64encode(h.digest()) sigfields = [x for x in [ (b'v', b"1"), (b'a', self.signature_algorithm), (b'c', canon_policy.to_c_value()), (b'd', domain), (b'i', identity or b"@"+domain), length and (b'l', str(len(body)).encode('ascii')), (b'q', b"dns/txt"), (b's', selector), (b't', str(int(time.time())).encode('ascii')), (b'h', b" : ".join(include_headers)), (b'bh', bodyhash), # Force b= to fold onto it's own line so that refolding after # adding sig doesn't change whitespace for previous tags. (b'b', b'0'*60), ] if x] res = self.gen_header(sigfields, include_headers, canon_policy, b"DKIM-Signature", pk) self.domain = domain self.selector = selector self.signature_fields = dict(sigfields) return b'DKIM-Signature: ' + res #: Checks if any DKIM signature is present #: @return: True if there is one or more DKIM signatures present or False otherwise def present(self): return (len([(x,y) for x,y in self.headers if x.lower() == b"dkim-signature"]) > 0) def verify_headerprep(self, idx=0): """Non-DNS verify parts to minimize asyncio code duplication.""" sigheaders = [(x,y) for x,y in self.headers if x.lower() == b"dkim-signature"] if len(sigheaders) <= idx: return False # By default, we validate the first DKIM-Signature line found. try: sig = parse_tag_value(sigheaders[idx][1]) self.signature_fields = sig except InvalidTagValueList as e: raise MessageFormatError(e) self.logger.debug("sig: %r" % sig) validate_signature_fields(sig) self.domain = sig[b'd'] self.selector = sig[b's'] include_headers = [x.lower() for x in re.split(br"\s*:\s*", sig[b'h'])] self.include_headers = tuple(include_headers) return sig, include_headers, sigheaders #: Verify a DKIM signature. #: @type idx: int #: @param idx: which signature to verify. The first (topmost) signature is 0. #: @type dnsfunc: callable #: @param dnsfunc: an option function to lookup TXT resource records #: for a DNS domain. The default uses dnspython or pydns. #: @return: True if signature verifies or False otherwise #: @raise DKIMException: when the message, signature, or key are badly formed def verify(self,idx=0,dnsfunc=get_txt): prep = self.verify_headerprep(idx) if prep: sig, include_headers, sigheaders = prep return self.verify_sig(sig, include_headers, sigheaders[idx], dnsfunc) return False # No signature #: Hold messages and options during ARC signing and verification. class ARC(DomainSigner): #: Header fields used by ARC ARC_HEADERS = (b'arc-seal', b'arc-message-signature', b'arc-authentication-results') #: Regex to extract i= value from ARC headers INSTANCE_RE = re.compile(br'[\s;]?i\s*=\s*(\d+)', re.MULTILINE | re.IGNORECASE) def sorted_arc_headers(self): headers = [] # Use relaxed canonicalization to unfold and clean up headers relaxed_headers = RelaxedCanonicalization.canonicalize_headers(self.headers) for x,y in relaxed_headers: if x.lower() in ARC.ARC_HEADERS: m = ARC.INSTANCE_RE.search(y) if m is not None: try: i = int(m.group(1)) headers.append((i, (x, y))) except ValueError: self.logger.debug("invalid instance number %s: '%s: %s'" % (m.group(1), x, y)) else: self.logger.debug("not instance number: '%s: %s'" % (x, y)) if len(headers) == 0: return 0, [] def arc_header_key(a): return [a[0], a[1][0].lower(), a[1][1].lower()] headers = sorted(headers, key=arc_header_key) headers.reverse() return headers[0][0], headers #: Sign an RFC822 message and return the list of ARC set header lines #: #: The include_headers option gives full control over which header fields #: are signed for the ARC-Message-Signature. Note that signing a header #: field that doesn't exist prevents #: that field from being added without breaking the signature. Repeated #: fields (such as Received) can be signed multiple times. Instances #: of the field are signed from bottom to top. Signing a header field more #: times than are currently present prevents additional instances #: from being added without breaking the signature. #: #: The default include_headers for this method differs from the backward #: compatible sign function, which signs all headers not #: in should_not_sign. The default list for this method can be modified #: by tweaking should_sign and frozen_sign (or even should_not_sign). #: It is only necessary to pass an include_headers list when precise control #: is needed. #: #: @param selector: the DKIM selector value for the signature #: @param domain: the DKIM domain value for the signature #: @param privkey: a PKCS#1 private key in base64-encoded text form #: @param srv_id: a srv_id for identifying AR headers to sign & extract cv from #: @param include_headers: a list of strings indicating which headers #: are to be signed (default rfc4871 recommended headers) #: @return: list of ARC set header fields #: @raise DKIMException: when the message, include_headers, or key are badly #: formed. def sign(self, selector, domain, privkey, srv_id, include_headers=None, timestamp=None, standardize=False): INSTANCE_LIMIT = 50 # Maximum allowed i= value self.add_should_not(('Authentication-Results',)) # check if authres has been imported try: authres.AuthenticationResultsHeader except: self.logger.debug("authres package not installed") raise authres.AuthresNotFoundError try: pk = parse_pem_private_key(privkey) except UnparsableKeyError as e: raise KeyFormatError(str(e)) # extract, parse, filter & group AR headers ar_headers = [res.strip() for [ar, res] in self.headers if ar == b'Authentication-Results'] grouped_headers = [] for res in ar_headers: try: # see LP: #1884044 grouped_headers.append((res, authres.AuthenticationResultsHeader.parse('Authentication-Results: ' + res.decode('utf-8')))) except authres.core.SyntaxError: # Skip over invalid AR header fields pass auth_headers = [res for res in grouped_headers if res[1].authserv_id == srv_id.decode('utf-8')] if len(auth_headers) == 0: self.logger.debug("no AR headers found, chain terminated") return [] # consolidate headers results_lists = [raw.replace(srv_id + b';', b'').strip() for (raw, parsed) in auth_headers] results_lists = [tags.split(b';') for tags in results_lists] results = [tag.strip() for sublist in results_lists for tag in sublist] auth_results = srv_id + b'; ' + (b';' + self.linesep + b' ').join(results) # extract cv parsed_auth_results = authres.AuthenticationResultsHeader.parse('Authentication-Results: ' + auth_results.decode('utf-8')) arc_results = [res for res in parsed_auth_results.results if res.method == 'arc'] if len(arc_results) == 0: chain_validation_status = CV_None elif len(arc_results) != 1: self.logger.debug("multiple AR arc stamps found, failing chain") chain_validation_status = CV_Fail else: chain_validation_status = arc_results[0].result.lower().encode('utf-8') # Setup headers if include_headers is None: include_headers = self.default_sign_headers() include_headers = tuple([x.lower() for x in include_headers]) # record what verify should extract self.include_headers = include_headers # rfc4871 says FROM is required if b'from' not in include_headers: raise ParameterError("The From header field MUST be signed") # raise exception for any SHOULD_NOT headers, call can modify # SHOULD_NOT if really needed. for x in set(include_headers).intersection(self.should_not_sign): raise ParameterError("The %s header field SHOULD NOT be signed"%x) max_instance, arc_headers_w_instance = self.sorted_arc_headers() instance = 1 if len(arc_headers_w_instance) != 0: instance = max_instance + 1 if instance > INSTANCE_LIMIT: raise ParameterError("Maximum instance tag value exceeded") if instance == 1 and chain_validation_status != CV_None: raise ParameterError("No existing chain found on message, cv should be none") elif instance != 1 and chain_validation_status == CV_None: self.logger.debug("no previous AR arc results found and instance > 1, chain terminated") return [] new_arc_set = [] if chain_validation_status != CV_Fail: arc_headers = [y for x,y in arc_headers_w_instance] else: # don't include previous sets for a failed/invalid chain arc_headers = [] # Compute ARC-Authentication-Results aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results if aar_value[-1] != b'\n': aar_value += b'\r\n' new_arc_set.append(b"ARC-Authentication-Results: " + aar_value) self.headers.insert(0, (b"arc-authentication-results", aar_value)) arc_headers.insert(0, (b"ARC-Authentication-Results", aar_value)) # Compute bh= canon_policy = CanonicalizationPolicy.from_c_value(b'relaxed/relaxed') self.hasher = HASH_ALGORITHMS[self.signature_algorithm] h = HashThrough(self.hasher(), self.debug_content) h.update(canon_policy.canonicalize_body(self.body)) if self.debug_content: self.logger.debug("sign ams body hashed: %r" % h.hashed()) bodyhash = base64.b64encode(h.digest()) # Compute ARC-Message-Signature timestamp = str(timestamp or int(time.time())).encode('ascii') ams_fields = [x for x in [ (b'i', str(instance).encode('ascii')), (b'a', self.signature_algorithm), (b'c', b'relaxed/relaxed'), (b'd', domain), (b's', selector), (b't', timestamp), (b'h', b" : ".join(include_headers)), (b'bh', bodyhash), # Force b= to fold onto it's own line so that refolding after # adding sig doesn't change whitespace for previous tags. (b'b', b'0'*60), ] if x] res = self.gen_header(ams_fields, include_headers, canon_policy, b"ARC-Message-Signature", pk, standardize) new_arc_set.append(b"ARC-Message-Signature: " + res) self.headers.insert(0, (b"ARC-Message-Signature", res)) arc_headers.insert(0, (b"ARC-Message-Signature", res)) # Compute ARC-Seal as_fields = [x for x in [ (b'i', str(instance).encode('ascii')), (b'cv', chain_validation_status), (b'a', self.signature_algorithm), (b'd', domain), (b's', selector), (b't', timestamp), # Force b= to fold onto it's own line so that refolding after # adding sig doesn't change whitespace for previous tags. (b'b', b'0'*60), ] if x] as_include_headers = [x[0].lower() for x in arc_headers] as_include_headers.reverse() # if our chain is failing or invalid, we only grab the most recent set # reversing the order of the headers accomplishes this if chain_validation_status == CV_Fail: self.headers.reverse() if b'h' in as_fields: raise ValidationError("h= tag not permitted in ARC-Seal header field") res = self.gen_header(as_fields, as_include_headers, canon_policy, b"ARC-Seal", pk, standardize) new_arc_set.append(b"ARC-Seal: " + res) self.headers.insert(0, (b"ARC-Seal", res)) arc_headers.insert(0, (b"ARC-Seal", res)) new_arc_set.reverse() return new_arc_set #: Verify an ARC set. #: @type instance: int #: @param instance: which ARC set to verify, based on i= instance. #: @type dnsfunc: callable #: @param dnsfunc: an optional function to lookup TXT resource records #: for a DNS domain. The default uses dnspython or pydns. #: @return: True if signature verifies or False otherwise #: @return: three-tuple of (CV Result (CV_Pass, CV_Fail, CV_None or None, for a chain that has ended), list of #: result dictionaries, result reason) #: @raise DKIMException: when the message, signature, or key are badly formed def verify(self,dnsfunc=get_txt): result_data = [] max_instance, arc_headers_w_instance = self.sorted_arc_headers() if max_instance == 0: return CV_None, result_data, "Message is not ARC signed" for instance in range(max_instance, 0, -1): try: result = self.verify_instance(arc_headers_w_instance, instance, dnsfunc=dnsfunc) result_data.append(result) except DKIMException as e: self.logger.error("%s" % e) return CV_Fail, result_data, "%s" % e # Most recent instance must ams-validate if not result_data[0]['ams-valid']: return CV_Fail, result_data, "Most recent ARC-Message-Signature did not validate" for result in result_data: if result['cv'] == CV_Fail: return None, result_data, "ARC-Seal[%d] reported failure, the chain is terminated" % result['instance'] elif not result['as-valid']: return CV_Fail, result_data, "ARC-Seal[%d] did not validate" % result['instance'] elif (result['instance'] == 1) and (result['cv'] != CV_None): return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv']) elif (result['instance'] != 1) and (result['cv'] == CV_None): return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv']) return CV_Pass, result_data, "success" #: Verify an ARC set. #: @type arc_headers_w_instance: list #: @param arc_headers_w_instance: list of tuples, (instance, (name, value)) of #: ARC headers #: @type instance: int #: @param instance: which ARC set to verify, based on i= instance. #: @type dnsfunc: callable #: @param dnsfunc: an optional function to lookup TXT resource records #: for a DNS domain. The default uses dnspython or pydns. #: @return: True if signature verifies or False otherwise #: @raise DKIMException: when the message, signature, or key are badly formed def verify_instance(self,arc_headers_w_instance,instance,dnsfunc=get_txt): if (instance == 0) or (len(arc_headers_w_instance) == 0): raise ParameterError("request to verify instance %d not present" % (instance)) aar_value = None ams_value = None as_value = None arc_headers = [] output = { 'instance': instance } for i, arc_header in arc_headers_w_instance: if i > instance: continue arc_headers.append(arc_header) if i == instance: if arc_header[0].lower() == b"arc-authentication-results": if aar_value is not None: raise MessageFormatError("Duplicate ARC-Authentication-Results for instance %d" % instance) aar_value = arc_header[1] elif arc_header[0].lower() == b"arc-message-signature": if ams_value is not None: raise MessageFormatError("Duplicate ARC-Message-Signature for instance %d" % instance) ams_value = arc_header[1] elif arc_header[0].lower() == b"arc-seal": if as_value is not None: raise MessageFormatError("Duplicate ARC-Seal for instance %d" % instance) as_value = arc_header[1] if (aar_value is None) or (ams_value is None) or (as_value is None): raise MessageFormatError("Incomplete ARC set for instance %d" % instance) output['aar-value'] = aar_value # Validate Arc-Message-Signature try: sig = parse_tag_value(ams_value) except InvalidTagValueList as e: raise MessageFormatError(e) self.logger.debug("ams sig[%d]: %r" % (instance, sig)) validate_signature_fields(sig, [b'i', b'a', b'b', b'bh', b'd', b'h', b's'], True) output['ams-domain'] = sig[b'd'] output['ams-selector'] = sig[b's'] include_headers = [x.lower() for x in re.split(br"\s*:\s*", sig[b'h'])] if b'arc-seal' in include_headers: raise ParameterError("The Arc-Message-Signature MUST NOT sign ARC-Seal") ams_header = (b'ARC-Message-Signature', b' ' + ams_value) # we can't use the AMS provided above, as it's already been canonicalized relaxed # for use in validating the AS. However the AMS is included in the AMS itself, # and this can use simple canonicalization raw_ams_header = [ (x, y) for (x, y) in self.headers if x.lower() == b'arc-message-signature' and b" i="+sig[b'i']+b";" in y.lower() ][0] # Only relaxed canonicalization used by ARC if b'c' not in sig: sig[b'c'] = b'relaxed/relaxed' try: ams_valid = self.verify_sig(sig, include_headers, raw_ams_header, dnsfunc) except DKIMException as e: self.logger.error("%s" % e) ams_valid = False output['ams-valid'] = ams_valid self.logger.debug("ams valid: %r" % ams_valid) # Validate Arc-Seal try: sig = parse_tag_value(as_value) except InvalidTagValueList as e: raise MessageFormatError(e) self.logger.debug("as sig[%d]: %r" % (instance, sig)) validate_signature_fields(sig, [b'i', b'a', b'b', b'cv', b'd', b's'], True) if b'h' in sig: raise ValidationError("h= tag not permitted in ARC-Seal header field") output['as-domain'] = sig[b'd'] output['as-selector'] = sig[b's'] output['cv'] = sig[b'cv'] as_include_headers = [x[0].lower() for x in arc_headers] as_include_headers.reverse() as_header = (b'ARC-Seal', b' ' + as_value) # Only relaxed canonicalization used by ARC if b'c' not in sig: sig[b'c'] = b'relaxed/relaxed' try: as_valid = self.verify_sig(sig, as_include_headers[:-1], as_header, dnsfunc) except DKIMException as e: self.logger.error("%s" % e) as_valid = False output['as-valid'] = as_valid self.logger.debug("as valid: %r" % as_valid) return output def sign(message, selector, domain, privkey, identity=None, canonicalize=(b'relaxed', b'simple'), signature_algorithm=b'rsa-sha256', include_headers=None, length=False, logger=None, linesep=b'\r\n', tlsrpt=False): # type: (bytes, bytes, bytes, bytes, bytes, tuple, bytes, list, bool, any) -> bytes """Sign an RFC822 message and return the DKIM-Signature header line. @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) @param selector: the DKIM selector value for the signature @param domain: the DKIM domain value for the signature @param privkey: a PKCS#1 private key in base64-encoded text form @param identity: the DKIM identity value for the signature (default "@"+domain) @param canonicalize: the canonicalization algorithms to use (default (Simple, Simple)) @param signature_algorithm: the signing algorithm to use when signing @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign) @param length: true if the l= tag should be included to indicate body length (default False) @param logger: a logger to which debug info will be written (default None) @param linesep: use this line seperator for folding the headers @param tlsrpt: message is an RFC 8460 TLS report (default False) False: Not a tlsrpt, True: Is a tlsrpt, 'strict': tlsrpt, invalid if service type is missing. For signing, if True, length is never used. @return: DKIM-Signature header field terminated by \\r\\n @raise DKIMException: when the message, include_headers, or key are badly formed. """ d = DKIM(message,logger=logger,signature_algorithm=signature_algorithm,linesep=linesep,tlsrpt=tlsrpt) return d.sign(selector, domain, privkey, identity=identity, canonicalize=canonicalize, include_headers=include_headers, length=length) def verify(message, logger=None, dnsfunc=get_txt, minkey=1024, timeout=5, tlsrpt=False): """Verify the first (topmost) DKIM signature on an RFC822 formatted message. @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) @param logger: a logger to which debug info will be written (default None) @param timeout: number of seconds for DNS lookup timeout (default = 5) @param tlsrpt: message is an RFC 8460 TLS report (default False) False: Not a tlsrpt, True: Is a tlsrpt, 'strict': tlsrpt, invalid if service type is missing. For signing, if True, length is never used. @return: True if signature verifies or False otherwise """ # type: (bytes, any, function, int) -> bool d = DKIM(message,logger=logger,minkey=minkey,timeout=timeout,tlsrpt=tlsrpt) try: return d.verify(dnsfunc=dnsfunc) except DKIMException as x: if logger is not None: logger.error("%s" % x) return False # aiodns requires Python 3.5+, so no async before that if sys.version_info >= (3, 5) and USE_ASYNC: try: import aiodns from dkim.asyncsupport import verify_async dkim_verify_async = verify_async except ImportError: # If aiodns is not installed, then async verification is not available pass # For consistency with ARC dkim_sign = sign dkim_verify = verify def arc_sign(message, selector, domain, privkey, srv_id, signature_algorithm=b'rsa-sha256', include_headers=None, timestamp=None, logger=None, standardize=False, linesep=b'\r\n'): # type: (bytes, bytes, bytes, bytes, bytes, bytes, list, any, any, bool) -> list """Sign an RFC822 message and return the ARC set header lines for the next instance @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) @param selector: the DKIM selector value for the signature @param domain: the DKIM domain value for the signature @param privkey: a PKCS#1 private key in base64-encoded text form @param srv_id: the authserv_id used to identify the ADMD's AR headers and to use for ARC authserv_id @param signature_algorithm: the signing algorithm to use when signing @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign) @param timestamp: the time in integer seconds when the message is sealed (default is int(time.time) based on platform, can be string or int) @param logger: a logger to which debug info will be written (default None) @param linesep: use this line seperator for folding the headers @return: A list containing the ARC set of header fields for the next instance @raise DKIMException: when the message, include_headers, or key are badly formed. """ a = ARC(message,logger=logger,signature_algorithm=b'rsa-sha256',linesep=linesep) if not include_headers: include_headers = a.default_sign_headers() return a.sign(selector, domain, privkey, srv_id, include_headers=include_headers, timestamp=timestamp, standardize=standardize) def arc_verify(message, logger=None, dnsfunc=get_txt, minkey=1024, timeout=5): # type: (bytes, any, function, int) -> tuple """Verify the ARC chain on an RFC822 formatted message. @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) @param logger: a logger to which debug info will be written (default None) @param dnsfunc: an optional function to lookup TXT resource records @param minkey: the minimum key size to accept @param timeout: number of seconds for DNS lookup timeout (default = 5) @return: three-tuple of (CV Result (CV_Pass, CV_Fail or CV_None), list of result dictionaries, result reason) """ a = ARC(message,logger=logger,minkey=minkey,timeout=5) try: return a.verify(dnsfunc=dnsfunc) except DKIMException as x: if logger is not None: logger.error("%s" % x) return CV_Fail, [], "%s" % x ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/__main__.py0000644000175100017510000000021313642525265016324 0ustar00kittermakittermaimport unittest import doctest import dkim from tests import test_suite doctest.testmod(dkim) unittest.TextTestRunner().run(test_suite()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/arcsign.py0000644000175100017510000000501213642525265016234 0ustar00kittermakitterma#!/usr/bin/env python # This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant # # This has been modified from the original software. # Copyright (c) 2016 Google, Inc. # Contact: Brandon Long # This has been modified from the original software. # Copyright (c) 2017, 2018, 2019 Scott Kitterman # # This has been modified from the original software. # Copyright (c) 2017 Valimail Inc # Contact: Gene Shuman from __future__ import print_function import logging import re import sys import dkim logging.basicConfig(level=10) def main(): if len(sys.argv) != 5: print("Usage: arcsign.py selector domain privatekeyfile srv_id", file=sys.stderr) sys.exit(1) if sys.version_info[0] >= 3: # Make sys.stdin and stdout binary streams. sys.stdin = sys.stdin.detach() sys.stdout = sys.stdout.detach() selector = sys.argv[1].encode('ascii') domain = sys.argv[2].encode('ascii') privatekeyfile = sys.argv[3] srv_id = sys.argv[4].encode('ascii') message = sys.stdin.read() # Pick a cv status cv = dkim.CV_None if re.search(b'arc-seal', message, re.IGNORECASE): cv = dkim.CV_Pass #try: sig = dkim.arc_sign(message, selector, domain, open(privatekeyfile, "rb").read(), srv_id, cv, linesep=dkim.util.get_linesep(message)) for line in sig: sys.stdout.write(line) sys.stdout.write(message) #except Exception as e: # print(e, file=sys.stderr) #sys.stdout.write(message) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/arcverify.py0000644000175100017510000000335613642525265016611 0ustar00kittermakitterma#!/usr/bin/env python # This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant # # This has been modified from the original software. # Copyright (c) 2016 Google, Inc. # Contact: Brandon Long from __future__ import print_function import logging import sys import dkim def main(): if sys.version_info[0] >= 3: # Make sys.stdin a binary stream. sys.stdin = sys.stdin.detach() message = sys.stdin.read() verbose = '-v' in sys.argv if verbose: logging.basicConfig(level=10) a = dkim.ARC(message) cv, results, comment = a.verify() else: cv, results, comment = dkim.arc_verify(message) print("arc verification: cv=%s %s" % (cv, comment)) if verbose: print(repr(results)) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/asn1.py0000644000175100017510000001031414423474310015441 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant __all__ = [ 'asn1_build', 'asn1_parse', 'ASN1FormatError', 'BIT_STRING', 'INTEGER', 'SEQUENCE', 'OBJECT_IDENTIFIER', 'OCTET_STRING', 'NULL', ] INTEGER = 0x02 BIT_STRING = 0x03 OCTET_STRING = 0x04 NULL = 0x05 OBJECT_IDENTIFIER = 0x06 SEQUENCE = 0x30 class ASN1FormatError(Exception): pass def asn1_parse(template, data): """Parse a data structure according to an ASN.1 template. @param template: tuples comprising the ASN.1 template @param data: byte string data to parse @return: decoded structure """ data = bytearray(data) r = [] i = 0 try: for t in template: tag = data[i] i += 1 if tag == t[0]: length = data[i] i += 1 if length & 0x80: n = length & 0x7f length = 0 for j in range(n): length = (length << 8) | data[i] i += 1 if tag == INTEGER: n = 0 for j in range(length): n = (n << 8) | data[i] i += 1 r.append(n) elif tag == BIT_STRING: r.append(data[i:i+length]) i += length elif tag == NULL: assert length == 0 r.append(None) elif tag == OBJECT_IDENTIFIER: r.append(data[i:i+length]) i += length elif tag == SEQUENCE: r.append(asn1_parse(t[1], data[i:i+length])) i += length elif tag == OCTET_STRING: r.append(data[i:i+length]) i += length else: raise ASN1FormatError( "Unexpected tag in template: %02x" % tag) else: raise ASN1FormatError( "Unexpected tag (got %02x, expecting %02x)" % (tag, t[0])) return r except IndexError: raise ASN1FormatError("Data truncated at byte %d"%i) def asn1_length(n): """Return a string representing a field length in ASN.1 format. @param n: integer field length @return: ASN.1 field length """ assert n >= 0 if n < 0x7f: return bytearray([n]) r = bytearray() while n > 0: r.insert(n & 0xff) n >>= 8 return r def asn1_encode(type, data): length = asn1_length(len(data)) length.insert(0, type) length.extend(data) return length def asn1_build(node): """Build a DER-encoded ASN.1 data structure. @param node: (type, data) tuples comprising the ASN.1 structure @return: DER-encoded ASN.1 byte string """ if node[0] == OCTET_STRING: return asn1_encode(OCTET_STRING, node[1]) if node[0] == NULL: assert node[1] is None return asn1_encode(NULL, b'') elif node[0] == OBJECT_IDENTIFIER: return asn1_encode(OBJECT_IDENTIFIER, node[1]) elif node[0] == SEQUENCE: r = bytearray() for x in node[1]: r += asn1_build(x) return asn1_encode(SEQUENCE, r) else: raise ASN1FormatError("Unexpected tag in template: %02x" % node[0]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682863114.0 dkimpy-1.1.6/dkim/asyncsupport.py0000644000175100017510000001133514423472012017352 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant # # This has been modified from the original software. # Copyright (c) 2016 Google, Inc. # Contact: Brandon Long # # This has been modified from the original software. # Copyright (c) 2016, 2017, 2018, 2019 Scott Kitterman # # This has been modified from the original software. # Copyright (c) 2017 Valimail Inc # Contact: Gene Shuman import asyncio import aiodns import base64 import dkim import re __all__ = [ 'get_txt_async', 'load_pk_from_dns_async', 'verify_async' ] async def get_txt_async(name, timeout=5): """Return a TXT record associated with a DNS name in an asnyc loop. For DKIM we can assume there is only one.""" # Note: This will use the existing loop or create one if needed loop = asyncio.get_event_loop() resolver = aiodns.DNSResolver(loop=loop, timeout=timeout) async def query(name, qtype): return await resolver.query(name, qtype) #q = query(name, 'TXT') try: result = await query(name, 'TXT') except aiodns.error.DNSError: result = None if result: return result[0].text else: return None async def load_pk_from_dns_async(name, dnsfunc, timeout=5): s = await dnsfunc(name, timeout=timeout) pk, keysize, ktag, seqtlsrpt = dkim.evaluate_pk(name, s) return pk, keysize, ktag, seqtlsrpt class DKIM(dkim.DKIM): #: Sign an RFC822 message and return the DKIM-Signature header line. #: #: Identical to dkim.DKIM, except uses aiodns and can be awaited in an #: ascyncio context. See dkim.DKIM for details. # Abstract helper method to verify a signed header #: @param sig: List of (key, value) tuples containing tag=values of the header #: @param include_headers: headers to validate b= signature against #: @param sig_header: (header_name, header_value) #: @param dnsfunc: interface to dns async def verify_sig(self, sig, include_headers, sig_header, dnsfunc): name = sig[b's'] + b"._domainkey." + sig[b'd'] + b"." try: self.pk, self.keysize, self.ktag, self.seqtlsrpt = await load_pk_from_dns_async(name, dnsfunc, timeout=self.timeout) except dkim.KeyFormatError as e: self.logger.error("%s" % e) return False return self.verify_sig_process(sig, include_headers, sig_header, dnsfunc) async def verify(self,idx=0,dnsfunc=get_txt_async): prep = self.verify_headerprep(idx) if prep: sig, include_headers, sigheaders = prep return await self.verify_sig(sig, include_headers, sigheaders[idx], dnsfunc) return False # No signature async def verify_async(message, logger=None, dnsfunc=None, minkey=1024, timeout=5, tlsrpt=False): """Verify the first (topmost) DKIM signature on an RFC822 formatted message in an asyncio contxt. @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) @param logger: a logger to which debug info will be written (default None) @param timeout: number of seconds for DNS lookup timeout (default = 5) @param tlsrpt: message is an RFC 8460 TLS report (default False) False: Not a tlsrpt, True: Is a tlsrpt, 'strict': tlsrpt, invalid if service type is missing. For signing, if True, length is never used. @return: True if signature verifies or False otherwise """ # type: (bytes, any, function, int) -> bool # Note: This will use the existing loop or create one if needed loop = asyncio.get_event_loop() if not dnsfunc: dnsfunc=get_txt_async d = DKIM(message,logger=logger,minkey=minkey,timeout=timeout,tlsrpt=tlsrpt) try: return await d.verify(dnsfunc=dnsfunc) except dkim.DKIMException as x: if logger is not None: logger.error("%s" % x) return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/canonicalization.py0000644000175100017510000001100213642525265020127 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant import re __all__ = [ 'CanonicalizationPolicy', 'InvalidCanonicalizationPolicyError', ] class InvalidCanonicalizationPolicyError(Exception): """The c= value could not be parsed.""" pass def strip_trailing_whitespace(content): return re.sub(b"[\t ]+\r\n", b"\r\n", content) def compress_whitespace(content): return re.sub(b"[\t ]+", b" ", content) def strip_trailing_lines(content): end = None while content.endswith(b"\r\n", 0, end): if end is None: end = -2 else: end -= 2 if end is None: return content + b"\r\n" end += 2 if end == 0: return content return content[:end] def unfold_header_value(content): return re.sub(b"\r\n", b"", content) def correct_empty_body(content): if content == b"\r\n": return b"" else: return content class Simple: """Class that represents the "simple" canonicalization algorithm.""" name = b"simple" @staticmethod def canonicalize_headers(headers): # No changes to headers. return headers @staticmethod def canonicalize_body(body): # Ignore all empty lines at the end of the message body. return strip_trailing_lines(body) class Relaxed: """Class that represents the "relaxed" canonicalization algorithm.""" name = b"relaxed" @staticmethod def canonicalize_headers(headers): # Convert all header field names to lowercase. # Unfold all header lines. # Compress WSP to single space. # Remove all WSP at the start or end of the field value (strip). return [ (x[0].lower().rstrip(), compress_whitespace(unfold_header_value(x[1])).strip() + b"\r\n") for x in headers] @staticmethod def canonicalize_body(body): # Remove all trailing WSP at end of lines. # Compress non-line-ending WSP to single space. # Ignore all empty lines at the end of the message body. return correct_empty_body(strip_trailing_lines( compress_whitespace(strip_trailing_whitespace(body)))) class CanonicalizationPolicy: def __init__(self, header_algorithm, body_algorithm): self.header_algorithm = header_algorithm self.body_algorithm = body_algorithm @classmethod def from_c_value(cls, c): """Construct the canonicalization policy described by a c= value. May raise an C{InvalidCanonicalizationPolicyError} if the given value is invalid @param c: c= value from a DKIM-Signature header field @return: a C{CanonicalizationPolicy} """ if c is None: c = b'simple/simple' m = c.split(b'/') if len(m) not in (1, 2): raise InvalidCanonicalizationPolicyError(c) if len(m) == 1: m.append(b'simple') can_headers, can_body = m try: header_algorithm = ALGORITHMS[can_headers] body_algorithm = ALGORITHMS[can_body] except KeyError as e: raise InvalidCanonicalizationPolicyError(e.args[0]) return cls(header_algorithm, body_algorithm) def to_c_value(self): return b'/'.join( (self.header_algorithm.name, self.body_algorithm.name)) def canonicalize_headers(self, headers): return self.header_algorithm.canonicalize_headers(headers) def canonicalize_body(self, body): return self.body_algorithm.canonicalize_body(body) ALGORITHMS = dict((c.name, c) for c in (Simple, Relaxed)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/crypto.py0000644000175100017510000001775514423474310016137 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant # Copyright (c) 2018 Scott Kitterman __all__ = [ 'DigestTooLargeError', 'HASH_ALGORITHMS', 'ARC_HASH_ALGORITHMS', 'parse_pem_private_key', 'parse_private_key', 'parse_public_key', 'RSASSA_PKCS1_v1_5_sign', 'RSASSA_PKCS1_v1_5_verify', 'UnparsableKeyError', ] import base64 import hashlib import re from dkim.asn1 import ( ASN1FormatError, asn1_build, asn1_parse, BIT_STRING, INTEGER, SEQUENCE, OBJECT_IDENTIFIER, OCTET_STRING, NULL, ) ASN1_PKCS8_PrivateKey = [ (SEQUENCE, [ (INTEGER,), (SEQUENCE, [ (OBJECT_IDENTIFIER,), (NULL,),]), (OCTET_STRING,), ]) ] ASN1_Object = [ (SEQUENCE, [ (SEQUENCE, [ (OBJECT_IDENTIFIER,), (NULL,), ]), (BIT_STRING,), ]) ] ASN1_RSAPublicKey = [ (SEQUENCE, [ (INTEGER,), (INTEGER,), ]) ] ASN1_RSAPrivateKey = [ (SEQUENCE, [ (INTEGER,), (INTEGER,), (INTEGER,), (INTEGER,), (INTEGER,), (INTEGER,), (INTEGER,), (INTEGER,), (INTEGER,), ]) ] HASH_ALGORITHMS = { b'rsa-sha1': hashlib.sha1, b'rsa-sha256': hashlib.sha256, b'ed25519-sha256': hashlib.sha256 } ARC_HASH_ALGORITHMS = { b'rsa-sha256': hashlib.sha256, } # These values come from RFC 8017, section 9.2 Notes, page 46. HASH_ID_MAP = { 'sha1': b"\x2b\x0e\x03\x02\x1a", 'sha256': b"\x60\x86\x48\x01\x65\x03\x04\x02\x01", } class DigestTooLargeError(Exception): """The digest is too large to fit within the requested length.""" pass class UnparsableKeyError(Exception): """The data could not be parsed as a key.""" pass def parse_public_key(data): """Parse an RSA public key. @param data: DER-encoded X.509 subjectPublicKeyInfo containing an RFC8017 RSAPublicKey. @return: RSA public key """ try: # Not sure why the [1:] is necessary to skip a byte. x = asn1_parse(ASN1_Object, data) pkd = asn1_parse(ASN1_RSAPublicKey, x[0][1][1:]) except ASN1FormatError as e_spki: try: pkd = asn1_parse(ASN1_RSAPublicKey, data) except ASN1FormatError as e_rsa: raise UnparsableKeyError('Unparsable public key; SubjectPublicKeyInfo: ' + str(e_spki) + '; RSAPublicKey: ' + str(e_rsa)) pk = { 'modulus': pkd[0][0], 'publicExponent': pkd[0][1], } return pk def parse_private_key(data): """Parse an RSA private key. @param data: DER-encoded RFC8017 RSAPrivateKey or CMS-encoded RFC5958 asymetric key package. @return: RSA private key """ try: pka = asn1_parse(ASN1_RSAPrivateKey, data) except ASN1FormatError: try: #If it fails it might be because of PKCS#8 (key generated with openSSL 3.X) pkt = asn1_parse(ASN1_PKCS8_PrivateKey, data) pka = asn1_parse(ASN1_RSAPrivateKey, pkt[0][2]) except ASN1FormatError as e: raise UnparsableKeyError("Unparsable private key (are your sure it is encoded in PKCS#1 or PKCS#8 standard ?):\n" + str(e)) pk = { 'version': pka[0][0], 'modulus': pka[0][1], 'publicExponent': pka[0][2], 'privateExponent': pka[0][3], 'prime1': pka[0][4], 'prime2': pka[0][5], 'exponent1': pka[0][6], 'exponent2': pka[0][7], 'coefficient': pka[0][8], } return pk def parse_pem_private_key(data): """Parse a PEM RSA private key. @param data: RFC8017 RSAPrivateKey in PEM format. @return: RSA private key """ m = re.search(b"--\n(.*?)\n--", data, re.DOTALL) if m is None: raise UnparsableKeyError("Private key not found") try: pkdata = base64.b64decode(m.group(1)) except TypeError as e: raise UnparsableKeyError(str(e)) return parse_private_key(pkdata) def EMSA_PKCS1_v1_5_encode(hash, mlen): """Encode a digest with RFC8017 EMSA-PKCS1-v1_5. @param hash: hash object to encode @param mlen: desired message length @return: encoded digest byte string """ dinfo = asn1_build( (SEQUENCE, [ (SEQUENCE, [ (OBJECT_IDENTIFIER, HASH_ID_MAP[hash.name.lower()]), (NULL, None), ]), (OCTET_STRING, hash.digest()), ])) if len(dinfo) + 11 > mlen: raise DigestTooLargeError() return b"\x00\x01"+b"\xff"*(mlen-len(dinfo)-3)+b"\x00"+dinfo def str2int(s): """Convert a byte string to an integer. @param s: byte string representing a positive integer to convert @return: converted integer """ s = bytearray(s) r = 0 for c in s: r = (r << 8) | c return r def int2str(n, length=-1): """Convert an integer to a byte string. @param n: positive integer to convert @param length: minimum length @return: converted bytestring, of at least the minimum length if it was specified """ assert n >= 0 r = bytearray() while length < 0 or len(r) < length: r.append(n & 0xff) n >>= 8 if length < 0 and n == 0: break r.reverse() assert length < 0 or len(r) == length return r def rsa_decrypt(message, pk, mlen): """Perform RSA decryption/signing @param message: byte string to operate on @param pk: private key data @param mlen: desired output length @return: byte string result of the operation """ c = str2int(message) m1 = pow(c, pk['exponent1'], pk['prime1']) m2 = pow(c, pk['exponent2'], pk['prime2']) if m1 < m2: h = pk['coefficient'] * (m1 + pk['prime1'] - m2) % pk['prime1'] else: h = pk['coefficient'] * (m1 - m2) % pk['prime1'] return int2str(m2 + h * pk['prime2'], mlen) def rsa_encrypt(message, pk, mlen): """Perform RSA encryption/verification @param message: byte string to operate on @param pk: public key data @param mlen: desired output length @return: byte string result of the operation """ m = str2int(message) return int2str(pow(m, pk['publicExponent'], pk['modulus']), mlen) def RSASSA_PKCS1_v1_5_sign(hash, private_key): """Sign a digest with RFC8017 RSASSA-PKCS1-v1_5. @param hash: hash object to sign @param private_key: private key data @return: signed digest byte string """ modlen = len(int2str(private_key['modulus'])) encoded_digest = EMSA_PKCS1_v1_5_encode(hash, modlen) return rsa_decrypt(encoded_digest, private_key, modlen) def RSASSA_PKCS1_v1_5_verify(hash, signature, public_key): """Verify a digest signed with RFC8017 RSASSA-PKCS1-v1_5. @param hash: hash object to check @param signature: signed digest byte string @param public_key: public key data @return: True if the signature is valid, False otherwise """ modlen = len(int2str(public_key['modulus'])) encoded_digest = EMSA_PKCS1_v1_5_encode(hash, modlen) signed_digest = rsa_encrypt(signature, public_key, modlen) return encoded_digest == signed_digest ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/dkimsign.py0000644000175100017510000000705413642525265016423 0ustar00kittermakitterma#!/usr/bin/env python # This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant # Copyright (c) 2017 Scott Kitterman from __future__ import print_function import sys import argparse import dkim def main(): # Backward compatibility hack because argparse doesn't support optional # positional arguments arguments=['--'+arg if arg[:8] == 'identity' else arg for arg in sys.argv[1:]] parser = argparse.ArgumentParser( description='Produce DKIM signature for email messages.', epilog="message to be signed follows commands on stdin") parser.add_argument('selector', action="store") parser.add_argument('domain', action="store") parser.add_argument('privatekeyfile', action="store") parser.add_argument('--hcanon', choices=['simple', 'relaxed'], default='relaxed', help='Header canonicalization algorithm: default=relaxed') parser.add_argument('--bcanon', choices=['simple', 'relaxed'], default='simple', help='Body canonicalization algorithm: default=simple') parser.add_argument('--signalg', choices=['rsa-sha256', 'ed25519-sha256', 'rsa-sha1'], default='rsa-sha256', help='Signature algorithm: default=rsa-sha256') parser.add_argument('--identity', help='Optional value for i= tag.') args=parser.parse_args(arguments) include_headers = None length = None logger = None if sys.version_info[0] >= 3: args.selector = bytes(args.selector, encoding='UTF-8') args.domain = bytes(args.domain, encoding='UTF-8') if args.identity is not None: args.identity = bytes(args.identity, encoding='UTF-8') args.hcanon = bytes(args.hcanon, encoding='UTF-8') args.bcanon = bytes(args.bcanon, encoding='UTF-8') args.signalg = bytes(args.signalg, encoding='UTF-8') # Make sys.stdin and stdout binary streams. sys.stdin = sys.stdin.detach() sys.stdout = sys.stdout.detach() canonicalize = (args.hcanon, args.bcanon) message = sys.stdin.read() try: d = dkim.DKIM(message,logger=logger, signature_algorithm=args.signalg, linesep=dkim.util.get_linesep(message)) sig = d.sign(args.selector, args.domain, open( args.privatekeyfile, "rb").read(), identity = args.identity, canonicalize=canonicalize, include_headers=include_headers, length=length) sys.stdout.write(sig) sys.stdout.write(message) except Exception as e: print(e, file=sys.stderr) sys.stdout.write(message) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/dkimverify.py0000644000175100017510000000363714423474310016762 0ustar00kittermakitterma#!/usr/bin/env python # This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant from __future__ import print_function import sys import argparse import dkim def main(): parser = argparse.ArgumentParser( description='Verify DKIM signature for email messages.', epilog="message to be verified follows commands on stdin") parser.add_argument('--index', metavar='N', type=int, default=0, help='Index of DKIM signature header to verify: default=0') args=parser.parse_args() if sys.version_info[0] >= 3: # Make sys.stdin a binary stream. sys.stdin = sys.stdin.detach() message = sys.stdin.read() verbose = '-v' in sys.argv if verbose: import logging d = dkim.DKIM(message, logger=logging) else: d = dkim.DKIM(message) res = d.verify(args.index) if not res: print("signature verification failed") sys.exit(1) print("signature ok") if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/dknewkey.py0000644000175100017510000001120114423474310016414 0ustar00kittermakitterma#!/usr/bin/python # This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2016 Google, Inc. # Contact: Brandon Long # Modified by Scott Kitterman # Copyright (c) 2017,2018 Scott Kitterman """Generates new domainkeys pairs. """ from __future__ import print_function import os import subprocess import sys import tempfile import argparse import hashlib import base64 # how strong are our keys? BITS_REQUIRED = 2048 # what openssl binary do we use to do key manipulation? OPENSSL_BINARY = '/usr/bin/openssl' def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) def GenRSAKeys(private_key_file, verbose=True): """ Generates a suitable private key. Output is unprotected. You should encrypt your keys. """ if verbose: eprint('generating ' + private_key_file) subprocess.check_call([OPENSSL_BINARY, 'genrsa', '-out', private_key_file, str(BITS_REQUIRED)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def GenEd25519Keys(private_key_file, verbose=True): """Generates a base64 encoded private key for ed25519 DKIM signing. Output is unprotected. You should protect your keys. """ import nacl.signing # Yes, pep-8, but let's not make everyone install nacl import nacl.encoding import os skg = nacl.signing.SigningKey(seed=os.urandom(32)) if verbose: eprint('generating ' + private_key_file) priv_key = skg.generate() if os.name == 'posix': old_umask = os.umask(0o077) with open(private_key_file, 'w') as pkf: pkf.write(priv_key.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8")) if os.name == 'posix': os.umask(old_umask) return(priv_key) def ExtractRSADnsPublicKey(private_key_file, dns_file, verbose=True): """ Given a key, extract the bit we should place in DNS. """ if verbose: eprint('extracting ' + private_key_file) working_file = tempfile.NamedTemporaryFile(delete=False).name subprocess.check_call([OPENSSL_BINARY, 'rsa', '-in', private_key_file, '-out', working_file, '-pubout', '-outform', 'PEM'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: with open(working_file) as wf: y = '' for line in wf.readlines(): if not line.startswith('---'): y+= line output = ''.join(y.split()) finally: os.unlink(working_file) with open(dns_file, 'w') as dns_fp: if verbose: eprint('writing ' + dns_file) dns_fp.write("v=DKIM1; k=rsa; h=sha256; p={0}".format(output)) def ExtractEd25519PublicKey(dns_file, priv_key, verbose=True): """ Given a ed25519 key, extract the bit we should place in DNS. """ import nacl.encoding # Yes, pep-8, but let's not make everyone install nacl pubkey = priv_key.verify_key output = pubkey.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8") with open(dns_file, 'w') as dns_fp: if verbose: eprint('writing ' + dns_file) dns_fp.write("v=DKIM1; k=ed25519; p={0}".format(output)) def main(): parser = argparse.ArgumentParser( description='Produce DKIM keys.',) parser.add_argument('key_name', action="store") parser.add_argument('--ktype', choices=['rsa', 'ed25519'], default='rsa', help='DKIM key type: Default is rsa') args=parser.parse_args() key_name = args.key_name key_type = args.ktype private_key_file = key_name + '.key' dns_file = key_name + '.dns' if key_type == 'rsa': GenRSAKeys(private_key_file) ExtractRSADnsPublicKey(private_key_file, dns_file) elif key_type == 'ed25519': priv_key = GenEd25519Keys(private_key_file) ExtractEd25519PublicKey(dns_file, priv_key) else: eprint("Unknown key type - no key generated.") if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690559698.0 dkimpy-1.1.6/dkim/dnsplug.py0000644000175100017510000000636314460762322016270 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant __all__ = [ 'get_txt' ] def get_txt_dnspython(name, timeout=5): """Return a TXT record associated with a DNS name.""" import dkim try: a = dns.resolver.resolve(name, dns.rdatatype.TXT,raise_on_no_answer=False, lifetime=timeout, search=True) for r in a.response.answer: if r.rdtype == dns.rdatatype.TXT: return b"".join(list(r.items)[0].strings) except dns.resolver.NXDOMAIN: pass except dns.resolver.NoNameservers: pass except dns.resolver.NoResolverConfiguration as e: raise dkim.DnsTimeoutError('dns.resolver.NoResolverConfiguration: {0}'.format(e)) except dns.exception.Timeout as e: raise dkim.DnsTimeoutError('dns.exception.Timeout: {0}'.format(e)) return None def get_txt_pydns(name, timeout=5): """Return a TXT record associated with a DNS name.""" # Older pydns releases don't like a trailing dot. if name.endswith('.'): name = name[:-1] response = DNS.DnsRequest(name, qtype='txt', timeout=timeout).req() if not response.answers: return None for answer in response.answers: if answer['typename'].lower() == 'txt': return b''.join(answer['data']) return None # No longer used since it doesn't support timeout def get_txt_Milter_dns(name, timeout=5): """Return a TXT record associated with a DNS name.""" # Older pydns releases don't like a trailing dot. if name.endswith('.'): name = name[:-1] sess = Session() a = sess.dns(name.encode('idna'),'TXT') if a: return b''.join(a[0]) return None # Prefer dnspython if it's there, otherwise use pydns. try: import dns.resolver _get_txt = get_txt_dnspython except ImportError: try: import DNS DNS.DiscoverNameServers() _get_txt = get_txt_pydns except: raise def get_txt(name, timeout=5): """Return a TXT record associated with a DNS name. @param name: The bytestring domain name to look up. """ # pydns needs Unicode, but DKIM's d= is ASCII (already punycoded). try: unicode_name = name.decode('UTF-8') except UnicodeDecodeError: return None txt = _get_txt(unicode_name, timeout) if type(txt) is str: txt = txt.encode('utf-8') return txt ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713123138.0665221 dkimpy-1.1.6/dkim/tests/0000755000175100017510000000000014607027502015371 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/tests/__init__.py0000644000175100017510000000335114423474310017503 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant # # This has been modified from the original software. # Copyright (c) 2016 Google, Inc. # Contact: Brandon Long # Copyright (c) 2018 Scott Kitterman import unittest def test_suite(): from dkim.tests import ( test_canonicalization, test_crypto, test_dkim, test_dkim_ed25519, test_dkim_rsavariants, test_dkim_tlsrpt, test_util, test_arc, test_dnsplug, test_dkim_generate, ) modules = [ test_canonicalization, test_crypto, test_dkim, test_dkim_ed25519, test_dkim_rsavariants, test_dkim_tlsrpt, test_util, test_arc, test_dnsplug, test_dkim_generate, ] suites = [x.test_suite() for x in modules] return unittest.TestSuite(suites) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1713123138.070522 dkimpy-1.1.6/dkim/tests/data/0000755000175100017510000000000014607027502016302 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/data/1024_testkey.key0000644000175100017510000000156713642527701021167 0ustar00kittermakitterma-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQC6aakWzruYAKX9OdOdSHMemqVdGQurNYLC1H7O/T2LQIHVbkKF 6KKjlgFM7lr8skfZMhJe/KRGMvVjCV5ZakZIGeP3Hi1qXCEvmjS4ElpMPMPyPrZi gt95ipqywPYZJWHbRiJ085VdkSCtLUvo5sypA0nTJeynEouAN+/wBaCO6QIDAQAB AoGBALdQsLd9FX2lJZagBznkxVCTQ4sJ1i/Hr49o6EslPiByjzsT8tc8rrOwEBdX TqKxr3Ynh2lhiSp/clCyJdZIbFNKwXnqdN6DaA+aVRKT5n2YxmZr1AGp8VdOX56a +JTWDFSrDBUeNgG0xU7ywJjAYpj5gWk4rwsaA0ez/7Kb/5sBAkEA9H3Rxmyi0T5z IqOpAMOg0oHJRk0gGbJ6etKVPsghd+PDU+gfz69Ga2eEpPBIGFv8SXAeBlWyQCAK SXIkEREn7wJBAMMv+nWQsjf71BmdFULvUrHux2eHE6R18+CIYak8xs8sEJuprjQf 7QfznUBaDqAww7+XufofdSrU1AcfEsXWnqcCQQDgeSCACMAYUzkMhaymSfa/L3An vqoJTefMGsnyQXcRByoQQJ/sY+XIuNt9QCtBSjtBBdJbSJUG8SPguQ5KKTE7AkBz RXJ02+9kgKTSO6LC1PPRgWLiETPYfJB6mhCTEgGXeD6Wr67hubkSaR0FVCovEJ2K GOEMhmd71BUj34YGsKkhAkBKkdEwDWSvPDCfmyBzgCKufu1tZzphRtxoL6Z6Vlcj 0/SRmoG8S1uQKzkm7z7ekRMLIl9UlUlKUvxF6oblccUo -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/data/1024_testkey_wo_markers.pub.rsa.txt0000644000175100017510000000032413642527701025006 0ustar00kittermakittermav=DKIM1; g=*; k=rsa; p=MIGJAoGBALppqRbOu5gApf05051Icx6apV0ZC6s1gsLUfs79PYtAgdVuQoXooqOWAUzuWvyyR9kyEl78pEYy9WMJXllqRkgZ4/ceLWpcIS+aNLgSWkw8w/I+tmKC33mKmrLA9hklYdtGInTzlV2RIK0tS+jmzKkDSdMl7KcSi4A37/AFoI7pAgMBAAE= ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/data/1024_testkey_wo_markers.pub.txt0000644000175100017510000000036013642527701024222 0ustar00kittermakittermav=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6aakWzruYAKX9OdOdSHMemqVdGQurNYLC1H7O/T2LQIHVbkKF6KKjlgFM7lr8skfZMhJe/KRGMvVjCV5ZakZIGeP3Hi1qXCEvmjS4ElpMPMPyPrZigt95ipqywPYZJWHbRiJ085VdkSCtLUvo5sypA0nTJeynEouAN+/wBaCO6QIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/data/2048_testkey.key0000644000175100017510000000321713642527701021170 0ustar00kittermakitterma-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA5Ru5WVhkPiBuE+mOf29uqontUfoQpcWX04sA/V6dRmZ0I2kR HOSKYXLW84sqPiWyfKH4WaP7f0p6RvENyIqFjCYWDPxSX5Ck/rj0jUzQ6FKloUb9 M/0hhdUy2xa7przy7wudLu7WpgxyMLogPZfWMV369vvPefyvuXF8B3rCuy5qSNyv nDf0Dn4/blILfyw5df4JWJIj0FfXxc3LMMLQM+Nn8q71iJmH5qXuB78GWUjT7erE XnrXdWKZimM9l6fMSOb6EBm0AwNVR+mOGwt05LPrSHFrwKJSNRB5gBOzXfmjdUpg hIq+wLF1trz+oJt5CcZs45GPjiuyUomI+HgFpQIDAQABAoIBACZDIAxEz9D0V43p xouPz5qZmFHFgnw86KNCprKp13hHagPz6xC5NkcaB736st5YK1vXLQPDeCAFDzDD 1zpzr+P9BFe82ADvxi5oMydZ+dWi39h0Y5sIHk4ckfa28DXjfmrftKt+5cgrcfKj UvkWX4Y1HNGcskS8J5i/1Cfnn5fk0qRcedRnRTI+EgtV5pdawCMxvQ5jZ+QEirfZ jvmY+PsZZHbYDwpj4kptro/R4CA6ROO8O70gWsOYDwc0Ag4NrNYw4TwXNJLZXPJ/ uOybF0uG9E8b9TUIkbuKPza6dH7eEziKrWzbdpc/7KHzjm2+w5cpL1FhmplypJ9z 0K3cMqECgYEA8+VqnauecO2k9OmcFHJK1vAmZDklO56DF0e4VluXUUHEl1hSq/gE KrQpObnFPuJ6s/94rcMfsHYxnmpRvqGQJfQelVPUw0sFCpi4Oef8DdWVs88K0W9/ 0OBxTUQDzn3BwOpkP1xpnkvgerf+vZLNsOKyR47crGAEnV/zSavXwbkCgYEA8Hpv VDqgKgxmZPBx5x2wiSP1n+1iVA7UejiENekrV45O0hgpTPIewzE5h4Znnku8TI8P R3udNRQhFbaUftfJIhbOiPd7Tkax+dpUuyE2pxviCfTYYtw3C/tkAK8BtvRR8F/1 N3U/bef/mUygWmy28heTm71Ul7NZ5+uVSQN9SU0CgYEAuC9EeJWmLXoJ3LqemAJH 6lGpbH6LU569ONN8rhq1BzQ3lmOo6x19aBJuH53fC0JVj6EaLX7xa32Wsx9vZBr2 lPLqNbL9Ng5tXDdAyNocSwZE62krrkYZrFASwFrg1Qu7Ski4M/FCAb0jZQmWYut+ Ulqgb9G/bJ2GXqKbQ8N2SGECgYB7k49OS2ou8XxU9rKxv7IOZ2o7qvNSmuRZgl1+ jrn1Hm4mzL+8iNeCGvOjT1BEKkszVHjXlRBuTE/MJq0cmasEV4//Fp5I1U90Q9wy 5Zh+O3fnAemIs/2drKvxJf3NlLPewJD01M2pnwTcNGJa1W8V7UOvTpX/9uyGn6tx f9eMKQKBgQDGuBHLDYSW5/uSAKq2462mDSa7keTUfONkFN2XR0f0JnYt3DIgcX3/ ciGzZumCEmp4XJu0HUi67Fjz1BeCmKQWFC0rMzkTSZimjxQMv2iPXwhWDyR1yMpe +yV8JAFAwBUUtMVXTMPZfWXeNLFep0dyCSk6qCRBT7ppydNOaLSnHg== -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/tests/data/2048_testkey_PKCS8.key0000644000175100017510000000325014423474310022070 0ustar00kittermakitterma-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYMGhEok34XiUg szO7cuYph/dw0lliI18+wwX0ybUNwjkmc89jfRNZ6OEF5BfuyMYAoHB96D9EhIAI qXzjmvFtjZIrYTyzb2X/4Cml7uxGLiekSFjrANL1mybg36FKeMAV59s078++IrcQ Trr1E6e7q127/YRwCGy+KZQ0Sl2CrL+IdPuCK5WAJtYT1zQ/h3PQ/xUcHLnUtem5 BOirg7sweoxdfZWsXu7WeGDYiFOOV5pMmdueFfun5CJe8MfdEJ4YsXqxCxa2yqrN klkX4RSphEfwreU/sv0DsWj/eaWmFDhagvd2AwixId/lFYGsY2X7G6bs/SqThoBk 9GftRGihAgMBAAECggEAYiHjCpiUBPQTLVs61dEridmWp8dL3IDK6K3VA88VmMe7 cmlqT7JEOPE9R5PIi1Lmkf1B4t0r7tmoVoY80wIPqhdzrK5IQ/kCl1n0/cXMyXSE +Q0AE7h9ihAh3zyTtb7HDop+1fIvXhLa/xOFyN5hqo34j+9dkQ858T3lcLD67mfO 1/lpBhwEeF/7+wg2MiwM/4Rd217ReUgdgXTKZ8DYtLr+TFagv/8E8zAgHVqnklHf yEGR5+O94WYpqbanagfivxmogHsW6Eu5Ub/RJuWj06AvhRQSkPgVV5pl1zv0EP2w vhAl2YVzorRJDEzclmP+PyTzp+sPbrIqywof+K89WwKBgQDddD6eqISrWC4Vo2CZ WvBetiC48KKKjWwcFGERHGYqrmZMTDVYKqSUO7PvcgHIVyElYnlcqJWqyyQDBhy/ ShOZjQ0oy9SblZMoVjLYONS0iFDoz2of8/kvY+kuzh+3iU9Qg6BI+lfRSd8KjGBB xsLOoUhzQceXqmx5SmOGIn6EmwKBgQD56efYqmYj/GGLz/gyfyElsQFUWG0tyNjm ujufTxjhEDWw3pnS3vUVG/gleGJH7f9vzCfEI4WlBuZUidrwM64cnF4QXQwuhT7p 5U+nKNVfJpMeglehgVl6vSi/L1ovilCNo5A4aQJxyS73KTcU1UAZ4/VZnrvYrV/f ysxWTrx1cwKBgDAAyKofoVJ69NJf7cqQOdZt6D3ue21JJowXpsrMuyC5WRdk1ZNc +vvezSw0LEq/CEJQTDpXmMnC6vV017pnVkRMnPOg618mVxXBSZgxCXpwqgktHLX8 bqFlKOCqcZmZPAYZ4h6vlWWae6yPrTXU3dlogInrUlZ/7K+F/njO9VnNAoGBAOvp QsPDruGfd9GMQ2YfngG/glrFkmKa6y16dZfgCcNDEvvgVeK6Ny5zFZ8Bcf0mjG9T j+JWCe2LgtggvfzrPBuj/COEQmCTxZzzq2pHYIwOlOhC8Ef0G6yCbbl0ELU54uqh kR2++uDAokYMsQNIftcx2kR8VCSpHQzbmmKKttpDAoGBAIGVTOJtRi3nvmz1q3Zy vad1gTrkyy/gtjuE+8KNKhWwnC+MYXsSOykh+x6GQQ1gHRjk6GMsQxwl3xbHvzdG yp7Fc3aWkJbARSINOcAblH8JWdCMGBY/FlSogl0ptROVFxkyM2DnchM1bSsJ9wvf xB4hqm1VSknnOCCK+NekIC48 -----END PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/tests/data/2048_testkey_PKCS8.key.pub.txt0000644000175100017510000000064014423474310023473 0ustar00kittermakittermav=DKIM1; g=*; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2DBoRKJN+F4lILMzu3LmKYf3cNJZYiNfPsMF9Mm1DcI5JnPPY30TWejhBeQX7sjGAKBwfeg/RISACKl845rxbY2SK2E8s29l/+Appe7sRi4npEhY6wDS9Zsm4N+hSnjAFefbNO/PviK3EE669ROnu6tdu/2EcAhsvimUNEpdgqy/iHT7giuVgCbWE9c0P4dz0P8VHBy51LXpuQToq4O7MHqMXX2VrF7u1nhg2IhTjleaTJnbnhX7p+QiXvDH3RCeGLF6sQsWtsqqzZJZF+EUqYRH8K3lP7L9A7Fo/3mlphQ4WoL3dgMIsSHf5RWBrGNl+xum7P0qk4aAZPRn7URooQIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/data/2048_testkey_wo_markers.pub.rsa.txt0000644000175100017510000000060013642527701025012 0ustar00kittermakittermav=DKIM1; g=*; k=rsa; p=MIIBCgKCAQEA5Ru5WVhkPiBuE+mOf29uqontUfoQpcWX04sA/V6dRmZ0I2kRHOSKYXLW84sqPiWyfKH4WaP7f0p6RvENyIqFjCYWDPxSX5Ck/rj0jUzQ6FKloUb9M/0hhdUy2xa7przy7wudLu7WpgxyMLogPZfWMV369vvPefyvuXF8B3rCuy5qSNyvnDf0Dn4/blILfyw5df4JWJIj0FfXxc3LMMLQM+Nn8q71iJmH5qXuB78GWUjT7erEXnrXdWKZimM9l6fMSOb6EBm0AwNVR+mOGwt05LPrSHFrwKJSNRB5gBOzXfmjdUpghIq+wLF1trz+oJt5CcZs45GPjiuyUomI+HgFpQIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/data/2048_testkey_wo_markers.pub.txt0000644000175100017510000000064013642527701024232 0ustar00kittermakittermav=DKIM1; g=*; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Ru5WVhkPiBuE+mOf29uqontUfoQpcWX04sA/V6dRmZ0I2kRHOSKYXLW84sqPiWyfKH4WaP7f0p6RvENyIqFjCYWDPxSX5Ck/rj0jUzQ6FKloUb9M/0hhdUy2xa7przy7wudLu7WpgxyMLogPZfWMV369vvPefyvuXF8B3rCuy5qSNyvnDf0Dn4/blILfyw5df4JWJIj0FfXxc3LMMLQM+Nn8q71iJmH5qXuB78GWUjT7erEXnrXdWKZimM9l6fMSOb6EBm0AwNVR+mOGwt05LPrSHFrwKJSNRB5gBOzXfmjdUpghIq+wLF1trz+oJt5CcZs45GPjiuyUomI+HgFpQIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/badk.txt0000644000175100017510000000037313642525265017756 0ustar00kittermakittermav=DKIM1; g=*; k=ed25519-sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/badversion.txt0000644000175100017510000000036013642525265021205 0ustar00kittermakittermav=DKIM1: g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/ed25519test.dns0000644000175100017510000000007213642525265020714 0ustar00kittermakittermak=ed25519; p=yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y= ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/ed25519test.key0000644000175100017510000000005513642525265020721 0ustar00kittermakittermafL+5V9EquCZAovKik3pA6Lk9zwCzoEtjIuIqK9ZXHHA= ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/ed25519test.msg0000644000175100017510000000053213642525265020717 0ustar00kittermakittermaAuthentication-Results: lists.example.org; arc=none; spf=pass smtp.mfrom=jqd@d1.example; dkim=pass (1024-bit key) header.i=@d1.example; dmarc=pass Received: from localhost Message-ID: Date: Mon, 01 Jan 2011 01:02:03 +0400 From: Test User To: somebody@example.com Subject: Testing This is a test message. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/ed25519test2.msg0000644000175100017510000000111413642525265020776 0ustar00kittermakittermaDKIM-Signature: v=1; a=ed25519-sha256; q=dns/txt; c=relaxed/relaxed; d=test.ex ; s=sed; h=From:To:Subject; bh=/Ab0giHZitYQbDhFszoqQRUkgqueaX9zatJttIU/plc=; b=5fhyD3EILDrnL4DnkD4hDaeis7+GSzL9GMHrhIDZJjuJ00WD5iI8SQ1q9rDfzFL/Kdw0VIyB4R Dq0a4H6HI+Bw==; Received: from jgh by myhost.test.ex with local (Exim x.yz) envelope-from ) 1dtXln-0000YP-Hb a@test.ex; Sun, 17 Sep 2017 12:29:51 +0100 From: nobody@example.com Message-Id: Sender: CALLER_NAME Date: Sun, 17 Sep 2017 12:29:51 +0100 content ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/eximtest.dns0000644000175100017510000000010313642525265020653 0ustar00kittermakittermav=DKIM1; k=ed25519; p=sPs07Vu29FpHT/80UXUcYHFOHifD4o2ZlP2+XUh9g6E= ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/message.mbox0000644000175100017510000001053413642525265020627 0ustar00kittermakittermaReturn-Path: Delivered-To: kitterma-kitterman:com-scott@kitterman.com X-Envelope-To: scott@kitterman.com Received: (qmail 84128 invoked by uid 3013); 7 Mar 2011 19:23:23 -0000 Delivered-To: kitterma-kitterman:com-bcc@kitterman.com Received: (qmail 84124 invoked from network); 7 Mar 2011 19:23:23 -0000 Received: from mailwash7.pair.com (66.39.2.7) by raung.pair.com with SMTP; 7 Mar 2011 19:23:23 -0000 Received: from localhost (localhost [127.0.0.1]) by mailwash7.pair.com (Postfix) with SMTP id 55353BC0C for ; Mon, 7 Mar 2011 14:23:23 -0500 (EST) X-Virus-Check-By: mailwash7.pair.com X-Spam-Check-By: mailwash7.pair.com X-Spam-Status: No, hits=-102.4 required=3.5 tests=BAYES_00,DKIM_SIGNED,DKIM_VERIFIED,SPF_HELO_PASS,USER_IN_WHITELIST autolearn=ham version=3.002005 X-Spam-Flag: NO X-Spam-Level: X-Spam-Filtered: e5ffa8d1346811c78a1c1beaefd60800 Received: from mailout00.controlledmail.com (mailout00.controlledmail.com [72.81.252.19]) by mailwash7.pair.com (Postfix) with ESMTP id 0CCA9BC14 for ; Mon, 7 Mar 2011 14:23:19 -0500 (EST) Received: from mailout00.controlledmail.com (localhost [127.0.0.1]) by mailout00.controlledmail.com (Postfix) with ESMTP id 6D9F438C28F; Mon, 7 Mar 2011 14:23:18 -0500 (EST) DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=kitterman.com; s=2007-00; t=1299525798; bh=n0HUwGCP28PkesXBPH82Kboy8LhNFWU9zUISIpAez7M=; h=From:To:Subject:Date:Cc:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id; b=K/UUOt8lCtgjp3kSTogqBm9lY1Yax/NwZ+bKm39/WKzo5KYe3L/6RoIA/0oiDX4kO Qut49HCV6ZUe6dY9V5qWBwLanRs1sCnObaOGMpFfs8tU4TWpDSVXaNZAqn15XVW0WH EzOzUfVuatpa1kF4voIgSbmZHR1vN3WpRtcTBe/I= From: Scott Kitterman To: Kerrick Staley , Nick Coghlan Subject: Comments on PEP 394 draft Date: Mon, 7 Mar 2011 14:22:57 -0500 User-Agent: KMail/1.13.5 (Linux/2.6.35-27-generic; KDE/4.5.1; i686; ; ) Cc: barry@python.org MIME-Version: 1.0 Content-Type: multipart/signed; boundary="nextPart1746914.gtVYRJxS1r"; protocol="application/pgp-signature"; micalg=pgp-sha1 Content-Transfer-Encoding: 7bit Message-Id: <201103071423.13147.scott@kitterman.com> X-AV-Checked: ClamAV using ClamSMTP X-UID: 63126 X-Length: 4427 Status: R X-Status: N X-KMail-EncryptionState: X-KMail-SignatureState: X-KMail-MDN-Sent: --nextPart1746914.gtVYRJxS1r Content-Type: Text/Plain; charset="us-ascii" Content-Transfer-Encoding: quoted-printable I'm one of the maintainers of the packages that provide /usr/bin/python,=20 python3, and potentially python2 in Debian and Ubuntu. I've read both your= =20 draft ( http://www.python.org/dev/peps/pep-0394/ ) and the thread on ptyhon- dev. I'm writing you directly since I'm not subscribed to python-dev and=20 that's what Barry suggested. I think that the PEP generally makes sense. The only comment I have is tha= t=20 the recommendation regarding pointing /usr/bin/python at /usr/bin/python3 i= s=20 far too aggressive. It will break lots of local scripts and python softwar= e=20 (updating distribution package repositories isn't nearly sufficient=20 preparation for the change). I know some distributions have or will do thi= s,=20 but I think it is not appropriate for an upstream recommendation. If you=20 would change: "For the time being, it is recommended that python should refer to python2,= =20 except on distributions which include only python3 in their base install, o= r=20 those that wish to push strongly for migration of user scripts to Python 3." to "For the time being, it is recommended that python should refer to python2." then it would be something Debian would likely (I'm not the only maintainer= )=20 support. Given that the previous position was that /usr/bin/python would=20 always refer to python2, just establishing that it should change as some po= int=20 is a step forward. I don't think pushing harder than that will be worth th= e=20 added controversy associated with being more aggressive. Scott K --nextPart1746914.gtVYRJxS1r Content-Type: application/pgp-signature; name=signature.asc Content-Description: This is a digitally signed message part. -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.10 (GNU/Linux) iEYEABECAAYFAk11MJQACgkQHajaM93NaGpohwCfZNhmnoyq51jlCJ/nJ1dpbLWc llUAn0y8pceuESDSfNLHW0DADEygs4aU =S7uo -----END PGP SIGNATURE----- --nextPart1746914.gtVYRJxS1r-- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/rfc6376.msg0000644000175100017510000000041513642525265020121 0ustar00kittermakittermaFrom: Joe SixPack To: Suzie Q Subject: Is dinner ready? Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT) Message-ID: <20030712040037.46341.5F8J@football.example.com> Hi. We lost the game. Are you hungry yet? Joe. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/rfc6376.signed.msg0000644000175100017510000000205513642525265021373 0ustar00kittermakittermaDKIM-Signature: v=1; a=ed25519-sha256; c=simple/simple; d=football.example.com; i=@football.example.com; q=dns/txt; s=brisbane; t=1518460054; h=from : to : subject : date : message-id : from : subject : date; bh=4bLNXImK9drULnmePzZNEBleUanJCX5PIsDIFoH4KTQ=; b=9/dsDChY0YMTtD5Eyw3wx7x22BlSJP7M5ECbJ7GWrR45nXlTCGb8l0YB o0wBLR++X5LqmsxXaOYLLJe46l10AQ== DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=football.example.com; i=@football.example.com; q=dns/txt; s=test; t=1527915362; h=from : to : subject : date : message-id : from : subject : date; bh=4bLNXImK9drULnmePzZNEBleUanJCX5PIsDIFoH4KTQ=; b=icKcLSEZYXJ95flvWE8FT6hl5iqd8MC/LEKYH0QjsqYy6MO/4pgVNCZH l/RAXAuADxE/40Fg7uTlxwwD1hjN2Ple6J//cJfslBdDOq6zTVbne1dqtl NOat7iamJ1AfRqyG+ja7a2AZsrpUuJ7VA6O+0zRYPqpwMEkEFIzI9i/Xk= From: Joe SixPack To: Suzie Q Subject: Is dinner ready? Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT) Message-ID: <20030712040037.46341.5F8J@football.example.com> Hi. We lost the game. Are you hungry yet? Joe. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/rfc6376.signed.rsa.msg0000644000175100017510000000130413642525265022153 0ustar00kittermakittermaDKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=football.example.com; i=@football.example.com; q=dns/txt; s=test; t=1527915362; h=from : to : subject : date : message-id : from : subject : date; bh=4bLNXImK9drULnmePzZNEBleUanJCX5PIsDIFoH4KTQ=; b=icKcLSEZYXJ95flvWE8FT6hl5iqd8MC/LEKYH0QjsqYy6MO/4pgVNCZH l/RAXAuADxE/40Fg7uTlxwwD1hjN2Ple6J//cJfslBdDOq6zTVbne1dqtl NOat7iamJ1AfRqyG+ja7a2AZsrpUuJ7VA6O+0zRYPqpwMEkEFIzI9i/Xk= From: Joe SixPack To: Suzie Q Subject: Is dinner ready? Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT) Message-ID: <20030712040037.46341.5F8J@football.example.com> Hi. We lost the game. Are you hungry yet? Joe. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/tests/data/rfc6376.w1258.msg0000644000175100017510000000042314423474310020675 0ustar00kittermakittermaFrom: Tomá? SixPack To: Suzie Q Subject: Is dinner ready? Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT) Message-ID: <20030712040037.46341.5F8J@football.example.com> Hi. We lost the game. Are you hungry yet? Tomá?. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/rfc8032_7_1.key0000644000175100017510000000005513642525265020560 0ustar00kittermakittermanWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A= ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test.message0000644000175100017510000000053213642525265020636 0ustar00kittermakittermaAuthentication-Results: lists.example.org; arc=none; spf=pass smtp.mfrom=jqd@d1.example; dkim=pass (1024-bit key) header.i=@d1.example; dmarc=pass Received: from localhost Message-ID: Date: Mon, 01 Jan 2011 01:02:03 +0400 From: Test User To: somebody@example.com Subject: Testing This is a test message. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/tests/data/test.message.baddomain0000644000175100017510000000141214423474310022541 0ustar00kittermakittermaDKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=legitimate.com(.attacker.com; i=@legitimate.com(.attacker.com; q=dns/txt; s=test; t=1587514615; h=message-id : date : from : to : subject : from; bh=wE7NXSkgnx9PGiavN4OZhJztvkqPDlemV3OGuEnLwNo=; b=LsTV4fcR29N8CuUyrGn92jsTb67oAHx88vVIefoaUDghWxF5TpCyqcWbk/94Nt4PyxwUZ pgzF4UM/zF1rclCeNm/V4m0wMj3X2eeOIUUa8GRQ0g7DzixiQ5qHLUGpRT4BHfPmdHZHYj8 xv7+1O0/SJDK0YkaBjvhjDfkOoJhMmc= Authentication-Results: lists.example.org; arc=none; spf=pass smtp.mfrom=jqd@d1.example; dkim=pass (1024-bit key) header.i=@d1.example; dmarc=pass Received: from localhost Message-ID: Date: Mon, 01 Jan 2011 01:02:03 +0400 From: Test User To: somebody@example.com Subject: Testing This is a test message. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test.private0000644000175100017510000000156713642525265020675 0ustar00kittermakitterma-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQi Y/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqM KrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB AoGAH0cxOhFZDgzXWhDhnAJDw5s4roOXN4OhjiXa8W7Y3rhX3FJqmJSPuC8N9vQm 6SVbaLAE4SG5mLMueHlh4KXffEpuLEiNp9Ss3O4YfLiQpbRqE7Tm5SxKjvvQoZZe zHorimOaChRL2it47iuWxzxSiRMv4c+j70GiWdxXnxe4UoECQQDzJB/0U58W7RZy 6enGVj2kWF732CoWFZWzi1FicudrBFoy63QwcowpoCazKtvZGMNlPWnC7x/6o8Gc uSe0ga2xAkEA8C7PipPm1/1fTRQvj1o/dDmZp243044ZNyxjg+/OPN0oWCbXIGxy WvmZbXriOWoSALJTjExEgraHEgnXssuk7QJBALl5ICsYMu6hMxO73gnfNayNgPxd WFV6Z7ULnKyV7HSVYF0hgYOHjeYe9gaMtiJYoo0zGN+L3AAtNP9huqkWlzECQE1a licIeVlo1e+qJ6Mgqr0Q7Aa7falZ448ccbSFYEPD6oFxiOl9Y9se9iYHZKKfIcst o7DUw1/hz2Ck4N5JrgUCQQCyKveNvjzkkd8HjYs0SwM0fPjK16//5qDZ2UiDGnOe uEzxBDAr518Z8VFbR41in3W4Y3yCDgQlLlcETrS+zYcL -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test.txt0000644000175100017510000000036013642525265020030 0ustar00kittermakittermav=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test2.message0000644000175100017510000000212013642525265020713 0ustar00kittermakittermaComment: degenerate folding is ugly but legal, reported in Debian bug#711751 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20120113; h=mime-version:sender:from:date:x-google-sender-auth:message-id :subject:to:content-type; bh=NRDGmXYX648Rm6cs06aAQIE77gu68nsSHYB4kAMm7QQ=; b=VaN3KmNPlU1uSNproy8wF+6qwTUKEcyzanoPSo/u8P0p8rtHgQpOW5/nJ+/ExQ9jKN FWTyZ9PLecg/0De0QYV18GQovYb3PVUCDHS7dYzfWp072lFPAhISUancFc30amzRPXcy J2lnvgoPcFuqDh5tLPchz8LdeIL0hMr2Xt+xEibHftqYT0JRXX4LXkZdO/b/i825qMtL W51wBB0V6L1ZU156A9cZWQWvwnQ/lV7PV7AwRqGbIESguRLfCbM+UIAGoCR8QtTO0lkY bGqPQucn+1eZZUNsEJAWFI6eo2MmxY/FABEURGYAukaTg13UC9W+O6kGPH5iS5aRpAAT eKbQ== MIME-Version: 1.0 Sender: kaner.mail@gmail.com Received: by 10.42.92.137 with HTTP; Sun, 9 Jun 2013 02:37:02 -0700 (PDT) From: Christian Fromme Date: Sun, 9 Jun 2013 11:37:02 +0200 X-Google-Sender-Auth: ZkDaYkXZHozJZyNGgvWFlv78IRY Message-ID: Subject: DKIM test mail #1 To: gettor@gettor.torproject.org Content-Type: text/plain; charset=ISO-8859-1 Hello, DKIM ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test2.private0000644000175100017510000000156713642525265020757 0ustar00kittermakitterma-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQi Y/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqM KrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB AoGAH0cxOhFZDgzXWhDhnAJDw5s4roOXN4OhjiXa8W7Y3rhX3FJqmJSPuC8N9vQm 6SVbaLAE4SG5mLMueHlh4KXffEpuLEiNp9Ss3O4YfLiQpbRqE7Tm5SxKjvvQoZZe zHorimOaChRL2it47iuWxzxSiRMv4c+j70GiWdxXnxe4UoECQQDzJB/0U58W7RZy 6enGVj2kWF732CoWFZWzi1FicudrBFoy63QwcowpoCazKtvZGMNlPWnC7x/6o8Gc uSe0ga2xAkEA8C7PipPm1/1fTRQvj1o/dDmZp243044ZNyxjg+/OPN0oWCbXIGxy WvmZbXriOWoSALJTjExEgraHEgnXssuk7QJBALl5ICsYMu6hMxO73gnfNayNgPxd WFV6Z7ULnKyV7HSVYF0hgYOHjeYe9gaMtiJYoo0zGN+L3AAtNP9huqkWlzECQE1a licIeVlo1e+qJ6Mgqr0Q7Aa7falZ448ccbSFYEPD6oFxiOl9Y9se9iYHZKKfIcst o7DUw1/hz2Ck4N5JrgUCQQCyKveNvjzkkd8HjYs0SwM0fPjK16//5qDZ2UiDGnOe uEzxBDAr518Z8VFbR41in3W4Y3yCDgQlLlcETrS+zYcL -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test2.txt0000644000175100017510000000035113642525265020112 0ustar00kittermakittermav=DKIM1; g=*; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test_bad.txt0000644000175100017510000000036013642525265020636 0ustar00kittermakittermav=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQA= ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test_extra.message0000644000175100017510000000033313642525265022040 0ustar00kittermakittermaReceived: from zulu Received: from localhost Message-ID: Date: Mon, 01 Jan 2011 01:02:03 +0400 From: Test User To: somebody@example.com Subject: Testing This is a test message. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/data/test_nofrom.message0000644000175100017510000000024413642525265022216 0ustar00kittermakittermaReceived: from localhost Message-ID: Date: Mon, 01 Jan 2013 01:02:03 +0400 To: somebody@example.com Subject: Testing This is a test message. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/data/test_tlsrpt.txt0000644000175100017510000000037213642527701021440 0ustar00kittermakittermav=DKIM1; g=*; k=rsa; s=tlsrpt; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/test_arc.py0000644000175100017510000001226413642527701017560 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant # # This has been modified from the original software. # Copyright (c) 2016 Google, Inc. # Contact: Brandon Long import os.path import unittest import time import dkim def read_test_data(filename): """Get the content of the given test data file. """ path = os.path.join(os.path.dirname(__file__), 'data', filename) with open(path, 'rb') as f: return f.read() class TestSignAndVerify(unittest.TestCase): """End-to-end signature and verification tests.""" def setUp(self): self.message = read_test_data("test.message") self.key = read_test_data("test.private") def dnsfunc(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("test.txt"), # dnsfunc returns empty if no txt record 'missing._domainkey.example.com.': '', '20120113._domainkey.gmail.com.': """k=rsa; \ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def test_signs_and_verifies(self): # A message verifies after being signed self.maxDiff = None sig_lines = dkim.arc_sign( self.message, b"test", b"example.com", self.key, b"lists.example.org", timestamp="12345") expected_sig = [b'ARC-Seal: i=1; cv=none; a=rsa-sha256; d=example.com; s=test; t=12345;\r\n b=MBw2+L1/4PuYWJlt1tZlDtbOvyfbyH2t2N6DinFV/BIaB2LqbDKTYjXXk9HuuK1/qEkTd\r\n TxCYScIrtVO7pFbGiSawMuLatVzHNCqTURa1zBTXr2mKW1hgdmrtMMUcMVCYxr1AJpu6IYX\r\n VMIoOAn7tIDdO0VLokK6FnIXTWEAplQ=\r\n', b'ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=example.com; s=test; t=12345; h=message-id : date : from : to :\r\n subject : from; bh=wE7NXSkgnx9PGiavN4OZhJztvkqPDlemV3OGuEnLwNo=;\r\n b=a0f6qc3k9eECTSR155A0TQS+LjqPFWfI/brQBA83EUz00SNxj1wmWykvs1hhBVeM0r1kE\r\n Qc6CKbzRYaBNSiFj4q8JBpRIujLz1qLyGmPuAI6ddu/Z/1hQxgpVcp/odmI1UMV2R+d+yQ7\r\n tUp3EQxF/GYNt22rV4rNmDmANZVqJ90=\r\n', b'ARC-Authentication-Results: i=1; lists.example.org; arc=none;\r\n spf=pass smtp.mfrom=jqd@d1.example;\r\n dkim=pass (1024-bit key) header.i=@d1.example;\r\n dmarc=pass\r\n'] self.assertEqual(expected_sig, sig_lines) (cv, res, reason) = dkim.arc_verify(b''.join(sig_lines) + self.message, dnsfunc=self.dnsfunc) self.assertEqual(cv, dkim.CV_Pass) def test_fails_h_in_as(self): # ARC 4.1.3, h= not allowed in AS self.maxDiff = None sig_lines = [b'ARC-Seal: i=1; cv=none; a=rsa-sha256; d=example.com; s=test; t=12345;\r\n h=message-id : date : from : to : subject : from;\r\n b=mIurIuLl0/wAxWhA4DBS1wsUE15IBnmJ7o3sH15hIuesdD4smz1cCLXVhRtxQE\r\n rVtVLv4OgNCgdFsB5zbSOUao2bSSYP6y0BGyCWvr+hU4tai5axIc1Kfwbtv/0Mqg\r\n waiGJPreOAAeZOJ4vPfdaAbSXlN5MI4PHW89U82FSIBKI=\r\n', b'ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=example.com; s=test; t=12345; h=message-id :\r\n date : from : to : subject : from;\r\n bh=wE7NXSkgnx9PGiavN4OZhJztvkqPDlemV3OGuEnLwNo=;\r\n b=a0f6qc3k9eECTSR155A0TQS+LjqPFWfI/brQBA83EUz00SNxj\r\n 1wmWykvs1hhBVeM0r1kEQc6CKbzRYaBNSiFj4q8JBpRIujLz1qL\r\n yGmPuAI6ddu/Z/1hQxgpVcp/odmI1UMV2R+d+yQ7tUp3EQxF/GY\r\n Nt22rV4rNmDmANZVqJ90=\r\n', b'ARC-Authentication-Results: i=1; lists.example.org; arc=none;\r\n spf=pass smtp.mfrom=jqd@d1.example;\r\n dkim=pass (1024-bit key) header.i=@d1.example;\r\n dmarc=pass\r\n'] (cv, res, reason) = dkim.arc_verify(b''.join(sig_lines) + self.message, dnsfunc=self.dnsfunc) self.assertEqual(cv, dkim.CV_Fail) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/test_canonicalization.py0000644000175100017510000001363513642525265022346 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant import unittest from dkim.canonicalization import ( CanonicalizationPolicy, InvalidCanonicalizationPolicyError, Simple, Relaxed, ) class BaseCanonicalizationTest(unittest.TestCase): def assertCanonicalForm(self, expected, input): self.assertEqual(expected, self.func(expected)) self.assertEqual(expected, self.func(input)) class TestSimpleAlgorithmHeaders(BaseCanonicalizationTest): func = staticmethod(Simple.canonicalize_headers) def test_untouched(self): test_headers = [(b'Foo ', b'bar\r\n'), (b'Foo', b'baz\r\n')] self.assertCanonicalForm( test_headers, test_headers) class TestSimpleAlgorithmBody(BaseCanonicalizationTest): func = staticmethod(Simple.canonicalize_body) def test_strips_trailing_empty_lines_from_body(self): self.assertCanonicalForm( b'Foo \tbar \r\n', b'Foo \tbar \r\n\r\n') def test_adds_crlf(self): self.assertCanonicalForm( b'Foo bar\r\n', b'Foo bar') def test_empty_body(self): self.assertCanonicalForm( b'\r\n', b'') def test_single_crlf_body(self): self.assertCanonicalForm( b'\r\n', b'\r\n') def test_multiple_crlf_body(self): self.assertCanonicalForm( b'\r\n', b'\r\n\r\n') class TestRelaxedAlgorithmHeaders(BaseCanonicalizationTest): func = staticmethod(Relaxed.canonicalize_headers) def test_lowercases_names(self): self.assertCanonicalForm( [(b'foo', b'Bar\r\n'), (b'baz', b'Foo\r\n')], [(b'Foo', b'Bar\r\n'), (b'BaZ', b'Foo\r\n')]) def test_unfolds_values(self): self.assertCanonicalForm( [(b'foo', b'Bar baz\r\n')], [(b'Foo', b'Bar\r\n baz\r\n')]) def test_wsp_compresses_values(self): self.assertCanonicalForm( [(b'foo', b'Bar baz\r\n')], [(b'Foo', b'Bar \t baz\r\n')]) def test_wsp_strips(self): self.assertCanonicalForm( [(b'foo', b'Bar baz\r\n')], [(b'Foo ', b' Bar \t baz \r\n')]) class TestRelaxedAlgorithmBody(BaseCanonicalizationTest): func = staticmethod(Relaxed.canonicalize_body) def test_strips_trailing_wsp(self): self.assertCanonicalForm( b'Foo\r\nbar\r\n', b'Foo \t\r\nbar\r\n') def test_wsp_compresses(self): self.assertCanonicalForm( b'Foo bar\r\n', b'Foo \t bar\r\n') def test_strips_trailing_empty_lines(self): self.assertCanonicalForm( b'Foo\r\nbar\r\n', b'Foo\r\nbar\r\n\r\n\r\n') def test_adds_crlf(self): self.assertCanonicalForm( b'Foo bar\r\n', b'Foo bar') def test_empty_body(self): self.assertCanonicalForm( b'', b'') def test_single_crlf_body(self): self.assertCanonicalForm( b'', b'\r\n') def test_multiple_crlf_body(self): self.assertCanonicalForm( b'', b'\r\n\r\n') class TestCanonicalizationPolicyFromCValue(unittest.TestCase): def assertAlgorithms(self, header_algo, body_algo, c_value): p = CanonicalizationPolicy.from_c_value(c_value) self.assertEqual( (header_algo, body_algo), (p.header_algorithm, p.body_algorithm)) def assertValueDoesNotParse(self, c_value): self.assertRaises( InvalidCanonicalizationPolicyError, CanonicalizationPolicy.from_c_value, c_value) def test_both_default_to_simple(self): self.assertAlgorithms(Simple, Simple, None) def test_relaxed_headers(self): self.assertAlgorithms(Relaxed, Simple, b'relaxed') def test_relaxed_body(self): self.assertAlgorithms(Simple, Relaxed, b'simple/relaxed') def test_relaxed_both(self): self.assertAlgorithms(Relaxed, Relaxed, b'relaxed/relaxed') def test_explict_simple_both(self): self.assertAlgorithms(Simple, Simple, b'simple/simple') def test_corruption_is_ignored(self): self.assertValueDoesNotParse(b'') self.assertValueDoesNotParse(b'simple/simple/simple') self.assertValueDoesNotParse(b'relaxed/stressed') self.assertValueDoesNotParse(b'worried') class TestCanonicalizationPolicyToCValue(unittest.TestCase): def assertCValue(self, c_value, header_algo, body_algo): self.assertEqual( c_value, CanonicalizationPolicy(header_algo, body_algo).to_c_value()) def test_both_simple(self): self.assertCValue(b'simple/simple', Simple, Simple) def test_relaxed_body(self): self.assertCValue(b'simple/relaxed', Simple, Relaxed) def test_both_relaxed(self): self.assertCValue(b'relaxed/relaxed', Relaxed, Relaxed) def test_relaxed_headers(self): self.assertCValue(b'relaxed/simple', Relaxed, Simple) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/test_crypto.py0000644000175100017510000001520213642525265020331 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant import base64 import binascii import hashlib import unittest from dkim.crypto import ( DigestTooLargeError, UnparsableKeyError, EMSA_PKCS1_v1_5_encode, int2str, parse_pem_private_key, parse_public_key, RSASSA_PKCS1_v1_5_sign, RSASSA_PKCS1_v1_5_verify, str2int, ) from dkim.tests.test_dkim import read_test_data from dkim.util import parse_tag_value # These are extracted from dkim/tests/data/test.private. TEST_KEY_MODULUS = int( '160190232090260054474895273563294777865179886824815261110923286158270437' '657769966074370477716411064825849317279563494735400250019233722215662302' '997403060159149904218292658425241195497467863155064737257198115261596066' '733086923624062366294295557722551666415445482671442053150678674937682352' '837105556539434741981') TEST_KEY_PUBLIC_EXPONENT = 65537 TEST_KEY_PRIVATE_EXPONENT = int( '219642251791061057038224045690185219631125389170665415924249912174530136' '074693824121380763959239792563755125360354847443780863736947713174228520' '489900956461640273471526152019568303807247290486052565153701534491987040' '131529720476525111651818771481293273124837542067061293644354088836358900' '29771161475005043329') TEST_KEY_PRIME1 = int( '127343333492908149956322715568115237787784712176275919666517073343689103' '280591709737233188193431204382936008602497360201661766158158969883295914' '16266272177') TEST_KEY_PRIME2 = int( '125793967926229270607412639516115399484604596465353856808629588968254772' '302339293254103556785310783521521266982500068526354237606773478050287350' '33316975853') TEST_KEY_EXPONENT1 = int( '971401692373919639404678505179789291960987093676634885925231250693661495' '080125935714710587508461815572290443270923375888685273287584323569222368' '5450962737') TEST_KEY_EXPONENT2 = int( '405135004809332318340885085107137607293826268763328174261828392259785080' '028911220030572618988900118679333717167345003034279703551607153395397272' '3014807045') TEST_KEY_COEFFICIENT = int( '933140693852464192207530806898449261372116224159220632563973880414444021' '989007318611849609226428922185905596238131661588470844906391982906126973' '1282880267') TEST_PK = { 'version': 0, 'modulus': TEST_KEY_MODULUS, 'publicExponent': TEST_KEY_PUBLIC_EXPONENT, 'privateExponent': TEST_KEY_PRIVATE_EXPONENT, 'prime1': TEST_KEY_PRIME1, 'prime2': TEST_KEY_PRIME2, 'exponent1': TEST_KEY_EXPONENT1, 'exponent2': TEST_KEY_EXPONENT2, 'coefficient': TEST_KEY_COEFFICIENT, } class TestStrIntConversion(unittest.TestCase): def test_str2int(self): self.assertEqual(1234, str2int(b'\x04\xd2')) def test_int2str(self): self.assertEqual(b'\x04\xd2', int2str(1234)) def test_int2str_with_length(self): self.assertEqual(b'\x00\x00\x04\xd2', int2str(1234, 4)) def test_int2str_fails_on_negative(self): self.assertRaises(AssertionError, int2str, -1) class TestParseKeys(unittest.TestCase): def test_parse_pem_private_key(self): key = parse_pem_private_key(read_test_data('test.private')) self.assertEqual(key, TEST_PK) def test_parse_public_key(self): data = read_test_data('test.txt') key = parse_public_key(base64.b64decode(parse_tag_value(data)[b'p'])) self.assertEqual(key['modulus'], TEST_KEY_MODULUS) self.assertEqual(key['publicExponent'], TEST_KEY_PUBLIC_EXPONENT) try: data = read_test_data('test_bad.txt') key = parse_public_key(base64.b64decode(parse_tag_value(data)[b'p'])) except UnparsableKeyError: return self.fail("failed to reject invalid public key") class TestEMSA_PKCS1_v1_5(unittest.TestCase): def test_encode_sha256(self): hash = hashlib.sha256(b'message') self.assertEqual( b'\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00' b'010\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04' b' ' + hash.digest(), EMSA_PKCS1_v1_5_encode(hash, 62)) def test_encode_sha1(self): hash = hashlib.sha1(b'message') self.assertEqual( b'\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00' b'0!0\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' + hash.digest(), EMSA_PKCS1_v1_5_encode(hash, 46)) def test_encode_forbids_too_short(self): # PKCS#1 requires at least 8 bytes of padding, so there must be # at least that much space. hash = hashlib.sha1(b'message') self.assertRaises( DigestTooLargeError, EMSA_PKCS1_v1_5_encode, hash, 45) class TestRSASSA(unittest.TestCase): def setUp(self): self.key = parse_pem_private_key(read_test_data('test.private')) self.hash = hashlib.sha1(self.test_digest) test_digest = b'0123456789abcdef0123' test_signature = binascii.unhexlify( b'cc8d3647d64dd3bc12984947a27bdfbb565041fcc9db781afb4b60d29d288d8d60d' b'e9e1916d6f81569c3e72af442538dd6aecb50a6de9a14565fdd679c46ff7842482e' b'15e5aa078549621b6f12ca8cd57ecfad95b18e53581e131c6c3c7cd01cb153adeb4' b'39d2d6ab8b215b19be0e69ef490885004a474eb26d747a219693e8c') def test_sign_and_verify(self): signature = RSASSA_PKCS1_v1_5_sign(self.hash, TEST_PK) self.assertEqual(self.test_signature, signature) self.assertTrue( RSASSA_PKCS1_v1_5_verify( self.hash, signature, TEST_PK)) def test_invalid_signature(self): invalid_key = TEST_PK.copy() invalid_key['modulus'] += 1 self.assertFalse( RSASSA_PKCS1_v1_5_verify( self.hash, self.test_signature, invalid_key)) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/tests/test_dkim.py0000644000175100017510000005613414423474310017736 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant import email import os.path import unittest import time import dkim def read_test_data(filename): """Get the content of the given test data file. The files live in dkim/tests/data. """ path = os.path.join(os.path.dirname(__file__), 'data', filename) with open(path, 'rb') as f: return f.read() class TestFold(unittest.TestCase): def test_short_line(self): self.assertEqual( b"foo", dkim.fold(b"foo")) def test_long_line(self): # The function is terribly broken, not passing even this simple # test. self.assertEqual( b"foo" * 24 + b"\r\n foo", dkim.fold(b"foo" * 25)) def test_linesep(self): self.assertEqual( b"foo" * 24 + b"\n foo", dkim.fold(b"foo" * 25, linesep=b"\n")) class TestSignAndVerify(unittest.TestCase): """End-to-end signature and verification tests.""" def setUp(self): self.message = read_test_data("test.message") self.message3 = read_test_data("rfc6376.msg") self.message4 = read_test_data("rfc6376.signed.msg") self.message5 = read_test_data("rfc6376.signed.rsa.msg") self.message6 = read_test_data("test.message.baddomain") self.message7 = read_test_data("rfc6376.w1258.msg") self.key = read_test_data("test.private") self.rfckey = read_test_data("rfc8032_7_1.key") def dnsfunc(self, domain, timeout=5): sample_dns = """\ k=rsa; s=email;\ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("test.txt"), '20120113._domainkey.gmail.com.': """k=rsa; \ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc2(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("test2.txt"), '20120113._domainkey.gmail.com.': """\ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc3(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("badversion.txt"), '20120113._domainkey.gmail.com.': """\ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc4(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("badk.txt"), '20120113._domainkey.gmail.com.': """\ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc5(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.football.example.com.': read_test_data("test.txt"), 'brisbane._domainkey.football.example.com.': """v=DKIM1; k=ed25519; \ p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc6(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'test._domainkey.football.example.com.': sample_dns, 'brisbane._domainkey.football.example.com.': """v=DKIM1; k=ed25519; \ p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc7(self, domain, timeout=5): sample_dns = """\ k=rsa; s=email;\ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'test._domainkey.legitimate.com(.attacker.com.': read_test_data("test.txt"), } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def test_verifies(self): # A message verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_verifies_nosig(self): # A message without signature does not verify. res = dkim.verify(self.message, dnsfunc=self.dnsfunc) self.assertFalse(res) def test_double_verifies(self): # A message also containing a ed25519 signature verifies after being signed with rsa. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message3, b"test", b"football.example.com", self.key, canonicalize=(header_algo, body_algo), signature_algorithm=b'rsa-sha256') res = dkim.verify(sig + self.message3, dnsfunc=self.dnsfunc5) self.assertTrue(res) def test_double_previous_verifies(self): # A message previously signed using both rsa and ed25519 verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message3, b"test", b"football.example.com", self.key, canonicalize=(header_algo, body_algo), signature_algorithm=b'rsa-sha256') d = dkim.DKIM(self.message4) res = d.verify(dnsfunc=self.dnsfunc5) self.assertTrue(res) def test_non_utf8(self): # A message with Windows-1258 encoding is signed and verifies. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message7, b"test", b"football.example.com", self.key, canonicalize=(header_algo, body_algo), signature_algorithm=b'rsa-sha256') d = dkim.DKIM(self.message7) res = d.verify(dnsfunc=self.dnsfunc5) # As of 1.1.0 this won't verify, but at least we don't crash. FIXME self.assertFalse(res) def test_catch_bad_key(self): # Raise correct error for defective public key. d = dkim.DKIM(self.message5) res = d.verify(dnsfunc=self.dnsfunc6) self.assertFalse(res) def test_verifies_lflinesep(self): # A message verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), linesep=b"\n") res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertFalse(b'\r\n' in sig) self.assertTrue(res) def test_implicit_k(self): # A message verifies after being signed when k= tag is not provided. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc2) self.assertTrue(res) def test_bad_version(self): # A error is detected if a bad version is used. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc3) self.assertFalse(res) def test_unknown_k(self): # A error is detected if an unknown algorithm is in the k= tag. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc4) self.assertFalse(res) def test_invalid_domain_sign(self): # RFC6376 says domain can be Alpha, Num, - only. sig = dkim.sign( self.message, b"test", b"legitimate.com(.attacker.com", self.key) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc7) self.assertFalse(res) def test_invalid_domain_verify(self): # RFC6376 says domain can be Alpha, Num, - only. res = dkim.verify(self.message6, dnsfunc=self.dnsfunc7) self.assertFalse(res) def test_simple_signature(self): # A message verifies after being signed with SHOULD headers for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), include_headers=(b'from',) + dkim.DKIM.SHOULD) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_string_include(self): # A message can be signed when the include_headers is string for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), include_headers=('from',) ) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_add_body_length(self): sig = dkim.sign( self.message, b"test", b"example.com", self.key, length=True) msg = email.message_from_string(self.message.decode('utf-8')) self.assertIn('; l=%s' % len(msg.get_payload() + '\n'), sig.decode('utf-8')) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_altered_body_fails(self): # An altered body fails verification. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key) res = dkim.verify( sig + self.message + b"foo", dnsfunc=self.dnsfunc) self.assertFalse(res) def test_l_verify(self): # Sign with l=, add text, should verify for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), length=True) self.message += b'added more text\n' res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_present(self): # Test DKIM.present(). d = dkim.DKIM(self.message,signature_algorithm=b'rsa-sha256') present = d.present() self.assertFalse(present) sig = d.sign(b"test", b"example.com", self.key) signed = sig + self.message d2 = dkim.DKIM(signed) present = d2.present() self.assertTrue(present) def test_badly_encoded_domain_fails(self): # Domains should be ASCII. Bad ASCII causes verification to fail. sig = dkim.sign(self.message, b"test", b"example.com\xe9", self.key) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertFalse(res) def test_dkim_signature_canonicalization(self): # # Relaxed-mode header signing is wrong # # Simple-mode signature header verification is wrong # (should ignore FWS anywhere in signature tag: b=) sample_msg = b"""\ From: mbp@canonical.com To: scottk@example.com Subject: this is my test message """.replace(b'\n', b'\r\n') sample_privkey = b"""\ -----BEGIN RSA PRIVATE KEY----- MIIBOwIBAAJBANmBe10IgY+u7h3enWTukkqtUD5PR52Tb/mPfjC0QJTocVBq6Za/ PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQJAYFUKsD+uMlcFu1D3YNaR EGYGXjJ6w32jYGJ/P072M3yWOq2S1dvDthI3nRT8MFjZ1wHDAYHrSpfDNJ3v2fvZ cQIhAPgRPmVYn+TGd59asiqG1SZqh+p+CRYHW7B8BsicG5t3AiEA4HYNOohlgWan 8tKgqLJgUdPFbaHZO1nDyBgvV8hvWZUCIQDDdCq6hYKuKeYUy8w3j7cgJq3ih922 2qNWwdJCfCWQbwIgTY0cBvQnNe0067WQIpj2pG7pkHZR6qqZ9SE+AjNTHX0CIQCI Mgq55Y9MCq5wqzy141rnxrJxTwK9ABo3IAFMWEov3g== -----END RSA PRIVATE KEY----- """ sample_pubkey = """\ -----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ== -----END PUBLIC KEY----- """ for header_mode in [dkim.Relaxed, dkim.Simple]: dkim_header = dkim.sign(sample_msg, b'example', b'canonical.com', sample_privkey, canonicalize=(header_mode, dkim.Relaxed)) # Folding dkim_header affects b= tag only, since dkim.sign folds # sig_value with empty b= before hashing, and then appends the # signature. So folding dkim_header again adds FWS to # the b= tag only. This should be ignored even with # simple canonicalization. # http://tools.ietf.org/html/rfc4871#section-3.5 signed = dkim.fold(dkim_header) + sample_msg result = dkim.verify(signed,dnsfunc=self.dnsfunc, minkey=512) self.assertTrue(result) dkim_header = dkim.fold(dkim_header) # use a tab for last fold to test tab in FWS bug pos = dkim_header.rindex(b'\r\n ') dkim_header = dkim_header[:pos]+b'\r\n\t'+dkim_header[pos+3:] result = dkim.verify(dkim_header + sample_msg, dnsfunc=self.dnsfunc, minkey=512) self.assertTrue(result) def test_degenerate_folding(self): # # degenerate folding is ugly but legal message = read_test_data("test2.message") dv = dkim.DKIM(message) res = dv.verify(dnsfunc=self.dnsfunc) self.assertTrue(res) def test_extra_headers(self): # # extra headers above From caused failure #message = read_test_data("test_extra.message") message = read_test_data("message.mbox") for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): d = dkim.DKIM(message) # bug requires a repeated header to manifest d.should_not_sign.remove(b'received') sig = d.sign(b"test", b"example.com", self.key, include_headers=d.all_sign_headers(), canonicalize=(header_algo, body_algo)) dv = dkim.DKIM(sig + message) res = dv.verify(dnsfunc=self.dnsfunc) self.assertEqual(d.include_headers,dv.include_headers) s = dkim.select_headers(d.headers,d.include_headers) sv = dkim.select_headers(dv.headers,dv.include_headers) self.assertEqual(s,sv) self.assertTrue(res) def test_multiple_from_fails(self): # # additional From header fields should cause verify failure hfrom = b'From: "Resident Evil" \r\n' h,b = self.message.split(b'\n\n',1) for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key) # adding an unknown header still verifies h1 = h+b'\r\n'+b'X-Foo: bar' message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertTrue(res) # adding extra from at end should not verify h1 = h+b'\r\n'+hfrom.strip() message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertFalse(res) # add extra from in front should not verify either h1 = hfrom+h message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertFalse(res) def test_no_from_fails(self): # Body From is mandatory to be in the message and mandatory to sign sigerror = False sig = '' message = read_test_data('test_nofrom.message') selector = 'test' domain = 'example.com' identity = None try: sig = dkim.sign(message, selector, domain, read_test_data('test.private'), identity = identity) except dkim.ParameterError as x: sigerror = True self.assertTrue(sigerror) def test_validate_signature_fields(self): sig = {b'v': b'1', b'a': b'rsa-sha256', b'b': b'K/UUOt8lCtgjp3kSTogqBm9lY1Yax/NwZ+bKm39/WKzo5KYe3L/6RoIA/0oiDX4kO\n \t Qut49HCV6ZUe6dY9V5qWBwLanRs1sCnObaOGMpFfs8tU4TWpDSVXaNZAqn15XVW0WH\n \t EzOzUfVuatpa1kF4voIgSbmZHR1vN3WpRtcTBe/I=', b'bh': b'n0HUwGCP28PkesXBPH82Kboy8LhNFWU9zUISIpAez7M=', b'c': b'simple/simple', b'd': b'kitterman.com', b'i': b'scott@Kitterman.com', b'h': b'From:To:Subject:Date:Cc:MIME-Version:Content-Type:\n \t Content-Transfer-Encoding:Message-Id', b's': b'2007-00', b't': b'1299525798'} dkim.validate_signature_fields(sig) # try new version sigVer = sig.copy() sigVer[b'v'] = 2 self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigVer) # try with x sigX = sig.copy() sigX[b'x'] = b'1399525798' dkim.validate_signature_fields(sig) # try with late t sigX[b't'] = b'1400000000' self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigX) # try without t now = int(time.time()) sigX[b'x'] = str(now+400000).encode('ascii') dkim.validate_signature_fields(sigX) # try when expired a day ago sigX[b'x'] = str(now - 24*3600).encode('ascii') self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigX) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/test_dkim_ed25519.py0000644000175100017510000003110213642527701021005 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant # Copyright (c) 2018 Scott Kitterman import email import os.path import unittest import time import dkim def read_test_data(filename): """Get the content of the given test data file. The files live in dkim/tests/data. """ path = os.path.join(os.path.dirname(__file__), 'data', filename) with open(path, 'rb') as f: return f.read() class TestFold(unittest.TestCase): def test_short_line(self): self.assertEqual( b"foo", dkim.fold(b"foo")) def test_long_line(self): # The function is terribly broken, not passing even this simple # test. self.assertEqual( b"foo" * 24 + b"\r\n foo", dkim.fold(b"foo" * 25)) class TestSignAndVerify(unittest.TestCase): """End-to-end signature and verification tests.""" def setUp(self): self.message = read_test_data("ed25519test.msg") self.message2 = read_test_data("ed25519test2.msg") self.message3 = read_test_data("rfc6376.msg") self.message4 = read_test_data("rfc6376.signed.msg") self.key = read_test_data("ed25519test.key") self.rfckey = read_test_data("rfc8032_7_1.key") def dnsfunc(self, domain, timeout=5): sample_dns = """\ k=ed25519; \ p=yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y=""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.net.': """v=DKIM1; k=ed25519; \ p=yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y=""", 'sed._domainkey.test.ex.': read_test_data("eximtest.dns"), 'brisbane._domainkey.football.example.com.': """v=DKIM1; k=ed25519; \ p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def test_verifies(self): # A message verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.net", self.key, canonicalize=(header_algo, body_algo), signature_algorithm=b'ed25519-sha256') res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_rfc8032_verifies(self): # A message using RFC 8032 sample keys verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message3, b"brisbane", b"football.example.com", self.rfckey, canonicalize=(header_algo, body_algo), signature_algorithm=b'ed25519-sha256') res = dkim.verify(sig + self.message3, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_rfc8032_previous_verifies(self): # A message previously signed using RFC 8032 sample keys verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message3, b"brisbane", b"football.example.com", self.rfckey, canonicalize=(header_algo, body_algo), signature_algorithm=b'ed25519-sha256') d = dkim.DKIM(self.message4) res = d.verify(dnsfunc=self.dnsfunc) self.assertTrue(res) def test_simple_signature(self): # A message verifies after being signed with SHOULD headers for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.net", self.key, canonicalize=(header_algo, body_algo), include_headers=(b'from',) + dkim.DKIM.SHOULD, signature_algorithm=b'ed25519-sha256') res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_verify_third_party(self): # Message signed by prototype Exim implementation res = dkim.verify(self.message2, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_add_body_length(self): sig = dkim.sign( self.message, b"test", b"example.net", self.key, length=True, signature_algorithm=b'ed25519-sha256') msg = email.message_from_string(self.message.decode('utf-8')) self.assertIn('; l=%s' % len(msg.get_payload() + '\n'), sig.decode('utf-8')) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_altered_body_fails(self): # An altered body fails verification. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.net", self.key, signature_algorithm=b'ed25519-sha256') res = dkim.verify( sig + self.message + b"foo", dnsfunc=self.dnsfunc) self.assertFalse(res) def test_badly_encoded_domain_fails(self): # Domains should be ASCII. Bad ASCII causes verification to fail. sig = dkim.sign(self.message, b"test", b"example.net\xe9", self.key, signature_algorithm=b'ed25519-sha256') res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertFalse(res) def test_dkim_signature_canonicalization(self): # # Relaxed-mode header signing is wrong # # Simple-mode signature header verification is wrong # (should ignore FWS anywhere in signature tag: b=) sample_msg = b"""\ From: mbp@canonical.com To: scottk@example.net Subject: this is my test message """.replace(b'\n', b'\r\n') sample_privkey = b"""\ fL+5V9EquCZAovKik3pA6Lk9zwCzoEtjIuIqK9ZXHHA=\ """ sample_pubkey = """\ yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y=\ """ for header_mode in [dkim.Relaxed, dkim.Simple]: dkim_header = dkim.sign(sample_msg, b'example', b'canonical.com', sample_privkey, canonicalize=(header_mode, dkim.Relaxed), signature_algorithm=b'ed25519-sha256') # Folding dkim_header affects b= tag only, since dkim.sign folds # sig_value with empty b= before hashing, and then appends the # signature. So folding dkim_header again adds FWS to # the b= tag only. This should be ignored even with # simple canonicalization. # http://tools.ietf.org/html/rfc4871#section-3.5 signed = dkim.fold(dkim_header) + sample_msg result = dkim.verify(signed,dnsfunc=self.dnsfunc) self.assertTrue(result) dkim_header = dkim.fold(dkim_header) # use a tab for last fold to test tab in FWS bug pos = dkim_header.rindex(b'\r\n ') dkim_header = dkim_header[:pos]+b'\r\n\t'+dkim_header[pos+3:] result = dkim.verify(dkim_header + sample_msg, dnsfunc=self.dnsfunc) self.assertTrue(result) def test_extra_headers(self): # # extra headers above From caused failure #message = read_test_data("test_extra.message") message = read_test_data("message.mbox") for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): d = dkim.DKIM(message) # bug requires a repeated header to manifest d.should_not_sign.remove(b'received') sig = d.sign(b"test", b"example.net", self.key, signature_algorithm=b'ed25519-sha256', include_headers=d.all_sign_headers(), canonicalize=(header_algo, body_algo)) dv = dkim.DKIM(sig + message) res = dv.verify(dnsfunc=self.dnsfunc) self.assertEqual(d.include_headers,dv.include_headers) s = dkim.select_headers(d.headers,d.include_headers) sv = dkim.select_headers(dv.headers,dv.include_headers) self.assertEqual(s,sv) self.assertTrue(res) def test_multiple_from_fails(self): # # additional From header fields should cause verify failure hfrom = b'From: "Resident Evil" \r\n' h,b = self.message.split(b'\n\n',1) for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.net", self.key, signature_algorithm=b'ed25519-sha256') # adding an unknown header still verifies h1 = h+b'\r\n'+b'X-Foo: bar' message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertTrue(res) # adding extra from at end should not verify h1 = h+b'\r\n'+hfrom.strip() message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertFalse(res) # add extra from in front should not verify either h1 = hfrom+h message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertFalse(res) def test_no_from_fails(self): # Body From is mandatory to be in the message and mandatory to sign sigerror = False sig = '' message = read_test_data('test_nofrom.message') selector = 'test' domain = 'example.net' identity = None try: sig = dkim.sign(message, selector, domain, read_test_data('ed25519test.key'), identity = identity, signature_algorithm=b'ed25519-sha256') except dkim.ParameterError as x: sigerror = True self.assertTrue(sigerror) def test_validate_signature_fields(self): sig = {b'v': b'1', b'a': b'ed25519-sha256', b'b': b'K/UUOt8lCtgjp3kSTogqBm9lY1Yax/NwZ+bKm39/WKzo5KYe3L/6RoIA/0oiDX4kO\n \t Qut49HCV6ZUe6dY9V5qWBwLanRs1sCnObaOGMpFfs8tU4TWpDSVXaNZAqn15XVW0WH\n \t EzOzUfVuatpa1kF4voIgSbmZHR1vN3WpRtcTBe/I=', b'bh': b'n0HUwGCP28PkesXBPH82Kboy8LhNFWU9zUISIpAez7M=', b'c': b'simple/simple', b'd': b'kitterman.com', b'i': b'scott@Kitterman.com', b'h': b'From:To:Subject:Date:Cc:MIME-Version:Content-Type:\n \t Content-Transfer-Encoding:Message-Id', b's': b'2007-00', b't': b'1299525798'} dkim.validate_signature_fields(sig) # try new version sigVer = sig.copy() sigVer[b'v'] = 2 self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigVer) # try with x sigX = sig.copy() sigX[b'x'] = b'1399525798' dkim.validate_signature_fields(sig) # try with late t sigX[b't'] = b'1400000000' self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigX) # try without t now = int(time.time()) sigX[b'x'] = str(now+400000).encode('ascii') dkim.validate_signature_fields(sigX) # try when expired a day ago sigX[b'x'] = str(now - 24*3600).encode('ascii') self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigX) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/tests/test_dkim_generate.py0000644000175100017510000001057414423474310021606 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant # Copyright (c) 2022 Adrien Precigout import os.path import tempfile import unittest import dkim import dkim.dknewkey as dknewkey def read_data(path): """Get the content of the given test data file.""" with open(path, 'rb') as f: return f.read() class TestSignAndVerify(unittest.TestCase): """End-to-end signature and verification tests with a generated key.""" def setUp(self): message_dir = os.path.join(os.path.dirname(__file__), 'data', "test.message") self.message = read_data(message_dir) self.ed25519_dns_key_file = "" self.rsa_dns_key_file = "" def test_generate_verifies_new_RSA_key(self): #Create temporary dir tmpdir = tempfile.TemporaryDirectory() keydir = tmpdir.name rsa_key_file = os.path.join(keydir, "dkim.rsa.key") self.rsa_dns_key_file = os.path.join(keydir, "dkim.rsa.key.pub.txt") #Generate a rsa key dknewkey.GenRSAKeys(rsa_key_file, False) dknewkey.ExtractRSADnsPublicKey(rsa_key_file, self.rsa_dns_key_file, False) #Load the key rsakey = read_data(rsa_key_file) #Test signature with the newely generated key for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", rsakey, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfuncRSA) self.assertTrue(res) tmpdir.cleanup() def test_generate_verifies_Ed25519_key(self): #Create temporary dir tmpdir = tempfile.TemporaryDirectory() keydir = tmpdir.name ed25519_key_file = os.path.join(keydir, "dkim.ed25519.key") self.ed25519_dns_key_file = os.path.join(keydir, "dkim.ed25519.key.pub.txt") #Generate a ed25519 key pkt = dknewkey.GenEd25519Keys(ed25519_key_file, False) dknewkey.ExtractEd25519PublicKey(self.ed25519_dns_key_file, pkt, False) #Load the key ed25519key = read_data(ed25519_key_file) #Test signature with the newely generated key for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test1", b"example.com", ed25519key, signature_algorithm=b'ed25519-sha256', canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfuncED25519) self.assertTrue(res) tmpdir.cleanup() def dnsfuncRSA(self, domain, timeout=5): _dns_responses = { 'test._domainkey.example.com.': read_data(self.rsa_dns_key_file), } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfuncED25519(self, domain, timeout=5): _dns_responses = { 'test1._domainkey.example.com.': read_data(self.ed25519_dns_key_file), } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/dkim/tests/test_dkim_rsavariants.py0000644000175100017510000001147314423474310022350 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant import email import os.path import unittest import time import dkim def read_test_data(filename): """Get the content of the given test data file. The files live in dkim/tests/data. """ path = os.path.join(os.path.dirname(__file__), 'data', filename) with open(path, 'rb') as f: return f.read() class TestSignAndVerify(unittest.TestCase): """End-to-end signature and verification tests.""" def setUp(self): self.message = read_test_data("test.message") self.key1024 = read_test_data("1024_testkey.key") self.key2048 = read_test_data("2048_testkey.key") self.key2048PKCS8 = read_test_data("2048_testkey_PKCS8.key") def dnsfunc(self, domain, timeout=5): _dns_responses = { 'test._domainkey.example.com.': read_test_data("1024_testkey_wo_markers.pub.txt"), 'test2._domainkey.example.com.': read_test_data("1024_testkey_wo_markers.pub.rsa.txt"), 'test3._domainkey.example.com.': read_test_data("2048_testkey_wo_markers.pub.txt"), 'test4._domainkey.example.com.': read_test_data("2048_testkey_wo_markers.pub.rsa.txt"), 'test5._domainkey.example.com.': read_test_data("2048_testkey_PKCS8.key.pub.txt") } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def test_verifies_SubjectPublicKeyInfo1024(self): # A message verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key1024, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_verifies_RSAPublicKey1024(self): # A message verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test2", b"example.com", self.key1024, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_verifiesSubjectPublicKeyInfo2048(self): # A message verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test3", b"example.com", self.key2048, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_verifiesRSAPublicKey2048(self): # A message verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test4", b"example.com", self.key2048, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_verifies_RSAPublicKey2048PKCS8(self): #A message verifies after being signed (with PKCS8 private key) for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test5", b"example.com", self.key2048PKCS8, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/test_dkim_tlsrpt.py0000644000175100017510000002731413642527701021351 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant import email import os.path import unittest import time import dkim def read_test_data(filename): """Get the content of the given test data file. The files live in dkim/tests/data. """ path = os.path.join(os.path.dirname(__file__), 'data', filename) with open(path, 'rb') as f: return f.read() class TestFold(unittest.TestCase): def test_short_line(self): self.assertEqual( b"foo", dkim.fold(b"foo")) def test_long_line(self): # The function is terribly broken, not passing even this simple # test. self.assertEqual( b"foo" * 24 + b"\r\n foo", dkim.fold(b"foo" * 25)) def test_linesep(self): self.assertEqual( b"foo" * 24 + b"\n foo", dkim.fold(b"foo" * 25, linesep=b"\n")) class TestSignAndVerify(unittest.TestCase): """End-to-end signature and verification tests.""" def setUp(self): self.message = read_test_data("test.message") self.message3 = read_test_data("rfc6376.msg") self.message4 = read_test_data("rfc6376.signed.msg") self.key = read_test_data("test.private") self.rfckey = read_test_data("rfc8032_7_1.key") def dnsfunc(self, domain, timeout=5): sample_dns = """\ k=rsa; s=email;\ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("test.txt"), '20120113._domainkey.gmail.com.': """k=rsa; \ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc2(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("test2.txt"), '20120113._domainkey.gmail.com.': """\ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc3(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("badversion.txt"), '20120113._domainkey.gmail.com.': """\ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc4(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("badk.txt"), '20120113._domainkey.gmail.com.': """\ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc5(self, domain, timeout=5): sample_dns = """\ k=rsa; \ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.football.example.com.': read_test_data("test.txt"), 'brisbane._domainkey.football.example.com.': """v=DKIM1; k=ed25519; \ p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def dnsfunc6(self, domain, timeout=5): sample_dns = """\ k=rsa; s=email;\ p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" _dns_responses = { 'example._domainkey.canonical.com.': sample_dns, 'test._domainkey.example.com.': read_test_data("test_tlsrpt.txt"), '20120113._domainkey.gmail.com.': """k=rsa; \ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" } try: domain = domain.decode('ascii') except UnicodeDecodeError: return None self.assertTrue(domain in _dns_responses,domain) return _dns_responses[domain] def test_ignores_tlsrptsvc(self): # A non-tlsrpt signed with a key record with s=tlsrpt shouldn't verify. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc6) self.assertFalse(res) def test_tlsrpt_with_strict_tlsrptsvc(self): # A tlsrpt signed with a key record with s=tlsrpt should verify with tlsrpt='strict'. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc6, tlsrpt='strict') self.assertTrue(res) def test_tlsrpt_with_tlsrptsvc(self): # A tlsrpt signed with a key record with s=tlsrpt should verify. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc6, tlsrpt=True) self.assertTrue(res) def test_tlsrpt_with_strict_no_tlsrptsvc(self): # A tlsrpt signed with a key record without s=tlsrpt not tlsrpt='strict' should not verify. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc, tlsrpt='strict') self.assertFalse(res) def test_tlsrpt_with_no_tlsrptsvc(self): # A tlsrpt signed with a key record without s=tlsrpt and tlsrpt=True should verify. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc, tlsrpt=True) self.assertTrue(res) def test_tlsrpt_ignore_l_verify(self): # For a tlsrpt, ignore l= on verify for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), length=True) self.message += b'added more text' res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc, tlsrpt=True) self.assertFalse(res) def test_tlsrpt_ignore_l_sign(self): # For a tlsrpt, don't add l= when signing tlsrpt for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), length=True, tlsrpt=True) self.message += b'added more text' res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertFalse(res) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586147265.0 dkimpy-1.1.6/dkim/tests/test_dnsplug.py0000644000175100017510000000302513642527701020462 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2017 Valimail Inc # Contact: Gene Shuman # Copyright (c) 2019 Scott Kitterman import unittest import sys try: import dkim.dnsplug except ImportError: # Need to not error out so we can test aiodns properly pass class TestDNSPlug(unittest.TestCase): def test_get_txt(self): try: dkim.dnsplug._get_txt = {"in": "out"}.get res = dkim.dnsplug.get_txt(b"in") except NameError: # We may be testing aiodns, so we don't care res = b"out" self.assertEqual(res, b"out") def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/tests/test_util.py0000644000175100017510000000655013642525265017774 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant import unittest from dkim.util import ( DuplicateTag, InvalidTagSpec, parse_tag_value, get_linesep, ) class TestParseTagValue(unittest.TestCase): """Tag=Value parsing tests.""" def test_single(self): self.assertEqual( {b'foo': b'bar'}, parse_tag_value(b'foo=bar')) def test_trailing_separator_ignored(self): self.assertEqual( {b'foo': b'bar'}, parse_tag_value(b'foo=bar;')) def test_multiple(self): self.assertEqual( {b'foo': b'bar', b'baz': b'foo'}, parse_tag_value(b'foo=bar;baz=foo')) def test_value_with_equals(self): self.assertEqual( {b'foo': b'bar', b'baz': b'foo=bar'}, parse_tag_value(b'foo=bar;baz=foo=bar')) def test_whitespace_is_stripped(self): self.assertEqual( {b'foo': b'bar', b'baz': b'f oo=bar'}, parse_tag_value(b' foo \t= bar;\tbaz= f oo=bar ')) def test_missing_value_is_an_error(self): self.assertRaises( InvalidTagSpec, parse_tag_value, b'foo=bar;baz') def test_duplicate_tag_is_an_error(self): self.assertRaises( DuplicateTag, parse_tag_value, b'foo=bar;foo=baz') def test_trailing_whitespace(self): hval = b'''v=1; a=rsa-sha256; d=facebookmail.com; s=s1024-2011-q2; c=relaxed/simple; q=dns/txt; i=@facebookmail.com; t=1308078492; h=From:Subject:Date:To:MIME-Version:Content-Type; bh=+qPyCOiDQkusTPstCoGjimgDgeZbUaJWIr1mdE6RFxk=; b=EUmDmdnAsNtjSEHGHNTa8PXgGaEUtOVezagmninX5Bs/Q26R9r3AMgawyUSKkbHp /bQZU6QPZfdvmLMPdIWCQPo8SP+gsz4dpox2efO61DlvgYaxBRhwFedAW9LjYhQc 3KzW0yB9JHwiDCw1EioVkv+OMHhAYzoIypA0bQyi2bc=; ''' sig = parse_tag_value(hval) self.assertEqual(sig[b't'],b'1308078492') self.assertEqual(len(sig),11) class TestGetLineSep(unittest.TestCase): """Line seperator probing tests.""" def test_default(self): self.assertEqual( b'\r\n', get_linesep(b'abc')) def test_withcrlf(self): self.assertEqual( b'\r\n', get_linesep(b'abc\r\n')) def test_withlf(self): self.assertEqual( b'\n', get_linesep(b'abc\n')) def test_toosmall(self): self.assertEqual( b'\r\n', get_linesep(b'a')) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/dkim/util.py0000644000175100017510000000470513642525265015573 0ustar00kittermakitterma# This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2011 William Grant import re import logging try: from logging import NullHandler except ImportError: class NullHandler(logging.Handler): def emit(self, record): pass __all__ = [ 'DuplicateTag', 'get_default_logger', 'InvalidTagSpec', 'InvalidTagValueList', 'parse_tag_value', 'get_linesep', ] class InvalidTagValueList(Exception): pass class DuplicateTag(InvalidTagValueList): pass class InvalidTagSpec(InvalidTagValueList): pass def parse_tag_value(tag_list): """Parse a DKIM Tag=Value list. Interprets the syntax specified by RFC6376 section 3.2. Assumes that folding whitespace is already unfolded. @param tag_list: A bytes string containing a DKIM Tag=Value list. """ tags = {} tag_specs = tag_list.strip().split(b';') # Trailing semicolons are valid. if not tag_specs[-1]: tag_specs.pop() for tag_spec in tag_specs: try: key, value = [x.strip() for x in tag_spec.split(b'=', 1)] except ValueError: raise InvalidTagSpec(tag_spec) if re.match(br'^[a-zA-Z](\w)*', key) is None: raise InvalidTagSpec(tag_spec) if key in tags: raise DuplicateTag(key) tags[key] = value return tags def get_default_logger(): """Get the default dkimpy logger.""" logger = logging.getLogger('dkimpy') if not logger.handlers: logger.addHandler(NullHandler()) return logger def get_linesep(msg): if msg[-2:] != b'\r\n' and msg[-1:] == b'\n': return b'\n' return b'\r\n' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1713123138.070522 dkimpy-1.1.6/dkimpy.egg-info/0000755000175100017510000000000014607027502016272 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713123137.0 dkimpy-1.1.6/dkimpy.egg-info/PKG-INFO0000644000175100017510000002170014607027501017366 0ustar00kittermakittermaMetadata-Version: 2.1 Name: dkimpy Version: 1.1.6 Summary: DKIM (DomainKeys Identified Mail), ARC (Authenticated Receive Chain), and TLSRPT (TLS Report) email signing and verification Home-page: https://launchpad.net/dkimpy Author: Scott Kitterman Author-email: scott@kitterman.com License: BSD-like Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: Developers Classifier: License :: DFSG approved Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Communications :: Email :: Mail Transport Agents Classifier: Topic :: Communications :: Email :: Filters Classifier: Topic :: Internet :: Name Service (DNS) Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/markdown Provides-Extra: ARC Provides-Extra: asyncio Provides-Extra: ed25519 Provides-Extra: testing License-File: LICENSE dkimpy - DKIM (DomainKeys Identified Mail) https://launchpad.net/dkimpy/ Friendly fork of: http://hewgill.com/pydkim/ # INTRODUCTION dkimpy is a library that implements DKIM (DomainKeys Identified Mail) email signing and verification. Basic DKIM requirements are defined in RFC 6376: https://tools.ietf.org/html/rfc6376 # VERSION This is dkimpy 1.1.6. # REQUIREMENTS Dependencies will be automatically included for normal DKIM usage. The extras_requires feature 'ed25519' will add the dependencies needed for signing and verifying using the new DCRUP ed25519-sha256 algorithm. The extras_requires feature 'ARC' will add the extra dependencies needed for ARC. Similarly, extras_requires feature 'asyncio' will add the extra dependencies needed for asyncio. - Python 3.x >= 3.5. Recent versions have not been on python3 < 3.4, but may still work on earlier python3 versions. - dnspython or py3dns. dnspython is preferred if both are present and installed to satisfy the DNS module requirement if neither are installed. - authres. Needed for ARC. - PyNaCl. Needed for use of ed25519 capability. - aiodns. Needed for asycnio (Requires python3.5 or later) # INSTALLATION This package includes a scripts 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: ```python3 setup.py install --single-version-externally-managed --record=/dev/null``` # DOCUMENTATION An online version of the package documentation for the most recent release can be found at: https://pymilter.org/pydkim/ # TESTING To run dkimpy's test suite: ```PYTHONPATH=. python3 dkim``` or ```python3 test.py``` or ```PYTHONPATH=. python3 -m unittest dkim.tests.test_suite``` Alternatively, if you have testrepository installed: ```testr init``` ```testr run``` You should install all optional dependencies required for the test suite, e.g. by creating a virtualenv and using: ```pip install -e '.[testing]'``` The included ARC tests are very limited. The primary testing method for ARC is using the ARC test suite: https://github.com/ValiMail/arc_test_suite As of 0.6.0, all tests pass for both python2.7 and python3. The test suite ships with test runners for dkimpy. After downloading the test suite, you can run the signing and validation tests like this: ```python3 ./testarc.py sign runners/arcsigntest.py``` ```python3 ./testarc.py validate runners/arcverifytest.py``` As ov version 1.1.0, python2.7 is no longer supported. # USAGE The dkimpy library offers one module called dkim. The sign() function takes an RFC822 formatted message, along with some signing options, and returns a DKIM-Signature header line that can be prepended to the message. The verify() function takes an RFC822 formatted message, and returns True or False depending on whether the signature verifies correctly. There is also a DKIM class which can be used to perform these functions in a more modern way. In version 0.9.0, the default set of header fields that are oversigned was changed from 'from', 'subject', 'date' to 'from' to reduce fragility of signatures. To restore the previous behavior, you can add them back after instantiating your DKIM class using the add_frozen function as shown in the following example: ```python >>> dkim = DKIM() >>> dkim.add_frozen((b'date',b'subject')) >>> [text(x) for x in sorted(dkim.frozen_sign)] ['date', 'from', 'subject'] ``` ## DKIM RSA MODERNIZATION (RFC 8301) RFC8301 updated DKIM requirements in two ways: 1. It set the minimum valid RSA key size to 1024 bits. 2. It removed use of rsa-sha1. As of version 0.7, the dkimpy defaults largely support these requirements. It is possible to override the minimum key size to a lower value, but this is strongly discouraged. As of 2018, keys much smaller than the minimum are not difficult to factor. The code for rsa-sha1 signing and verification is retained, but not used for signing by default. Future releases will raise warnings and then errors when verifying rsa-sha1 signatures. There are still some significant users of rsa-sha1 signatures, so operationally it's premature to disable verification of rsa-sha1. ## ED25519 (RFC 8463) SUPPORT As of version 0.7, experimental signing and verifying of DKIM Ed25519 signatures is supported as described in draft-ietf-dcrup-dkim-crypto: https://datatracker.ietf.org/doc/draft-ietf-dcrup-dkim-crypto/ The RFC that documents ed25519 DKIM signatures, RFC 8463, has been released and dkimpy 0.7 and later are aligned to its requirements. As of 0.8, ed25519 need not be considered experimental. The dkimpy implementation has successfully interoperated with three other implementations and the technical parameters for ed25519-sha256 are defined and stable. To install from pypi with the required optional depenencies, use the ed25519 option: ```pip install -e '.[ed25519]'``` ## DKIM SCRIPTS Three helper programs are also supplied: dknewkey, dkimsign and dkimverify dknewkey is s script that produces private and public key pairs suitable for use with DKIM. Note that the private key file format used for ed25519 is not standardized (there is no standard) and is unique to dkimpy. Creation of keys should be done in a secure environment. If an unauthorized entity gains access to current private keys they can generate signed email that will pass DKIM checkes and will be difficult to repudiate. dkimsign is a filter that reads an RFC822 message on standard input, and writes the same message on standard output with a DKIM-Signature line prepended. The signing options are specified on the command line: dkimsign selector domain privatekeyfile [identity] The identity is optional and defaults to "@domain". dkimverify reads an RFC822 message on standard input, and returns with exit code 0 if the signature verifies successfully. Otherwise, it returns with exit code 1. ## ARC (Authenticated Receive Chain) As of version 0.6.0, dkimpy provides experimental support for ARC (Authenticated Received Chain). See RFC 8617 for the current version of ARC: https://tools.ietf.org/html/rfc8617 In addition to arcsign and arcverify, the dkim module now provides arc_sign and arc_verify functions as well as an ARC class. If an invalid authentication results header field is included in the set for ARC, it is ignored and no error is raised. Both DKIM ed25519 and ARC are now considered stable (no longer experimantal). ## ASYNC SUPPORT As of version 1.0, an alternative to dkim.verify for use in an async environment is provied. It requires aiodns, https://pypi.org/project/aiodns/. Here is a simple example of dkim.verify_async usage: ```python >>> sys.stdin = sys.stdin.detach() >>> >>> async def main(): >>> res = await dkim.verify_async(message) >>> return res >>> >>> if __name__ == "__main__": >>> res = asyncio.run(main()) ``` This feature requires python3.5 or newer. If aiodns is available, the async functions will be used. To avoide async when aiodns is availale, set dkim.USE_ASYNC = False. ## TLSRPT (TLS Report) As of version 1.0, the RFC 8460 tlsrpt service type is supported: https://tools.ietf.org/html/rfc8460 A non-tlsrpt signed with a key record with s=tlsrpt won't verify. Since the service type (s=) is optional in the DKIM public key record, it is not required by RFC 8460. When checking for a tlsrpt signature, set the tlsrpt= flag when verifying the signature: ```python >>> res = dkim.verify(smessage, tlsrpt='strict') ``` If tlsrpt='strict', only public key records with s=tlsrpt will be considered valid. If set to tlsrpt=True, the service type is not required, but other RFC 8460 requirements are applied. # LIMITATIONS Dkimpy will correctly sign/verify messages with ASCII or UTF-8 content. Messages that contain other types of content will not verify correctly. It does not yet implement RFC 8616, Email Authentication for Internationalized Mail. # FEEDBACK Bug reports may be submitted to the bug tracker for the dkimpy project on launchpad. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713123138.0 dkimpy-1.1.6/dkimpy.egg-info/SOURCES.txt0000644000175100017510000000370214607027502020160 0ustar00kittermakittermaChangeLog LICENSE MANIFEST.in README.md setup.py test.py dkim/__init__.py dkim/__main__.py dkim/arcsign.py dkim/arcverify.py dkim/asn1.py dkim/asyncsupport.py dkim/canonicalization.py dkim/crypto.py dkim/dkimsign.py dkim/dkimverify.py dkim/dknewkey.py dkim/dnsplug.py dkim/util.py dkim/tests/__init__.py dkim/tests/test_arc.py dkim/tests/test_canonicalization.py dkim/tests/test_crypto.py dkim/tests/test_dkim.py dkim/tests/test_dkim_ed25519.py dkim/tests/test_dkim_generate.py dkim/tests/test_dkim_rsavariants.py dkim/tests/test_dkim_tlsrpt.py dkim/tests/test_dnsplug.py dkim/tests/test_util.py dkim/tests/data/1024_testkey.key dkim/tests/data/1024_testkey_wo_markers.pub.rsa.txt dkim/tests/data/1024_testkey_wo_markers.pub.txt dkim/tests/data/2048_testkey.key dkim/tests/data/2048_testkey_PKCS8.key dkim/tests/data/2048_testkey_PKCS8.key.pub.txt dkim/tests/data/2048_testkey_wo_markers.pub.rsa.txt dkim/tests/data/2048_testkey_wo_markers.pub.txt dkim/tests/data/badk.txt dkim/tests/data/badversion.txt dkim/tests/data/ed25519test.dns dkim/tests/data/ed25519test.key dkim/tests/data/ed25519test.msg dkim/tests/data/ed25519test2.msg dkim/tests/data/eximtest.dns dkim/tests/data/message.mbox dkim/tests/data/rfc6376.msg dkim/tests/data/rfc6376.signed.msg dkim/tests/data/rfc6376.signed.rsa.msg dkim/tests/data/rfc6376.w1258.msg dkim/tests/data/rfc8032_7_1.key dkim/tests/data/test.message dkim/tests/data/test.message.baddomain dkim/tests/data/test.private dkim/tests/data/test.txt dkim/tests/data/test2.message dkim/tests/data/test2.private dkim/tests/data/test2.txt dkim/tests/data/test_bad.txt dkim/tests/data/test_extra.message dkim/tests/data/test_nofrom.message dkim/tests/data/test_tlsrpt.txt dkimpy.egg-info/PKG-INFO dkimpy.egg-info/SOURCES.txt dkimpy.egg-info/dependency_links.txt dkimpy.egg-info/entry_points.txt dkimpy.egg-info/not-zip-safe dkimpy.egg-info/requires.txt dkimpy.egg-info/top_level.txt man/arcsign.1 man/arcverify.1 man/dkimsign.1 man/dkimverify.1 man/dknewkey.1././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713123137.0 dkimpy-1.1.6/dkimpy.egg-info/dependency_links.txt0000644000175100017510000000000114607027501022337 0ustar00kittermakitterma ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713123137.0 dkimpy-1.1.6/dkimpy.egg-info/entry_points.txt0000644000175100017510000000025414607027501021570 0ustar00kittermakitterma[console_scripts] arcsign = dkim.arcsign:main arcverify = dkim.arcverify:main dkimsign = dkim.dkimsign:main dkimverify = dkim.dkimverify:main dknewkey = dkim.dknewkey:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586175270.0 dkimpy-1.1.6/dkimpy.egg-info/not-zip-safe0000644000175100017510000000000113642616446020531 0ustar00kittermakitterma ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713123137.0 dkimpy-1.1.6/dkimpy.egg-info/requires.txt0000644000175100017510000000012414607027501020666 0ustar00kittermakittermaPy3DNS [ARC] authres [asyncio] aiodns [ed25519] pynacl [testing] authres pynacl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713123137.0 dkimpy-1.1.6/dkimpy.egg-info/top_level.txt0000644000175100017510000000000514607027501021016 0ustar00kittermakittermadkim ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1713123138.070522 dkimpy-1.1.6/man/0000755000175100017510000000000014607027502014056 5ustar00kittermakitterma././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/man/arcsign.10000644000175100017510000001045013642525265015575 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 arcsign 1 "2019-04-14" .SH "NAME" arcsign \- Script for ARC signing messages on stdin .SH "VERSION" 0\.9\.2 .SH "DESCRIPTION" arcsign is a filter that reads an RFC822 message on standard input, and writes the same message on standard output with a ARC-Signature lines prepended. .SH "USAGE" The signing options are specified on the command line: arcsign selector domain privatekeyfile srv_id Note: arcsign will only use authentication results header fields with an authserv-id that matches srv_id as the input fields for the ARC chain. .SH "AUTHORS" This version of \fBarcsign\fR was written by Brandon Long and is derived from \fBdkimsign\fR, written by Greg Hewgill . .PP This man-page was created by Scott Kitterman and is licensed under the same terms as dkimpy. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/man/arcverify.10000644000175100017510000001007013642525265016137 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 dkimverify 1 "2017-01-23" .SH "NAME" dkimverify \- Script for DKIM verifying messages on stdin .SH "VERSION" 0\.6\.0 .SH "DESCRIPTION" dkimverify reads an RFC822 message on standard input, and returns with exit code 0 if the signature verifies successfully. Otherwise, it returns with exit code 1. .SH "AUTHORS" This version of \fBarcverify\fR was written by Brandon Long and is derived from \fBdkimverify\fR, written by Greg Hewgill . .PP This man-page was created by Scott Kitterman and is licensed under the same terms as dkimpy. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/man/dkimsign.10000644000175100017510000001153513642525265015761 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 dkimsign 1 "2018-02-05" .SH "NAME" dkimsign \- Script for DKIM signing messages on stdin .SH "VERSION" 0\.7\.0 .SH "DESCRIPTION" dkimsign is a filter that reads an RFC822 message on standard input, and writes the same message on standard output with a DKIM-Signature line prepended. .SH "USAGE" usage: dkimsign.py [\-h] [\-\-hcanon {simple,relaxed}] [\-\-bcanon {simple,relaxed}] [\-\-signalg {rsa\-sha256,ed25519\-sha256,rsa\-sha1}] [\-\-identity IDENTITY] selector domain privatekeyfile mandatory positional arguments: selector domain privatekeyfile optional arguments: \-h, \-\-help show this help message and exit \-\-hcanon {simple,relaxed} Header canonicalization algorithm: default=relaxed \-\-bcanon {simple,relaxed} Body canonicalization algorithm: default=simple \-\-signalg {rsa\-sha256,ed25519\-sha256,rsa\-sha1} Signature algorithm: default=rsa\-sha256 \-\-identity IDENTITY Optional value for i= tag. .SH "AUTHORS" The original version of \fBdkimsign\fR was written by Greg Hewgill . It has been substantially rewritten by Scott Kitterman . .PP This man-page was created by Scott Kitterman and is licensed under the same terms as dkimpy. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682864328.0 dkimpy-1.1.6/man/dkimverify.10000644000175100017510000001026114423474310016310 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 dkimverify 1 "2017-01-23" .SH "NAME" dkimverify \- Script for DKIM verifying messages on stdin .SH "VERSION" 1\.1\.0 .SH "DESCRIPTION" dkimverify reads an RFC822 message on standard input, and returns with exit code 0 if the signature verifies successfully. Otherwise, it returns with exit code 1. .SH "USAGE" usage: dkimverify.py [\-h] [\-\-index N] . .PP This man-page was created by Scott Kitterman and is licensed under the same terms as dkimpy. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/man/dknewkey.10000644000175100017510000001147313642525265015776 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 dknewkey 1 "2018-02-05" .SH "NAME" dknewkey \- Generates new DKIM public/private key pairs .SH "VERSION" 0\.8\.0 .SH "DESCRIPTION" dknewykey generates new DKIM keys. For RSA keys, it defaults to 2048 bit key size. This is controlled by the BITS_REQUIRED variable. ed25519 keys do not have a variable size. For RSA keys, it uses openssl to do the generation. By default it assumes this is located at /usr/bin/openssl. This is controlled by the OPENSSL_BINARY variable. For ed25519 keys, PyNaCl (python-nacl in Debian and derivatives) is used. For RSA keys k=sha256 is now included in the public DNS record to prevent inadvertent use with the now obsolete sha1 hash algorithm (See RFC 8301). .SH "USAGE" dknewkey.py [\-h] [\-\-ktype {rsa,ed25519}] key_name mandatory positional arguments: key_name optional arguments: \-h, \-\-help show this help message and exit \-\-ktype {rsa,ed25519} DKIM key type: Default is rsa NOTE: Depending on the packaging and distribution, the exact path and name for the executable may vary. .SH "AUTHORS" This version of \fBdknewkey\fR was written by Brandon Long . It has been substantially rewritten by Scott Kitterman . .PP This man-page was created by Scott Kitterman and is licensed under the same terms as dkimpy. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713123138.0745223 dkimpy-1.1.6/setup.cfg0000644000175100017510000000004614607027502015124 0ustar00kittermakitterma[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713122820.0 dkimpy-1.1.6/setup.py0000644000175100017510000000647214607027004015023 0ustar00kittermakitterma#!/usr/bin/env python # This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011,2012,2018 Scott Kitterman from setuptools import setup import os import sys version = "1.1.6" kw = {} # Work-around for lack of 'or' requires in setuptools. try: import DNS kw['install_requires'] = ['Py3DNS'] except ImportError: # If PyDNS is not installed, prefer dnspython kw['install_requires'] = ['dnspython>=2.0.0'] with open("README.md", "r") as fh: long_description = fh.read() setup( name = "dkimpy", version = version, description = "DKIM (DomainKeys Identified Mail), ARC (Authenticated Receive Chain), and TLSRPT (TLS Report) email signing and verification", long_description=long_description, long_description_content_type='text/markdown', author = "Scott Kitterman", author_email = "scott@kitterman.com", url = "https://launchpad.net/dkimpy", license = "BSD-like", packages = ["dkim"], entry_points = { 'console_scripts' : [ 'arcsign = dkim.arcsign:main', 'arcverify = dkim.arcverify:main', 'dkimsign = dkim.dkimsign:main', 'dkimverify = dkim.dkimverify:main', 'dknewkey = dkim.dknewkey:main' ], }, data_files = [(os.path.join('share', 'man', 'man1'), ['man/arcsign.1']), (os.path.join('share', 'man', 'man1'), ['man/arcverify.1']),(os.path.join('share', 'man', 'man1'), ['man/dkimsign.1']), (os.path.join('share', 'man', 'man1'), ['man/dkimverify.1']),(os.path.join('share', 'man', 'man1'), ['man/dknewkey.1']),], classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: Developers', 'License :: DFSG approved', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', 'Topic :: Communications :: Email :: Mail Transport Agents', 'Topic :: Communications :: Email :: Filters', 'Topic :: Internet :: Name Service (DNS)', 'Topic :: Software Development :: Libraries :: Python Modules' ], zip_safe = False, extras_require={ 'testing': [ 'authres', 'pynacl', ], 'ed25519': ['pynacl'], 'ARC': ['authres'], 'asyncio': ['aiodns'] }, **kw ) if os.name != 'posix': data_files = '' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1586145973.0 dkimpy-1.1.6/test.py0000644000175100017510000000041413642525265014642 0ustar00kittermakittermaimport unittest import doctest import dkim from dkim.tests import test_suite from dkim.tests.test_arc import test_suite as arc_test_suite import logging doctest.testmod(dkim) unittest.TextTestRunner().run(test_suite()) unittest.TextTestRunner().run(arc_test_suite())