From 0099485a97c2e766d3c3a518eb4b07395a9e6984 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Mon, 18 Apr 2022 09:55:18 +0200 Subject: [PATCH 1/5] RSA PSS openssl constant --- src/_cffi_src/openssl/evp.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/_cffi_src/openssl/evp.py b/src/_cffi_src/openssl/evp.py index 8828cdc92936..a9fc9897550f 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; @@ -293,4 +294,10 @@ #else 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. +#if !defined(EVP_PKEY_RSA_PSS) +#define EVP_PKEY_RSA_PSS 912 +#endif """ From 2e161cf7ee14db486ebdce2cc4eebbbe03a7b571 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Mon, 18 Apr 2022 20:39:54 -0500 Subject: [PATCH 2/5] 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( From 8f8c0b264510c4fd7e4ee490368499f319c04e9c Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Mon, 25 Apr 2022 21:52:07 -0500 Subject: [PATCH 3/5] empty commit for CI, sigh From b5ebceeca2eba36ebc489186e097e2ddfc11ce6c Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Mon, 25 Apr 2022 22:16:15 -0500 Subject: [PATCH 4/5] review feedback --- src/cryptography/hazmat/backends/openssl/backend.py | 9 +-------- tests/hazmat/primitives/test_rsa.py | 9 ++++++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index c4d7f01249aa..a663056abadf 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -660,14 +660,7 @@ def _evp_pkey_to_private_key(self, evp_pkey) -> PRIVATE_KEY_TYPES: 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 - ) + return self.load_der_private_key(self._read_mem_bio(bio), 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 3d96ef289159..b45c4e6e5c9d 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -283,7 +283,7 @@ 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 + p.read(), password=None ), mode="rb", ) @@ -291,7 +291,10 @@ def test_load_pss_keys_strips_constraints(self, path, backend): # 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()) + 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: ( @@ -310,7 +313,7 @@ def test_load_pss_unsupported(self, backend): "asymmetric", "PKCS8", "rsa_pss_2048.pem" ), loader=lambda p: serialization.load_pem_private_key( - p.read(), None + p.read(), password=None ), mode="rb", ) From f67bc6b2ab51cc956b2988c548f82cc467bf3aae Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Mon, 25 Apr 2022 22:26:24 -0500 Subject: [PATCH 5/5] nit --- src/cryptography/hazmat/backends/openssl/backend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index a663056abadf..055b23fbd6d1 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -660,7 +660,9 @@ def _evp_pkey_to_private_key(self, evp_pkey) -> PRIVATE_KEY_TYPES: 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), None) + 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)