dkimpy-0.5.6/0000775000175000017500000000000012631406324014504 5ustar kittermakitterma00000000000000dkimpy-0.5.6/setup.py0000664000175000017500000000452212631405372016223 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.5.6" setup( name = "dkimpy", version = version, description = "DKIM (DomainKeys Identified Mail)", long_description = """dkimpy is a Python library that implements DKIM (DomainKeys Identified Mail) email signing and verification.""", author = "Scott Kitterman", author_email = "scott@kitterman.com", url = "https://launchpad.net/dkimpy", license = "BSD-like", packages = ["dkim"], scripts = ["dkimsign.py", "dkimverify.py"], data_files = [(os.path.join('share', 'man', 'man1'), ['man/dkimsign.1']), (os.path.join('share', 'man', 'man1'), ['man/dkimverify.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.5.6/dnsplug.py0000664000175000017500000001365512155231624016544 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.5.6/README0000664000175000017500000000327112631405504015366 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.5.6. REQUIREMENTS - Python 2.x >= 2.6, or Python 3.x >= 3.1. - dnspython or pydns. dnspython is preferred if both are present. INSTALLATION To build and install dkimpy: python setup.py install 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 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. Two helper programs are also supplied: dkimsign.py and dkimverify.py. 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. FEEDBACK Bug reports may be submitted to the bug tracker for the dkimpy project on launchpad. dkimpy-0.5.6/dkim/0000775000175000017500000000000012631406324015430 5ustar kittermakitterma00000000000000dkimpy-0.5.6/dkim/dnsplug.py0000664000175000017500000000534311766012544017470 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 txt: txt = txt.encode('utf-8') return txt dkimpy-0.5.6/dkim/__init__.py0000664000175000017500000006025312561145653017556 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 base64 import hashlib import logging import re import time from dkim.canonicalization import ( CanonicalizationPolicy, InvalidCanonicalizationPolicyError, ) 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", "Relaxed", "Simple", "DKIM", "sign", "verify", ] Relaxed = b'relaxed' # for clients passing dkim.Relaxed Simple = b'simple' # for clients passing dkim.Simple 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 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 validate_signature_fields(sig): """Validate DKIM-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. """ mandatory_fields = (b'v', b'a', b'b', b'bh', b'd', b'h', b's') for field in mandatory_fields: if field not in sig: raise ValidationError("signature missing %s=" % field) if sig[b'v'] != b"1": raise ValidationError("v= value is not 1 (%s)" % sig[b'v']) 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 re.match(br"[\s0-9A-Za-z+/]+=*$", sig[b'bh']) is None: raise ValidationError( "bh= value is not valid base64 (%s)" % sig[b'bh']) # Nasty hack to support both str and bytes... check for both the # character and integer values. if 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']) now = int(time.time()) slop = 36000 # 10H leeway for mailers with inaccurate clocks t_sign = 0 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']) 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'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']) 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): """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:] while len(header) > 72: i = header[:72].rfind(b" ") if i == -1: j = 72 else: j = i + 1 pre += header[:j] + b"\r\n " header = header[j:] return pre + header #: Hold messages and options during DKIM signing and verification. class DKIM(object): # NOTE - the first 2 indentation levels are 2 instead of 4 # to minimize changed lines from the function only version. #: 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') #: 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 rfc4871 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', 'references', b'list-id', b'list-help', b'list-unsubscribe', b'list-subscribe', b'list-post', b'list-owner', b'list-archive' ) #: The rfc4871 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' ) #: Create a DKIM instance to sign and verify rfc5322 messages. #: #: @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 from RFC4871 self.should_sign = set(DKIM.SHOULD) #: Header fields which should not be signed. The default is from RFC4871. #: 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 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] #: 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, identity=None, canonicalize=(b'relaxed',b'simple'), include_headers=None, length=False): try: pk = parse_pem_private_key(privkey) except UnparsableKeyError as e: raise KeyFormatError(str(e)) 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)) headers = canon_policy.canonicalize_headers(self.headers) if include_headers is None: include_headers = self.default_sign_headers() # rfc4871 says FROM is required if b'from' not in ( x.lower() for x 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 include_headers: if x.lower() in self.should_not_sign: raise ParameterError("The %s header field SHOULD NOT be signed"%x) body = canon_policy.canonicalize_body(self.body) hasher = HASH_ALGORITHMS[self.signature_algorithm] h = 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', len(body)), (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] include_headers = [x.lower() for x in include_headers] # record what verify should extract self.include_headers = tuple(include_headers) sig_value = fold(b"; ".join(b"=".join(x) for x in sigfields)) sig_value = RE_BTAG.sub(b'\\1',sig_value) dkim_header = (b'DKIM-Signature', b' ' + sig_value) h = hasher() sig = dict(sigfields) self.signed_headers = hash_headers( h, canon_policy, headers, include_headers, dkim_header,sig) self.logger.debug("sign headers: %r" % self.signed_headers) try: sig2 = RSASSA_PKCS1_v1_5_sign(h, pk) except DigestTooLargeError: raise ParameterError("digest too large for modulus") # Folding b= is explicity allowed, but yahoo and live.com are broken #sig_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. sig_value = fold(sig_value + base64.b64encode(bytes(sig2))) self.domain = domain self.selector = selector self.signature_fields = sig return b'DKIM-Signature: ' + sig_value + b"\r\n" #: 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) logger = self.logger logger.debug("sig: %r" % sig) validate_signature_fields(sig) self.domain = sig[b'd'] self.selector = sig[b's'] try: canon_policy = CanonicalizationPolicy.from_c_value(sig.get(b'c')) except InvalidCanonicalizationPolicyError as e: raise MessageFormatError("invalid c= value: %s" % e.args[0]) headers = canon_policy.canonicalize_headers(self.headers) body = canon_policy.canonicalize_body(self.body) try: hasher = HASH_ALGORITHMS[sig[b'a']] except KeyError as e: raise MessageFormatError("unknown signature algorithm: %s" % e.args[0]) if b'l' in sig: body = body[:int(sig[b'l'])] h = hasher() h.update(body) bodyhash = h.digest() 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'])) name = sig[b's'] + b"._domainkey." + sig[b'd'] + b"." 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: pk = parse_public_key(base64.b64decode(pub[b'p'])) self.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)) include_headers = [x.lower() for x in re.split(br"\s*:\s*", sig[b'h'])] self.include_headers = tuple(include_headers) # 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 = hasher() self.signed_headers = hash_headers( h, canon_policy, headers, include_headers, sigheaders[idx], sig) try: signature = base64.b64decode(re.sub(br"\s+", b"", sig[b'b'])) res = RSASSA_PKCS1_v1_5_verify(h, signature, pk) 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) 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 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) if not include_headers: include_headers = d.default_sign_headers() 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 dkimpy-0.5.6/dkim/util.py0000664000175000017500000000434012472234420016757 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 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 RFC4871 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 = tag_spec.split(b'=', 1) except ValueError: raise InvalidTagSpec(tag_spec) if key.strip() in tags: raise DuplicateTag(key.strip()) tags[key.strip()] = value.strip() 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.5.6/dkim/crypto.py0000664000175000017500000001617112002440247017322 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__ = [ '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, } # These values come from RFC 3447, section 9.2 Notes, page 43. 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 RFC3447 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 RFC3447 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: RFC3447 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 RFC3447 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 RFC3447 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 RFC3447 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.5.6/dkim/canonicalization.py0000664000175000017500000001023411766012544021334 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) 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 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.5.6/dkim/asn1.py0000664000175000017500000001013012120435755016642 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.5.6/dkim/__main__.py0000664000175000017500000000021311766012544017523 0ustar kittermakitterma00000000000000import unittest import doctest import dkim from tests import test_suite doctest.testmod(dkim) unittest.TextTestRunner().run(test_suite()) dkimpy-0.5.6/dkim/tests/0000775000175000017500000000000012631406324016572 5ustar kittermakitterma00000000000000dkimpy-0.5.6/dkim/tests/__init__.py0000664000175000017500000000237211766012544020714 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 def test_suite(): from dkim.tests import ( test_canonicalization, test_crypto, test_dkim, test_util, ) modules = [ test_canonicalization, test_crypto, test_dkim, test_util, ] suites = [x.test_suite() for x in modules] return unittest.TestSuite(suites) dkimpy-0.5.6/dkim/tests/test_canonicalization.py0000664000175000017500000001203311766012544023534 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') 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') 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.5.6/dkim/tests/test_crypto.py0000664000175000017500000001520112120435755021525 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.5.6/dkim/tests/test_util.py0000664000175000017500000000551712472234420021167 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.5.6/dkim/tests/data/0000775000175000017500000000000012631406324017503 5ustar kittermakitterma00000000000000dkimpy-0.5.6/dkim/tests/data/test_bad.txt0000664000175000017500000000036012120435755022033 0ustar kittermakitterma00000000000000v=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQA= dkimpy-0.5.6/dkim/tests/data/test2.message0000664000175000017500000000212012472234420022104 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.5.6/dkim/tests/data/test.message0000664000175000017500000000030711766012544022035 0ustar kittermakitterma00000000000000Received: 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.5.6/dkim/tests/data/test_extra.message0000664000175000017500000000033311766012544023237 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.5.6/dkim/tests/data/message.mbox0000664000175000017500000001053411766012544022026 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.5.6/dkim/tests/data/test.txt0000664000175000017500000000036011766012544021227 0ustar kittermakitterma00000000000000v=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB dkimpy-0.5.6/dkim/tests/data/test.private0000664000175000017500000000156711766012544022074 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.5.6/dkim/tests/data/test_nofrom.message0000664000175000017500000000024412137265402023411 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.5.6/dkim/tests/test_dkim.py0000664000175000017500000002537112561145653021146 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 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 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_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.5.6/dkim/.__init__.py.swo0000644000175000017500000006000012137375562020432 0ustar kittermakitterma00000000000000b0VIM 7.3}Q(2$kittermaScott-Latitude-E6320~kitterma/devel/dkim/dkimpy/dkim/__init__.pyutf-8 U3210#"! Utp{V|b4O:[]!@ad&&{tKI; 3 { y D  ] W = $  ^ V D w b L 2  a`P) [RQ,\;21 |{,g< [F3y'&%RE_BTAG = re.compile(r'([;\s]b'+FWS+r'=)(?:'+FWS+r'[a-zA-Z0-9+/=])*(?:\r?\n\Z)?')FWS = r'(?:\r?\n\s+)?' return sign_headers lastindex[h] = i break sign_headers.append(headers[i]) if h == headers[i][0].lower(): i -= 1 while i > 0: i = lastindex.get(h, len(headers)) assert h == h.lower() for h in include_headers: lastindex = {} sign_headers = [] """ [('From', 'biz'), ('Subject', 'Boring')] >>> select_headers(h,i) >>> i = ['from','subject','to','from'] >>> h = [('From','biz'),('Foo','bar'),('Subject','Boring')] [('from', 'baz'), ('subject', 'boring'), ('from', 'biz')] >>> select_headers(h,i) >>> i = ['from','subject','to','from'] >>> h = [('from','biz'),('foo','bar'),('from','baz'),('subject','boring')] """Select message header fields to be signed/verified.def select_headers(headers, include_headers): pass """Validation error."""class ValidationError(DKIMException): pass """Input parameter error."""class ParameterError(DKIMException): pass """RFC822 message format error."""class MessageFormatError(DKIMException): pass """Key format error while parsing an RSA public or private key."""class KeyFormatError(DKIMException): pass """Internal error in dkim module. Should never happen."""class InternalError(DKIMException): pass """Base class for DKIM errors."""class DKIMException(Exception): return len(bin(x)) - 2 """Return size of long in bits."""def bitsize(x):Simple = b'simple' # for clients passing dkim.SimpleRelaxed = b'relaxed' # for clients passing dkim.Relaxed] "verify", "sign", "DKIM", "Simple", "Relaxed", "ParameterError", "MessageFormatError", "KeyFormatError", "InternalError", "DKIMException",__all__ = [ ) parse_tag_value, InvalidTagValueList, get_default_logger,from dkim.util import ( raise RuntimeError("DKIM.verify requires DNS or dnspython module") def get_txt(s):except: from dkim.dnsplug import get_txttry: ) UnparsableKeyError, RSASSA_PKCS1_v1_5_verify, RSASSA_PKCS1_v1_5_sign, parse_public_key, parse_pem_private_key, HASH_ALGORITHMS, DigestTooLargeError,from dkim.crypto import ( ) InvalidCanonicalizationPolicyError, CanonicalizationPolicy,from dkim.canonicalization import (import timeimport reimport loggingimport hashlibimport base64# Copyright (c) 2011 William Grant # This has been modified from the original software.## Copyright (c) 2008 Greg Hewgill http://hewgill.com## 3. This notice may not be removed or altered from any source distribution.# misrepresented as being the original software.# 2. Altered source versions must be plainly marked as such, and must not be# appreciated but is not required.# in a product, an acknowledgment in the product documentation would be# claim that you wrote the original software. If you use this software# 1. The origin of this software must not be misrepresented; you must not## freely, subject to the following restrictions:# including commercial applications, and to alter it and redistribute it# Permission is granted to anyone to use this software for any purpose,## arising from the use of this software.# warranty. In no event will the author be held liable for any damages# This software is provided 'as-is', without any express or impliedad!|#d ) y ; m P m e 3 *   return False logger.error("%s" % x) if logger is not None: except DKIMException as x: return d.verify(dnsfunc=dnsfunc) try: d = DKIM(message,logger=logger,minkey=minkey) """ @return: True if signature verifies or False otherwise @param logger: a logger to which debug info will be written (default None) @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) """Verify the first (topmost) DKIM signature on an RFC822 formatted message.def verify(message, logger=None, dnsfunc=get_txt, minkey=1024): return d.sign(selector, domain, privkey, identity=identity, canonicalize=canonicalize, include_headers=include_headers, length=length) include_headers = d.all_sign_headers() if not include_headers: d = DKIM(message,logger=logger) """ @raise DKIMException: when the message, include_headers, or key are badly formed. @return: DKIM-Signature header field terminated by \\r\\n @param logger: a logger to which debug info will be written (default None) @param length: true if the l= tag should be included to indicate body length (default False) @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign) @param canonicalize: the canonicalization algorithms to use (default (Simple, Simple)) @param identity: the DKIM identity value for the signature (default "@"+domain) @param privkey: a PKCS#1 private key in base64-encoded text form @param domain: the DKIM domain value for the signature @param selector: the DKIM selector value for the signature @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) """Sign an RFC822 message and return the DKIM-Signature header line. include_headers=None, length=False, logger=None):adIOvfeOX&% { j I 9  N 0 n - k V U 3 { L  x.~2P:C`PK I #: @param identity: the DKIM identity value for the signature #: @param privkey: a PKCS#1 private key in base64-encoded text form #: @param domain: the DKIM domain value for the signature #: @param selector: the DKIM selector value for the signature #: #: is needed. #: It is only necessary to pass an include_headers list when precise control #: by tweaking should_sign and frozen_sign (or even should_not_sign). #: in should_not_sign. The default list for this method can be modified #: compatible sign function, which signs all headers not #: The default include_headers for this method differs from the backward #: #: without breaking the signature. #: enroute (e.g. mailing lists that append unsubscribe information) #: The length option allows the message body to be appended to by MTAs #: #: from being added without breaking the signature. #: times than are currently present prevents additional instances #: of the field are signed from bottom to top. Signing a header field more #: fields (such as Received) can be signed multiple times. Instances #: that field from being added without breaking the signature. Repeated #: are signed. Note that signing a header field that doesn't exist prevents #: The include_headers option gives full control over which header fields #: #: Sign an RFC822 message and return the DKIM-Signature header line. return [x for x,y in self.headers if x.lower() not in self.should_not_sign] @since: 0.5""" """Return header list of all existing headers not in should_not_sign. def all_sign_headers(self): if x.lower() in self.frozen_sign] return include_headers + [ x for x in include_headers if x.lower() in hset ] include_headers = [ x for x,y in self.headers hset = self.should_sign | self.frozen_sign @since: 0.5""" additions. frozen_sign, with those in frozen_sign signed an extra time to prevent """Return the default list of headers to sign: those in should_sign or def default_sign_headers(self): self.keysize = 0 #: The public key size last verified. self.signed_headers = [] #: This could be more useful as original headers. #: is a name,value tuple. FIXME: The headers are canonicalized. #: The list of headers last signed or verified. Each header self.signature_fields = {} #: use L{dkim.util.parse_tag_value}. #: a DKIM-Signature header field that you have in hand, #: Signature parameters of last sign or verify. To parse self.selector = 'default' #: The DKIM key selector last signed or verified. self.domain = None #: The DKIM signing domain last signed or verified. self.headers, self.body = [],'' else: self.headers, self.body = rfc822_parse(message) if message: def set_message(self,message): #: @since: 0.5 #: (with either \\n or \\r\\n line endings) #: @param message: an RFC822 formatted message to be signed or verified #: Load a new message to be signed or verified. if x.lower() not in self.should_not_sign) self.frozen_sign.update(x.lower() for x in s """ ['cc', 'date', 'from', 'in-reply-to', 'message-id', 'references', 'reply-to', 'sender', 'subject', 'to'] >>> sorted(dkim.frozen_sign) >>> dkim.add_frozen(DKIM.RFC5322_SINGLETON) >>> dkim = DKIM() @since: 0.5 @param s: list of headers to add to frozen_sign """ Add headers not in should_not_sign to frozen_sign. def add_frozen(self,s):adO8n> w E D  h X "  m O  L  u t R  k9M:Qo!Yboj*h #: @param identity: the DKIM identity value for the signature #: @param privkey: a PKCS#1 private key in base64-encoded text form #: @param domain: the DKIM domain value for the signature #: @param selector: the DKIM selector value for the signature #: #: is needed. #: It is only necessary to pass an include_headers list when precise control #: by tweaking should_sign and frozen_sign (or even should_not_sign). #: in should_not_sign. The default list for this method can be modified #: compatible sign function, which signs all headers not #: The default include_headers for this method differs from the backward #: #: without breaking the signature. #: enroute (e.g. mailing lists that append unsubscribe information) #: The length option allows the message body to be appended to by MTAs #: #: from being added without breaking the signature. #: times than are currently present prevents additional instances #: of the field are signed from bottom to top. Signing a header field more #: fields (such as Received) can be signed multiple times. Instances #: that field from being added without breaking the signature. Repeated #: are signed. Note that signing a header field that doesn't exist prevents #: The include_headers option gives full control over which header fields #: #: Sign an RFC822 message and return the DKIM-Signature header line. return [x for x,y in self.headers if x.lower() not in self.should_not_sign] @since: 0.5""" """Return header list of all existing headers not in should_not_sign. def all_sign_headers(self): if x.lower() in self.frozen_sign] return include_headers + [ x for x in include_headers if x.lower() in hset ] include_headers = [ x for x,y in self.headers hset = self.should_sign | self.frozen_sign @since: 0.5""" additions. frozen_sign, with those in frozen_sign signed an extra time to prevent """Return the default list of headers to sign: those in should_sign or def default_sign_headers(self): self.keysize = 0 #: The public key size last verified. self.signed_headers = [] #: This could be more useful as original headers. #: is a name,value tuple. FIXME: The headers are canonicalized. #: The list of headers last signed or verified. Each header self.signature_fields = {} #: use L{dkim.util.parse_tag_value}. #: a DKIM-Signature header field that you have in hand, #: Signature parameters of last sign or verify. To parse self.selector = 'default' #: The DKIM key selector last signed or verified. self.domain = None #: The DKIM signing domain last signed or verified. self.headers, self.body = [],'' else: self.headers, self.body = rfc822_parse(message) if message: def set_message(self,message): #: @since: 0.5 #: (with either \\n or \\r\\n line endings) #: @param message: an RFC822 formatted message to be signed or verified #: Load a new message to be signed or verified. if x.lower() not in self.should_not_sign) self.frozen_sign.update(x.lower() for x in s """ ['cc', 'date', 'from', 'in-reply-to', 'message-id', 'references', 'reply-to', 'sender', 'subject', 'to'] >>> sorted(dkim.frozen_sign) >>> dkim.add_frozen(DKIM.RFC5322_SINGLETON) >>> dkim = DKIM() @since: 0.5 @param s: list of headers to add to frozen_sign """ Add headers not in should_not_sign to frozen print(self.signature_headers) self.minkey = minkey #: default is 1024 #: Minimum public key size. Shorter keys raise KeyFormatError. The self.frozen_sign = set(DKIM.FROZEN) #: Header fields to sign an extra time to prevent additions.dkimpy-0.5.6/MANIFEST.in0000664000175000017500000000031611766015741016251 0ustar kittermakitterma00000000000000include dkim/* include dkim/tests/* include dkim/tests/data/* include README include ChangeLog include MANIFEST.in include dkimsign.py include dkimverify.py include dnsplug.py include man/* include test.py dkimpy-0.5.6/man/0000775000175000017500000000000012631406324015257 5ustar kittermakitterma00000000000000dkimpy-0.5.6/man/dkimverify.10000664000175000017500000001007711766012544017524 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 "2008-11-07" .SH "NAME" dkimverify \- Script for DKIM verifying messages on stdin .SH "VERSION" .IX Header "VERSION" 0\.3 .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 \fBdkimsign\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.5.6/man/dkimsign.10000664000175000017500000001036111766012544017154 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 "2008-11-07" .SH "NAME" dkimsign \- Script for DKIM signing messages on stdin .SH "VERSION" .IX Header "VERSION" 0\.3 .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" The signing options are specified on the command line: dkimsign selector domain privatekeyfile [identity] The identity is optional and defaults to "@domain". .SH "AUTHORS" .IX Header "AUTHORS" This version of \fBdkimsign\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.5.6/ChangeLog0000664000175000017500000000553112631405470016263 0ustar kittermakitterma000000000000002015-12-07 Verion 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.5.6/dkimverify.py0000664000175000017500000000264411766012544017242 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: d = dkim.DKIM(message) res = d.verify() else: res = dkim.verify(message) if not res: print("signature verification failed") sys.exit(1) print("signature ok") dkimpy-0.5.6/test.py0000664000175000017500000000022011766012544016034 0ustar kittermakitterma00000000000000import unittest import doctest import dkim from dkim.tests import test_suite doctest.testmod(dkim) unittest.TextTestRunner().run(test_suite()) dkimpy-0.5.6/PKG-INFO0000664000175000017500000000171712631406324015607 0ustar kittermakitterma00000000000000Metadata-Version: 1.1 Name: dkimpy Version: 0.5.6 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) 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.5.6/dkimsign.py0000664000175000017500000000352311766012544016673 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 len(sys.argv) < 4 or len(sys.argv) > 5: print("Usage: dkimsign.py selector domain privatekeyfile [identity]", 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] if len(sys.argv) > 5: identity = sys.argv[4].encode('ascii') else: identity = None message = sys.stdin.read() try: sig = dkim.sign(message, selector, domain, open(privatekeyfile, "rb").read(), identity = identity) sys.stdout.write(sig) sys.stdout.write(message) except Exception as e: print(e, file=sys.stderr) sys.stdout.write(message)