diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 055b23fbd6d1..bf34946cbbfc 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -906,8 +906,20 @@ def load_pem_private_key( def load_pem_public_key(self, data: bytes) -> PUBLIC_KEY_TYPES: mem_bio = self._bytes_to_bio(data) + # In OpenSSL 3.0.x the PEM_read_bio_PUBKEY function will invoke + # the default password callback if you pass an encrypted private + # key. This is very, very, very bad as the default callback can + # trigger an interactive console prompt, which will hang the + # Python process. We therefore provide our own callback to + # catch this and error out properly. + userdata = self._ffi.new("CRYPTOGRAPHY_PASSWORD_DATA *") evp_pkey = self._lib.PEM_read_bio_PUBKEY( - mem_bio.bio, self._ffi.NULL, self._ffi.NULL, self._ffi.NULL + mem_bio.bio, + self._ffi.NULL, + self._ffi.addressof( + self._lib._original_lib, "Cryptography_pem_password_cb" + ), + userdata, ) if evp_pkey != self._ffi.NULL: evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free) @@ -920,7 +932,12 @@ def load_pem_public_key(self, data: bytes) -> PUBLIC_KEY_TYPES: res = self._lib.BIO_reset(mem_bio.bio) self.openssl_assert(res == 1) rsa_cdata = self._lib.PEM_read_bio_RSAPublicKey( - mem_bio.bio, self._ffi.NULL, self._ffi.NULL, self._ffi.NULL + mem_bio.bio, + self._ffi.NULL, + self._ffi.addressof( + self._lib._original_lib, "Cryptography_pem_password_cb" + ), + userdata, ) if rsa_cdata != self._ffi.NULL: rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index e4758b131b4e..999c5a811478 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -39,6 +39,7 @@ ) +from .fixtures_rsa import RSA_KEY_2048 from .test_ec import _skip_curve_unsupported from .utils import ( _check_dsa_private_numbers, @@ -513,6 +514,20 @@ def test_load_pem_rsa_public_key(self, key_file, backend): numbers = key.public_numbers() assert numbers.e == 65537 + def test_load_priv_key_with_public_key_api_fails(self, backend): + # In OpenSSL 3.0.x the PEM_read_bio_PUBKEY function will invoke + # the default password callback if you pass an encrypted private + # key. This is very, very, very bad as the default callback can + # trigger an interactive console prompt, which will hang the + # Python process. This test makes sure we don't do that. + priv_key_serialized = RSA_KEY_2048.private_key().private_bytes( + Encoding.PEM, + PrivateFormat.PKCS8, + BestAvailableEncryption(b"password"), + ) + with pytest.raises(ValueError): + load_pem_public_key(priv_key_serialized) + @pytest.mark.supported( only_if=lambda backend: backend.dsa_supported(), skip_message="Does not support DSA.",