From 8393ed73e01af0a70a322cd5e1806c1d150ea3b7 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Mon, 25 Apr 2022 22:39:59 -0500 Subject: [PATCH] Load RSA PSS keys as regular RSA keys (#7112) * RSA PSS openssl constant * load PSS keys (OpenSSL only) but strip the constraints * empty commit for CI, sigh * review feedback * nit --- src/_cffi_src/openssl/cryptography.py | 3 + src/_cffi_src/openssl/evp.py | 7 +++ .../hazmat/backends/openssl/backend.py | 19 ++++++ tests/hazmat/primitives/test_rsa.py | 62 +++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/src/_cffi_src/openssl/cryptography.py b/src/_cffi_src/openssl/cryptography.py index 7d36feb5efe9..e4829c4490d5 100644 --- a/src/_cffi_src/openssl/cryptography.py +++ b/src/_cffi_src/openssl/cryptography.py @@ -67,6 +67,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 @@ -82,6 +84,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_IS_LIBRESSL; diff --git a/src/_cffi_src/openssl/evp.py b/src/_cffi_src/openssl/evp.py index ad7a0e71abcb..251117ca6266 100644 --- a/src/_cffi_src/openssl/evp.py +++ b/src/_cffi_src/openssl/evp.py @@ -16,6 +16,7 @@ typedef ... EVP_PKEY; typedef ... EVP_PKEY_CTX; static const int EVP_PKEY_RSA; +static const int EVP_PKEY_RSA_PSS; static const int EVP_PKEY_DSA; static const int EVP_PKEY_DH; static const int EVP_PKEY_DHX; @@ -288,4 +289,10 @@ #else static const long Cryptography_HAS_EVP_PKEY_DH = 1; #endif + +// 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 a6d0e8872cb7..39966a86b821 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -594,6 +594,25 @@ def _evp_pkey_to_private_key(self, evp_pkey): 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) + return self.load_der_private_key( + self._read_mem_bio(bio), password=None + ) 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 0315489dc611..281e97238c84 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -12,6 +12,7 @@ from cryptography.exceptions import ( AlreadyFinalized, InvalidSignature, + UnsupportedAlgorithm, _Reasons, ) from cryptography.hazmat.primitives import hashes, serialization @@ -257,6 +258,67 @@ 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(), password=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) + signature = key.sign(b"whatever", padding.PKCS1v15(), hashes.SHA224()) + key.public_key().verify( + signature, 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(), password=None + ), + mode="rb", + ) + @pytest.mark.parametrize( "vector", load_vectors_from_file(