Skip to content

Commit

Permalink
Add rsa_recover_private_exponent function (pyca#11193)
Browse files Browse the repository at this point in the history
Given the RSA public exponent (`e`), and the RSA primes (`p`, `q`), it is possible
to calculate the corresponding private exponent `d = e⁻¹ mod λ(n)` where
`λ(n) = lcm(p-1, q-1)`.

With this function added, it becomes possible to use the library to reconstruct an RSA
private key given *only* `p`, `q`, and `e`:

    from cryptography.hazmat.primitives.asymmetric import rsa

    n = p * q
    d = rsa.rsa_recover_private_exponent(e, p, q)  # newly-added piece
    iqmp = rsa.rsa_crt_iqmp(p, q)                  # preexisting
    dmp1 = rsa.rsa_crt_dmp1(d, p)                  # preexisting
    dmq1 = rsa.rsa_crt_dmq1(d, q)                  # preexisting

    assert rsa.rsa_recover_prime_factors(n, e, d) in ((p, q), (q, p))  # verify consistency

    privk = rsa.RSAPrivateNumbers(p, q, d, dmp1, dmq1, iqmp, rsa.RSAPublicNumbers(e, n)).private_key()

Older RSA implementations, including the original RSA paper, often used the
Euler totient function `ɸ(n) = (p-1) * (q-1)` instead of `λ(n)`.  The
private exponents generated by that method work equally well, but may be
larger than strictly necessary (`λ(n)` always divides `ɸ(n)`).  This commit
additionally implements `_rsa_recover_euler_private_exponent`, so that tests
of the internal structure of RSA private keys can allow for either the Euler
or the Carmichael versions of the private exponents.

It makes sense to expose only the more modern version (using the Carmichael
totient function) for public usage, given that it is slightly more
computationally efficient to use the keys in this form, and that some
standards like FIPS 186-4 require this form.  (See
https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf#page=63)
  • Loading branch information
dlenskiSB authored Jul 5, 2024
1 parent 5b23baa commit 8a7f27b
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Changelog
:meth:`~cryptography.x509.ocsp.OCSPSingleResponse.next_update_utc`,
These are timezone-aware variants of existing properties that return naïve
``datetime`` objects.
* Added
:func:`~cryptography.hazmat.primitives.asymmetric.rsa.rsa_recover_private_exponent`

.. _v42-0-8:

Expand Down
17 changes: 17 additions & 0 deletions docs/hazmat/primitives/asymmetric/rsa.rst
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,23 @@ this without having to do the math themselves.
Computes the ``dmq1`` parameter from the RSA private exponent (``d``) and
prime ``q``.

.. function:: rsa_recover_private_exponent(e, p, q)

.. versionadded:: 43.0.0

Computes the RSA private_exponent (``d``) given the public exponent (``e``)
and the RSA primes ``p`` and ``q``.

.. note::

This implementation uses the Carmichael totient function to return the
smallest working value of ``d``. Older RSA implementations, including the
original RSA paper, often used the Euler totient function, which results
in larger but equally functional private exponents. The private exponents
resulting from the Carmichael totient function, as returned here, are
slightly more computationally efficient to use, and some modern standards
require them.

.. function:: rsa_recover_prime_factors(n, e, d)

.. versionadded:: 0.8
Expand Down
3 changes: 3 additions & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Botan
Brainpool
Bullseye
Capitan
Carmichael
CentOS
changelog
Changelog
Expand Down Expand Up @@ -51,6 +52,7 @@ Docstrings
El
Encodings
endian
Euler
extendable
facto
fallback
Expand Down Expand Up @@ -128,6 +130,7 @@ Thawte
timestamp
timestamps
toolchain
totient
Trixie
tunable
Ubuntu
Expand Down
21 changes: 21 additions & 0 deletions src/cryptography/hazmat/primitives/asymmetric/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,27 @@ def rsa_crt_dmq1(private_exponent: int, q: int) -> int:
return private_exponent % (q - 1)


def rsa_recover_private_exponent(e: int, p: int, q: int) -> int:
"""
Compute the RSA private_exponent (d) given the public exponent (e)
and the RSA primes p and q.
This uses the Carmichael totient function to generate the
smallest possible working value of the private exponent.
"""
# This lambda_n is the Carmichael totient function.
# The original RSA paper uses the Euler totient function
# here: phi_n = (p - 1) * (q - 1)
# Either version of the private exponent will work, but the
# one generated by the older formulation may be larger
# than necessary. (lambda_n always divides phi_n)
#
# TODO: Replace with lcm(p - 1, q - 1) once the minimum
# supported Python version is >= 3.9.
lambda_n = (p - 1) * (q - 1) // gcd(p - 1, q - 1)
return _modinv(e, lambda_n)


# Controls the number of iterations rsa_recover_prime_factors will perform
# to obtain the prime factors. Each iteration increments by 2 so the actual
# maximum attempts is half this number.
Expand Down
31 changes: 30 additions & 1 deletion tests/hazmat/primitives/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,13 +522,42 @@ def rsa_verification_test(backend, params, hash_alg, pad_factory):
public_key.verify(signature, msg, pad, hash_alg)


def _rsa_recover_euler_private_exponent(e: int, p: int, q: int) -> int:
"""
Compute the RSA private_exponent (d) given the public exponent (e)
and the RSA primes p and q, following the usage of the original
RSA paper.
As in the original RSA paper, this uses the Euler totient function
instead of the Carmichael totient function, and thus may generate a
larger value of the private exponent than necessary.
See cryptography.hazmat.primitives.asymmetric.rsa_recover_private_exponent
for the public-facing version of this function, which uses the
preferred Carmichael totient function.
"""
phi_n = (p - 1) * (q - 1)
return rsa._modinv(e, phi_n)


def _check_rsa_private_numbers(skey):
assert skey
pkey = skey.public_numbers
assert pkey
assert pkey.e
assert pkey.n
assert skey.d

# Historically there have been two ways to calculate valid values of the
# private_exponent (d) given the public exponent (e):
# - using the Carmichael totient function (gives smaller and more
# computationally-efficient values, and is required by some standards)
# - using the Euler totient function (matching the original RSA paper)
# Allow for either here.
assert skey.d in (
rsa.rsa_recover_private_exponent(pkey.e, skey.p, skey.q),
_rsa_recover_euler_private_exponent(pkey.e, skey.p, skey.q),
)

assert skey.p * skey.q == pkey.n
assert skey.dmp1 == rsa.rsa_crt_dmp1(skey.d, skey.p)
assert skey.dmq1 == rsa.rsa_crt_dmq1(skey.d, skey.q)
Expand Down

0 comments on commit 8a7f27b

Please sign in to comment.