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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Kitterman | Approve | ||
Review via email: mp+328210@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
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:/
> 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) |
I removed the stray import sys after merging.