Repository: pyca/cryptography

Issue:
malformed RSA private key can trigger OpenSSL internal error
A private key with a q value of 2 triggers an internal openssl error with python cryptography.

Try this key:
https://github.com/vcsjones/badkeys/blob/main/rsa-1025-q-is-2/key.pem

Example code:
```
#!/usr/bin/python3
from cryptography.hazmat.primitives import serialization
key="""-----BEGIN RSA PRIVATE KEY-----
MIICoQIBAAKBgQHuRPTpIUVPb2xakO/6v/MuBfDglctfg1BYKkXg9Q9S5CKmrRfp
zDMygdqlTjr46SF1vqcsMPk1mswE0O+AW7ljXjq0nrkHoIQiZnDZbHMAyXIgsENd
lZ1ZLGuwf8QD2hMUqGPYYfoVH0LeNFJkZU045znqYGuWXarmZv+9V6mkcgIDAQAB
AoGBAKrZWCTblMcz9yrJDcLJpefjMtOWw8FEtTl8h0IOw1i+NgISNAFjTdEoFKlu
RLE0eJXoLIX4ebQfSWViyV6/lfH0qOs1MDUkNXhkm550P75mY5ZMB0UqicTAt4q/
Z0SDlXPfDwBFQboaX7VKNa6xW4OPXbQX1yiXQRM6SC0tR5zpAoGBAPcienSQoqe3
ti1Id/1f+ZcC+HBK5a/BqCwVIvB6h6lyEVNWi/TmGZlA7VKnHXx0kLrfU5YYfJrN
ZgJod8At3LGvHVpPXIPQQhEzOGy2OYBkuRBYIa7KzqyWNdg/4gHtCYpUMeww/QqP
oW8aKTIyppxznPUwNcsu1XMzf96r1NI5AgECAoGBAKrZWCTblMcz9yrJDcLJpefj
MtOWw8FEtTl8h0IOw1i+NgISNAFjTdEoFKluRLE0eJXoLIX4ebQfSWViyV6/lfH0
qOs1MDUkNXhkm550P75mY5ZMB0UqicTAt4q/Z0SDlXPfDwBFQboaX7VKNa6xW4OP
XbQX1yiXQRM6SC0tR5zpAgEAAoGAe5E9OkhRU9vbFqQ7/q/8y4F8OCVy1+DUFgqR
eD1D1LkIqatF+nMMzKB2qVOOvjpIXW+pyww+TWazATQ74BbuWNeOrSeuQeghCJmc
NlscwDJciCwQ12VnVksa7B/xAPaExSoY9hh+hUfQt40UmRlTTjnOepga5ZdquZm/
71XqaR0=
-----END RSA PRIVATE KEY-----
""".encode()
serialization.load_pem_private_key(key,password=None)
```

