Skip to content

Commit 06ec1ea

Browse files
committed
Fix #162: Blinding uses slow algorithm
Store blinding factor + its inverse, so that they can be reused & updated on every blinding operation. This avoids expensive computations. The reuse of the previous blinding factor is done via squaring (mod n), as per section 9 of 'A Timing Attack against RSA with the Chinese Remainder Theorem' by Werner Schindler, https://tls.mbed.org/public/WSchindler-RSA_Timing_Attack.pdf
1 parent 341e5c4 commit 06ec1ea

File tree

3 files changed

+47
-24
lines changed

3 files changed

+47
-24
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
code
88
- Add padding length check as described by PKCS#1 v1.5 (Fixes
99
[#164](https://github.com/sybrenstuvel/python-rsa/issues/164))
10+
- Reuse of blinding factors to speed up blinding operations.
11+
Fixes [#162](https://github.com/sybrenstuvel/python-rsa/issues/162).
1012

1113

1214
## Version 4.4 & 4.6 - released 2020-06-12

rsa/key.py

+32-20
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,15 @@
4949
class AbstractKey:
5050
"""Abstract superclass for private and public keys."""
5151

52-
__slots__ = ('n', 'e')
52+
__slots__ = ('n', 'e', 'blindfac', 'blindfac_inverse')
5353

5454
def __init__(self, n: int, e: int) -> None:
5555
self.n = n
5656
self.e = e
5757

58+
# These will be computed properly on the first call to blind().
59+
self.blindfac = self.blindfac_inverse = -1
60+
5861
@classmethod
5962
def _load_pkcs1_pem(cls, keyfile: bytes) -> 'AbstractKey':
6063
"""Loads a key in PKCS#1 PEM format, implement in a subclass.
@@ -145,7 +148,7 @@ def save_pkcs1(self, format: str = 'PEM') -> bytes:
145148
method = self._assert_format_exists(format, methods)
146149
return method()
147150

148-
def blind(self, message: int, r: int) -> int:
151+
def blind(self, message: int) -> int:
149152
"""Performs blinding on the message using random number 'r'.
150153
151154
:param message: the message, as integer, to blind.
@@ -159,10 +162,10 @@ def blind(self, message: int, r: int) -> int:
159162
160163
See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29
161164
"""
165+
self._update_blinding_factor()
166+
return (message * pow(self.blindfac, self.e, self.n)) % self.n
162167

163-
return (message * pow(r, self.e, self.n)) % self.n
164-
165-
def unblind(self, blinded: int, r: int) -> int:
168+
def unblind(self, blinded: int) -> int:
166169
"""Performs blinding on the message using random number 'r'.
167170
168171
:param blinded: the blinded message, as integer, to unblind.
@@ -174,8 +177,27 @@ def unblind(self, blinded: int, r: int) -> int:
174177
See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29
175178
"""
176179

177-
return (rsa.common.inverse(r, self.n) * blinded) % self.n
180+
return (self.blindfac_inverse * blinded) % self.n
178181

182+
def _initial_blinding_factor(self) -> int:
183+
for _ in range(1000):
184+
blind_r = rsa.randnum.randint(self.n - 1)
185+
if rsa.prime.are_relatively_prime(self.n, blind_r):
186+
return blind_r
187+
raise RuntimeError('unable to find blinding factor')
188+
189+
def _update_blinding_factor(self):
190+
if self.blindfac < 0:
191+
# Compute initial blinding factor, which is rather slow to do.
192+
self.blindfac = self._initial_blinding_factor()
193+
self.blindfac_inverse = rsa.common.inverse(self.blindfac, self.n)
194+
else:
195+
# Reuse previous blinding factor as per section 9 of 'A Timing
196+
# Attack against RSA with the Chinese Remainder Theorem' by Werner
197+
# Schindler.
198+
# See https://tls.mbed.org/public/WSchindler-RSA_Timing_Attack.pdf
199+
self.blindfac = pow(self.blindfac, 2, self.n)
200+
self.blindfac_inverse = pow(self.blindfac_inverse, 2, self.n)
179201

180202
class PublicKey(AbstractKey):
181203
"""Represents a public RSA key.
@@ -414,13 +436,6 @@ def __ne__(self, other: typing.Any) -> bool:
414436
def __hash__(self) -> int:
415437
return hash((self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef))
416438

417-
def _get_blinding_factor(self) -> int:
418-
for _ in range(1000):
419-
blind_r = rsa.randnum.randint(self.n - 1)
420-
if rsa.prime.are_relatively_prime(self.n, blind_r):
421-
return blind_r
422-
raise RuntimeError('unable to find blinding factor')
423-
424439
def blinded_decrypt(self, encrypted: int) -> int:
425440
"""Decrypts the message using blinding to prevent side-channel attacks.
426441
@@ -431,11 +446,9 @@ def blinded_decrypt(self, encrypted: int) -> int:
431446
:rtype: int
432447
"""
433448

434-
blind_r = self._get_blinding_factor()
435-
blinded = self.blind(encrypted, blind_r) # blind before decrypting
449+
blinded = self.blind(encrypted) # blind before decrypting
436450
decrypted = rsa.core.decrypt_int(blinded, self.d, self.n)
437-
438-
return self.unblind(decrypted, blind_r)
451+
return self.unblind(decrypted)
439452

440453
def blinded_encrypt(self, message: int) -> int:
441454
"""Encrypts the message using blinding to prevent side-channel attacks.
@@ -447,10 +460,9 @@ def blinded_encrypt(self, message: int) -> int:
447460
:rtype: int
448461
"""
449462

450-
blind_r = self._get_blinding_factor()
451-
blinded = self.blind(message, blind_r) # blind before encrypting
463+
blinded = self.blind(message) # blind before encrypting
452464
encrypted = rsa.core.encrypt_int(blinded, self.d, self.n)
453-
return self.unblind(encrypted, blind_r)
465+
return self.unblind(encrypted)
454466

455467
@classmethod
456468
def _load_pkcs1_der(cls, keyfile: bytes) -> 'PrivateKey':

tests/test_key.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,20 @@ def test_blinding(self):
2121
message = 12345
2222
encrypted = rsa.core.encrypt_int(message, pk.e, pk.n)
2323

24-
blinded = pk.blind(encrypted, 4134431) # blind before decrypting
25-
decrypted = rsa.core.decrypt_int(blinded, pk.d, pk.n)
26-
unblinded = pk.unblind(decrypted, 4134431)
24+
blinded_1 = pk.blind(encrypted) # blind before decrypting
25+
decrypted = rsa.core.decrypt_int(blinded_1, pk.d, pk.n)
26+
unblinded_1 = pk.unblind(decrypted)
2727

28-
self.assertEqual(unblinded, message)
28+
self.assertEqual(unblinded_1, message)
29+
30+
# Re-blinding should use a different blinding factor.
31+
blinded_2 = pk.blind(encrypted) # blind before decrypting
32+
self.assertNotEqual(blinded_1, blinded_2)
33+
34+
# The unblinding should still work, though.
35+
decrypted = rsa.core.decrypt_int(blinded_2, pk.d, pk.n)
36+
unblinded_2 = pk.unblind(decrypted)
37+
self.assertEqual(unblinded_2, message)
2938

3039

3140
class KeyGenTest(unittest.TestCase):

0 commit comments

Comments
 (0)