Repository: pyca/cryptography

Issue:
Clarification of expected behavior for functions recovering partial RSA keys (rsa_crt_dmq1, rsa_crt_iqmp)
I am observing some unexpected behavior with cryptography's functions to recover partial RSA keys. I can send pull requests to address this behavior, but I want to clarify what expectations are.

With invalid inputs, it is possible to trigger ZeroDivisionError exceptions in some of those functions.

Example 1:
```
#!/usr/bin/python3
from cryptography.hazmat.primitives.asymmetric import rsa
rsa.rsa_crt_dmq1(1, 1)
```

Example 2:
```
#!/usr/bin/python3
from cryptography.hazmat.primitives.asymmetric import rsa
rsa.rsa_crt_iqmp(0, 0)
```

My usual expectation would be that these functions should never cause an exception like ZeroDivisionError, and in case of invalid inputs, they should throw a ValueError. (This would also be in line with my changes submitted here: https://github.com/pyca/cryptography/pull/12272 )

Do you agree to that? If yes, I would submit some changes to catch invalid inputs and raise a ValueError. (I believe all those cases can be prevented in a similar way as #12272, i.e., there should never be any valid inputs <=1 for any parameter, but I'd have to check whether other inputs also trigger similar problems.)


Discussion:
I’d be fine with a PR addressing this.



Patch:
diff --git a/src/cryptography/hazmat/primitives/asymmetric/rsa.py b/src/cryptography/hazmat/primitives/asymmetric/rsa.py
index 2e192aaaa749..f94812e186f5 100644
--- a/src/cryptography/hazmat/primitives/asymmetric/rsa.py
+++ b/src/cryptography/hazmat/primitives/asymmetric/rsa.py
@@ -184,6 +184,8 @@ def rsa_crt_iqmp(p: int, q: int) -> int:
     """
     Compute the CRT (q ** -1) % p value from RSA primes p and q.
     """
+    if p <= 1 or q <= 1:
+        raise ValueError("Values can't be <= 1")
     return _modinv(q, p)
 
 
@@ -192,6 +194,8 @@ def rsa_crt_dmp1(private_exponent: int, p: int) -> int:
     Compute the CRT private_exponent % (p - 1) value from the RSA
     private_exponent (d) and p.
     """
+    if private_exponent <= 1 or p <= 1:
+        raise ValueError("Values can't be <= 1")
     return private_exponent % (p - 1)
 
 
@@ -200,6 +204,8 @@ def rsa_crt_dmq1(private_exponent: int, q: int) -> int:
     Compute the CRT private_exponent % (q - 1) value from the RSA
     private_exponent (d) and q.
     """
+    if private_exponent <= 1 or q <= 1:
+        raise ValueError("Values can't be <= 1")
     return private_exponent % (q - 1)
 
 
@@ -220,6 +226,8 @@ def rsa_recover_private_exponent(e: int, p: int, q: int) -> int:
     #
     # TODO: Replace with lcm(p - 1, q - 1) once the minimum
     # supported Python version is >= 3.9.
+    if e <= 1 or p <= 1 or q <= 1:
+        raise ValueError("Values can't be <= 1")
     lambda_n = (p - 1) * (q - 1) // gcd(p - 1, q - 1)
     return _modinv(e, lambda_n)
 


Test patch:
diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py
index 17c8c7c1f543..25edfb07592c 100644
--- a/tests/hazmat/primitives/test_rsa.py
+++ b/tests/hazmat/primitives/test_rsa.py
@@ -2388,6 +2388,28 @@ def test_invalid_recover_prime_factors(self):
             rsa.rsa_recover_prime_factors(21, -1, -1)
 
 
+class TestRSAPartial:
+    def test_rsa_partial(self):
+        # Toy RSA key values
+        p = 521
+        q = 491
+        e = 3
+        d = 16987
+        assert rsa.rsa_crt_iqmp(p, q) == 191
+        assert rsa.rsa_crt_dmp1(d, p) == 347
+        assert rsa.rsa_crt_dmq1(d, q) == 327
+        assert rsa.rsa_recover_private_exponent(e, p, q) == d
+
+        with pytest.raises(ValueError):
+            rsa.rsa_crt_iqmp(0, 0)
+        with pytest.raises(ValueError):
+            rsa.rsa_crt_dmp1(1, 1)
+        with pytest.raises(ValueError):
+            rsa.rsa_crt_dmq1(1, 1)
+        with pytest.raises(ValueError):
+            rsa.rsa_recover_private_exponent(0, 1, 0)
+
+
 class TestRSAPrivateKeySerialization:
     @pytest.mark.parametrize(
         ("fmt", "password"),