Merge lp:~valimail/dkimpy/gene into lp:dkimpy

Proposed by Scott Kitterman
Status: Merged
Merge reported by: Scott Kitterman
Merged at revision: not available
Proposed branch: lp:~valimail/dkimpy/gene
Merge into: lp:dkimpy
Diff against target: 212 lines (+86/-20)
1 file modified
dkim/__init__.py (+86/-20)
To merge this branch: bzr merge lp:~valimail/dkimpy/gene
Reviewer Review Type Date Requested Status
Scott Kitterman Approve
Review via email: mp+328210@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Scott Kitterman (kitterman) wrote :

I removed the stray import sys after merging.

review: Approve
Revision history for this message
Gene Shuman (geneshuman) wrote :

Great, thanks again. AFAIK, dkimpy is now 100% up to arc spec compliance.

On Thu, Jul 27, 2017 at 10:42 PM, Scott Kitterman <email address hidden>
wrote:

> Review: Approve
>
> I removed the stray import sys after merging.
> --
> https://code.launchpad.net/~valimail/dkimpy/gene/+merge/328210
> Your team valimail is subscribed to branch lp:~valimail/dkimpy/gene.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dkim/__init__.py'
2--- dkim/__init__.py 2017-06-23 22:29:37 +0000
3+++ dkim/__init__.py 2017-07-28 05:26:35 +0000
4@@ -37,6 +37,14 @@
5 import logging
6 import re
7 import time
8+import sys
9+
10+# only needed for arc
11+try:
12+ from authres import AuthenticationResultsHeader
13+except:
14+ pass
15+
16
17 from dkim.canonicalization import (
18 CanonicalizationPolicy,
19@@ -142,6 +150,10 @@
20 """Validation error."""
21 pass
22
23+class AuthresNotFoundError(DKIMException):
24+ """ Authres Package not installed, needed for ARC """
25+ pass
26+
27 def select_headers(headers, include_headers):
28 """Select message header fields to be signed/verified.
29
30@@ -776,27 +788,59 @@
31 #: @param selector: the DKIM selector value for the signature
32 #: @param domain: the DKIM domain value for the signature
33 #: @param privkey: a PKCS#1 private key in base64-encoded text form
34- #: @param auth_results: RFC 7601 Authentication-Results header value for the message
35- #: @param chain_validation_status: CV_Pass, CV_Fail, CV_None
36+ #: @param srv_id: an srv_id for identitfying AR headers to sign & extract cv from
37 #: @param include_headers: a list of strings indicating which headers
38 #: are to be signed (default rfc4871 recommended headers)
39 #: @return: list of ARC set header fields
40 #: @raise DKIMException: when the message, include_headers, or key are badly
41 #: formed.
42- def sign(self, selector, domain, privkey, auth_results, chain_validation_status,
43- include_headers=None, timestamp=None, standardize=False):
44+ def sign(self, selector, domain, privkey, srv_id, include_headers=None,
45+ timestamp=None, standardize=False):
46+
47+ # check if authres has been imported
48+ try:
49+ AuthenticationResultsHeader
50+ except:
51+ self.logger.debug("authres package not installed")
52+ raise AuthresNotFoundError
53+
54 try:
55 pk = parse_pem_private_key(privkey)
56 except UnparsableKeyError as e:
57 raise KeyFormatError(str(e))
58
59+ # extract, parse, filter & group AR headers
60+ ar_headers = [res.strip() for [ar, res] in self.headers if ar == b'Authentication-Results']
61+ grouped_headers = [(res, AuthenticationResultsHeader.parse('Authentication-Results: ' + res.decode('utf-8')))
62+ for res in ar_headers]
63+ auth_headers = [res for res in grouped_headers if res[1].authserv_id == srv_id.decode('utf-8')]
64+
65+ if len(auth_headers) == 0:
66+ self.logger.debug("no AR headers found, chain terminated")
67+ return b''
68+
69+ # consolidate headers
70+ results_lists = [raw.replace(srv_id + b';', b'').strip() for (raw, parsed) in auth_headers]
71+ results_lists = [tags.split(b';') for tags in results_lists]
72+ results = [tag.strip() for sublist in results_lists for tag in sublist]
73+ auth_results = srv_id + b'; ' + b';\r\n '.join(results)
74+
75+ # extract cv
76+ parsed_auth_results = AuthenticationResultsHeader.parse('Authentication-Results: ' + auth_results.decode('utf-8'))
77+ arc_results = [res for res in parsed_auth_results.results if res.method == 'arc']
78+ if len(arc_results) == 0:
79+ self.logger.debug("no AR arc stamps found, chain terminated")
80+ return b''
81+ elif len(arc_results) != 1:
82+ self.logger.debug("multiple AR arc stamps found, failing chain")
83+ chain_validation_status = CV_Fail
84+ else:
85+ chain_validation_status = arc_results[0].result.lower().encode('utf-8')
86+
87 # Setup headers
88 if include_headers is None:
89 include_headers = self.default_sign_headers()
90
91- if b'arc-authentication-results' not in include_headers:
92- include_headers.append(b'arc-authentication-results')
93-
94 include_headers = tuple([x.lower() for x in include_headers])
95
96 # record what verify should extract
97@@ -822,7 +866,10 @@
98 raise ParameterError("cv=none not allowed on instance %d" % instance)
99
100 new_arc_set = []
101- arc_headers = [y for x,y in arc_headers_w_instance]
102+ if chain_validation_status != CV_Fail:
103+ arc_headers = [y for x,y in arc_headers_w_instance]
104+ else: # don't include previous sets for a failed/invalid chain
105+ arc_headers = []
106
107 # Compute ARC-Authentication-Results
108 aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results
109@@ -880,6 +927,11 @@
110 as_include_headers = [x[0].lower() for x in arc_headers]
111 as_include_headers.reverse()
112
113+ # if our chain is failing or invalid, we only grab the most recent set
114+ # reversing the order of the headers accomplishes this
115+ if chain_validation_status == CV_Fail:
116+ self.headers.reverse()
117+
118 res = self.gen_header(as_fields, as_include_headers, canon_policy,
119 b"ARC-Seal", pk, standardize)
120
121@@ -898,7 +950,7 @@
122 #: @param dnsfunc: an optional function to lookup TXT resource records
123 #: for a DNS domain. The default uses dnspython or pydns.
124 #: @return: True if signature verifies or False otherwise
125- #: @return: three-tuple of (CV Result (CV_Pass, CV_Fail or CV_None), list of
126+ #: @return: three-tuple of (CV Result (CV_Pass, CV_Fail, CV_None or None, for a chain that has ended), list of
127 #: result dictionaries, result reason)
128 #: @raise DKIMException: when the message, signature, or key are badly formed
129 def verify(self,dnsfunc=get_txt):
130@@ -918,10 +970,10 @@
131 if not result_data[0]['ams-valid']:
132 return CV_Fail, result_data, "Most recent ARC-Message-Signature did not validate"
133 for result in result_data:
134- if not result['as-valid']:
135+ if result['cv'] == CV_Fail:
136+ return None, result_data, "ARC-Seal[%d] reported failure, the chain is terminated" % result['instance']
137+ elif not result['as-valid']:
138 return CV_Fail, result_data, "ARC-Seal[%d] did not validate" % result['instance']
139- if result['cv'] == CV_Fail:
140- return CV_Fail, result_data, "ARC-Seal[%d] reported failure" % result['instance']
141 elif (result['instance'] == 1) and (result['cv'] != CV_None):
142 return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv'])
143 elif (result['instance'] != 1) and (result['cv'] == CV_None):
144@@ -988,7 +1040,18 @@
145 raise ParameterError("The Arc-Message-Signature MUST NOT sign ARC-Seal")
146
147 ams_header = (b'ARC-Message-Signature', b' ' + ams_value)
148- ams_valid = self.verify_sig(sig, include_headers, ams_header, dnsfunc)
149+
150+
151+ # we can't use the AMS provided above, as it's already been canonicalized relaxed
152+ # for use in validating the AS. However the AMS is included in the AMS itself,
153+ # and this can use simple canonicalization
154+ raw_ams_header = [(x, y) for (x, y) in self.headers if x.lower() == b'arc-message-signature'][0]
155+
156+ try:
157+ ams_valid = self.verify_sig(sig, include_headers, raw_ams_header, dnsfunc)
158+ except DKIMException as e:
159+ self.logger.error("%s" % e)
160+ ams_valid = False
161
162 output['ams-valid'] = ams_valid
163 self.logger.debug("ams valid: %r" % ams_valid)
164@@ -1009,7 +1072,11 @@
165 as_include_headers = [x[0].lower() for x in arc_headers]
166 as_include_headers.reverse()
167 as_header = (b'ARC-Seal', b' ' + as_value)
168- as_valid = self.verify_sig(sig, as_include_headers[:-1], as_header, dnsfunc)
169+ try:
170+ as_valid = self.verify_sig(sig, as_include_headers[:-1], as_header, dnsfunc)
171+ except DKIMException as e:
172+ self.logger.error("%s" % e)
173+ as_valid = False
174
175 output['as-valid'] = as_valid
176 self.logger.debug("as valid: %r" % as_valid)
177@@ -1056,8 +1123,7 @@
178 dkim_verify = verify
179
180 def arc_sign(message, selector, domain, privkey,
181- auth_results, chain_validation_status,
182- signature_algorithm=b'rsa-sha256',
183+ srv_id, signature_algorithm=b'rsa-sha256',
184 include_headers=None, timestamp=None,
185 logger=None, standardize=False):
186 """Sign an RFC822 message and return the ARC set header lines for the next instance
187@@ -1065,19 +1131,19 @@
188 @param selector: the DKIM selector value for the signature
189 @param domain: the DKIM domain value for the signature
190 @param privkey: a PKCS#1 private key in base64-encoded text form
191- @param auth_results: the RFC 7601 authentication-results header field value for this instance
192- @param chain_validation_status: the validation status of the existing chain on the message (P (pass), F (fail)) or N (none) for no existing chain
193+ @param srv_id: the authserv_id used to identify the ADMD's AR headers
194 @param signature_algorithm: the signing algorithm to use when signing
195 @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign)
196 @param logger: a logger to which debug info will be written (default None)
197 @return: A list containing the ARC set of header fields for the next instance
198 @raise DKIMException: when the message, include_headers, or key are badly formed.
199 """
200+
201 a = ARC(message,logger=logger,signature_algorithm=signature_algorithm)
202 if not include_headers:
203 include_headers = a.default_sign_headers()
204- return a.sign(selector, domain, privkey, auth_results, chain_validation_status,
205- include_headers=include_headers, timestamp=timestamp, standardize=standardize)
206+ return a.sign(selector, domain, privkey, srv_id, include_headers=include_headers,
207+ timestamp=timestamp, standardize=standardize)
208
209 def arc_verify(message, logger=None, dnsfunc=get_txt, minkey=1024):
210 """Verify the ARC chain on an RFC822 formatted message.
211
212=== modified file 'setup.py' (properties changed: -x to +x)

Subscribers

People subscribed via source and target branches