Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private key operations don't use CRT #163

Closed
tomato42 opened this issue Oct 18, 2020 · 4 comments
Closed

Private key operations don't use CRT #163

tomato42 opened this issue Oct 18, 2020 · 4 comments

Comments

@tomato42
Copy link

The private key operations:

python-rsa/rsa/pkcs1.py

Lines 270 to 300 in 4beb68d

def sign_hash(hash_value: bytes, priv_key: key.PrivateKey, hash_method: str) -> bytes:
"""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 hash_value: A precomputed hash to sign (ignores message).
: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-224', 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.
"""
# Get the ASN1 code for this hash method
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_value
keylength = common.byte_size(priv_key.n)
padded = _pad_for_signing(cleartext, keylength)
payload = transform.bytes2int(padded)
encrypted = priv_key.blinded_encrypt(payload)
block = transform.int2bytes(encrypted, keylength)
return block

python-rsa/rsa/key.py

Lines 440 to 453 in 4beb68d

def blinded_encrypt(self, message: int) -> int:
"""Encrypts the message using blinding to prevent side-channel attacks.
:param message: the message to encrypt
:type message: int
:returns: the encrypted message
:rtype: int
"""
blind_r = self._get_blinding_factor()
blinded = self.blind(message, blind_r) # blind before encrypting
encrypted = rsa.core.encrypt_int(blinded, self.d, self.n)
return self.unblind(encrypted, blind_r)

python-rsa/rsa/core.py

Lines 29 to 53 in 4beb68d

def encrypt_int(message: int, ekey: int, n: int) -> int:
"""Encrypts a message using encryption key 'ekey', working modulo n"""
assert_int(message, 'message')
assert_int(ekey, 'ekey')
assert_int(n, 'n')
if message < 0:
raise ValueError('Only non-negative numbers are supported')
if message > n:
raise OverflowError("The message %i is too long for n=%i" % (message, n))
return pow(message, ekey, n)
def decrypt_int(cyphertext: int, dkey: int, n: int) -> int:
"""Decrypts a cypher text using the decryption key 'dkey', working modulo n"""
assert_int(cyphertext, 'cyphertext')
assert_int(dkey, 'dkey')
assert_int(n, 'n')
message = pow(cyphertext, dkey, n)
return message

Use simple pow(x, d, n) operation to calculate the signature or decrypt a message. Because n is composite, it's possible to use Chinese remainder theorem. This will speed up the private key operations by a factor of 2 up to 4.

I.e. Instead of doing:

pow(message, ekey, n)

the code should precompute values for the CRT (with d used instead of ekey as the private exponent):

d_p = d % (p - 1)
d_q = d % (q - 1)
q_inv = rsa.common.inverse(q, p)

and then it can compute the power modulo like so:

s1 = pow(message, d_p, p)
s2 = pow(message, d_q, q)
h = ((s1 - s2) * q_inv) % p
c = s2 + q * h
return c

(or course, as the CRT parameters are closely related to p and q, they should be considered part of the private key and treated accordingly)

@tomato42
Copy link
Author

This is despite the code actually calculating the values necessary for CRT:

python-rsa/rsa/key.py

Lines 370 to 379 in 4beb68d

def __init__(self, n: int, e: int, d: int, p: int, q: int) -> None:
AbstractKey.__init__(self, n, e)
self.d = d
self.p = p
self.q = q
# Calculate exponents and coefficient.
self.exp1 = int(d % (p - 1))
self.exp2 = int(d % (q - 1))
self.coef = rsa.common.inverse(q, p)

@sybrenstuvel
Copy link
Owner

I think I initially just added the code for computing those values for exporting to some PKCS standard file. Good catch, thanks.

@sybrenstuvel
Copy link
Owner

sybrenstuvel commented Mar 29, 2021

I wrote this little benchmark, just to see the effect:

import timeit

import rsa
from rsa import common, transform, core, pkcs1

with open("privatekey.pem", "rb") as infile:
    priv_key: rsa.PrivateKey = rsa.PrivateKey.load_pkcs1(infile.read())

with open(__file__, "rb") as infile:
    message = infile.read(500)


keylength = common.byte_size(priv_key.n)
padded = pkcs1._pad_for_encryption(message, keylength)
payload = transform.bytes2int(padded)


def decrypt_standard():
    return core.decrypt_int(payload, priv_key.d, priv_key.n)


def decrypt_crt():
    s1 = pow(payload, priv_key.exp1, priv_key.p)
    s2 = pow(payload, priv_key.exp2, priv_key.q)
    h = ((s1 - s2) * priv_key.coef) % priv_key.p
    c = s2 + priv_key.q * h
    return c


crypto_standard = decrypt_standard()
crypto_crt = decrypt_crt()
assert crypto_standard == crypto_crt, f"Expected {crypto_standard} == {crypto_crt}"

