diff --git a/dkim/__init__.py b/dkim/__init__.py index 72b762a..ab2347d 100644 --- a/dkim/__init__.py +++ b/dkim/__init__.py @@ -189,10 +189,6 @@ def validate_signature_fields(sig, debuglog=None): return False return True -# These values come from RFC 3447, section 9.2 Notes, page 43. -HASHID_SHA1 = "\x2b\x0e\x03\x02\x1a" -HASHID_SHA256 = "\x60\x86\x48\x01\x65\x03\x04\x02\x01" - def rfc822_parse(message): """Parse a message in RFC822 format. @@ -321,13 +317,10 @@ def sign(message, selector, domain, privkey, identity=None, canonicalize=(Simple h.update(x[0]) h.update(":") h.update(x[1]) - d = h.digest() - if debuglog is not None: - print >>debuglog, "sign digest:", " ".join("%02x" % ord(x) for x in d) try: sig2 = RSASSA_PKCS1_v1_5_sign( - d, HASHID_SHA256, pk['privateExponent'], pk['modulus']) + h, pk['privateExponent'], pk['modulus']) except DigestTooLargeError: raise ParameterError("digest too large for modulus") sig_value += base64.b64encode(sig2) @@ -392,10 +385,8 @@ def verify(message, debuglog=None, dnsfunc=dnstxt): if sig['a'] == "rsa-sha1": hasher = hashlib.sha1 - hashid = HASHID_SHA1 elif sig['a'] == "rsa-sha256": hasher = hashlib.sha256 - hashid = HASHID_SHA256 else: if debuglog is not None: print >>debuglog, "Unknown signature algorithm (%s)" % sig['a'] @@ -432,13 +423,10 @@ def verify(message, debuglog=None, dnsfunc=dnstxt): h = hasher() hash_headers( h, canonicalize_headers, headers, include_headers, sigheaders, sig) - d = h.digest() - if debuglog is not None: - print >>debuglog, "verify digest:", " ".join("%02x" % ord(x) for x in d) signature = base64.b64decode(re.sub(r"\s+", "", sig['b'])) try: return RSASSA_PKCS1_v1_5_verify( - d, hashid, signature, pk['publicExponent'], pk['modulus']) + h, signature, pk['publicExponent'], pk['modulus']) except DigestTooLargeError: if debuglog is not None: print >>debuglog, "digest too large for modulus" diff --git a/dkim/crypto.py b/dkim/crypto.py index 9ed6116..99d809a 100644 --- a/dkim/crypto.py +++ b/dkim/crypto.py @@ -76,6 +76,13 @@ ASN1_RSAPrivateKey = [ ] +# These values come from RFC 3447, section 9.2 Notes, page 43. +HASH_ID_MAP = { + 'sha1': "\x2b\x0e\x03\x02\x1a", + 'sha256': "\x60\x86\x48\x01\x65\x03\x04\x02\x01", + } + + class DigestTooLargeError(Exception): """The digest is too large to fit within the requested length.""" pass @@ -146,21 +153,20 @@ def parse_pem_private_key(data): return parse_private_key(pkdata) -def EMSA_PKCS1_v1_5_encode(digest, mlen, hashid): +def EMSA_PKCS1_v1_5_encode(hash, mlen): """Encode a digest with RFC3447 EMSA-PKCS1-v1_5. - @param digest: digest byte string to encode + @param hash: hash object to encode @param mlen: desired message length - @param hashid: ID of the hash used to generate the digest @return: encoded digest byte string """ dinfo = asn1_build( (SEQUENCE, [ (SEQUENCE, [ - (OBJECT_IDENTIFIER, hashid), + (OBJECT_IDENTIFIER, HASH_ID_MAP[hash.name]), (NULL, None), ]), - (OCTET_STRING, digest), + (OCTET_STRING, hash.digest()), ])) if len(dinfo)+3 > mlen: raise DigestTooLargeError() @@ -211,32 +217,29 @@ def perform_rsa(message, exponent, modulus, mlen): return int2str(pow(str2int(message), exponent, modulus), mlen) -def RSASSA_PKCS1_v1_5_sign(digest, hashid, private_exponent, modulus): +def RSASSA_PKCS1_v1_5_sign(hash, private_exponent, modulus): """Sign a digest with RFC3447 RSASSA-PKCS1-v1_5. - @param digest: digest byte string to sign - @param hashid: ID of the hash used to generate the digest + @param hash: hash object to sign @param private_exponent: private key exponent @param modulus: key modulus @return: signed digest byte string """ modlen = len(int2str(modulus)) - encoded_digest = EMSA_PKCS1_v1_5_encode(digest, modlen, hashid) + encoded_digest = EMSA_PKCS1_v1_5_encode(hash, modlen) return perform_rsa(encoded_digest, private_exponent, modulus, modlen) -def RSASSA_PKCS1_v1_5_verify(digest, hashid, signature, public_exponent, - modulus): +def RSASSA_PKCS1_v1_5_verify(hash, signature, public_exponent, modulus): """Verify a digest signed with RFC3447 RSASSA-PKCS1-v1_5. - @param digest: digest byte string to check - @param hashid: ID of the hash used to generate the digest + @param hash: hash object to check @param signature: signed digest byte string @param public_exponent: public key exponent @param modulus: key modulus @return: True if the signature is valid, False otherwise """ modlen = len(int2str(modulus)) - encoded_digest = EMSA_PKCS1_v1_5_encode(digest, modlen, hashid) + encoded_digest = EMSA_PKCS1_v1_5_encode(hash, modlen) signed_digest = perform_rsa(signature, public_exponent, modulus, modlen) return encoded_digest == signed_digest diff --git a/dkim/tests/test_crypto.py b/dkim/tests/test_crypto.py index d08e454..e25079c 100644 --- a/dkim/tests/test_crypto.py +++ b/dkim/tests/test_crypto.py @@ -17,12 +17,9 @@ # Copyright (c) 2011 William Grant import base64 +import hashlib import unittest -from dkim import ( - HASHID_SHA1, - HASHID_SHA256, - ) from dkim.crypto import ( DigestTooLargeError, EMSA_PKCS1_v1_5_encode, @@ -87,28 +84,28 @@ class TestParseKeys(unittest.TestCase): class TestEMSA_PKCS1_v1_5(unittest.TestCase): def test_encode_sha256(self): - digest = '0123456789abcdef0123456789abcdef' + hash = hashlib.sha256('message') self.assertEquals( '\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00' '010\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04 ' - + digest, - EMSA_PKCS1_v1_5_encode(digest, 62, HASHID_SHA256)) + + hash.digest(), + EMSA_PKCS1_v1_5_encode(hash, 62)) def test_encode_sha1(self): - digest = '0123456789abcdef0123' + hash = hashlib.sha1('message') self.assertEquals( '\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00' '0!0\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' - + digest, - EMSA_PKCS1_v1_5_encode(digest, 46, HASHID_SHA1)) + + hash.digest(), + EMSA_PKCS1_v1_5_encode(hash, 46)) def test_encode_forbids_too_short(self): # PKCS#1 requires at least 8 bytes of padding, so there must be # at least that much space. - digest = '0123456789abcdef0123' + hash = hashlib.sha1('message') self.assertRaises( DigestTooLargeError, - EMSA_PKCS1_v1_5_encode, digest, 45, HASHID_SHA1) + EMSA_PKCS1_v1_5_encode, hash, 45) class TestRSA(unittest.TestCase): @@ -136,31 +133,30 @@ class TestRSASSA(unittest.TestCase): def setUp(self): self.key = parse_pem_private_key(read_test_data('test.private')) + self.hash = hashlib.sha1(self.test_digest) test_digest = '0123456789abcdef0123' test_signature = ( - '3702809f62db933a5c3d18c2c76a3470658d2e79868fac98eaaca7e87d0cdc7' - 'fd091182673ed57c66531835d814ff367ffa3d764e74ca8ab301982d13eabb5' - 'dbe90e5c46ea223c5d3ee835aa74aaffe06e8018affeb78b5178818cb33656c' - 'ed462905bc0dc608e354f6ed3d4ec160ce9326ed227ccb0c1e5ba22098e10e6' - 'c083').decode('hex') + 'cc8d3647d64dd3bc12984947a27bdfbb565041fcc9db781afb4b60d29d288d8d60de' + '9e1916d6f81569c3e72af442538dd6aecb50a6de9a14565fdd679c46ff7842482e15' + 'e5aa078549621b6f12ca8cd57ecfad95b18e53581e131c6c3c7cd01cb153adeb439d' + '2d6ab8b215b19be0e69ef490885004a474eb26d747a219693e8c').decode('hex') def test_sign_and_verify(self): signature = RSASSA_PKCS1_v1_5_sign( - self.test_digest, HASHID_SHA1, TEST_KEY_PRIVATE_EXPONENT, - TEST_KEY_MODULUS) + self.hash, TEST_KEY_PRIVATE_EXPONENT, TEST_KEY_MODULUS) self.assertEquals( self.test_signature, signature) self.assertTrue( RSASSA_PKCS1_v1_5_verify( - self.test_digest, HASHID_SHA1, signature, - TEST_KEY_PUBLIC_EXPONENT, TEST_KEY_MODULUS)) + self.hash, signature, TEST_KEY_PUBLIC_EXPONENT, + TEST_KEY_MODULUS)) def test_invalid_signature(self): self.assertFalse( RSASSA_PKCS1_v1_5_verify( - self.test_digest, HASHID_SHA1, self.test_signature, - TEST_KEY_PUBLIC_EXPONENT, TEST_KEY_MODULUS + 1)) + self.hash, self.test_signature, TEST_KEY_PUBLIC_EXPONENT, + TEST_KEY_MODULUS + 1)) def test_suite():