dkimpy-0.7.1/0000775000175000017500000000000013242075365014507 5ustar kittermakitterma00000000000000dkimpy-0.7.1/setup.py0000664000175000017500000000517213242074274016224 0ustar kittermakitterma00000000000000#!/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 Scott Kitterman from distutils.core import setup import os version = "0.7.1" setup( name = "dkimpy", version = version, description = "DKIM (DomainKeys Identified Mail)", long_description = """dkimpy is a Python library that implements DKIM (DomainKeys Identified Mail) and ARC (Authenticated Received Chain) email signing and verification.""", author = "Scott Kitterman", author_email = "scott@kitterman.com", url = "https://launchpad.net/dkimpy", license = "BSD-like", packages = ["dkim"], scripts = ["arcsign.py", "arcverify.py", "dknewkey.py", "dkimsign.py", "dkimverify.py"], 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', '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' ] ) if os.name != 'posix': data_files = '' dkimpy-0.7.1/dnsplug.py0000664000175000017500000001365512155231624016541 0ustar kittermakitterma00000000000000## @package dnsplug # Provide a higher level interface to pydns or dnspython (or other provider). # NOT RELEASED: this is a proposed API and implementation. # Goals - work with both pydns and dnspython (and possibly other libraries) # at a simplied level. # TODO: # 1. map exceptions to common dnsplug.DNSError exception (with # original exception saved as a member). # 2. include dict based implementation (handy for test suites) # 3. move implementations to subpackages to enable autoselect on first call. ## Maximum number of CNAME records to follow MAX_CNAME = 10 ## Lookup DNS records by label and RR type. # The response can include records of other types that the DNS # server thinks we might need. FIXME: empty result # could mean NXDOMAIN or NOANSWER. # @param name the DNS label to lookup # @param qtype the name of the DNS RR type to lookup # @param tcpfallback if False, raise exception instead of TCP fallback # @return a list of ((name,type),data) tuples def DNSLookup(name, qtype, tcpfallback=True, timeout=30): raise NotImplementedError('No supported dns library found') class Session(object): """A Session object has a simple cache with no TTL that is valid for a single "session", for example an SMTP conversation.""" def __init__(self): self.cache = {} ## Additional DNS RRs we can safely cache. # We have to be careful which additional DNS RRs we cache. For # instance, PTR records are controlled by the connecting IP, and they # could poison our local cache with bogus A and MX records. # Each entry is a tuple of (query_type,rr_type). So for instance, # the entry ('MX','A') says it is safe (for milter purposes) to cache # any 'A' RRs found in an 'MX' query. SAFE2CACHE = frozenset(( ('MX','MX'), ('MX','A'), ('CNAME','CNAME'), ('CNAME','A'), ('A','A'), ('AAAA','AAAA'), ('PTR','PTR'), ('NS','NS'), ('NS','A'), ('TXT','TXT'), ('SPF','SPF') )) ## Cached DNS lookup. # @param name the DNS label to query # @param qtype the query type, e.g. 'A' # @param cnames tracks CNAMES already followed in recursive calls def dns(self, name, qtype, cnames=None): """DNS query. If the result is in cache, return that. Otherwise pull the result from DNS, and cache ALL answers, so additional info is available for further queries later. CNAMEs are followed. If there is no data, [] is returned. pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF'] post: isinstance(__return__, types.ListType) """ result = self.cache.get( (name, qtype) ) cname = None if not result: safe2cache = Session.SAFE2CACHE for k, v in DNSLookup(name, qtype): if k == (name, 'CNAME'): cname = v if (qtype,k[1]) in safe2cache: self.cache.setdefault(k, []).append(v) result = self.cache.get( (name, qtype), []) if not result and cname: if not cnames: cnames = {} elif len(cnames) >= MAX_CNAME: #return result # if too many == NX_DOMAIN raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME) cnames[name] = cname if cname in cnames: raise DNSError, 'CNAME loop' result = self.dns(cname, qtype, cnames=cnames) return result def DNSLookup_pydns(name, qtype, tcpfallback=True, timeout=30): try: # FIXME: To be thread safe, we create a fresh DnsRequest with # each call. It would be more efficient to reuse # a req object stored in a Session. req = DNS.DnsRequest(name, qtype=qtype, timeout=timeout) resp = req.req() #resp.show() # key k: ('wayforward.net', 'A'), value v # FIXME: pydns returns AAAA RR as 16 byte binary string, but # A RR as dotted quad. For consistency, this driver should # return both as binary string. # if resp.header['tc'] == True: if not tcpfallback: raise DNS.DNSError, 'DNS: Truncated UDP Reply, SPF records should fit in a UDP packet' try: req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp', timeout=timeout) resp = req.req() except DNS.DNSError, x: raise DNS.DNSError, 'TCP Fallback error: ' + str(x) return [((a['name'], a['typename']), a['data']) for a in resp.answers] except IOError, x: raise DNS.DNSError, 'DNS: ' + str(x) def DNSLookup_dnspython(name,qtype,tcpfallback=True,timeout=30): retVal = [] try: # FIXME: how to disable TCP fallback in dnspython if not tcpfallback? answers = dns.resolver.query(name, qtype) for rdata in answers: if qtype == 'A' or qtype == 'AAAA': retVal.append(((name, qtype), rdata.address)) elif qtype == 'MX': retVal.append(((name, qtype), (rdata.preference, rdata.exchange))) elif qtype == 'PTR': retVal.append(((name, qtype), rdata.target.to_text(True))) elif qtype == 'TXT' or qtype == 'SPF': retVal.append(((name, qtype), rdata.strings)) except dns.resolver.NoAnswer: pass except dns.resolver.NXDOMAIN: pass return retVal try: # prefer dnspython (the more complete library) import dns import dns.resolver # http://www.dnspython.org import dns.exception if not hasattr(dns.rdatatype,'SPF'): # patch in type99 support dns.rdatatype.SPF = 99 dns.rdatatype._by_text['SPF'] = dns.rdatatype.SPF DNSLookup = DNSLookup_dnspython except: import DNS # http://pydns.sourceforge.net if not hasattr(DNS.Type, 'SPF'): # patch in type99 support DNS.Type.SPF = 99 DNS.Type.typemap[99] = 'SPF' DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata # Fails on Mac OS X? Add domain to /etc/resolv.conf DNS.DiscoverNameServers() DNSLookup = DNSLookup_pydns if __name__ == '__main__': import sys s = Session() for n,t in zip(*[iter(sys.argv[1:])]*2): print n,t print s.dns(n,t) dkimpy-0.7.1/README0000664000175000017500000001150013242074331015354 0ustar kittermakitterma00000000000000dkimpy - 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. VERSION This is dkimpy 0.7.1. REQUIREMENTS - Python 2.x >= 2.7, or Python 3.x >= 3.4. Recent versions have not been tested on python < 2.7 or python3 < 3.4, but may still work on python2.6 and python 3.1 - 3.3. - dnspython or pydns. dnspython is preferred if both are present. - argparse. Standard library in python2.7 and later. - authres. Needed for ARC. - nacl. Needed for use of experimental ed25519 capability. INSTALLATION To build and install dkimpy: python setup.py install DOCUMENTATION An online version of the package documentation can be found at: https://gathman.org/pydkim/ TESTING To run dkimpy's test suite: PYTHONPATH=. python dkim or python test.py or PYTHONPATH=. python -m unittest dkim.tests.test_suite Alternatively, if you have testrepository installed: testr init testr run 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 except as_fields_b_512 pass for both python2.7 and python3.5. The test suite ships with test runners for dkimpy. After downloading the test suite, you can run the signing and validation tests like this: python2.7 ./testarc.py sign runners/arcsigntest.py python2.7 ./testarc.py validate runners/arcverifytest.py The reason for the test failure is that the ARC specification (as of 20170120) sets the minimum key size to 512 bits. This is operationally inappropriate, so dkimpy sets the default minkey=1024, the same as is used for DKIM. This can be overridden, but that is not recommended. The minimum key size requirement for DKIM (and thus ARC) has recently been updated to require at least a 1024 bit key. See RFC 8301. 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. 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. 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 dkimpy 0.7 implementation matches the -08 revision of the draft, except it uses Ed25519 vice Ed25519ph (a change to Ed25519 is planned for -09, but that had not been published yet as of the release of dkimpy 0.7). Three helper programs are also supplied: dknewkey.py, dkimsign.py and dkimverify.py. dknewkey.py 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. dkimsign.py 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.py selector domain privatekeyfile [identity] The identity is optional and defaults to "@domain". dkimverify.py 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. As of version 0.6.0, dkimpy provides experimental support for ARC (Authenticated Received Chain): https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-01 This new functionality is marked experimental because the protocol is still under development. There are no guarantees about API stability or compatibility. In addition to arcsign.py and arcverify.py, the dkim module now provides arc_sign and arc_verify functions as well as an ARC class. FEEDBACK Bug reports may be submitted to the bug tracker for the dkimpy project on launchpad. dkimpy-0.7.1/dkim/0000775000175000017500000000000013242075365015433 5ustar kittermakitterma00000000000000dkimpy-0.7.1/dkim/dnsplug.py0000664000175000017500000000536013123377365017470 0ustar kittermakitterma00000000000000# 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): """Return a TXT record associated with a DNS name.""" try: a = dns.resolver.query(name, dns.rdatatype.TXT,raise_on_no_answer=False) for r in a.response.answer: if r.rdtype == dns.rdatatype.TXT: return b"".join(r.items[0].strings) except dns.resolver.NXDOMAIN: pass return None def get_txt_pydns(name): """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').req() if not response.answers: return None return b''.join(response.answers[0]['data']) def get_txt_Milter_dns(name): """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,'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: from Milter.dns import Session _get_txt = get_txt_Milter_dns except ImportError: import DNS DNS.DiscoverNameServers() _get_txt = get_txt_pydns def get_txt(name): """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('ascii') except UnicodeDecodeError: return None txt = _get_txt(unicode_name) if type(txt) is str: txt = txt.encode('utf-8') return txt dkimpy-0.7.1/dkim/__init__.py0000664000175000017500000013664113240160672017551 0ustar kittermakitterma00000000000000# 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 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 time # only needed for arc try: from authres import AuthenticationResultsHeader except: pass # only needed for ed25519 signing/verification try: import nacl.signing import nacl.encoding except: pass from dkim.canonicalization import ( CanonicalizationPolicy, InvalidCanonicalizationPolicyError, ) from dkim.canonicalization import Relaxed as RelaxedCanonicalization from dkim.crypto import ( DigestTooLargeError, 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: def get_txt(s): 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", "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): self.data = [] self.hasher = hasher self.name = hasher.name def update(self, data): 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) """ 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 """ 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 HASH_ALGORITHMS: raise ValidationError("unknown signature algorithm: %s" % sig[b'a']) if b'b' in sig: if re.match(br"[\s0-9A-Za-z+/]+=*$", 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+/]+=*$", sig[b'bh']) 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']) # 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): """Fold a header line into multiple crlf-separated lines at column 72. >>> text(fold(b'foo')) 'foo' >>> text(fold(b'foo '+b'foo'*24).splitlines()[0]) 'foo ' >>> text(fold(b'foo'*25).splitlines()[-1]) ' foo' >>> len(fold(b'foo'*25).splitlines()[0]) 72 """ i = header.rfind(b"\r\n ") if i == -1: pre = b"" else: i += 3 pre = header[:i] header = header[i:] # 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 while len(header) > maxleng: i = header[:maxleng].rfind(b" ") if i == -1: j = maxleng else: j = i + 1 pre += header[:j] + b"\r\n " header = header[j:] namelen = 0 return pre + header def load_pk_from_dns(name, dnsfunc=get_txt): s = dnsfunc(name) 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'k'] == b'ed25519': pk = nacl.signing.VerifyKey(pub[b'p'], encoder=nacl.encoding.Base64Encoder) 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 public key: %s" % s) except (TypeError,UnparsableKeyError) as e: raise KeyFormatError("could not parse public key (%s): %s" % (pub[b'p'],e)) ktag = b'rsa' return pk, keysize, ktag #: 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 def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256', minkey=1024): self.set_message(message) if logger is None: logger = get_default_logger() self.logger = logger 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 #: 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',b'date',b'subject') #: The rfc6376 recommended header fields to sign #: @since: 0.5 SHOULD = ( 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', 'subject', 'to'] """ self.frozen_sign.update(x.lower() for x in s if x.lower() not in self.should_not_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)) header_value = RE_BTAG.sub(b'\\1',header_value) header = (header_name, b' ' + header_value) h = HashThrough(self.hasher()) sig = dict(fields) headers = canon_policy.canonicalize_headers(self.headers) self.signed_headers = hash_headers( h, canon_policy, headers, include_headers, header, sig) 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) + b"\r\n" if not standardize: header_value = fold(header_value, namelen=len(header_name)) return header_value # 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: pk, self.keysize, ktag = load_pk_from_dns(name, dnsfunc) except KeyFormatError as e: self.logger.error("%s" % e) return False try: canon_policy = CanonicalizationPolicy.from_c_value(sig.get(b'c', b'relaxed/relaxed')) 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()) body = canon_policy.canonicalize_body(self.body) if b'l' in sig: body = body[:int(sig[b'l'])] h.update(body) 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()) headers = canon_policy.canonicalize_headers(self.headers) self.signed_headers = hash_headers( h, canon_policy, headers, include_headers, sig_header, sig) self.logger.debug("signed for %s: %r" % (sig_header[0], h.hashed())) signature = base64.b64decode(re.sub(br"\s+", b"", sig[b'b'])) if ktag == b'rsa': try: res = RSASSA_PKCS1_v1_5_verify(h, signature, 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 ktag == b'ed25519': try: 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(ktag) #: 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': pk = nacl.signing.SigningKey(privkey, encoder=nacl.encoding.Base64Encoder) 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() 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) 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 #: 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): 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 self.verify_sig(sig, include_headers, sigheaders[idx], dnsfunc) #: 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: an srv_id for identitfying 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): # check if authres has been imported try: AuthenticationResultsHeader except: self.logger.debug("authres package not installed") raise 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 = [(res, AuthenticationResultsHeader.parse('Authentication-Results: ' + res.decode('utf-8'))) for res in ar_headers] 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';\r\n '.join(results) # extract cv parsed_auth_results = 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: self.logger.debug("no AR arc stamps found, chain terminated") return [] 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 == 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: raise ParameterError("cv=none not allowed on instance %d" % instance) 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()) h.update(canon_policy.canonicalize_body(self.body)) 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() 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'][0] 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) 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) 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): """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) @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) 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): """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) @return: True if signature verifies or False otherwise """ d = DKIM(message,logger=logger,minkey=minkey) try: return d.verify(dnsfunc=dnsfunc) except DKIMException as x: if logger is not None: logger.error("%s" % x) return False # 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): """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 @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 logger: a logger to which debug info will be written (default None) @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=signature_algorithm) 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): """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 @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) try: return a.verify(dnsfunc=dnsfunc) except DKIMException as x: if logger is not None: logger.error("%s" % x) return CV_Fail, [], "%s" % x dkimpy-0.7.1/dkim/util.py0000664000175000017500000000450113113455776016767 0ustar kittermakitterma00000000000000# 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', ] 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 dkimpy-0.7.1/dkim/crypto.py0000664000175000017500000001633213236102657017330 0ustar kittermakitterma00000000000000# 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', '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_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 } # 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: raise UnparsableKeyError('Unparsable public key: ' + str(e)) 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. @return: RSA private key """ try: pka = asn1_parse(ASN1_RSAPrivateKey, data) except ASN1FormatError as e: raise UnparsableKeyError('Unparsable private key: ' + 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 dkimpy-0.7.1/dkim/canonicalization.py0000664000175000017500000001044213205013644021322 0ustar kittermakitterma00000000000000# 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): return re.sub(b"(\r\n)*$", b"\r\n", content) 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)) dkimpy-0.7.1/dkim/asn1.py0000664000175000017500000001013012120435755016637 0ustar kittermakitterma00000000000000# 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 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]) dkimpy-0.7.1/dkim/__main__.py0000664000175000017500000000021311766012544017520 0ustar kittermakitterma00000000000000import unittest import doctest import dkim from tests import test_suite doctest.testmod(dkim) unittest.TextTestRunner().run(test_suite()) dkimpy-0.7.1/dkim/tests/0000775000175000017500000000000013242075365016575 5ustar kittermakitterma00000000000000dkimpy-0.7.1/dkim/tests/__init__.py0000664000175000017500000000307713236016664020715 0ustar kittermakitterma00000000000000# 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_util, test_arc, test_dnsplug, ) modules = [ test_canonicalization, test_crypto, test_dkim, test_dkim_ed25519, test_util, test_arc, test_dnsplug, ] suites = [x.test_suite() for x in modules] return unittest.TestSuite(suites) dkimpy-0.7.1/dkim/tests/test_canonicalization.py0000664000175000017500000001363513205013211023520 0ustar kittermakitterma00000000000000# 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__) dkimpy-0.7.1/dkim/tests/test_crypto.py0000664000175000017500000001520112120435755021522 0ustar kittermakitterma00000000000000# 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__) dkimpy-0.7.1/dkim/tests/test_dkim_ed25519.py0000664000175000017500000003107013242075340022202 0ustar kittermakitterma00000000000000# 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): 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.assertEquals(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.assertEquals(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__) dkimpy-0.7.1/dkim/tests/test_util.py0000664000175000017500000000551712472234420021164 0ustar kittermakitterma00000000000000# 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, ) 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.assertEquals(sig[b't'],b'1308078492') self.assertEquals(len(sig),11) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) dkimpy-0.7.1/dkim/tests/data/0000775000175000017500000000000013242075365017506 5ustar kittermakitterma00000000000000dkimpy-0.7.1/dkim/tests/data/test_bad.txt0000664000175000017500000000036012120435755022030 0ustar kittermakitterma00000000000000v=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQA= dkimpy-0.7.1/dkim/tests/data/test2.private0000664000175000017500000000156713210056160022137 0ustar kittermakitterma00000000000000-----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----- dkimpy-0.7.1/dkim/tests/data/test2.message0000664000175000017500000000212012472234420022101 0ustar kittermakitterma00000000000000Comment: 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 dkimpy-0.7.1/dkim/tests/data/ed25519test.key0000664000175000017500000000005513236015230022102 0ustar kittermakitterma00000000000000fL+5V9EquCZAovKik3pA6Lk9zwCzoEtjIuIqK9ZXHHA= dkimpy-0.7.1/dkim/tests/data/rfc6376.signed.msg0000664000175000017500000000117513240360202022553 0ustar kittermakitterma00000000000000DKIM-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== 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. dkimpy-0.7.1/dkim/tests/data/test.message0000664000175000017500000000053213205013154022016 0ustar kittermakitterma00000000000000Authentication-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. dkimpy-0.7.1/dkim/tests/data/rfc8032_7_1.key0000664000175000017500000000005513240157060021744 0ustar kittermakitterma00000000000000nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A= dkimpy-0.7.1/dkim/tests/data/test_extra.message0000664000175000017500000000033311766012544023234 0ustar kittermakitterma00000000000000Received: 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. dkimpy-0.7.1/dkim/tests/data/ed25519test3.unsigned.msg0000664000175000017500000000045013236015230023775 0ustar kittermakitterma00000000000000Authentication-Results: lists.example.org; arc=none; spf=pass smtp.mfrom=example.com; 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. dkimpy-0.7.1/dkim/tests/data/eximtest.dns0000664000175000017500000000010313236015230022034 0ustar kittermakitterma00000000000000v=DKIM1; k=ed25519; p=sPs07Vu29FpHT/80UXUcYHFOHifD4o2ZlP2+XUh9g6E= dkimpy-0.7.1/dkim/tests/data/message.mbox0000664000175000017500000001053411766012544022023 0ustar kittermakitterma00000000000000Return-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-- dkimpy-0.7.1/dkim/tests/data/test.txt0000664000175000017500000000036011766012544021224 0ustar kittermakitterma00000000000000v=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB dkimpy-0.7.1/dkim/tests/data/ed25519test.dns0000664000175000017500000000007213236015230022075 0ustar kittermakitterma00000000000000k=ed25519; p=yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y= dkimpy-0.7.1/dkim/tests/data/test.private0000664000175000017500000000156711766012544022071 0ustar kittermakitterma00000000000000-----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----- dkimpy-0.7.1/dkim/tests/data/ed25519test2.msg0000664000175000017500000000111413236122244022163 0ustar kittermakitterma00000000000000DKIM-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 dkimpy-0.7.1/dkim/tests/data/ed25519test.msg0000664000175000017500000000053213236015230022100 0ustar kittermakitterma00000000000000Authentication-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. dkimpy-0.7.1/dkim/tests/data/test2.txt0000664000175000017500000000035113210056160021272 0ustar kittermakitterma00000000000000v=DKIM1; g=*; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB dkimpy-0.7.1/dkim/tests/data/test_nofrom.message0000664000175000017500000000024412137265402023406 0ustar kittermakitterma00000000000000Received: from localhost Message-ID: Date: Mon, 01 Jan 2013 01:02:03 +0400 To: somebody@example.com Subject: Testing This is a test message. dkimpy-0.7.1/dkim/tests/data/rfc6376.msg0000664000175000017500000000041513240157060021305 0ustar kittermakitterma00000000000000From: 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. dkimpy-0.7.1/dkim/tests/data/ed25519test.verify.msg0000664000175000017500000000123113236015230023400 0ustar kittermakitterma00000000000000DKIM-Signature: v=1; a=ed25519; c=relaxed/simple; d=example.com; i=@example.com; q=dns/txt; s=test; t=5; h=message-id : date : from : to : subject : date : from : subject; bh=wE7NXSkgnx9PGiavN4OZhJztvkqPDlemV3OGuEnLwNo=; b=wt7P+9DoBwcln1RKE3LN7069ZEEiSyVE/NH1YXnqnJy4JcrSCZUbeIEh vXssPHelX4yNSXG9eTGTwwk5NxYqBw== 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. dkimpy-0.7.1/dkim/tests/data/ed25519test3.msg0000664000175000017500000000120713236102657022175 0ustar kittermakitterma00000000000000DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/simple; d=kitterman.org; i=@kitterman.org; q=dns/txt; s=ed25519; t=1517847601; h=message-id : date : from : to : subject : date : from : subject; bh=wE7NXSkgnx9PGiavN4OZhJztvkqPDlemV3OGuEnLwNo=; b=sEnnE99Xsjpcqa/cNf8k/KQCEgjJ/4tswIKoNvq2q0fFQL6XBORJ2fQb Fvt34Tb4sOxlZtBYu01kEJlmGz4uCw== Authentication-Results: lists.example.org; arc=none; spf=pass smtp.mfrom=example.com; 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. dkimpy-0.7.1/dkim/tests/test_dnsplug.py0000664000175000017500000000241313042752531021655 0ustar kittermakitterma00000000000000# 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 # import unittest import dkim.dnsplug class TestDNSPlug(unittest.TestCase): def test_get_txt(self): dkim.dnsplug._get_txt = {"in": "out"}.get res = dkim.dnsplug.get_txt(b"in") self.assertEqual(res, b"out") def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) dkimpy-0.7.1/dkim/tests/test_arc.py0000664000175000017500000001003113205013154020731 0ustar kittermakitterma00000000000000# 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): 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. 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=3jOfBfTKcq+3r3Xv158DybT4mWFxrGcop+cgyLUX2ETCMHqNXYwGx2h+NY46tr\r\n k0Lg6R8i+560+KC8PLcCURYYJNJUHLHPIifhddy1aMNL9l4CoI+Oz+rocd2IZeb/\r\n I9V5amOUOWnAlOvyrSt0XfzLJRTS8qJW3Is1CRkkgyLoI=\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 : date : from : \r\n subject; \r\n bh=wE7NXSkgnx9PGiavN4OZhJztvkqPDlemV3OGuEnLwNo=; \r\n b=Bj/AEKhmzMbltWXrfLA8UZNp6/5cj8/IzqbgQec4vGobDZRsa\r\n C0YIPM4tcqK2uTS62kwh40cndXTDsCppvRsBy1sIO3eRNyuLUOh\r\n 0XGrz0AdLQMv+IOdyQqZfMVkq8DuQ4Qdl7ee99uYf3D8S+L7GuD\r\n wJSk7dyH+P2BKxz2nyB0=\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.assertEquals(expected_sig, sig_lines) (cv, res, reason) = dkim.arc_verify(b''.join(sig_lines) + self.message, dnsfunc=self.dnsfunc) self.assertEquals(cv, dkim.CV_Pass) def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) dkimpy-0.7.1/dkim/tests/test_dkim.py0000664000175000017500000003224713236015230021126 0ustar kittermakitterma00000000000000# 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)) 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): 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"), '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): 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 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_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_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_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_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.assertEquals(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.assertEquals(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__) dkimpy-0.7.1/arcsign.py0000664000175000017500000000404213001243077016475 0ustar kittermakitterma00000000000000#!/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 re import sys import dkim logging.basicConfig(level=10) if len(sys.argv) != 4: print("Usage: arcsign.py selector domain privatekeyfile", 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] message = sys.stdin.read() # Pick a cv status cv = dkim.CV_None if re.search('arc-seal', message, re.IGNORECASE): cv = dkim.CV_Pass #try: sig = dkim.arc_sign(message, selector, domain, open(privatekeyfile, "rb").read(), domain + ": none", cv) 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) dkimpy-0.7.1/dknewkey.py0000664000175000017500000001055713242072471016705 0ustar kittermakitterma00000000000000#!/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. """ 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 GenRSAKeys(private_key_file): """ Generates a suitable private key. Output is unprotected. You should encrypt your keys. """ print >> sys.stderr, 'generating ' + private_key_file subprocess.check_call([OPENSSL_BINARY, 'genrsa', '-out', private_key_file, str(BITS_REQUIRED)]) def GenEd25519Keys(private_key_file): """Generates a base64 encoded private key for ed25519 DKIM signing. Output is unprotected. You should encrypt 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)) priv_key = skg.generate() print >> sys.stderr, 'generating ' + private_key_file pkf = open(private_key_file, "w+") print >> pkf, priv_key.encode(encoder=nacl.encoding.Base64Encoder) pkf.close() return(priv_key) def ExtractRSADnsPublicKey(private_key_file, dns_file): """ Given a key, extract the bit we should place in DNS. """ print >> sys.stderr, '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']) cmd = 'grep -v ^-- %s | tr -d \'\\n\'' % working_file try: output = subprocess.check_output(cmd, shell=True) finally: os.unlink(working_file) dns_fp = open(dns_file, "w+") print >> sys.stderr, 'writing ' + dns_file print >> dns_fp, "k=rsa; h=sha256; p={0}".format(output) dns_fp.close() def ExtractEd25519PublicKey(private_key_file, dns_file, priv_key): """ 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) dns_fp = open(dns_file, "w+") print >> sys.stderr, 'writing ' + dns_file print >> dns_fp, "k=ed25519; p={0}".format(output) dns_fp.close() def main(argv): 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() if sys.version_info[0] >= 3: args.key_name = bytes(args.key_name, encoding='UTF-8') args.ktype = bytes(args.ktype, encoding='UTF-8') # Make sys.stdin and stdout binary streams. sys.stdin = sys.stdin.detach() sys.stdout = sys.stdout.detach() 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(private_key_file, dns_file, priv_key) else: print >> sys.stderr, "Unknown key type - no key generated." if __name__ == '__main__': main(sys.argv) dkimpy-0.7.1/MANIFEST.in0000664000175000017500000000041213041436731016235 0ustar kittermakitterma00000000000000include dkim/* include dkim/tests/* include dkim/tests/data/* include README include ChangeLog include MANIFEST.in include dknewkey.py include dkimsign.py include dkimverify.py include arcsign.py include arcverify.py include dnsplug.py include man/* include test.py dkimpy-0.7.1/arcverify.py0000664000175000017500000000316713042741604017054 0ustar kittermakitterma00000000000000#!/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 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)) dkimpy-0.7.1/man/0000775000175000017500000000000013242075365015262 5ustar kittermakitterma00000000000000dkimpy-0.7.1/man/dknewkey.10000664000175000017500000001165013242074644017167 0ustar kittermakitterma00000000000000\" .\" 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 .\" ======================================================================== .\" .IX Title "dknewkey 1" .TH dknewkey 1 "2018-02-05" .SH "NAME" dknewkey \- Generates new DKIM public/private key pairs .SH "VERSION" .IX Header "VERSION" 0\.7\.1 .SH "DESCRIPTION" .IX Header "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=2ha256 is now included in the public DNS record to prevent inadvertent use with the now obsolete sha1 hash algorithm (See RFC 8301). .SH "USAGE" .IX Header "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" .IX Header "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. dkimpy-0.7.1/man/arcverify.10000664000175000017500000001022413041441701017321 0ustar kittermakitterma00000000000000\" .\" 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 .\" ======================================================================== .\" .IX Title "dkimverify 1" .TH dkimverify 1 "2017-01-23" .SH "NAME" dkimverify \- Script for DKIM verifying messages on stdin .SH "VERSION" .IX Header "VERSION" 0\.6\.0 .SH "DESCRIPTION" .IX Header "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" .IX Header "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. dkimpy-0.7.1/man/arcsign.10000664000175000017500000001037413041441604016765 0ustar kittermakitterma00000000000000\" .\" 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 .\" ======================================================================== .\" .IX Title "arcsign 1" .TH arcsign 1 "2017-01-23" .SH "NAME" arcsign \- Script for ARC signing messages on stdin .SH "VERSION" .IX Header "VERSION" 0\.6\.0 .SH "DESCRIPTION" .IX Header "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" .IX Header "USAGE" The signing options are specified on the command line: arcsign selector domain privatekeyfile .SH "AUTHORS" .IX Header "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. dkimpy-0.7.1/man/dkimverify.10000664000175000017500000001010413041441724017502 0ustar kittermakitterma00000000000000\" .\" 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 .\" ======================================================================== .\" .IX Title "dkimverify 1" .TH dkimverify 1 "2017-01-23" .SH "NAME" dkimverify \- Script for DKIM verifying messages on stdin .SH "VERSION" .IX Header "VERSION" 0\.6\.0 .SH "DESCRIPTION" .IX Header "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" .IX Header "AUTHORS" This version of \fBdkimverify\fR was written by Greg Hewgill . .PP This man-page was created by Scott Kitterman and is licensed under the same terms as dkimpy. dkimpy-0.7.1/man/dkimsign.10000664000175000017500000001171213236127773017157 0ustar kittermakitterma00000000000000\" .\" 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 .\" ======================================================================== .\" .IX Title "dkimsign 1" .TH dkimsign 1 "2018-02-05" .SH "NAME" dkimsign \- Script for DKIM signing messages on stdin .SH "VERSION" .IX Header "VERSION" 0\.7\.0 .SH "DESCRIPTION" .IX Header "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" .IX Header "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" .IX Header "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. dkimpy-0.7.1/ChangeLog0000664000175000017500000001220113242072633016250 0ustar kittermakitterma000000000000002018-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 dkimpy-0.7.1/dkimverify.py0000664000175000017500000000270513210200007017210 0ustar kittermakitterma00000000000000#!/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 dkim 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) res = d.verify() else: res = dkim.verify(message) if not res: print("signature verification failed") sys.exit(1) print("signature ok") dkimpy-0.7.1/test.py0000664000175000017500000000041413123037623016030 0ustar kittermakitterma00000000000000import 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()) dkimpy-0.7.1/PKG-INFO0000664000175000017500000000177713242075365015620 0ustar kittermakitterma00000000000000Metadata-Version: 1.1 Name: dkimpy Version: 0.7.1 Summary: DKIM (DomainKeys Identified Mail) Home-page: https://launchpad.net/dkimpy Author: Scott Kitterman Author-email: scott@kitterman.com License: BSD-like Description: dkimpy is a Python library that implements DKIM (DomainKeys Identified Mail) and ARC (Authenticated Received Chain) email signing and verification. Platform: UNKNOWN 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 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 dkimpy-0.7.1/dkimsign.py0000664000175000017500000000641713236102657016674 0ustar kittermakitterma00000000000000#!/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 # 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) 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)