number = 10

time = timeit.timeit(decrypt_standard, globals=locals(), number=number) / number
print(f"decrypt_standard: {time:.4f} sec average")

time = timeit.timeit(decrypt_crt, globals=locals(), number=number) / number
print(f"decrypt_crt     : {time:.4f} sec average")

The result:

$ python benchmark_encryption.py 
decrypt_standard: 0.1458 sec average
decrypt_crt     : 0.0424 sec average

So yes, CRT is 3x faster in this case.

Update: this is for decryption, not encryption.

@tomato42
Copy link
Author

the same change can be applied to creation of signatures

mtremer pushed a commit to ipfire/ipfire-2.x that referenced this issue Feb 14, 2022
- Update from 4.0 to 4.8
- Update of rootfile
- Changelog
- Switch to [Poetry](https://python-poetry.org/) for dependency and release management.
- Compatibility with Python 3.10.
- Chain exceptions using `raise new_exception from old_exception`
  ([#157](sybrenstuvel/python-rsa#157))
- Added marker file for PEP 561. This will allow type checking tools in dependent projects
  to use type annotations from Python-RSA
  ([#136](sybrenstuvel/python-rsa#136)).
- Use the Chinese Remainder Theorem when decrypting with a private key. This
  makes decryption 2-4x faster
  ([#163](sybrenstuvel/python-rsa#163)).
- Fix picking/unpickling issue introduced in 4.7
  ([#173](sybrenstuvel/python-rsa#173))
- Fix threading issue introduced in 4.7
  ([#173](sybrenstuvel/python-rsa#173))
- Fix [#165](sybrenstuvel/python-rsa#165):
  CVE-2020-25658 - Bleichenbacher-style timing oracle in PKCS#1 v1.5 decryption
  code
- Add padding length check as described by PKCS#1 v1.5 (Fixes
  [#164](sybrenstuvel/python-rsa#164))
- Reuse of blinding factors to speed up blinding operations.
  Fixes [#162](sybrenstuvel/python-rsa#162).
- Declare & test support for Python 3.9
Version 4.4 and 4.6 are almost a re-tagged release of version 4.2. It requires
Python 3.5+. To avoid older Python installations from trying to upgrade to RSA
4.4, this is now made explicit in the `python_requires` argument in `setup.py`.
There was a mistake releasing 4.4 as "3.5+ only", which made it necessary to
retag 4.4 as 4.6 as well.
No functional changes compared to version 4.2.
Version 4.3 and 4.5 are almost a re-tagged release of version 4.0. It is the
last to support Python 2.7. This is now made explicit in the `python_requires`
argument in `setup.py`. Python 3.4 is not supported by this release. There was a
mistake releasing 4.4 as "3.5+ only", which made it necessary to retag 4.3 as
4.5 as well.
Two security fixes have also been backported, so 4.3 = 4.0 + these two fixes.
- Choose blinding factor relatively prime to N. Thanks Christian Heimes for pointing this out.
- Reject cyphertexts (when decrypting) and signatures (when verifying) that have
  been modified by prepending zero bytes. This resolves CVE-2020-13757. Thanks
  Carnil for pointing this out.
- Rolled back the switch to Poetry, and reverted back to using Pipenv + setup.py
  for dependency management. There apparently is an issue no-binary installs of
  packages build with Poetry. This fixes
  [#148](sybrenstuvel/python-rsa#148)
- Limited SHA3 support to those Python versions (3.6+) that support it natively.
  The third-party library that adds support for this to Python 3.5 is a binary
  package, and thus breaks the pure-Python nature of Python-RSA.
  This should fix [#147](sybrenstuvel/python-rsa#147).
- Added support for Python 3.8.
- Dropped support for Python 2 and 3.4.
- Added type annotations to the source code. This will make Python-RSA easier to use in
  your IDE, and allows better type checking.
- Added static type checking via [MyPy](http://mypy-lang.org/).
- Fix [#129](sybrenstuvel/python-rsa#129) Installing from source
  gives UnicodeDecodeError.
- Switched to using [Poetry](https://poetry.eustace.io/) for package
  management.
- Added support for SHA3 hashing: SHA3-256, SHA3-384, SHA3-512. This
  is natively supported by Python 3.6+ and supported via a third-party
  library on Python 3.5.
- Choose blinding factor relatively prime to N. Thanks Christian Heimes for pointing this out.
- Reject cyphertexts (when decrypting) and signatures (when verifying) that have
  been modified by prepending zero bytes. This resolves CVE-2020-13757. Thanks
  Adelapie for pointing this out.

Signed-off-by: Adolf Belka <[email protected]>
Reviewed-by: Peter Müller <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants