From decec8795ae10ca30e5be0a00fb53db2fe867dfb Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 18 Mar 2022 06:47:33 +0200 Subject: [PATCH] Dedicated check for signature hash algorithms (#6931) * Dedicated check for signature hash algorithms Move the check for FIPS mode and blocked SHA1 signature algorithm into the backend code. Some distros will block SHA1 for RSA signatures in the near future. The new ``signature_hash_supported()`` method will allow us to flip the switch in one place. Note: The ban of SHA1 signatures does not affect MGF1's inner hash algorithm. Signed-off-by: Christian Heimes * Address flake issues * Update src/cryptography/hazmat/backends/openssl/backend.py --- .../hazmat/backends/openssl/backend.py | 14 +++- tests/hazmat/primitives/test_pkcs7.py | 7 +- tests/hazmat/primitives/test_rsa.py | 79 ++++++++++++++++--- tests/hazmat/primitives/utils.py | 5 ++ tests/x509/test_x509.py | 30 ++++++- 5 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 543db5f28..8a77cb78a 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -326,6 +326,15 @@ class Backend: evp_md = self._evp_md_from_algorithm(algorithm) return evp_md != self._ffi.NULL + def signature_hash_supported( + self, algorithm: hashes.HashAlgorithm + ) -> bool: + # Dedicated check for hashing algorithm use in message digest for + # signatures, e.g. RSA PKCS#1 v1.5 SHA1 (sha1WithRSAEncryption). + if self._fips_enabled and isinstance(algorithm, hashes.SHA1): + return False + return self.hash_supported(algorithm) + def scrypt_supported(self) -> bool: if self._fips_enabled: return False @@ -723,7 +732,8 @@ class Backend: if isinstance(padding, PKCS1v15): return True elif isinstance(padding, PSS) and isinstance(padding._mgf, MGF1): - # SHA1 is permissible in MGF1 in FIPS + # SHA1 is permissible in MGF1 in FIPS even when SHA1 is blocked + # as signature algorithm. if self._fips_enabled and isinstance( padding._mgf._algorithm, hashes.SHA1 ): @@ -854,7 +864,7 @@ class Backend: def dsa_hash_supported(self, algorithm: hashes.HashAlgorithm) -> bool: if not self.dsa_supported(): return False - return self.hash_supported(algorithm) + return self.signature_hash_supported(algorithm) def cmac_algorithm_supported(self, algorithm) -> bool: return self.cipher_supported( diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index 108fb2bee..3a106617a 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -14,6 +14,7 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ed25519, rsa from cryptography.hazmat.primitives.serialization import pkcs7 +from .utils import skip_signature_hash from ...utils import load_vectors_from_file, raises_unsupported_algorithm @@ -346,8 +347,7 @@ class TestPKCS7Builder: def test_sign_alternate_digests_der( self, hash_alg, expected_value, backend ): - if isinstance(hash_alg, hashes.SHA1) and backend._fips_enabled: - pytest.skip("SHA1 not supported in FIPS mode") + skip_signature_hash(backend, hash_alg) data = b"hello world" cert, key = _load_cert_key() @@ -375,8 +375,7 @@ class TestPKCS7Builder: def test_sign_alternate_digests_detached( self, hash_alg, expected_value, backend ): - if isinstance(hash_alg, hashes.SHA1) and backend._fips_enabled: - pytest.skip("SHA1 not supported in FIPS mode") + skip_signature_hash(backend, hash_alg) data = b"hello world" cert, key = _load_cert_key() diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index eab744ed6..3c95c96ae 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -369,7 +369,12 @@ class TestRSASignature: ), skip_message="Does not support PKCS1v1.5.", ) - @pytest.mark.skip_fips(reason="SHA1 signing not supported in FIPS mode.") + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) def test_pkcs1v15_signing(self, backend, disable_rsa_checks, subtests): vectors = _flatten_pkcs1_examples( load_vectors_from_file( @@ -406,7 +411,12 @@ class TestRSASignature: ), skip_message="Does not support PSS.", ) - @pytest.mark.skip_fips(reason="SHA1 signing not supported in FIPS mode.") + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) def test_pss_signing(self, subtests, backend): for private, public, example in _flatten_pkcs1_examples( load_vectors_from_file( @@ -549,7 +559,7 @@ class TestRSASignature: private_key.sign( b"msg", "notpadding", # type: ignore[arg-type] - hashes.SHA1(), + hashes.SHA256(), ) @pytest.mark.supported( @@ -686,6 +696,12 @@ class TestRSAVerification: ), skip_message="Does not support PKCS1v1.5.", ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) def test_pkcs1v15_verification(self, backend, subtests): vectors = _flatten_pkcs1_examples( load_vectors_from_file( @@ -811,6 +827,12 @@ class TestRSAVerification: ), skip_message="Does not support PSS.", ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) def test_pss_verification(self, subtests, backend): for private, public, example in _flatten_pkcs1_examples( load_vectors_from_file( @@ -843,6 +865,12 @@ class TestRSAVerification: ), skip_message="Does not support PSS.", ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) @pytest.mark.skip_fips(reason="Unsupported key size in FIPS mode.") def test_invalid_pss_signature_wrong_data(self, backend): public_key = rsa.RSAPublicNumbers( @@ -878,6 +906,12 @@ class TestRSAVerification: ), skip_message="Does not support PSS.", ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) @pytest.mark.skip_fips(reason="Unsupported key size in FIPS mode.") def test_invalid_pss_signature_wrong_key(self, backend): signature = binascii.unhexlify( @@ -915,6 +949,12 @@ class TestRSAVerification: ), skip_message="Does not support PSS.", ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) @pytest.mark.skip_fips(reason="Unsupported key size in FIPS mode.") def test_invalid_pss_signature_data_too_large_for_modulus(self, backend): # 2048 bit PSS signature @@ -941,6 +981,12 @@ class TestRSAVerification: hashes.SHA1(), ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) def test_invalid_pss_signature_recover(self, backend): private_key = RSA_KEY_2048.private_key(backend) public_key = private_key.public_key() @@ -967,7 +1013,7 @@ class TestRSAVerification: public_key = private_key.public_key() with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_PADDING): public_key.verify( - b"sig", b"msg", DummyAsymmetricPadding(), hashes.SHA1() + b"sig", b"msg", DummyAsymmetricPadding(), hashes.SHA256() ) def test_padding_incorrect_type(self, backend): @@ -978,7 +1024,7 @@ class TestRSAVerification: b"sig", b"msg", "notpadding", # type: ignore[arg-type] - hashes.SHA1(), + hashes.SHA256(), ) @pytest.mark.supported( @@ -997,7 +1043,7 @@ class TestRSAVerification: padding.PSS( mgf=DummyMGF(), salt_length=padding.PSS.MAX_LENGTH ), - hashes.SHA1(), + hashes.SHA256(), ) @pytest.mark.supported( @@ -1041,6 +1087,12 @@ class TestRSAVerification: ), skip_message="Does not support PSS.", ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA1 signature.", + ) @pytest.mark.skip_fips(reason="Unsupported key size in FIPS mode.") def test_pss_verify_salt_length_too_long(self, backend): signature = binascii.unhexlify( @@ -1109,8 +1161,11 @@ class TestRSAPSSMGF1Verification: mgf=padding.MGF1(hashes.SHA1()), salt_length=padding.PSS.MAX_LENGTH, ) + ) + and backend.signature_hash_supported(hashes.SHA1()), + skip_message=( + "Does not support PSS using MGF1 with SHA1 or SHA1 signature." ), - skip_message="Does not support PSS using MGF1 with SHA1.", )( generate_rsa_verification_test( load_rsa_nist_vectors, @@ -1242,7 +1297,7 @@ class TestRSAPSSMGF1Verification: class TestRSAPKCS1Verification: test_rsa_pkcs1v15_verify_sha1 = pytest.mark.supported( only_if=lambda backend: ( - backend.hash_supported(hashes.SHA1()) + backend.signature_hash_supported(hashes.SHA1()) and backend.rsa_padding_supported(padding.PKCS1v15()) ), skip_message="Does not support SHA1 and PKCS1v1.5.", @@ -1262,7 +1317,7 @@ class TestRSAPKCS1Verification: test_rsa_pkcs1v15_verify_sha224 = pytest.mark.supported( only_if=lambda backend: ( - backend.hash_supported(hashes.SHA224()) + backend.signature_hash_supported(hashes.SHA224()) and backend.rsa_padding_supported(padding.PKCS1v15()) ), skip_message="Does not support SHA224 and PKCS1v1.5.", @@ -1282,7 +1337,7 @@ class TestRSAPKCS1Verification: test_rsa_pkcs1v15_verify_sha256 = pytest.mark.supported( only_if=lambda backend: ( - backend.hash_supported(hashes.SHA256()) + backend.signature_hash_supported(hashes.SHA256()) and backend.rsa_padding_supported(padding.PKCS1v15()) ), skip_message="Does not support SHA256 and PKCS1v1.5.", @@ -1302,7 +1357,7 @@ class TestRSAPKCS1Verification: test_rsa_pkcs1v15_verify_sha384 = pytest.mark.supported( only_if=lambda backend: ( - backend.hash_supported(hashes.SHA384()) + backend.signature_hash_supported(hashes.SHA384()) and backend.rsa_padding_supported(padding.PKCS1v15()) ), skip_message="Does not support SHA384 and PKCS1v1.5.", @@ -1322,7 +1377,7 @@ class TestRSAPKCS1Verification: test_rsa_pkcs1v15_verify_sha512 = pytest.mark.supported( only_if=lambda backend: ( - backend.hash_supported(hashes.SHA512()) + backend.signature_hash_supported(hashes.SHA512()) and backend.rsa_padding_supported(padding.PKCS1v15()) ), skip_message="Does not support SHA512 and PKCS1v1.5.", diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py index 33a45d58b..03ca72d14 100644 --- a/tests/hazmat/primitives/utils.py +++ b/tests/hazmat/primitives/utils.py @@ -579,3 +579,8 @@ def skip_fips_traditional_openssl(backend, fmt): pytest.skip( "Traditional OpenSSL key format is not supported in FIPS mode." ) + + +def skip_signature_hash(backend, hash_alg: hashes.HashAlgorithm): + if not backend.signature_hash_supported(hash_alg): + pytest.skip(f"{hash_alg} is not a supported signature hash algorithm.") diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index 97b0284c8..df21078db 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -830,6 +830,12 @@ class TestRSACertificate: assert isinstance(public_key, rsa.RSAPublicKey) assert len(cert.signature) == public_key.key_size // 8 + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA-1 signature.", + ) def test_tbs_certificate_bytes(self, backend): cert = _load_cert( os.path.join("x509", "custom", "post2000utctime.pem"), @@ -1616,6 +1622,12 @@ class TestRSACertificateRequest: b"e36181e8c4c270c354b7f52c128db1b70639823324c7ea24791b7bc3d7005f3b" ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA-1 signature.", + ) def test_tbs_certrequest_bytes(self, backend): request = _load_cert( os.path.join("x509", "requests", "rsa_sha1.pem"), @@ -1776,8 +1788,8 @@ class TestRSACertificateRequest: ], ) def test_build_cert(self, hashalg, hashalg_oid, backend): - if not backend.hash_supported(hashalg()): - pytest.skip(f"{hashalg} not supported in FIPS mode") + if not backend.signature_hash_supported(hashalg()): + pytest.skip(f"{hashalg} signature not supported") issuer_private_key = RSA_KEY_2048.private_key(backend) subject_private_key = RSA_KEY_2048.private_key(backend) @@ -2714,8 +2726,8 @@ class TestCertificateBuilder: self, hashalg, hashalg_oid, backend ): _skip_curve_unsupported(backend, ec.SECP256R1()) - if not backend.hash_supported(hashalg()): - pytest.skip(f"{hashalg} not supported in FIPS mode") + if not backend.signature_hash_supported(hashalg()): + pytest.skip(f"{hashalg} signature not supported") issuer_private_key = ec.generate_private_key(ec.SECP256R1(), backend) subject_private_key = ec.generate_private_key(ec.SECP256R1(), backend) @@ -4389,6 +4401,12 @@ class TestCertificateSigningRequestBuilder: skip_message="Does not support DSA.", ) class TestDSACertificate: + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA1() + ), + skip_message="Does not support SHA-1 signature.", + ) def test_load_dsa_cert(self, backend): cert = _load_cert( os.path.join("x509", "custom", "dsa_selfsigned_ca.pem"), @@ -4517,6 +4535,10 @@ class TestDSACertificate: only_if=lambda backend: backend.dsa_supported(), skip_message="Does not support DSA.", ) +@pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported(hashes.SHA1()), + skip_message="Does not support SHA-1 signature.", +) class TestDSACertificateRequest: @pytest.mark.parametrize( ("path", "loader_func"),