Result:
```
Traceback (most recent call last):
  File "/tmp/x/./foo.py", line 21, in <module>
    serialization.load_pem_private_key(key,password=None)
  File "/usr/lib/python3.10/site-packages/cryptography/hazmat/primitives/serialization/base.py", line 22, in load_pem_private_key
    return ossl.load_pem_private_key(data, password)
  File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 823, in load_pem_private_key
    return self._load_key(
  File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 1088, in _load_key
    return convert_func(evp_pkey)
  File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 595, in _evp_pkey_to_private_key
    return _RSAPrivateKey(
  File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/rsa.py", line 403, in __init__
    backend.openssl_assert(res == 1)
  File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 192, in openssl_assert
    return binding._openssl_assert(self._lib, ok, errors=errors)
  File "/usr/lib/python3.10/site-packages/cryptography/hazmat/bindings/openssl/binding.py", line 77, in _openssl_assert
    raise InternalError(
cryptography.exceptions.InternalError: Unknown OpenSSL error. This error is commonly encountered when another library is not cleaning up the OpenSSL error stack. If you are using cryptography with another library that uses OpenSSL try disabling it before reporting a bug. Otherwise please file an issue at https://github.com/pyca/cryptography/issues with information on how to reproduce this. ([_OpenSSLErrorWithText(code=50843714, lib=3, reason=66, reason_text=b'error:0307D042:bignum routines:BN_mod_exp_recp:called a function you should not call'), _OpenSSLErrorWithText(code=67665923, lib=4, reason=3, reason_text=b'error:04088003:rsa routines:RSA_setup_blinding:BN lib')])



Discussion:
Hmm, so we can easily handle the return value here, but the question is
whether or not there's a more correct API to use to detect this
circumstance.

On Mon, Apr 11, 2022 at 6:33 AM Hanno Böck ***@***.***> wrote:

> A private key with a q value of 2 triggers an internal openssl error with
> python cryptography.
>
> Try this key:
> https://github.com/vcsjones/badkeys/blob/main/rsa-1025-q-is-2/key.pem
>
> Example code:
>
> #!/usr/bin/python3
> from cryptography.hazmat.primitives import serialization
> key="""-----BEGIN RSA PRIVATE KEY-----
> MIICoQIBAAKBgQHuRPTpIUVPb2xakO/6v/MuBfDglctfg1BYKkXg9Q9S5CKmrRfp
> zDMygdqlTjr46SF1vqcsMPk1mswE0O+AW7ljXjq0nrkHoIQiZnDZbHMAyXIgsENd
> lZ1ZLGuwf8QD2hMUqGPYYfoVH0LeNFJkZU045znqYGuWXarmZv+9V6mkcgIDAQAB
> AoGBAKrZWCTblMcz9yrJDcLJpefjMtOWw8FEtTl8h0IOw1i+NgISNAFjTdEoFKlu
> RLE0eJXoLIX4ebQfSWViyV6/lfH0qOs1MDUkNXhkm550P75mY5ZMB0UqicTAt4q/
> Z0SDlXPfDwBFQboaX7VKNa6xW4OPXbQX1yiXQRM6SC0tR5zpAoGBAPcienSQoqe3
> ti1Id/1f+ZcC+HBK5a/BqCwVIvB6h6lyEVNWi/TmGZlA7VKnHXx0kLrfU5YYfJrN
> ZgJod8At3LGvHVpPXIPQQhEzOGy2OYBkuRBYIa7KzqyWNdg/4gHtCYpUMeww/QqP
> oW8aKTIyppxznPUwNcsu1XMzf96r1NI5AgECAoGBAKrZWCTblMcz9yrJDcLJpefj
> MtOWw8FEtTl8h0IOw1i+NgISNAFjTdEoFKluRLE0eJXoLIX4ebQfSWViyV6/lfH0
> qOs1MDUkNXhkm550P75mY5ZMB0UqicTAt4q/Z0SDlXPfDwBFQboaX7VKNa6xW4OP
> XbQX1yiXQRM6SC0tR5zpAgEAAoGAe5E9OkhRU9vbFqQ7/q/8y4F8OCVy1+DUFgqR
> eD1D1LkIqatF+nMMzKB2qVOOvjpIXW+pyww+TWazATQ74BbuWNeOrSeuQeghCJmc
> NlscwDJciCwQ12VnVksa7B/xAPaExSoY9hh+hUfQt40UmRlTTjnOepga5ZdquZm/
> 71XqaR0=
> -----END RSA PRIVATE KEY-----
> """.encode()
> serialization.load_pem_private_key(key,password=None)
>
> Result:
>
> Traceback (most recent call last):
>   File "/tmp/x/./foo.py", line 21, in <module>
>     serialization.load_pem_private_key(key,password=None)
>   File "/usr/lib/python3.10/site-packages/cryptography/hazmat/primitives/serialization/base.py", line 22, in load_pem_private_key
>     return ossl.load_pem_private_key(data, password)
>   File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 823, in load_pem_private_key
>     return self._load_key(
>   File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 1088, in _load_key
>     return convert_func(evp_pkey)
>   File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 595, in _evp_pkey_to_private_key
>     return _RSAPrivateKey(
>   File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/rsa.py", line 403, in __init__
>     backend.openssl_assert(res == 1)
>   File "/usr/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 192, in openssl_assert
>     return binding._openssl_assert(self._lib, ok, errors=errors)
>   File "/usr/lib/python3.10/site-packages/cryptography/hazmat/bindings/openssl/binding.py", line 77, in _openssl_assert
>     raise InternalError(
> cryptography.exceptions.InternalError: Unknown OpenSSL error. This error is commonly encountered when another library is not cleaning up the OpenSSL error stack. If you are using cryptography with another library that uses OpenSSL try disabling it before reporting a bug. Otherwise please file an issue at https://github.com/pyca/cryptography/issues with information on how to reproduce this. ([_OpenSSLErrorWithText(code=50843714, lib=3, reason=66, reason_text=b'error:0307D042:bignum routines:BN_mod_exp_recp:called a function you should not call'), _OpenSSLErrorWithText(code=67665923, lib=4, reason=3, reason_text=b'error:04088003:rsa routines:RSA_setup_blinding:BN lib')])
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/pyca/cryptography/issues/7056>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAAAGBFQUZKYXBWTEBTKGRLVEP5YFANCNFSM5TCXONOQ>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>


-- 
All that is necessary for evil to succeed is for good people to do nothing.


Resolving patch:
diff --git a/src/_cffi_src/openssl/bignum.py b/src/_cffi_src/openssl/bignum.py
--- a/src/_cffi_src/openssl/bignum.py
+++ b/src/_cffi_src/openssl/bignum.py
@@ -54,6 +54,7 @@
 
 int BN_cmp(const BIGNUM *, const BIGNUM *);
 int BN_is_negative(const BIGNUM *);
+int BN_is_odd(const BIGNUM *);
 int BN_add(BIGNUM *, const BIGNUM *, const BIGNUM *);
 int BN_sub(BIGNUM *, const BIGNUM *, const BIGNUM *);
 int BN_nnmod(BIGNUM *, const BIGNUM *, const BIGNUM *, BN_CTX *);
diff --git a/src/cryptography/hazmat/backends/openssl/rsa.py b/src/cryptography/hazmat/backends/openssl/rsa.py
--- a/src/cryptography/hazmat/backends/openssl/rsa.py
+++ b/src/cryptography/hazmat/backends/openssl/rsa.py
@@ -368,6 +368,18 @@ def __init__(
             if res != 1:
                 errors = backend._consume_errors_with_text()
                 raise ValueError("Invalid private key", errors)
+            # 2 is prime and passes an RSA key check, so we also check
+            # if p and q are odd just to be safe.
+            p = backend._ffi.new("BIGNUM **")
+            q = backend._ffi.new("BIGNUM **")
+            backend._lib.RSA_get0_factors(rsa_cdata, p, q)
+            backend.openssl_assert(p[0] != backend._ffi.NULL)
+            backend.openssl_assert(q[0] != backend._ffi.NULL)
+            p_odd = backend._lib.BN_is_odd(p[0])
+            q_odd = backend._lib.BN_is_odd(q[0])
+            if p_odd != 1 or q_odd != 1:
+                errors = backend._consume_errors_with_text()
+                raise ValueError("Invalid private key", errors)
 
         # Blinding is on by default in many versions of OpenSSL, but let's
         # just be conservative here.


Test patch:
diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py
--- a/tests/hazmat/primitives/test_serialization.py
+++ b/tests/hazmat/primitives/test_serialization.py
@@ -267,6 +267,17 @@ def test_wrong_format(self, backend):
                 key_data, b"this password will not be used", backend
             )
 
+    def test_invalid_rsa_even_q(self, backend):
+        data = load_vectors_from_file(
+            os.path.join(
+                "asymmetric", "PEM_Serialization", "rsa-bad-1025-q-is-2.pem"
+            ),
+            lambda pemfile: pemfile.read(),
+            mode="rb",
+        )
+        with pytest.raises(ValueError):
+            load_pem_private_key(data, None)
+
     def test_corrupt_der_pkcs8(self, backend):
         # unenc-rsa-pkcs8 with a bunch of data missing.
         key_data = textwrap.dedent(