From 2e161cf7ee14db486ebdce2cc4eebbbe03a7b571 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Mon, 18 Apr 2022 20:39:54 -0500 Subject: [PATCH] load PSS keys (OpenSSL only) but strip the constraints --- CHANGELOG.rst | 6 ++ src/_cffi_src/openssl/cryptography.py | 3 + src/_cffi_src/openssl/evp.py | 4 +- .../hazmat/backends/openssl/backend.py | 24 ++++++++ tests/hazmat/primitives/test_rsa.py | 59 +++++++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f999a356ea5e..eb47e372d70c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -70,6 +70,12 @@ Changelog to :class:`~cryptography.hazmat.primitives.asymmetric.padding.PSS`. This constant will set the salt length to the same length as the ``PSS`` hash algorithm. +* Added support for loading RSA-PSS key types with + :func:`~cryptography.hazmat.primitives.serialization.load_pem_private_key` + and + :func:`~cryptography.hazmat.primitives.serialization.load_der_private_key`. + This functionality is limited to OpenSSL 1.1.1e+ and loads the key as a + normal RSA private key, discarding the PSS constraint information. .. _v36-0-2: diff --git a/src/_cffi_src/openssl/cryptography.py b/src/_cffi_src/openssl/cryptography.py index 1ad7fb616b93..f92dd2a0a2d9 100644 --- a/src/_cffi_src/openssl/cryptography.py +++ b/src/_cffi_src/openssl/cryptography.py @@ -70,6 +70,8 @@ (OPENSSL_VERSION_NUMBER < 0x10101020 || CRYPTOGRAPHY_IS_LIBRESSL) #define CRYPTOGRAPHY_OPENSSL_LESS_THAN_111D \ (OPENSSL_VERSION_NUMBER < 0x10101040 || CRYPTOGRAPHY_IS_LIBRESSL) +#define CRYPTOGRAPHY_OPENSSL_LESS_THAN_111E \ + (OPENSSL_VERSION_NUMBER < 0x10101050 || CRYPTOGRAPHY_IS_LIBRESSL) #if (CRYPTOGRAPHY_OPENSSL_LESS_THAN_111D && !CRYPTOGRAPHY_IS_LIBRESSL && \ !defined(OPENSSL_NO_ENGINE)) || defined(USE_OSRANDOM_RNG_FOR_TESTING) #define CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE 1 @@ -84,6 +86,7 @@ static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_111; static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B; +static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_111E; static const int CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE; static const int CRYPTOGRAPHY_LIBRESSL_LESS_THAN_340; diff --git a/src/_cffi_src/openssl/evp.py b/src/_cffi_src/openssl/evp.py index a9fc9897550f..f4d9fb953cd5 100644 --- a/src/_cffi_src/openssl/evp.py +++ b/src/_cffi_src/openssl/evp.py @@ -295,8 +295,8 @@ static const long Cryptography_HAS_EVP_PKEY_DH = 1; #endif -// OpenSSL 1.1.0 doesn't define this value. But its presence isn't -// unsafe so we don't need to remove it if unsupported. +// This can be removed when we drop OpenSSL 1.1.0 support +// OPENSSL_LESS_THAN_111 #if !defined(EVP_PKEY_RSA_PSS) #define EVP_PKEY_RSA_PSS 912 #endif diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 22e575fad8a9..c4d7f01249aa 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -644,6 +644,30 @@ def _evp_pkey_to_private_key(self, evp_pkey) -> PRIVATE_KEY_TYPES: return _RSAPrivateKey( self, rsa_cdata, evp_pkey, self._rsa_skip_check_key ) + elif ( + key_type == self._lib.EVP_PKEY_RSA_PSS + and not self._lib.CRYPTOGRAPHY_IS_LIBRESSL + and not self._lib.CRYPTOGRAPHY_IS_BORINGSSL + and not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111E + ): + # At the moment the way we handle RSA PSS keys is to strip the + # PSS constraints from them and treat them as normal RSA keys + # Unfortunately the RSA * itself tracks this data so we need to + # extract, serialize, and reload it without the constraints. + rsa_cdata = self._lib.EVP_PKEY_get1_RSA(evp_pkey) + self.openssl_assert(rsa_cdata != self._ffi.NULL) + rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free) + bio = self._create_mem_bio_gc() + res = self._lib.i2d_RSAPrivateKey_bio(bio, rsa_cdata) + self.openssl_assert(res == 1) + new_evp_pkey = self._lib.d2i_PrivateKey_bio(bio, self._ffi.NULL) + self.openssl_assert(new_evp_pkey != self._ffi.NULL) + new_rsa_cdata = self._lib.EVP_PKEY_get1_RSA(new_evp_pkey) + self.openssl_assert(new_rsa_cdata != self._ffi.NULL) + new_rsa_cdata = self._ffi.gc(new_rsa_cdata, self._lib.RSA_free) + return _RSAPrivateKey( + self, new_rsa_cdata, new_evp_pkey, self._rsa_skip_check_key + ) elif key_type == self._lib.EVP_PKEY_DSA: dsa_cdata = self._lib.EVP_PKEY_get1_DSA(evp_pkey) self.openssl_assert(dsa_cdata != self._ffi.NULL) diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index 089768b99222..3d96ef289159 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -11,6 +11,7 @@ from cryptography.exceptions import ( InvalidSignature, + UnsupportedAlgorithm, _Reasons, ) from cryptography.hazmat.primitives import hashes, serialization @@ -256,6 +257,64 @@ def test_load_pss_vect_example_keys(self, pkcs1_example): assert public_num.n == public_num2.n assert public_num.e == public_num2.e + @pytest.mark.supported( + only_if=lambda backend: ( + not backend._lib.CRYPTOGRAPHY_IS_LIBRESSL + and not backend._lib.CRYPTOGRAPHY_IS_BORINGSSL + and not backend._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111E + ), + skip_message="Does not support RSA PSS loading", + ) + @pytest.mark.parametrize( + "path", + [ + os.path.join("asymmetric", "PKCS8", "rsa_pss_2048.pem"), + os.path.join("asymmetric", "PKCS8", "rsa_pss_2048_hash.pem"), + os.path.join("asymmetric", "PKCS8", "rsa_pss_2048_hash_mask.pem"), + os.path.join( + "asymmetric", "PKCS8", "rsa_pss_2048_hash_mask_diff.pem" + ), + os.path.join( + "asymmetric", "PKCS8", "rsa_pss_2048_hash_mask_salt.pem" + ), + ], + ) + def test_load_pss_keys_strips_constraints(self, path, backend): + key = load_vectors_from_file( + filename=path, + loader=lambda p: serialization.load_pem_private_key( + p.read(), None + ), + mode="rb", + ) + # These keys have constraints that prohibit PKCS1v15 signing, + # but for now we load them without the constraint and test that + # it's truly removed by performing a disallowed signature. + assert isinstance(key, rsa.RSAPrivateKey) + key.sign(b"whatever", padding.PKCS1v15(), hashes.SHA224()) + + @pytest.mark.supported( + only_if=lambda backend: ( + backend._lib.CRYPTOGRAPHY_IS_LIBRESSL + or backend._lib.CRYPTOGRAPHY_IS_BORINGSSL + or backend._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111E + ), + skip_message="Test requires a backend without RSA-PSS key support", + ) + def test_load_pss_unsupported(self, backend): + # Key loading errors unfortunately have multiple paths so + # we need to allow ValueError and UnsupportedAlgorithm + with pytest.raises((UnsupportedAlgorithm, ValueError)): + load_vectors_from_file( + filename=os.path.join( + "asymmetric", "PKCS8", "rsa_pss_2048.pem" + ), + loader=lambda p: serialization.load_pem_private_key( + p.read(), None + ), + mode="rb", + ) + @pytest.mark.parametrize( "vector", load_vectors_from_file(