From c6523a3d72e766560c5a8af2cf43d7760b9d76ba Mon Sep 17 00:00:00 2001 From: Stuart D Gathman Date: Sat, 21 Feb 2015 19:34:20 -0500 Subject: [PATCH] Merges some of the changes from Diane Trout to pass unit tests in python 2/3. --- dkim/__init__.py | 78 ++++++++++++++++++++++++----------------- dkim/tests/test_dkim.py | 14 ++++---- dkim/tests/test_util.py | 4 +-- 3 files changed, 55 insertions(+), 41 deletions(-) diff --git a/dkim/__init__.py b/dkim/__init__.py index 500c727..8a6e330 100644 --- a/dkim/__init__.py +++ b/dkim/__init__.py @@ -119,8 +119,8 @@ def select_headers(headers, include_headers): return sign_headers # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space [RFC5322] -FWS = r'(?:(?:\s*\r?\n)?\s+)?' -RE_BTAG = re.compile(r'([;\s]b'+FWS+r'=)(?:'+FWS+r'[a-zA-Z0-9+/=])*(?:\r?\n\Z)?') +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): @@ -166,7 +166,7 @@ def validate_signature_fields(sig): 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['l']) is None: + 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": @@ -176,20 +176,20 @@ def validate_signature_fields(sig): 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']) + 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']) + 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)" % @@ -224,16 +224,28 @@ def rfc822_parse(message): 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. - >>> fold(b'foo') + >>> text(fold(b'foo')) 'foo' - >>> fold(b'foo '+b'foo'*24).splitlines()[0] + >>> text(fold(b'foo '+b'foo'*24).splitlines()[0]) 'foo ' - >>> fold(b'foo'*25).splitlines()[-1] + >>> text(fold(b'foo'*25).splitlines()[-1]) ' foo' >>> len(fold(b'foo'*25).splitlines()[0]) 72 @@ -268,31 +280,31 @@ class DKIM(object): #: be in the default FROZEN list, but that could also make signatures #: more fragile than necessary. #: @since: 0.5 - RFC5322_SINGLETON = ('date','from','sender','reply-to','to','cc','bcc', - 'message-id','in-reply-to','references') + 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 = ('from','date','subject') + FROZEN = (b'from',b'date',b'subject') #: The rfc4871 recommended header fields to sign #: @since: 0.5 SHOULD = ( - 'sender', 'reply-to', 'subject', 'date', 'message-id', 'to', 'cc', - 'mime-version', 'content-type', 'content-transfer-encoding', 'content-id', - 'content- description', 'resent-date', 'resent-from', 'resent-sender', - 'resent-to', 'resent-cc', 'resent-message-id', 'in-reply-to', 'references', - 'list-id', 'list-help', 'list-unsubscribe', 'list-subscribe', 'list-post', - 'list-owner', 'list-archive' + 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 = ( - 'return-path', 'received', 'comments', 'keywords', 'bcc', 'resent-bcc', - 'dkim-signature' + 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. @@ -331,7 +343,7 @@ class DKIM(object): >>> dkim = DKIM() >>> dkim.add_frozen(DKIM.RFC5322_SINGLETON) - >>> sorted(dkim.frozen_sign) + >>> [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 @@ -430,7 +442,7 @@ class DKIM(object): include_headers = self.default_sign_headers() # rfc4871 says FROM is required - if 'from' not in ( x.lower() for x in include_headers ): + 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 @@ -552,6 +564,8 @@ class DKIM(object): 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) @@ -568,8 +582,8 @@ class DKIM(object): # 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 'from' in include_headers: - include_headers.append('from') + 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) diff --git a/dkim/tests/test_dkim.py b/dkim/tests/test_dkim.py index f59837b..ea79317 100644 --- a/dkim/tests/test_dkim.py +++ b/dkim/tests/test_dkim.py @@ -108,14 +108,14 @@ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" # # Simple-mode signature header verification is wrong # (should ignore FWS anywhere in signature tag: b=) - sample_msg = """\ + sample_msg = b"""\ From: mbp@canonical.com To: scottk@example.com Subject: this is my test message -""".replace('\n', '\r\n') +""".replace(b'\n', b'\r\n') - sample_privkey = """\ + sample_privkey = b"""\ -----BEGIN RSA PRIVATE KEY----- MIIBOwIBAAJBANmBe10IgY+u7h3enWTukkqtUD5PR52Tb/mPfjC0QJTocVBq6Za/ PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQJAYFUKsD+uMlcFu1D3YNaR @@ -136,7 +136,7 @@ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ== for header_mode in [dkim.Relaxed, dkim.Simple]: - dkim_header = dkim.sign(sample_msg, 'example', 'canonical.com', + 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 @@ -173,7 +173,7 @@ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ== for body_algo in (b"simple", b"relaxed"): d = dkim.DKIM(message) # bug requires a repeated header to manifest - d.should_not_sign.remove('received') + 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)) @@ -220,8 +220,8 @@ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ== identity = None try: sig = dkim.sign(message, selector, domain, read_test_data('test.private'), identity = identity) - except dkim.ParameterError as sigerror: - pass + except dkim.ParameterError as x: + sigerror = True self.assertTrue(sigerror) def test_suite(): diff --git a/dkim/tests/test_util.py b/dkim/tests/test_util.py index 85e8106..2da48ea 100644 --- a/dkim/tests/test_util.py +++ b/dkim/tests/test_util.py @@ -62,7 +62,7 @@ class TestParseTagValue(unittest.TestCase): DuplicateTag, parse_tag_value, b'foo=bar;foo=baz') def test_trailing_whitespace(self): - hval = '''v=1; a=rsa-sha256; d=facebookmail.com; s=s1024-2011-q2; c=relaxed/simple; + 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=; @@ -71,7 +71,7 @@ class TestParseTagValue(unittest.TestCase): 3KzW0yB9JHwiDCw1EioVkv+OMHhAYzoIypA0bQyi2bc=; ''' sig = parse_tag_value(hval) - self.assertEquals(sig[b't'],'1308078492') + self.assertEquals(sig[b't'],b'1308078492') self.assertEquals(len(sig),11)