refactor(api): reuse SDK certificate validator and fix schema drift

Delete the API copy of `validate_certificate_bundle` and re-export the
SDK one. The local copy diverged: it missed `UnsupportedAlgorithm` on
PKCS#12 loading, `TypeError` on password-protected PEM keys, `DSA` and
`ENCRYPTED`/`OPENSSH` PEM label prefixes, and the leaf-first
normalization the SDK now depends on for azure-identity's thumbprint.
A single source keeps future SDK fixes from silently skipping the API.

Log the caught exception in `AzureProviderSecret.validate_certificate_content`
before raising the client-facing ValidationError so root-causing a bundle
parse failure doesn't need a rerun with debug on.

Drop `additionalProperties: false` from the two new Azure oneOf variants
in the ProviderSecretField schema. No other credential variant sets it,
so codegen'd clients that enforced the flag would reject payloads the
DRF serializer accepts.
This commit is contained in:
Lydia Vilchez
2026-08-31 18:22:57 +02:00
committed by GitHub
parent a1e9d243be
commit fcf06e148c
5 changed files with 11 additions and 63 deletions
-9
View File
@@ -21307,7 +21307,6 @@ components:
- client_id
- client_secret
- tenant_id
additionalProperties: false
- type: object
title: Azure Certificate Credentials
properties:
@@ -21327,7 +21326,6 @@ components:
- client_id
- certificate_content
- tenant_id
additionalProperties: false
- type: object
title: M365 Static Credentials
properties:
@@ -23458,7 +23456,6 @@ components:
- client_id
- client_secret
- tenant_id
additionalProperties: false
- type: object
title: Azure Certificate Credentials
properties:
@@ -23478,7 +23475,6 @@ components:
- client_id
- certificate_content
- tenant_id
additionalProperties: false
- type: object
title: M365 Static Credentials
properties:
@@ -23925,7 +23921,6 @@ components:
- client_id
- client_secret
- tenant_id
additionalProperties: false
- type: object
title: Azure Certificate Credentials
properties:
@@ -23945,7 +23940,6 @@ components:
- client_id
- certificate_content
- tenant_id
additionalProperties: false
- type: object
title: M365 Static Credentials
properties:
@@ -24414,7 +24408,6 @@ components:
- client_id
- client_secret
- tenant_id
additionalProperties: false
- type: object
title: Azure Certificate Credentials
properties:
@@ -24434,7 +24427,6 @@ components:
- client_id
- certificate_content
- tenant_id
additionalProperties: false
- type: object
title: M365 Static Credentials
properties:
@@ -27073,7 +27065,6 @@ components:
required:
- type
- id
additionalProperties: false
properties:
type:
type: string
@@ -402,10 +402,6 @@ class TestProviderSecretFieldSchema:
"client_secret",
"tenant_id",
}
assert (
azure_schemas["Azure Client Secret Credentials"]["additionalProperties"]
is False
)
assert azure_schemas["Azure Certificate Credentials"]["required"] == [
"client_id",
"certificate_content",
@@ -416,10 +412,6 @@ class TestProviderSecretFieldSchema:
"certificate_content",
"tenant_id",
}
assert (
azure_schemas["Azure Certificate Credentials"]["additionalProperties"]
is False
)
def test_oraclecloud_schema_includes_legacy_region_field(self):
schema = ProviderSecretField._spectacular_annotation["field"]
@@ -96,7 +96,6 @@ from rest_framework_json_api import serializers
},
},
"required": ["client_id", "client_secret", "tenant_id"],
"additionalProperties": False,
},
{
"type": "object",
@@ -117,7 +116,6 @@ from rest_framework_json_api import serializers
},
},
"required": ["client_id", "certificate_content", "tenant_id"],
"additionalProperties": False,
},
{
"type": "object",
+3
View File
@@ -1812,6 +1812,9 @@ class AzureProviderSecret(serializers.Serializer):
certificate_data = base64.b64decode(certificate_content, validate=True)
validate_certificate_bundle(certificate_data)
except Exception as e:
logger.error(
f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}"
)
# Field validators are invoked per-field; DRF already knows
# this error belongs to `certificate_content` and will nest
# the message under that key. Raising a dict here would
+8 -44
View File
@@ -1,17 +1,20 @@
import ipaddress
import re
import socket
import string
from urllib.parse import urlparse
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import pkcs12
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
# Re-exported so the SDK stays the single source of truth for bundle parsing:
# it covers PKCS#12 UnsupportedAlgorithm, encrypted PEM keys (TypeError), the
# full private-key PEM label set, and leaf-first normalization for
# azure-identity's thumbprint. A local copy silently drifted before.
from prowler.providers.azure.lib.certificate import ( # noqa: F401
validate_certificate_bundle,
)
LIGHTHOUSE_OPENAI_COMPATIBLE_ALLOWED_SCHEMES = frozenset({"https"})
LIGHTHOUSE_NAT64_WELL_KNOWN_PREFIX = ipaddress.IPv6Network("64:ff9b::/96")
LIGHTHOUSE_BLOCKED_METADATA_HOSTS = frozenset(
@@ -25,45 +28,6 @@ LIGHTHOUSE_BLOCKED_METADATA_HOSTS = frozenset(
)
def validate_certificate_bundle(certificate_data: bytes) -> None:
"""Validate that certificate data contains a matching certificate and key."""
try:
private_key, certificate, _ = pkcs12.load_key_and_certificates(
certificate_data, None, default_backend()
)
except ValueError:
certificate_match = re.search(
rb"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----",
certificate_data,
re.DOTALL,
)
private_key_match = re.search(
rb"-----BEGIN (?:RSA |EC )?PRIVATE KEY-----.*?-----END (?:RSA |EC )?PRIVATE KEY-----",
certificate_data,
re.DOTALL,
)
if not certificate_match or not private_key_match:
raise ValueError(
"the payload must contain a certificate and its private key"
)
certificate = x509.load_pem_x509_certificate(
certificate_match.group(), default_backend()
)
private_key = serialization.load_pem_private_key(
private_key_match.group(), password=None, backend=default_backend()
)
if certificate is None or private_key is None:
raise ValueError("the payload must contain a certificate and its private key")
encoding = serialization.Encoding.DER
public_format = serialization.PublicFormat.SubjectPublicKeyInfo
if certificate.public_key().public_bytes(
encoding, public_format
) != private_key.public_key().public_bytes(encoding, public_format):
raise ValueError("the certificate does not match the private key")
def _normalize_hostname(hostname: str) -> str:
return hostname.rstrip(".").lower()