From d8247aae674f72a2d6fcfe16e480cdb9c848b5a5 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Wed, 27 Apr 2022 11:41:35 -0500 Subject: [PATCH] Fix parsing of priv keys via pub key APIs to error properly in ossl3 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. --- .../hazmat/backends/openssl/backend.py | 21 +++++++++++++++++-- tests/hazmat/primitives/test_serialization.py | 15 +++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) 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.",