Merge ~dick-mrns/dkimpy:linesep into dkimpy:master

Proposed by meeuw
Status: Merged
Approved by: Scott Kitterman
Approved revision: 6bc38aaf9b1ed7cae32d21a93f5084c3b3f8a5da
Merge reported by: Scott Kitterman
Merged at revision: 6bc38aaf9b1ed7cae32d21a93f5084c3b3f8a5da
Proposed branch: ~dick-mrns/dkimpy:linesep
Merge into: dkimpy:master
Diff against target: 251 lines (+69/-15)
6 files modified
dkim/__init__.py (+19/-12)
dkim/arcsign.py (+1/-1)
dkim/dkimsign.py (+2/-2)
dkim/tests/test_dkim.py (+16/-0)
dkim/tests/test_util.py (+25/-0)
dkim/util.py (+6/-0)
Reviewer Review Type Date Requested Status
Scott Kitterman Approve
Review via email: mp+361264@code.launchpad.net
To post a comment you must log in.
Revision history for this message
meeuw (dick-mrns) wrote :

On Mon, Dec 17, 2018 at 04:04:13AM -0000, Scott Kitterman wrote:
> I'm fine with the concept, but I think it needs more work.
>
> As far as the change in dkimsign.py, I'd rather see '\r\n' the default of the
> check. As an example, a single long line with no newline at the end should be
> folded the same as before the change.
>
> Arcsign.py will need the same changes.
>
> For the internal dkim/__init__.py changes, I'm fine with the API extension,
> but for class DomainSigner(object), it should be added to the @param list (I
> know there are others missing too, but let's not make it worse).
>
> Finally, it needs a test case in dkim/tests/test_dkim.py, so we know this
> works. It needs to pass on at least python2.7, python3.6, and python3.7.
> Testing python3.3 - 3.5 would be nice.

Will do!

Revision history for this message
Scott Kitterman (kitterman) wrote :

Responding needs fixing until the tests, etc I mentioned in the bug are included.

review: Needs Fixing
Revision history for this message
meeuw (dick-mrns) wrote :

Okay, I think I've fixed all your suggestions, could you please have a look?

Revision history for this message
Scott Kitterman (kitterman) wrote :

Looks good. I'll see about merging this.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/dkim/__init__.py b/dkim/__init__.py
2index a8d3bbe..b0b5721 100644
3--- a/dkim/__init__.py
4+++ b/dkim/__init__.py
5@@ -365,7 +365,7 @@ def text(s):
6 return s.encode('ascii')
7
8
9-def fold(header, namelen=0):
10+def fold(header, namelen=0, linesep=b'\r\n'):
11 """Fold a header line into multiple crlf-separated lines at column 72.
12
13 >>> text(fold(b'foo'))
14@@ -399,10 +399,10 @@ def fold(header, namelen=0):
15 i = header[:maxleng].rfind(b" ")
16 if i == -1:
17 j = maxleng
18- pre += header[:j] + b"\r\n "
19+ pre += header[:j] + linesep + b" "
20 else:
21 j = i + 1
22- pre += header[:i] + b"\r\n "
23+ pre += header[:i] + linesep + b" "
24 header = header[j:]
25 maxleng = 72
26 if len(header) > 2:
27@@ -460,8 +460,9 @@ class DomainSigner(object):
28 #: (with either \\n or \\r\\n line endings)
29 #: @param logger: a logger to which debug info will be written (default None)
30 #: @param signature_algorithm: the signing algorithm to use when signing
31+ #: @param linesep: use this line seperator for folding the headers
32 def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256',
33- minkey=1024):
34+ minkey=1024, linesep=b'\r\n'):
35 self.set_message(message)
36 if logger is None:
37 logger = get_default_logger()
38@@ -482,6 +483,9 @@ class DomainSigner(object):
39 #: Minimum public key size. Shorter keys raise KeyFormatError. The
40 #: default is 1024
41 self.minkey = minkey
42+ # use this line seperator for output
43+ self.linesep = linesep
44+
45
46 #: Header fields to protect from additions by default.
47 #:
48@@ -608,7 +612,7 @@ class DomainSigner(object):
49
50 header_value = b"; ".join(b"=".join(x) for x in fields)
51 if not standardize:
52- header_value = fold(header_value, namelen=len(header_name))
53+ header_value = fold(header_value, namelen=len(header_name), linesep=b'\r\n')
54 header_value = RE_BTAG.sub(b'\\1',header_value)
55 header = (header_name, b' ' + header_value)
56 h = HashThrough(self.hasher())
57@@ -634,10 +638,10 @@ class DomainSigner(object):
58 # relaxed/simple (for broken receivers), and fold now.
59 idx = [i for i in range(len(fields)) if fields[i][0] == b'b'][0]
60 fields[idx] = (b'b', base64.b64encode(bytes(sig2)))
61- header_value = b"; ".join(b"=".join(x) for x in fields) + b"\r\n"
62+ header_value = b"; ".join(b"=".join(x) for x in fields) + self.linesep
63
64 if not standardize:
65- header_value = fold(header_value, namelen=len(header_name))
66+ header_value = fold(header_value, namelen=len(header_name), linesep=self.linesep)
67
68 return header_value
69
70@@ -947,7 +951,7 @@ class ARC(DomainSigner):
71 results_lists = [raw.replace(srv_id + b';', b'').strip() for (raw, parsed) in auth_headers]
72 results_lists = [tags.split(b';') for tags in results_lists]
73 results = [tag.strip() for sublist in results_lists for tag in sublist]
74- auth_results = srv_id + b'; ' + b';\r\n '.join(results)
75+ auth_results = srv_id + b'; ' + (b';' + self.linesep + b' ').join(results)
76
77 # extract cv
78 parsed_auth_results = AuthenticationResultsHeader.parse('Authentication-Results: ' + auth_results.decode('utf-8'))
79@@ -1216,7 +1220,8 @@ class ARC(DomainSigner):
80 def sign(message, selector, domain, privkey, identity=None,
81 canonicalize=(b'relaxed', b'simple'),
82 signature_algorithm=b'rsa-sha256',
83- include_headers=None, length=False, logger=None):
84+ include_headers=None, length=False, logger=None,
85+ linesep=b'\r\n'):
86 # type: (bytes, bytes, bytes, bytes, bytes, tuple, bytes, list, bool, any) -> bytes
87 """Sign an RFC822 message and return the DKIM-Signature header line.
88 @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings)
89@@ -1229,11 +1234,12 @@ def sign(message, selector, domain, privkey, identity=None,
90 @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign)
91 @param length: true if the l= tag should be included to indicate body length (default False)
92 @param logger: a logger to which debug info will be written (default None)
93+ @param linesep: use this line seperator for folding the headers
94 @return: DKIM-Signature header field terminated by \\r\\n
95 @raise DKIMException: when the message, include_headers, or key are badly formed.
96 """
97
98- d = DKIM(message,logger=logger,signature_algorithm=signature_algorithm)
99+ d = DKIM(message,logger=logger,signature_algorithm=signature_algorithm,linesep=linesep)
100 return d.sign(selector, domain, privkey, identity=identity, canonicalize=canonicalize, include_headers=include_headers, length=length)
101
102
103@@ -1260,7 +1266,7 @@ dkim_verify = verify
104 def arc_sign(message, selector, domain, privkey,
105 srv_id, signature_algorithm=b'rsa-sha256',
106 include_headers=None, timestamp=None,
107- logger=None, standardize=False):
108+ logger=None, standardize=False, linesep=b'\r\n'):
109 # type: (bytes, bytes, bytes, bytes, bytes, bytes, list, any, any, bool) -> list
110 """Sign an RFC822 message and return the ARC set header lines for the next instance
111 @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings)
112@@ -1272,11 +1278,12 @@ def arc_sign(message, selector, domain, privkey,
113 @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign)
114 @param timestamp: the time in integer seconds when the message is sealed (default is int(time.time) based on platform, can be string or int)
115 @param logger: a logger to which debug info will be written (default None)
116+ @param linesep: use this line seperator for folding the headers
117 @return: A list containing the ARC set of header fields for the next instance
118 @raise DKIMException: when the message, include_headers, or key are badly formed.
119 """
120
121- a = ARC(message,logger=logger,signature_algorithm=b'rsa-sha256')
122+ a = ARC(message,logger=logger,signature_algorithm=b'rsa-sha256',linesep=linesep)
123 if not include_headers:
124 include_headers = a.default_sign_headers()
125 return a.sign(selector, domain, privkey, srv_id, include_headers=include_headers,
126diff --git a/dkim/arcsign.py b/dkim/arcsign.py
127index 2d668b9..ff5a263 100644
128--- a/dkim/arcsign.py
129+++ b/dkim/arcsign.py
130@@ -59,7 +59,7 @@ def main():
131
132 #try:
133 sig = dkim.arc_sign(message, selector, domain, open(privatekeyfile, "rb").read(),
134- domain + ": none", cv)
135+ domain + ": none", cv, dkim.util.get_linesep(message))
136 for line in sig:
137 sys.stdout.write(line)
138 sys.stdout.write(message)
139diff --git a/dkim/dkimsign.py b/dkim/dkimsign.py
140index 73f9de4..227b8eb 100644
141--- a/dkim/dkimsign.py
142+++ b/dkim/dkimsign.py
143@@ -70,8 +70,8 @@ def main():
144
145 message = sys.stdin.read()
146 try:
147- d = dkim.DKIM(message,logger=logger,
148- signature_algorithm=args.signalg)
149+ d = dkim.DKIM(message,logger=logger, signature_algorithm=args.signalg,
150+ linesep=dkim.util.get_linesep(message))
151 sig = d.sign(args.selector, args.domain, open(
152 args.privatekeyfile, "rb").read(), identity = args.identity,
153 canonicalize=canonicalize, include_headers=include_headers,
154diff --git a/dkim/tests/test_dkim.py b/dkim/tests/test_dkim.py
155index fbe241b..a94937c 100644
156--- a/dkim/tests/test_dkim.py
157+++ b/dkim/tests/test_dkim.py
158@@ -46,6 +46,11 @@ class TestFold(unittest.TestCase):
159 self.assertEqual(
160 b"foo" * 24 + b"\r\n foo", dkim.fold(b"foo" * 25))
161
162+ def test_linesep(self):
163+ self.assertEqual(
164+ b"foo" * 24 + b"\n foo", dkim.fold(b"foo" * 25, linesep=b"\n"))
165+
166+
167
168 class TestSignAndVerify(unittest.TestCase):
169 """End-to-end signature and verification tests."""
170@@ -203,6 +208,17 @@ p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="""
171 res = d.verify(dnsfunc=self.dnsfunc5)
172 self.assertTrue(res)
173
174+ def test_verifies_lflinesep(self):
175+ # A message verifies after being signed.
176+ for header_algo in (b"simple", b"relaxed"):
177+ for body_algo in (b"simple", b"relaxed"):
178+ sig = dkim.sign(
179+ self.message, b"test", b"example.com", self.key,
180+ canonicalize=(header_algo, body_algo), linesep=b"\n")
181+ res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc)
182+ self.assertFalse(b'\r\n' in sig)
183+ self.assertTrue(res)
184+
185 def test_implicit_k(self):
186 # A message verifies after being signed when k= tag is not provided.
187 for header_algo in (b"simple", b"relaxed"):
188diff --git a/dkim/tests/test_util.py b/dkim/tests/test_util.py
189index e01b3b5..bf39566 100644
190--- a/dkim/tests/test_util.py
191+++ b/dkim/tests/test_util.py
192@@ -22,6 +22,7 @@ from dkim.util import (
193 DuplicateTag,
194 InvalidTagSpec,
195 parse_tag_value,
196+ get_linesep,
197 )
198
199
200@@ -75,6 +76,30 @@ class TestParseTagValue(unittest.TestCase):
201 self.assertEqual(len(sig),11)
202
203
204+class TestGetLineSep(unittest.TestCase):
205+ """Line seperator probing tests."""
206+
207+ def test_default(self):
208+ self.assertEqual(
209+ b'\r\n',
210+ get_linesep(b'abc'))
211+
212+ def test_withcrlf(self):
213+ self.assertEqual(
214+ b'\r\n',
215+ get_linesep(b'abc\r\n'))
216+
217+ def test_withlf(self):
218+ self.assertEqual(
219+ b'\n',
220+ get_linesep(b'abc\n'))
221+
222+ def test_toosmall(self):
223+ self.assertEqual(
224+ b'\r\n',
225+ get_linesep(b'a'))
226+
227+
228 def test_suite():
229 from unittest import TestLoader
230 return TestLoader().loadTestsFromName(__name__)
231diff --git a/dkim/util.py b/dkim/util.py
232index 3d1f722..97b59c9 100644
233--- a/dkim/util.py
234+++ b/dkim/util.py
235@@ -33,6 +33,7 @@ __all__ = [
236 'InvalidTagSpec',
237 'InvalidTagValueList',
238 'parse_tag_value',
239+ 'get_linesep',
240 ]
241
242
243@@ -80,3 +81,8 @@ def get_default_logger():
244 if not logger.handlers:
245 logger.addHandler(NullHandler())
246 return logger
247+
248+def get_linesep(msg):
249+ if msg[-2:] != b'\r\n' and msg[-1:] == b'\n':
250+ return b'\n'
251+ return b'\r\n'

Subscribers

People subscribed via source and target branches

to all changes: