From 425eb24854f1c3397aaaba61fa1cf71c76b27c4b Mon Sep 17 00:00:00 2001 From: Justin Simon Date: Sun, 7 May 2017 04:39:47 -0500 Subject: Support signing a pre-calculated hash (#87) * Split the hashing out of the sign method This code change adds support to split the hashing of a message and the actual signing of the message. * Updating unit test and documentation This commit updates the unit test and usage docs. In addition, This change removes a redundant error check inside rsa.sign(). * Refactore unit tests and code comments Removed the print statements from the unit test and refactored a few code comments to improve readability. * Rename hash function The new hash function had the same name as a function in the standard library. This commit changes the name to avoid conflicts. * Rename hash function to compute_hash() This commit renames the hash function to compute_hash(). --- doc/usage.rst | 9 +++++++++ rsa/__init__.py | 5 +++-- rsa/pkcs1.py | 50 ++++++++++++++++++++++++++++++++++---------------- tests/test_pkcs1.py | 21 +++++++++++++++++++++ 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/doc/usage.rst b/doc/usage.rst index b4f8426..6ac9e82 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -203,6 +203,15 @@ This hashes the message using SHA-1. Other hash methods are also possible, check the :py:func:`rsa.sign` function documentation for details. The hash is then signed with the private key. +It is possible to calculate the hash and signature in separate operations +(i.e for generating the hash on a client machine and then sign with a +private key on remote server). To hash a message use the :py:func:`rsa.compute_hash` +function and then use the :py:func:`rsa.sign_hash` function to sign the hash: + + >>> message = 'Go left at the blue tree' + >>> hash = rsa.compute_hash(message, 'SHA-1') + >>> signature = rsa.sign_hash(hash, privkey, 'SHA-1') + In order to verify the signature, use the :py:func:`rsa.verify` function. This function returns True if the verification is successful: diff --git a/rsa/__init__.py b/rsa/__init__.py index 95cd3fd..ce3a341 100644 --- a/rsa/__init__.py +++ b/rsa/__init__.py @@ -25,7 +25,7 @@ prevent repetitions, or other common security improvements. Use with care. from rsa.key import newkeys, PrivateKey, PublicKey from rsa.pkcs1 import encrypt, decrypt, sign, verify, DecryptionError, \ - VerificationError, find_signature_hash + VerificationError, find_signature_hash, sign_hash, compute_hash __author__ = "Sybren Stuvel, Barry Mead and Yesudeep Mangalapilly" __date__ = "2016-03-29" @@ -38,4 +38,5 @@ if __name__ == "__main__": doctest.testmod() __all__ = ["newkeys", "encrypt", "decrypt", "sign", "verify", 'PublicKey', - 'PrivateKey', 'DecryptionError', 'VerificationError'] + 'PrivateKey', 'DecryptionError', 'VerificationError', + 'compute_hash', 'sign_hash'] diff --git a/rsa/pkcs1.py b/rsa/pkcs1.py index 41f543c..323bf48 100644 --- a/rsa/pkcs1.py +++ b/rsa/pkcs1.py @@ -245,17 +245,16 @@ def decrypt(crypto, priv_key): return cleartext[sep_idx + 1:] -def sign(message, priv_key, hash): - """Signs the message with the private key. +def sign_hash(hash_value, priv_key, hash_method): + """Signs a precomputed hash with the private key. Hashes the message, then signs the hash with the given key. This is known as a "detached signature", because the message itself isn't altered. - - :param message: the message to sign. Can be an 8-bit string or a file-like - object. If ``message`` has a ``read()`` method, it is assumed to be a - file-like object. + + :param hash_value: A precomputed hash to sign (ignores message). Should be set to + None if needing to hash and sign message. :param priv_key: the :py:class:`rsa.PrivateKey` to sign with - :param hash: the hash method used on the message. Use 'MD5', 'SHA-1', + :param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1', 'SHA-256', 'SHA-384' or 'SHA-512'. :return: a message signature block. :raise OverflowError: if the private key is too small to contain the @@ -264,15 +263,12 @@ def sign(message, priv_key, hash): """ # Get the ASN1 code for this hash method - if hash not in HASH_ASN1: - raise ValueError('Invalid hash method: %s' % hash) - asn1code = HASH_ASN1[hash] - - # Calculate the hash - hash = _hash(message, hash) + if hash_method not in HASH_ASN1: + raise ValueError('Invalid hash method: %s' % hash_method) + asn1code = HASH_ASN1[hash_method] # Encrypt the hash with the private key - cleartext = asn1code + hash + cleartext = asn1code + hash_value keylength = common.byte_size(priv_key.n) padded = _pad_for_signing(cleartext, keylength) @@ -283,6 +279,28 @@ def sign(message, priv_key, hash): return block +def sign(message, priv_key, hash_method): + """Signs the message with the private key. + + Hashes the message, then signs the hash with the given key. This is known + as a "detached signature", because the message itself isn't altered. + + :param message: the message to sign. Can be an 8-bit string or a file-like + object. If ``message`` has a ``read()`` method, it is assumed to be a + file-like object. + :param priv_key: the :py:class:`rsa.PrivateKey` to sign with + :param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1', + 'SHA-256', 'SHA-384' or 'SHA-512'. + :return: a message signature block. + :raise OverflowError: if the private key is too small to contain the + requested hash. + + """ + + msg_hash = compute_hash(message, hash_method) + return sign_hash(msg_hash, priv_key, hash_method) + + def verify(message, signature, pub_key): """Verifies that the signature matches the message. @@ -305,7 +323,7 @@ def verify(message, signature, pub_key): # Get the hash method method_name = _find_method_hash(clearsig) - message_hash = _hash(message, method_name) + message_hash = compute_hash(message, method_name) # Reconstruct the expected padded hash cleartext = HASH_ASN1[method_name] + message_hash @@ -358,7 +376,7 @@ def yield_fixedblocks(infile, blocksize): break -def _hash(message, method_name): +def compute_hash(message, method_name): """Returns the message digest. :param message: the signed message. Can be an 8-bit string or a file-like diff --git a/tests/test_pkcs1.py b/tests/test_pkcs1.py index a8afea7..a96c8be 100644 --- a/tests/test_pkcs1.py +++ b/tests/test_pkcs1.py @@ -111,3 +111,24 @@ class SignatureTest(unittest.TestCase): signature2 = pkcs1.sign(message, self.priv, 'SHA-1') self.assertEqual(signature1, signature2) + + def test_split_hash_sign(self): + """Hashing and then signing should match with directly signing the message. """ + + message = b'je moeder' + msg_hash = pkcs1.compute_hash(message, 'SHA-256') + signature1 = pkcs1.sign_hash(msg_hash, self.priv, 'SHA-256') + + # Calculate the signature using the unified method + signature2 = pkcs1.sign(message, self.priv, 'SHA-256') + + self.assertEqual(signature1, signature2) + + def test_hash_sign_verify(self): + """Test happy flow of hash, sign, and verify""" + + message = b'je moeder' + msg_hash = pkcs1.compute_hash(message, 'SHA-256') + signature = pkcs1.sign_hash(msg_hash, self.priv, 'SHA-256') + + self.assertTrue(pkcs1.verify(message, signature, self.pub)) -- cgit v1.2.1