From 01a2f7748157399238f35ae01914ccae63f8f13e Mon Sep 17 00:00:00 2001 From: Scott Kitterman Date: Sat, 18 Nov 2017 20:17:35 -0500 Subject: [PATCH] Revert a bunch of rsafp stuff now that DCRUP isn't going to do rsafp --- ChangeLog | 19 ++---- README | 3 - dkim/__init__.py | 126 ++++++++----------------------------- dkim/crypto.py | 10 --- dkim/tests/__init__.py | 2 - dkim/tests/data/testfp.dns | 1 - dkim/tests/data/testfp.key | 27 -------- dkim/tests/data/testfp.pub | 1 - dkimsign.py | 6 +- setup.py | 0 10 files changed, 32 insertions(+), 163 deletions(-) delete mode 100644 dkim/tests/data/testfp.dns delete mode 100644 dkim/tests/data/testfp.key delete mode 100644 dkim/tests/data/testfp.pub mode change 100755 => 100644 setup.py diff --git a/ChangeLog b/ChangeLog index 54ae92e..0d5f113 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,21 +1,14 @@ UNRELEASED Version 0.7.0 - - Updated ARC implementation to support https://tools.ietf.org/html/ - draft-ietf-dmarc-arc-protocol-08 - - Authres module now required for ARC - - Ported dkimsign.py to use argparse; now gives standard usage message and + - Port dkimsign.py to use argparse; now gives standard usage message and is more extensible - - Added command line options to dkimsign.py to select header and body + - Add command line options to dkimsign.py to select header and body canonicalization algorithmns (LP: #1272724) - - Added command line option to dkimsign.py to select signing algorithm - - For dknewkey.py made default to include h=sha256 in the DNS record to + - Add command line option to dkimsign.py to select signing algorithm + - For dknewkey.py make default to include h=sha256 in the DNS record to exclude usage with sha1. Can be overriden. - - Updated dknewkey.py to use argparse. Add --ktype option to specify + - Update dknewkey.py to use argparse. Add --ktype option to specify different key type options in anticipation of the DCRUP WG output. - - Added generation of rsafp DNS records per - draft-ietf-dcrup-dkim-crypto-02 - - Added generation of rsafp DKIM signatures per - draft-ietf-dcrup-dkim-crypto-02 - + - Add generation of rsafp DNS records per draft-ietf-dcrup-dkim-crypto-02 2017-05-30 Version 0.6.2 - Fixed problem with header folding that caused the first line to be folded too long (Updated test test_add_body_length since l= tag is no diff --git a/README b/README index 082f3d0..3047946 100644 --- a/README +++ b/README @@ -20,7 +20,6 @@ REQUIREMENTS and python 3.1 - 3.3. - dnspython or pydns. dnspython is preferred if both are present. - argparse. Standard library in python2.7 and later. - - authres. Needed only for ARC. INSTALLATION @@ -96,8 +95,6 @@ Received Chain): https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-01 -Updated to support -08 for 0.7. - This new functionality is marked experimental because the protocol is still under development. There are no guarantees about API stability or compatibility. diff --git a/dkim/__init__.py b/dkim/__init__.py index c711530..7d3b75b 100644 --- a/dkim/__init__.py +++ b/dkim/__init__.py @@ -38,13 +38,6 @@ import logging import re import time -# only needed for arc -try: - from authres import AuthenticationResultsHeader -except: - pass - - from dkim.canonicalization import ( CanonicalizationPolicy, InvalidCanonicalizationPolicyError, @@ -56,7 +49,6 @@ from dkim.crypto import ( HASH_ALGORITHMS, parse_pem_private_key, parse_public_key, - get_rsa_pubkey, RSASSA_PKCS1_v1_5_sign, RSASSA_PKCS1_v1_5_verify, UnparsableKeyError, @@ -149,10 +141,6 @@ class ValidationError(DKIMException): """Validation error.""" pass -class AuthresNotFoundError(DKIMException): - """ Authres Package not installed, needed for ARC """ - pass - def select_headers(headers, include_headers): """Select message header fields to be signed/verified. @@ -373,7 +361,7 @@ class DomainSigner(object): #: @param logger: a logger to which debug info will be written (default None) #: @param signature_algorithm: the signing algorithm to use when signing def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256', - minkey=1024,ktype=b'rsa'): + minkey=1024): self.set_message(message) if logger is None: logger = get_default_logger() @@ -394,8 +382,6 @@ class DomainSigner(object): #: Minimum public key size. Shorter keys raise KeyFormatError. The #: default is 1024 self.minkey = minkey - #: Key type to use. rsa, rsafp (DCRUP) - self.ktype = ktype #: Header fields to protect from additions by default. #: @@ -634,16 +620,12 @@ class DKIM(DomainSigner): #: @raise DKIMException: when the message, include_headers, or key are badly #: formed. def sign(self, selector, domain, privkey, identity=None, - canonicalize=(b'relaxed',b'simple'), include_headers=None, - length=False): + canonicalize=(b'relaxed',b'simple'), include_headers=None, length=False): try: pk = parse_pem_private_key(privkey) except UnparsableKeyError as e: raise KeyFormatError(str(e)) - if self.ktype == b'rsafp': - pubkey = get_rsa_pubkey(privkey) - else: - pubkey = False + if identity is not None and not identity.endswith(domain): raise ParameterError("identity must end with domain") @@ -671,13 +653,10 @@ class DKIM(DomainSigner): h = self.hasher() h.update(body) bodyhash = base64.b64encode(h.digest()) - if self.ktype == b'rsafp': - atag = b'rsafp-sha256' - else: - atag = self.signature_algorithm + sigfields = [x for x in [ (b'v', b"1"), - (b'a', atag), + (b'a', self.signature_algorithm), (b'c', canon_policy.to_c_value()), (b'd', domain), (b'i', identity or b"@"+domain), @@ -686,7 +665,6 @@ class DKIM(DomainSigner): (b's', selector), (b't', str(int(time.time())).encode('ascii')), (b'h', b" : ".join(include_headers)), - pubkey and (b'k', pubkey), (b'bh', bodyhash), # Force b= to fold onto it's own line so that refolding after # adding sig doesn't change whitespace for previous tags. @@ -787,59 +765,27 @@ class ARC(DomainSigner): #: @param selector: the DKIM selector 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 srv_id: an srv_id for identitfying AR headers to sign & extract cv from + #: @param auth_results: RFC 7601 Authentication-Results header value for the message + #: @param chain_validation_status: CV_Pass, CV_Fail, CV_None #: @param include_headers: a list of strings indicating which headers #: are to be signed (default rfc4871 recommended headers) #: @return: list of ARC set header fields #: @raise DKIMException: when the message, include_headers, or key are badly #: formed. - def sign(self, selector, domain, privkey, srv_id, include_headers=None, - timestamp=None, standardize=False): - - # check if authres has been imported - try: - AuthenticationResultsHeader - except: - self.logger.debug("authres package not installed") - raise AuthresNotFoundError - + def sign(self, selector, domain, privkey, auth_results, chain_validation_status, + include_headers=None, timestamp=None, standardize=False): try: pk = parse_pem_private_key(privkey) except UnparsableKeyError as 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 if include_headers is None: 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]) # record what verify should extract @@ -865,10 +811,7 @@ class ARC(DomainSigner): raise ParameterError("cv=none not allowed on instance %d" % instance) new_arc_set = [] - 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 = [] + arc_headers = [y for x,y in arc_headers_w_instance] # Compute ARC-Authentication-Results aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results @@ -926,11 +869,6 @@ class ARC(DomainSigner): as_include_headers = [x[0].lower() for x in arc_headers] 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, b"ARC-Seal", pk, standardize) @@ -949,7 +887,7 @@ class ARC(DomainSigner): #: @param dnsfunc: an optional function to lookup TXT resource records #: for a DNS domain. The default uses dnspython or pydns. #: @return: True if signature verifies or False otherwise - #: @return: three-tuple of (CV Result (CV_Pass, CV_Fail, CV_None or None, for a chain that has ended), list of + #: @return: three-tuple of (CV Result (CV_Pass, CV_Fail or CV_None), list of #: result dictionaries, result reason) #: @raise DKIMException: when the message, signature, or key are badly formed def verify(self,dnsfunc=get_txt): @@ -969,10 +907,10 @@ class ARC(DomainSigner): if not result_data[0]['ams-valid']: return CV_Fail, result_data, "Most recent ARC-Message-Signature did not validate" for result in result_data: - if result['cv'] == CV_Fail: - return None, result_data, "ARC-Seal[%d] reported failure, the chain is terminated" % result['instance'] - elif not result['as-valid']: + if not result['as-valid']: return CV_Fail, result_data, "ARC-Seal[%d] did not validate" % result['instance'] + if result['cv'] == CV_Fail: + return CV_Fail, result_data, "ARC-Seal[%d] reported failure" % result['instance'] 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']) elif (result['instance'] != 1) and (result['cv'] == CV_None): @@ -1039,18 +977,7 @@ class ARC(DomainSigner): raise ParameterError("The Arc-Message-Signature MUST NOT sign ARC-Seal") ams_header = (b'ARC-Message-Signature', b' ' + ams_value) - - - # 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 + ams_valid = self.verify_sig(sig, include_headers, ams_header, dnsfunc) output['ams-valid'] = ams_valid self.logger.debug("ams valid: %r" % ams_valid) @@ -1071,11 +998,7 @@ class ARC(DomainSigner): as_include_headers = [x[0].lower() for x in arc_headers] as_include_headers.reverse() as_header = (b'ARC-Seal', b' ' + as_value) - 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 + as_valid = self.verify_sig(sig, as_include_headers[:-1], as_header, dnsfunc) output['as-valid'] = as_valid self.logger.debug("as valid: %r" % as_valid) @@ -1122,7 +1045,8 @@ dkim_sign = sign dkim_verify = verify def arc_sign(message, selector, domain, privkey, - srv_id, signature_algorithm=b'rsa-sha256', + auth_results, chain_validation_status, + signature_algorithm=b'rsa-sha256', include_headers=None, timestamp=None, logger=None, standardize=False): """Sign an RFC822 message and return the ARC set header lines for the next instance @@ -1130,19 +1054,19 @@ def arc_sign(message, selector, domain, privkey, @param selector: the DKIM selector 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 srv_id: the authserv_id used to identify the ADMD's AR headers + @param auth_results: the RFC 7601 authentication-results header field value for this instance + @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 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) @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. """ - a = ARC(message,logger=logger,signature_algorithm=signature_algorithm) if not include_headers: include_headers = a.default_sign_headers() - return a.sign(selector, domain, privkey, srv_id, include_headers=include_headers, - timestamp=timestamp, standardize=standardize) + return a.sign(selector, domain, privkey, auth_results, chain_validation_status, + include_headers=include_headers, timestamp=timestamp, standardize=standardize) def arc_verify(message, logger=None, dnsfunc=get_txt, minkey=1024): """Verify the ARC chain on an RFC822 formatted message. diff --git a/dkim/crypto.py b/dkim/crypto.py index 41e1da0..c46c97a 100644 --- a/dkim/crypto.py +++ b/dkim/crypto.py @@ -25,7 +25,6 @@ __all__ = [ 'parse_pem_private_key', 'parse_private_key', 'parse_public_key', - 'get_rsa_pubkey', 'RSASSA_PKCS1_v1_5_sign', 'RSASSA_PKCS1_v1_5_verify', 'UnparsableKeyError', @@ -34,7 +33,6 @@ __all__ = [ import base64 import hashlib import re -import rsa from dkim.asn1 import ( ASN1FormatError, @@ -235,14 +233,6 @@ def rsa_decrypt(message, pk, mlen): return int2str(m2 + h * pk['prime2'], mlen) -def get_rsa_pubkey(privkey): - """Extract RSA public key from PEM encoded RSA private key for use with - rsafp. Returns base64 encoded public key suitable for use in DKIM key - records. Using python-rsa instead of making a stack of custom code. - @since: 0.7""" - pkobj = rsa.PrivateKey.load_pkcs1(privkey, 'PEM') - pubobj = rsa.key.PublicKey(pkobj.n, pkobj.e) - return(base64.b64encode(rsa.PublicKey.save_pkcs1(pubobj, 'DER'))) def rsa_encrypt(message, pk, mlen): """Perform RSA encryption/verification diff --git a/dkim/tests/__init__.py b/dkim/tests/__init__.py index 8b1b04e..40cb64a 100644 --- a/dkim/tests/__init__.py +++ b/dkim/tests/__init__.py @@ -28,7 +28,6 @@ def test_suite(): test_canonicalization, test_crypto, test_dkim, - test_rsafp, test_util, test_arc, test_dnsplug, @@ -37,7 +36,6 @@ def test_suite(): test_canonicalization, test_crypto, test_dkim, - test_rsafp, test_util, test_arc, test_dnsplug, diff --git a/dkim/tests/data/testfp.dns b/dkim/tests/data/testfp.dns deleted file mode 100644 index 86d8786..0000000 --- a/dkim/tests/data/testfp.dns +++ /dev/null @@ -1 +0,0 @@ -k=rsafp; p=VO1v0Ybphw9AlrLvHB2ly/x6RD/1zJxEhYeWT/v/RtY= diff --git a/dkim/tests/data/testfp.key b/dkim/tests/data/testfp.key deleted file mode 100644 index e54914a..0000000 --- a/dkim/tests/data/testfp.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0EGLyxVqSMvpQ/THu4XH+0sbqQrnGRu36/TBcsJc7p5vGKIY -f7lfAiaxceGGVX1NzBoE8SEqmLlNuRtdHSsnItC82m5hGDhTec8ZcGonfnOCPJvd -Dou8mDXhtX9knES4PC/Uup3HL84RnsXZ0GWpUtB+fIsHlZryFjsTJtOZVdJSH5H5 -B0arJCitLVJqPqF5bluIVuXmwXD2Wi0hQkpmSm+SJJQ1efTkpK2UYkJvu8cMF7aA -7Lgxzimatqg4oDg9ED5VCHFz82wacjNrnQRnezeOcWF8q76dfCDo5MLUO+CWChFC -H897B6kjMmCo3IavLTHB8Dc7q70I8lxn2qAZzwIDAQABAoIBAQCU+R5oExSpqHxg -KV0rbqLEx1CtFuiD1Ik32Cj9z72s0FcGlwXRcChfoJA0t7BhvTYGt+IrH5+aCoxn -ywRL1k+znwBJfMYmDjzgmN5IxRclZLmYY6K5QgqSE5E3RT8SbjIgk9KoAC+5qb5/ -BmcupVp/rDefHdE6GtHsRywHDTzV30f8zXb/51w30FkcSQS85v8W4VbyqpGcAgq1 -ah1gEuSd7HtxQ4HWyvFiMdw+0bBh8gt14Dh3nq8i6tDhlPtykbM4etm16fcpl5cz -DOg5srIOKxW4vGOng68v6KQ/gz8/BQMAPbZHgFd5l2grejC9p27hwTYer909AfEO -EUcn5oJhAoGBAPVoqvcqA4AL3G46SrYeAnXz/bpaPiBhKat1YgnPQc+5cuGauJIN -YtiCv5xza1u8Gc5rRfEn/41mSpDAXcix0d95qpUAToNVRJUA23e28EoKGL8rWaXF -lc2IecGNI7HF7XL0V+BF59dAPH4q44mKval6U/wTo0hDAFfBnRy0V03RAoGBANk+ -Z9G7dYaQpw/nCxVouCeGOX4LECt4m4Y+LIhFPqI8mnTM8S97dnySW3OpafwF6RIk -UDMzXSJc6BPOMEk9WVI+1ztKXssjbvVa3fSHZEBMP9d1xFJw4/SRSp3uFjGzLa4u -pea8DFRz5jBR2uyGh0+/1E9v+hem7WD6pwJGwrWfAoGAVeiEIO4GN6bvTW7+hG5Q -8jWtlrTMls0spyb6YPC62xrSSoO9JPOmrKBorz4AUSax6f7Hhzo3TaqHY9DTg9Qr -4g1XV725vmP1FCwup+PUjjamnxVv1oYqgL7L0KO14R+mld1PfeU62bFU+93LtXRq -HJAEInRFbqB12EKg21GOVmECgYA6Lox/j0UalQMpLye4xCMN8tTS/QOHoPWGLoCe -vmzX5oM3ZOvzW42QL2Jux6Cq7qpNQYx5Kfh3i4pcQ4yLEPMrI8lhB7n7jbHZ5Ewt -gVVIIyO2AMRjj/h9N6xUP7+R7/r7+2JTOWnT9HZh2opXbnAu26Fw5PyF+R76Kicw -ZOMg4QKBgQDMAJ9nENCiZw7CkYRm+z5UoMeSR3jwEPj1gK3KKhIYWeOw99i2XmBt -1aN0UgAeEidleguqeZWxVXJkdVqvxfAPqpPfhGneN5GgEh2gOfFTDrvHhqoMblsw -uuNlIixfy4w1ENApV2n7qcBzptMHV2uPDUHCctAZ9+ACg7nkiJEaCw== ------END RSA PRIVATE KEY----- diff --git a/dkim/tests/data/testfp.pub b/dkim/tests/data/testfp.pub deleted file mode 100644 index 911c4eb..0000000 --- a/dkim/tests/data/testfp.pub +++ /dev/null @@ -1 +0,0 @@ -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0EGLyxVqSMvpQ/THu4XH+0sbqQrnGRu36/TBcsJc7p5vGKIYf7lfAiaxceGGVX1NzBoE8SEqmLlNuRtdHSsnItC82m5hGDhTec8ZcGonfnOCPJvdDou8mDXhtX9knES4PC/Uup3HL84RnsXZ0GWpUtB+fIsHlZryFjsTJtOZVdJSH5H5B0arJCitLVJqPqF5bluIVuXmwXD2Wi0hQkpmSm+SJJQ1efTkpK2UYkJvu8cMF7aA7Lgxzimatqg4oDg9ED5VCHFz82wacjNrnQRnezeOcWF8q76dfCDo5MLUO+CWChFCH897B6kjMmCo3IavLTHB8Dc7q70I8lxn2qAZzwIDAQAB diff --git a/dkimsign.py b/dkimsign.py index 13c85be..b73d0b6 100644 --- a/dkimsign.py +++ b/dkimsign.py @@ -47,9 +47,6 @@ parser.add_argument('--bcanon', choices=['simple', 'relaxed'], parser.add_argument('--signalg', choices=['rsa-sha256', 'rsa-sha1'], default='rsa-sha256', help='Signature algorithm: default=rsa-sha256') -parser.add_argument('--ktype', choices=['rsa', 'rsafp'], - default='rsa', - help='DKIM key type: Default is rsa') parser.add_argument('--identity', help='Optional value for i= tag.') args=parser.parse_args(arguments) include_headers = None @@ -64,7 +61,6 @@ if sys.version_info[0] >= 3: args.hcanon = bytes(args.hcanon, encoding='UTF-8') args.bcanon = bytes(args.bcanon, encoding='UTF-8') args.signalg = bytes(args.signalg, encoding='UTF-8') - args.ktype = bytes(args.ktype, encoding='UTF-8') # Make sys.stdin and stdout binary streams. sys.stdin = sys.stdin.detach() sys.stdout = sys.stdout.detach() @@ -73,7 +69,7 @@ canonicalize = (args.hcanon, args.bcanon) message = sys.stdin.read() try: d = dkim.DKIM(message,logger=logger, - signature_algorithm=args.signalg, ktype=args.ktype) + signature_algorithm=args.signalg) sig = d.sign(args.selector, args.domain, open( args.privatekeyfile, "rb").read(), identity = args.identity, canonicalize=canonicalize, include_headers=include_headers, diff --git a/setup.py b/setup.py old mode 100755 new mode 100644