Skip to content

Commit 425eb24

Browse files
jls5177sybrenstuvel
authored andcommitted
Support signing a pre-calculated hash (sybrenstuvel#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().
1 parent 000e84a commit 425eb24

File tree

4 files changed

+67
-18
lines changed

4 files changed

+67
-18
lines changed

doc/usage.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ This hashes the message using SHA-1. Other hash methods are also
203203
possible, check the :py:func:`rsa.sign` function documentation for
204204
details. The hash is then signed with the private key.
205205

206+
It is possible to calculate the hash and signature in separate operations
207+
(i.e for generating the hash on a client machine and then sign with a
208+
private key on remote server). To hash a message use the :py:func:`rsa.compute_hash`
209+
function and then use the :py:func:`rsa.sign_hash` function to sign the hash:
210+
211+
>>> message = 'Go left at the blue tree'
212+
>>> hash = rsa.compute_hash(message, 'SHA-1')
213+
>>> signature = rsa.sign_hash(hash, privkey, 'SHA-1')
214+
206215
In order to verify the signature, use the :py:func:`rsa.verify`
207216
function. This function returns True if the verification is successful:
208217

rsa/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from rsa.key import newkeys, PrivateKey, PublicKey
2727
from rsa.pkcs1 import encrypt, decrypt, sign, verify, DecryptionError, \
28-
VerificationError, find_signature_hash
28+
VerificationError, find_signature_hash, sign_hash, compute_hash
2929

3030
__author__ = "Sybren Stuvel, Barry Mead and Yesudeep Mangalapilly"
3131
__date__ = "2016-03-29"
@@ -38,4 +38,5 @@
3838
doctest.testmod()
3939

4040
__all__ = ["newkeys", "encrypt", "decrypt", "sign", "verify", 'PublicKey',
41-
'PrivateKey', 'DecryptionError', 'VerificationError']
41+
'PrivateKey', 'DecryptionError', 'VerificationError',
42+
'compute_hash', 'sign_hash']

rsa/pkcs1.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -245,17 +245,16 @@ def decrypt(crypto, priv_key):
245245
return cleartext[sep_idx + 1:]
246246

247247

248-
def sign(message, priv_key, hash):
249-
"""Signs the message with the private key.
248+
def sign_hash(hash_value, priv_key, hash_method):
249+
"""Signs a precomputed hash with the private key.
250250
251251
Hashes the message, then signs the hash with the given key. This is known
252252
as a "detached signature", because the message itself isn't altered.
253-
254-
:param message: the message to sign. Can be an 8-bit string or a file-like
255-
object. If ``message`` has a ``read()`` method, it is assumed to be a
256-
file-like object.
253+
254+
:param hash_value: A precomputed hash to sign (ignores message). Should be set to
255+
None if needing to hash and sign message.
257256
:param priv_key: the :py:class:`rsa.PrivateKey` to sign with
258-
:param hash: the hash method used on the message. Use 'MD5', 'SHA-1',
257+
:param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1',
259258
'SHA-256', 'SHA-384' or 'SHA-512'.
260259
:return: a message signature block.
261260
:raise OverflowError: if the private key is too small to contain the
@@ -264,15 +263,12 @@ def sign(message, priv_key, hash):
264263
"""
265264

266265
# Get the ASN1 code for this hash method
267-
if hash not in HASH_ASN1:
268-
raise ValueError('Invalid hash method: %s' % hash)
269-
asn1code = HASH_ASN1[hash]
270-
271-
# Calculate the hash
272-
hash = _hash(message, hash)
266+
if hash_method not in HASH_ASN1:
267+
raise ValueError('Invalid hash method: %s' % hash_method)
268+
asn1code = HASH_ASN1[hash_method]
273269

274270
# Encrypt the hash with the private key
275-
cleartext = asn1code + hash
271+
cleartext = asn1code + hash_value
276272
keylength = common.byte_size(priv_key.n)
277273
padded = _pad_for_signing(cleartext, keylength)
278274

@@ -283,6 +279,28 @@ def sign(message, priv_key, hash):
283279
return block
284280

285281

282+
def sign(message, priv_key, hash_method):
283+
"""Signs the message with the private key.
284+
285+
Hashes the message, then signs the hash with the given key. This is known
286+
as a "detached signature", because the message itself isn't altered.
287+
288+
:param message: the message to sign. Can be an 8-bit string or a file-like
289+
object. If ``message`` has a ``read()`` method, it is assumed to be a
290+
file-like object.
291+
:param priv_key: the :py:class:`rsa.PrivateKey` to sign with
292+
:param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1',
293+
'SHA-256', 'SHA-384' or 'SHA-512'.
294+
:return: a message signature block.
295+
:raise OverflowError: if the private key is too small to contain the
296+
requested hash.
297+
298+
"""
299+
300+
msg_hash = compute_hash(message, hash_method)
301+
return sign_hash(msg_hash, priv_key, hash_method)
302+
303+
286304
def verify(message, signature, pub_key):
287305
"""Verifies that the signature matches the message.
288306
@@ -305,7 +323,7 @@ def verify(message, signature, pub_key):
305323

306324
# Get the hash method
307325
method_name = _find_method_hash(clearsig)
308-
message_hash = _hash(message, method_name)
326+
message_hash = compute_hash(message, method_name)
309327

310328
# Reconstruct the expected padded hash
311329
cleartext = HASH_ASN1[method_name] + message_hash
@@ -358,7 +376,7 @@ def yield_fixedblocks(infile, blocksize):
358376
break
359377

360378

361-
def _hash(message, method_name):
379+
def compute_hash(message, method_name):
362380
"""Returns the message digest.
363381
364382
:param message: the signed message. Can be an 8-bit string or a file-like

tests/test_pkcs1.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,24 @@ def test_multiple_signings(self):
111111
signature2 = pkcs1.sign(message, self.priv, 'SHA-1')
112112

113113
self.assertEqual(signature1, signature2)
114+
115+
def test_split_hash_sign(self):
116+
"""Hashing and then signing should match with directly signing the message. """
117+
118+
message = b'je moeder'
119+
msg_hash = pkcs1.compute_hash(message, 'SHA-256')
120+
signature1 = pkcs1.sign_hash(msg_hash, self.priv, 'SHA-256')
121+
122+
# Calculate the signature using the unified method
123+
signature2 = pkcs1.sign(message, self.priv, 'SHA-256')
124+
125+
self.assertEqual(signature1, signature2)
126+
127+
def test_hash_sign_verify(self):
128+
"""Test happy flow of hash, sign, and verify"""
129+
130+
message = b'je moeder'
131+
msg_hash = pkcs1.compute_hash(message, 'SHA-256')
132+
signature = pkcs1.sign_hash(msg_hash, self.priv, 'SHA-256')
133+
134+
self.assertTrue(pkcs1.verify(message, signature, self.pub))

0 commit comments

Comments
 (0)