Skip to content

Commit 2b72e0f

Browse files
myheroyukihswong3i
authored andcommitted
multiprime support
added fast CRT-based decryption to core added multiprime key support correction (see issue sybrenstuvel#205, PR sybrenstuvel#206) added multiprime tests
1 parent 18f5faf commit 2b72e0f

File tree

7 files changed

+245
-23
lines changed

7 files changed

+245
-23
lines changed

rsa/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
)
3636

3737
__author__ = "Sybren Stuvel, Barry Mead and Yesudeep Mangalapilly"
38-
__date__ = "2023-04-23"
39-
__version__ = "4.10-dev0"
38+
__date__ = "2025-04-16"
39+
__version__ = "4.9.1"
4040

4141
# Do doctest if we're run directly
4242
if __name__ == "__main__":

rsa/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def assert_int(var: int, name: str) -> None:
2323
if isinstance(var, int):
2424
return
2525

26-
raise TypeError("{} should be an integer, not {}".format(name, var.__class__))
26+
raise TypeError("%s should be an integer, not %s" % (name, var.__class__))
2727

2828

2929
def encrypt_int(message: int, ekey: int, n: int) -> int:
@@ -36,7 +36,7 @@ def encrypt_int(message: int, ekey: int, n: int) -> int:
3636
if message < 0:
3737
raise ValueError("Only non-negative numbers are supported")
3838

39-
if message >= n:
39+
if message > n:
4040
raise OverflowError("The message %i is too long for n=%i" % (message, n))
4141

4242
return pow(message, ekey, n)

rsa/key.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
3232
"""
3333

34-
import abc
3534
import threading
3635
import typing
3736
import warnings
@@ -49,7 +48,7 @@
4948
T = typing.TypeVar("T", bound="AbstractKey")
5049

5150

52-
class AbstractKey(metaclass=abc.ABCMeta):
51+
class AbstractKey:
5352
"""Abstract superclass for private and public keys."""
5453

5554
__slots__ = ("n", "e", "blindfac", "blindfac_inverse", "mutex")
@@ -66,7 +65,6 @@ def __init__(self, n: int, e: int) -> None:
6665
self.mutex = threading.Lock()
6766

6867
@classmethod
69-
@abc.abstractmethod
7068
def _load_pkcs1_pem(cls: typing.Type[T], keyfile: bytes) -> T:
7169
"""Loads a key in PKCS#1 PEM format, implement in a subclass.
7270
@@ -79,7 +77,6 @@ def _load_pkcs1_pem(cls: typing.Type[T], keyfile: bytes) -> T:
7977
"""
8078

8179
@classmethod
82-
@abc.abstractmethod
8380
def _load_pkcs1_der(cls: typing.Type[T], keyfile: bytes) -> T:
8481
"""Loads a key in PKCS#1 PEM format, implement in a subclass.
8582
@@ -91,15 +88,13 @@ def _load_pkcs1_der(cls: typing.Type[T], keyfile: bytes) -> T:
9188
:rtype: AbstractKey
9289
"""
9390

94-
@abc.abstractmethod
9591
def _save_pkcs1_pem(self) -> bytes:
9692
"""Saves the key in PKCS#1 PEM format, implement in a subclass.
9793
9894
:returns: the PEM-encoded key.
9995
:rtype: bytes
10096
"""
10197

102-
@abc.abstractmethod
10398
def _save_pkcs1_der(self) -> bytes:
10499
"""Saves the key in PKCS#1 DER format, implement in a subclass.
105100
@@ -491,6 +486,19 @@ def blinded_decrypt(self, encrypted: int) -> int:
491486

492487
return self.unblind(decrypted, blindfac_inverse)
493488

489+
def blinded_encrypt(self, message: int) -> int:
490+
"""Encrypts the message using blinding to prevent side-channel attacks.
491+
492+
:param message: the message to encrypt
493+
:type message: int
494+
495+
:returns: the encrypted message
496+
:rtype: int
497+
"""
498+
499+
blinded, blindfac_inverse = self.blind(message)
500+
encrypted = rsa.core.encrypt_int(blinded, self.d, self.n)
501+
return self.unblind(encrypted, blindfac_inverse)
494502

