Reapply lost ARC updates

This commit is contained in:
Scott Kitterman
2017-11-18 20:38:47 -05:00
parent 18d95309f8
commit 04b1a8dddf
+85 -20
View File
@@ -38,6 +38,13 @@ import logging
import re import re
import time import time
# only needed for arc
try:
from authres import AuthenticationResultsHeader
except:
pass
from dkim.canonicalization import ( from dkim.canonicalization import (
CanonicalizationPolicy, CanonicalizationPolicy,
InvalidCanonicalizationPolicyError, InvalidCanonicalizationPolicyError,
@@ -141,6 +148,10 @@ class ValidationError(DKIMException):
"""Validation error.""" """Validation error."""
pass pass
class AuthresNotFoundError(DKIMException):
""" Authres Package not installed, needed for ARC """
pass
def select_headers(headers, include_headers): def select_headers(headers, include_headers):
"""Select message header fields to be signed/verified. """Select message header fields to be signed/verified.
@@ -765,27 +776,59 @@ class ARC(DomainSigner):
#: @param selector: the DKIM selector value for the signature #: @param selector: the DKIM selector value for the signature
#: @param domain: the DKIM domain 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 privkey: a PKCS#1 private key in base64-encoded text form
#: @param auth_results: RFC 7601 Authentication-Results header value for the message #: @param srv_id: an srv_id for identitfying AR headers to sign & extract cv from
#: @param chain_validation_status: CV_Pass, CV_Fail, CV_None
#: @param include_headers: a list of strings indicating which headers #: @param include_headers: a list of strings indicating which headers
#: are to be signed (default rfc4871 recommended headers) #: are to be signed (default rfc4871 recommended headers)
#: @return: list of ARC set header fields #: @return: list of ARC set header fields
#: @raise DKIMException: when the message, include_headers, or key are badly #: @raise DKIMException: when the message, include_headers, or key are badly
#: formed. #: formed.
def sign(self, selector, domain, privkey, auth_results, chain_validation_status, def sign(self, selector, domain, privkey, srv_id, include_headers=None,
include_headers=None, timestamp=None, standardize=False): timestamp=None, standardize=False):
# check if authres has been imported
try:
AuthenticationResultsHeader
except:
self.logger.debug("authres package not installed")
raise AuthresNotFoundError
try: try:
pk = parse_pem_private_key(privkey) pk = parse_pem_private_key(privkey)
except UnparsableKeyError as e: except UnparsableKeyError as e:
raise KeyFormatError(str(e)) raise KeyFormatError(str(e))
# extract, parse, filter & group AR headers
ar_headers = [res.strip() for [ar, res] in self.headers if ar == b'Authentication-Results']
grouped_headers = [(res, AuthenticationResultsHeader.parse('Authentication-Results: ' + res.decode('utf-8')))
for res in ar_headers]
auth_headers = [res for res in grouped_headers if res[1].authserv_id == srv_id.decode('utf-8')]
if len(auth_headers) == 0:
self.logger.debug("no AR headers found, chain terminated")
return b''
# consolidate headers
results_lists = [raw.replace(srv_id + b';', b'').strip() for (raw, parsed) in auth_headers]
results_lists = [tags.split(b';') for tags in results_lists]
results = [tag.strip() for sublist in results_lists for tag in sublist]
auth_results = srv_id + b'; ' + b';\r\n '.join(results)
# extract cv
parsed_auth_results = AuthenticationResultsHeader.parse('Authentication-Results: ' + auth_results.decode('utf-8'))
arc_results = [res for res in parsed_auth_results.results if res.method == 'arc']
if len(arc_results) == 0:
self.logger.debug("no AR arc stamps found, chain terminated")
return b''
elif len(arc_results) != 1:
self.logger.debug("multiple AR arc stamps found, failing chain")
chain_validation_status = CV_Fail
else:
chain_validation_status = arc_results[0].result.lower().encode('utf-8')
# Setup headers # Setup headers
if include_headers is None: if include_headers is None:
include_headers = self.default_sign_headers() include_headers = self.default_sign_headers()
if b'arc-authentication-results' not in include_headers:
include_headers.append(b'arc-authentication-results')
include_headers = tuple([x.lower() for x in include_headers]) include_headers = tuple([x.lower() for x in include_headers])
# record what verify should extract # record what verify should extract
@@ -811,7 +854,10 @@ class ARC(DomainSigner):
raise ParameterError("cv=none not allowed on instance %d" % instance) raise ParameterError("cv=none not allowed on instance %d" % instance)
new_arc_set = [] new_arc_set = []
arc_headers = [y for x,y in arc_headers_w_instance] if chain_validation_status != CV_Fail:
arc_headers = [y for x,y in arc_headers_w_instance]
else: # don't include previous sets for a failed/invalid chain
arc_headers = []
# Compute ARC-Authentication-Results # Compute ARC-Authentication-Results
aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results
@@ -869,6 +915,11 @@ class ARC(DomainSigner):
as_include_headers = [x[0].lower() for x in arc_headers] as_include_headers = [x[0].lower() for x in arc_headers]
as_include_headers.reverse() as_include_headers.reverse()
# if our chain is failing or invalid, we only grab the most recent set
# reversing the order of the headers accomplishes this
if chain_validation_status == CV_Fail:
self.headers.reverse()
res = self.gen_header(as_fields, as_include_headers, canon_policy, res = self.gen_header(as_fields, as_include_headers, canon_policy,
b"ARC-Seal", pk, standardize) b"ARC-Seal", pk, standardize)
@@ -887,7 +938,7 @@ class ARC(DomainSigner):
#: @param dnsfunc: an optional function to lookup TXT resource records #: @param dnsfunc: an optional function to lookup TXT resource records
#: for a DNS domain. The default uses dnspython or pydns. #: for a DNS domain. The default uses dnspython or pydns.
#: @return: True if signature verifies or False otherwise #: @return: True if signature verifies or False otherwise
#: @return: three-tuple of (CV Result (CV_Pass, CV_Fail or CV_None), list of #: @return: three-tuple of (CV Result (CV_Pass, CV_Fail, CV_None or None, for a chain that has ended), list of
#: result dictionaries, result reason) #: result dictionaries, result reason)
#: @raise DKIMException: when the message, signature, or key are badly formed #: @raise DKIMException: when the message, signature, or key are badly formed
def verify(self,dnsfunc=get_txt): def verify(self,dnsfunc=get_txt):
@@ -907,10 +958,10 @@ class ARC(DomainSigner):
if not result_data[0]['ams-valid']: if not result_data[0]['ams-valid']:
return CV_Fail, result_data, "Most recent ARC-Message-Signature did not validate" return CV_Fail, result_data, "Most recent ARC-Message-Signature did not validate"
for result in result_data: for result in result_data:
if not result['as-valid']:
return CV_Fail, result_data, "ARC-Seal[%d] did not validate" % result['instance']
if result['cv'] == CV_Fail: if result['cv'] == CV_Fail:
return CV_Fail, result_data, "ARC-Seal[%d] reported failure" % result['instance'] return None, result_data, "ARC-Seal[%d] reported failure, the chain is terminated" % result['instance']
elif not result['as-valid']:
return CV_Fail, result_data, "ARC-Seal[%d] did not validate" % result['instance']
elif (result['instance'] == 1) and (result['cv'] != CV_None): elif (result['instance'] == 1) and (result['cv'] != CV_None):
return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv']) return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv'])
elif (result['instance'] != 1) and (result['cv'] == CV_None): elif (result['instance'] != 1) and (result['cv'] == CV_None):
@@ -977,7 +1028,18 @@ class ARC(DomainSigner):
raise ParameterError("The Arc-Message-Signature MUST NOT sign ARC-Seal") raise ParameterError("The Arc-Message-Signature MUST NOT sign ARC-Seal")
ams_header = (b'ARC-Message-Signature', b' ' + ams_value) ams_header = (b'ARC-Message-Signature', b' ' + ams_value)
ams_valid = self.verify_sig(sig, include_headers, ams_header, dnsfunc)
# we can't use the AMS provided above, as it's already been canonicalized relaxed
# for use in validating the AS. However the AMS is included in the AMS itself,
# and this can use simple canonicalization
raw_ams_header = [(x, y) for (x, y) in self.headers if x.lower() == b'arc-message-signature'][0]
try:
ams_valid = self.verify_sig(sig, include_headers, raw_ams_header, dnsfunc)
except DKIMException as e:
self.logger.error("%s" % e)
ams_valid = False
output['ams-valid'] = ams_valid output['ams-valid'] = ams_valid
self.logger.debug("ams valid: %r" % ams_valid) self.logger.debug("ams valid: %r" % ams_valid)
@@ -998,7 +1060,11 @@ class ARC(DomainSigner):
as_include_headers = [x[0].lower() for x in arc_headers] as_include_headers = [x[0].lower() for x in arc_headers]
as_include_headers.reverse() as_include_headers.reverse()
as_header = (b'ARC-Seal', b' ' + as_value) as_header = (b'ARC-Seal', b' ' + as_value)
as_valid = self.verify_sig(sig, as_include_headers[:-1], as_header, dnsfunc) try:
as_valid = self.verify_sig(sig, as_include_headers[:-1], as_header, dnsfunc)
except DKIMException as e:
self.logger.error("%s" % e)
as_valid = False
output['as-valid'] = as_valid output['as-valid'] = as_valid
self.logger.debug("as valid: %r" % as_valid) self.logger.debug("as valid: %r" % as_valid)
@@ -1045,8 +1111,7 @@ dkim_sign = sign
dkim_verify = verify dkim_verify = verify
def arc_sign(message, selector, domain, privkey, def arc_sign(message, selector, domain, privkey,
auth_results, chain_validation_status, srv_id, signature_algorithm=b'rsa-sha256',
signature_algorithm=b'rsa-sha256',
include_headers=None, timestamp=None, include_headers=None, timestamp=None,
logger=None, standardize=False): logger=None, standardize=False):
"""Sign an RFC822 message and return the ARC set header lines for the next instance """Sign an RFC822 message and return the ARC set header lines for the next instance
@@ -1054,19 +1119,19 @@ def arc_sign(message, selector, domain, privkey,
@param selector: the DKIM selector value for the signature @param selector: the DKIM selector value for the signature
@param domain: the DKIM domain 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 privkey: a PKCS#1 private key in base64-encoded text form
@param auth_results: the RFC 7601 authentication-results header field value for this instance @param srv_id: the authserv_id used to identify the ADMD's AR headers
@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
@param signature_algorithm: the signing algorithm to use when signing @param signature_algorithm: the signing algorithm to use when signing
@param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign) @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign)
@param logger: a logger to which debug info will be written (default None) @param logger: a logger to which debug info will be written (default None)
@return: A list containing the ARC set of header fields for the next instance @return: A list containing the ARC set of header fields for the next instance
@raise DKIMException: when the message, include_headers, or key are badly formed. @raise DKIMException: when the message, include_headers, or key are badly formed.
""" """
a = ARC(message,logger=logger,signature_algorithm=signature_algorithm) a = ARC(message,logger=logger,signature_algorithm=signature_algorithm)
if not include_headers: if not include_headers:
include_headers = a.default_sign_headers() include_headers = a.default_sign_headers()
return a.sign(selector, domain, privkey, auth_results, chain_validation_status, return a.sign(selector, domain, privkey, srv_id, include_headers=include_headers,
include_headers=include_headers, timestamp=timestamp, standardize=standardize) timestamp=timestamp, standardize=standardize)
def arc_verify(message, logger=None, dnsfunc=get_txt, minkey=1024): def arc_verify(message, logger=None, dnsfunc=get_txt, minkey=1024):
"""Verify the ARC chain on an RFC822 formatted message. """Verify the ARC chain on an RFC822 formatted message.