From 5317c589b3481da8d70aade99ef59cc6cb511134 Mon Sep 17 00:00:00 2001 From: Lydia Vilchez Date: Tue, 25 Aug 2026 17:11:53 +0200 Subject: [PATCH] refactor(azure): handle encrypted PEM keys and tighten bundle test `cryptography.hazmat.primitives.serialization.load_pem_private_key` raises TypeError, not ValueError, when the caller passes password=None against an encrypted key. Add TypeError to verify_client's except tuple so that path becomes AzureNotValidCertificateContentError or AzureNotValidCertificatePathError instead of leaking. Assert the leaf- first ordering explicitly in the bundle test so a regression that returns the input unchanged cannot silently pass. --- prowler/providers/azure/azure_provider.py | 7 +++-- prowler/providers/azure/lib/certificate.py | 4 ++- tests/providers/azure/azure_provider_test.py | 33 ++++++++++++++++++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/prowler/providers/azure/azure_provider.py b/prowler/providers/azure/azure_provider.py index 8b4e97765f..6a79ccea5f 100644 --- a/prowler/providers/azure/azure_provider.py +++ b/prowler/providers/azure/azure_provider.py @@ -1833,10 +1833,13 @@ class AzureProvider(Provider): PermissionError, IsADirectoryError, OSError, + TypeError, ValueError, ) as error: - # Malformed base64, unreadable path, or bytes CertificateCredential - # cannot parse. Route to the typed certificate errors. + # Malformed base64, unreadable path, encrypted PEM key + # (`load_pem_private_key(password=None)` raises `TypeError`), or + # bytes CertificateCredential cannot parse. Route to the typed + # certificate errors. logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) diff --git a/prowler/providers/azure/lib/certificate.py b/prowler/providers/azure/lib/certificate.py index 5b02dbfcf2..c76cf1aa2f 100644 --- a/prowler/providers/azure/lib/certificate.py +++ b/prowler/providers/azure/lib/certificate.py @@ -27,7 +27,9 @@ def validate_certificate_bundle(certificate_data: bytes) -> bytes: Accepts either a PKCS#12/PFX blob (encrypted or unencrypted with a null password) or a concatenated PEM bundle. Raises ``ValueError`` when the payload is missing a certificate, missing a private key, or contains a - pair whose public keys do not match. + pair whose public keys do not match. Raises ``TypeError`` for + password-protected PEM private keys, which cryptography surfaces from + ``load_pem_private_key(..., password=None)``. The normalized bytes always place the leaf certificate before the private key so ``azure.identity.CertificateCredential`` — which uses the first diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index 37735a50b9..f669f004a8 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -2448,9 +2448,36 @@ class TestValidateCertificateBundleOrdering: bundle = other_cert_pem + leaf_cert_pem + leaf_key_pem - # No exception: `validate_certificate_bundle` walks every cert block - # and pairs the private key with the one that actually matches. - validate_certificate_bundle(bundle) + normalized = validate_certificate_bundle(bundle) + + # The matching leaf must come first: azure-identity uses the first + # BEGIN CERTIFICATE block to compute the credential thumbprint. + assert normalized.startswith(leaf_cert_pem) + assert leaf_key_pem in normalized + assert other_cert_pem not in normalized + + def test_rejects_password_protected_pem_key(self): + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + from prowler.providers.azure.lib.certificate import ( + validate_certificate_bundle, + ) + + cert_pem, _ = self._self_signed() + encrypted_key_pem = rsa.generate_private_key( + public_exponent=65537, key_size=2048 + ).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(b"prowler"), + ) + + # `load_pem_private_key(password=None)` raises TypeError for + # encrypted keys; the caller relies on that exception type to route + # to the typed certificate errors. + with pytest.raises(TypeError): + validate_certificate_bundle(cert_pem + encrypted_key_pem) def serialization_no_encryption():