495503
@classmethod
496504
def _load_pkcs1_der(cls, keyfile: bytes) -> "PrivateKey":

rsa/pkcs1.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@
4747
"SHA-256": b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20",
4848
"SHA-384": b"\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30",
4949
"SHA-512": b"\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40",
50-
"SHA3-256": b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x08\x05\x00\x04\x20",
51-
"SHA3-384": b"\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x09\x05\x00\x04\x30",
52-
"SHA3-512": b"\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x0a\x05\x00\x04\x40",
5350
}
5451

5552
HASH_METHODS: typing.Dict[str, typing.Callable[[], HashType]] = {
@@ -59,13 +56,29 @@
5956
"SHA-256": hashlib.sha256,
6057
"SHA-384": hashlib.sha384,
6158
"SHA-512": hashlib.sha512,
62-
"SHA3-256": hashlib.sha3_256,
63-
"SHA3-384": hashlib.sha3_384,
64-
"SHA3-512": hashlib.sha3_512,
6559
}
6660
"""Hash methods supported by this library."""
6761

6862

63+
if sys.version_info >= (3, 6):
64+
# Python 3.6 introduced SHA3 support.
65+
HASH_ASN1.update(
66+
{
67+
"SHA3-256": b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x08\x05\x00\x04\x20",
68+
"SHA3-384": b"\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x09\x05\x00\x04\x30",
69+
"SHA3-512": b"\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x0a\x05\x00\x04\x40",
70+
}
71+
)
72+
73+
HASH_METHODS.update(
74+
{
75+
"SHA3-256": hashlib.sha3_256,
76+
"SHA3-384": hashlib.sha3_384,
77+
"SHA3-512": hashlib.sha3_512,
78+
}
79+
)
80+
81+
6982
class CryptoError(Exception):
7083
"""Base class for all exceptions in this module."""
7184

@@ -274,8 +287,8 @@ def decrypt(crypto: bytes, priv_key: key.PrivateKey) -> bytes:
274287
def sign_hash(hash_value: bytes, priv_key: key.PrivateKey, hash_method: str) -> bytes:
275288
"""Signs a precomputed hash with the private key.
276289
277-
Signs the hash with the given key. This is known as a "detached signature",
278-
because the message itself isn't altered.
290+
Hashes the message, then signs the hash with the given key. This is known
291+
as a "detached signature", because the message itself isn't altered.
279292
280293
:param hash_value: A precomputed hash to sign (ignores message).
281294
:param priv_key: the :py:class:`rsa.PrivateKey` to sign with
@@ -298,7 +311,7 @@ def sign_hash(hash_value: bytes, priv_key: key.PrivateKey, hash_method: str) ->
298311
padded = _pad_for_signing(cleartext, keylength)
299312

300313
payload = transform.bytes2int(padded)
301-
encrypted = priv_key.blinded_decrypt(payload)
314+
encrypted = priv_key.blinded_encrypt(payload)
302315
block = transform.int2bytes(encrypted, keylength)
303316

304317
return block
@@ -342,11 +355,8 @@ def verify(message: bytes, signature: bytes, pub_key: key.PublicKey) -> str:
342355
"""
343356

344357
keylength = common.byte_size(pub_key.n)
345-
if len(signature) != keylength:
346-
raise VerificationError("Verification failed")
347-
348358
encrypted = transform.bytes2int(signature)
349-
decrypted = core.encrypt_int(encrypted, pub_key.e, pub_key.n)
359+
decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n)
350360
clearsig = transform.int2bytes(decrypted, keylength)
351361

352362
# Get the hash method
@@ -357,6 +367,9 @@ def verify(message: bytes, signature: bytes, pub_key: key.PublicKey) -> str:
357367
cleartext = HASH_ASN1[method_name] + message_hash
358368
expected = _pad_for_signing(cleartext, keylength)
359369

370+
if len(signature) != keylength:
371+
raise VerificationError("Verification failed")
372+
360373
# Compare with the signed one
361374
if expected != clearsig:
362375
raise VerificationError("Verification failed")

tests/test_key.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ def getprime(_):
7575
self.assertEqual(39317, p)
7676
self.assertEqual(33107, q)
7777

78+
def test_multiprime(self):
79+
primes = [64123, 50957, 39317, 33107]
80+
exponent = 2**2**4 + 1
81+
82+
def getprime(_):
83+
return primes.pop(0)
84+
(p, q, e, d, rs) = rsa.key.gen_keys(
85+
128, accurate=False, getprime_func=getprime, exponent=exponent, nprimes=4
86+
)
87+
self.assertEqual(64123, p)
88+
self.assertEqual(50957, q)
89+
self.assertEqual(rs, [39317, 33107])
90+
7891

7992
class HashTest(unittest.TestCase):
8093
"""Test hashing of keys"""

tests/test_load_save_keys.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,155 @@ def test_load_from_disk(self):
209209
self.assertEqual(14617195220284816877, privkey.q)
210210

211211

212+
MP_B64PRIV_DER = b"MEoCAQACCDsGeWW1Om5xAgMBAAECBQDHn4npAgMA+nsCAwDHDQICcw0CAwCWDQICTjYCAwCZlQICaukCAioVAgMAgVMCAksjAgIq3g=="
213+
MP_PRIVATE_DER = base64.standard_b64decode(MP_B64PRIV_DER)
214+
215+
MP_B64PUB_DER = b"MA8CCDsGeWW1Om5xAgMBAAE="
216+
MP_PUBLIC_DER = base64.standard_b64decode(MP_B64PUB_DER)
217+
218+
MP_PRIVATE_PEM = (
219+
b"""\
220+
-----BEGIN CONFUSING STUFF-----
221+
Cruft before the key
222+
223+
-----BEGIN RSA PRIVATE KEY-----
224+
Comment: something blah
225+
226+
"""
227+
+ MP_B64PRIV_DER
228+
+ b"""
229+
-----END RSA PRIVATE KEY-----
230+
231+
Stuff after the key
232+
-----END CONFUSING STUFF-----
233+
"""
234+
)
235+
236+
MP_CLEAN_PRIVATE_PEM = (
237+
b"""\
238+
-----BEGIN RSA PRIVATE KEY-----
239+
"""
240+
+ MP_B64PRIV_DER
241+
+ b"""
242+
-----END RSA PRIVATE KEY-----
243+
"""
244+
)
245+
246+
MP_PUBLIC_PEM = (
247+
b"""\
248+
-----BEGIN CONFUSING STUFF-----
249+
Cruft before the key
250+
251+
-----BEGIN RSA PUBLIC KEY-----
252+
Comment: something blah
253+
254+
"""
255+
+ MP_B64PUB_DER
256+
+ b"""
257+
-----END RSA PUBLIC KEY-----
258+
259+
Stuff after the key
260+
-----END CONFUSING STUFF-----
261+
"""
262+
)
263+
264+
MP_CLEAN_PUBLIC_PEM = (
265+
b"""\
266+
-----BEGIN RSA PUBLIC KEY-----
267+
"""
268+
+ MP_B64PUB_DER
269+
+ b"""
270+
-----END RSA PUBLIC KEY-----
271+
"""
272+
)
273+
274+
275+
class MultiprimeDerTest(unittest.TestCase):
276+
"""Test saving and loading multiprime DER keys."""
277+
278+
def test_load_multiprime_private_key(self):
279+
"""Test loading private DER keys."""
280+
281+
key = rsa.key.PrivateKey.load_pkcs1(MP_PRIVATE_DER, "DER")
282+
expected = rsa.key.PrivateKey(4253220375837175409, 65537, 3349121513, 64123, 50957, [39317, 33107])
283+
284+
self.assertEqual(expected, key)
285+
self.assertEqual(key.exp1, 29453)
286+
self.assertEqual(key.exp2, 38413)
287+
self.assertEqual(key.coef, 20022)
288+
self.assertEqual(key.ds, [27369, 19235])
289+
self.assertEqual(key.ts, [10773, 10974])
290+
291+
def test_save_multiprime_private_key(self):
292+
"""Test saving private DER keys."""
293+
294+
key = rsa.key.PrivateKey(4253220375837175409, 65537, 3349121513, 64123, 50957, [39317, 33107])
295+
der = key.save_pkcs1("DER")
296+
297+
self.assertIsInstance(der, bytes)
298+
self.assertEqual(MP_PRIVATE_DER, der)
299+
300+
def test_load_multiprime_public_key(self):
301+
"""Test loading public DER keys."""
302+
303+
key = rsa.key.PublicKey.load_pkcs1(MP_PUBLIC_DER, "DER")
304+
expected = rsa.key.PublicKey(4253220375837175409, 65537)
305+
306+
self.assertEqual(expected, key)
307+
308+
def test_save_multiprime_public_key(self):
309+
"""Test saving public DER keys."""
310+
311+
key = rsa.key.PublicKey(4253220375837175409, 65537)
312+
der = key.save_pkcs1("DER")
313+
314+
self.assertIsInstance(der, bytes)
315+
self.assertEqual(MP_PUBLIC_DER, der)
316+
317+
318+
class MultiprimePemTest(unittest.TestCase):
319+
"""Test saving and loading multiprime PEM keys."""
320+
321+
def test_load_multiprime_private_key(self):
322+
"""Test loading private PEM files."""
323+
324+
key = rsa.key.PrivateKey.load_pkcs1(MP_PRIVATE_PEM, "PEM")
325+
expected = rsa.key.PrivateKey(4253220375837175409, 65537, 3349121513, 64123, 50957, [39317, 33107])
326+
327+
self.assertEqual(expected, key)
328+
self.assertEqual(key.exp1, 29453)
329+
self.assertEqual(key.exp2, 38413)
330+
self.assertEqual(key.coef, 20022)
331+
self.assertEqual(key.ds, [27369, 19235])
332+
self.assertEqual(key.ts, [10773, 10974])
333+
334+
def test_save_multiprime_private_key(self):
335+
"""Test saving private PEM files."""
336+
337+
key = rsa.key.PrivateKey(4253220375837175409, 65537, 3349121513, 64123, 50957, [39317, 33107])
338+
pem = key.save_pkcs1("PEM")
339+
340+
self.assertIsInstance(pem, bytes)
341+
self.assertEqual(MP_CLEAN_PRIVATE_PEM.replace(b"\n", b""), pem.replace(b"\n", b""))
342+
343+
def test_load_multiprime_public_key(self):
344+
"""Test loading public PEM files."""
345+
346+
key = rsa.key.PublicKey.load_pkcs1(MP_PUBLIC_PEM, "PEM")
347+
expected = rsa.key.PublicKey(4253220375837175409, 65537)
348+
349+
self.assertEqual(expected, key)
350+
351+
def test_save_multiprime_public_key(self):
352+
"""Test saving public PEM files."""
353+
354+
key = rsa.key.PublicKey(4253220375837175409, 65537)
355+
pem = key.save_pkcs1("PEM")
356+
357+
self.assertIsInstance(pem, bytes)
358+
self.assertEqual(MP_CLEAN_PUBLIC_PEM, pem)
359+
360+
212361
class PickleTest(unittest.TestCase):
213362
"""Test saving and loading keys by pickling."""
214363

@@ -222,6 +371,16 @@ def test_private_key(self):
222371
for attr in rsa.key.AbstractKey.__slots__:
223372
self.assertTrue(hasattr(unpickled, attr))
224373

374+
def test_multiprime_private_key(self):
375+
pk = rsa.key.PrivateKey(4253220375837175409, 65537, 3349121513, 64123, 50957, [39317, 33107])
376+
377+
pickled = pickle.dumps(pk)
378+
unpickled = pickle.loads(pickled)
379+
self.assertEqual(pk, unpickled)
380+
381+
for attr in rsa.key.AbstractKey.__slots__:
382+
self.assertTrue(hasattr(unpickled, attr))
383+
225384
def test_public_key(self):
226385
pk = rsa.key.PublicKey(3727264081, 65537)
227386

0 commit comments

Comments
 (0)