WinHTTP与Python Requests证书验证异常:CN有效SAN无效时未触发连接失败
Got it, let's tackle this issue for both WinHTTP and Python Requests since neither is enforcing the strict SAN validation required by Common Criteria FCS_TLSC_EXT.1.2 Test 2. The core requirement here is clear: when a server certificate includes a SAN extension, the CN must be ignored entirely—only matching SAN entries are valid for hostname verification. Here's how to fix each component:
WinHTTP's default behavior may fall back to the CN even when a SAN extension exists (which violates the Common Criteria requirement). To enforce strict SAN checking, you'll need to implement a custom certificate validation callback:
- Register a status callback using
WinHttpSetStatusCallbackfor your WinHTTP session. - Intercept certificate receipt in the callback (look for
WINHTTP_CALLBACK_STATUS_CERTIFICATE_RECEIVEDevents). - Parse the certificate's SAN extension: Use Windows CryptoAPI functions (like
CryptQueryObject,CryptDecodeObjectEx) to extract the SAN entries from the certificate. - Enforce SAN-only matching: If the SAN extension exists, verify that at least one DNS entry in the SAN matches your reference identifier. If no match is found, return an error to terminate the connection. Ignore the CN entirely in this case.
Example Snippet (C++)
void CALLBACK WinHttpStatusCallback( HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpStatusInformation, DWORD dwStatusInformationLength ) { if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_CERTIFICATE_RECEIVED) { PWINHTTP_CERTIFICATE_INFO pCertInfo = (PWINHTTP_CERTIFICATE_INFO)lpStatusInformation; HCERTSTORE hCertStore = CertOpenSystemStore(NULL, "MY"); PCCERT_CONTEXT pCertContext = CertCreateCertificateContext(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, pCertInfo->pbCertEncoded, pCertInfo->dwCertEncodedSize); // Extract SAN extension DWORD dwDataLen = 0; CertGetCertificateContextProperty(pCertContext, CERT_SUBJECT_ALT_NAME2_PROP_ID, NULL, &dwDataLen); std::vector<BYTE> sanData(dwDataLen); CertGetCertificateContextProperty(pCertContext, CERT_SUBJECT_ALT_NAME2_PROP_ID, sanData.data(), &dwDataLen); // Parse SAN entries (simplified example) BOOL sanMatch = FALSE; PCERT_ALT_NAME_INFO pSanInfo = (PCERT_ALT_NAME_INFO)sanData.data(); for (DWORD i = 0; i < pSanInfo->cAltEntry; i++) { if (pSanInfo->rgAltEntry[i].dwAltNameChoice == CERT_ALT_NAME_DNS_NAME) { if (_wcsicmp(pSanInfo->rgAltEntry[i].pwszDNSName, L"your-reference-identifier.com") == 0) { sanMatch = TRUE; break; } } } // If SAN exists but no match, abort the connection if (pSanInfo->cAltEntry > 0 && !sanMatch) { WinHttpCloseHandle(hInternet); } CertFreeCertificateContext(pCertContext); CertCloseStore(hCertStore, 0); } }
Python Requests relies on urllib3 for SSL validation. While newer urllib3 versions follow RFC 6125 (ignoring CN when SAN exists), older versions or custom setups might still allow CN fallback. Here are two approaches:
Option 1: Upgrade Dependencies
First, ensure you're using the latest versions of requests and urllib3:
pip install --upgrade requests urllib3
Newer versions should automatically enforce SAN-only checking when a SAN extension is present.
Option 2: Custom Validation Callback
If upgrading doesn't work (or you need explicit control), implement a custom adapter to enforce strict SAN validation:
from cryptography import x509 from cryptography.x509.oid import NameOID import requests from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager from urllib3.exceptions import SSLError class StrictSANAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): # Override the default hostname assertion logic kwargs["assert_hostname"] = self._strict_san_assertion super().init_poolmanager(*args, **kwargs) def _strict_san_assertion(self, cert_data, hostname): # Parse the PEM-encoded certificate cert = x509.load_pem_x509_certificate(cert_data.encode("utf-8")) # Check for SAN extension try: san_ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME) san_dns_names = san_ext.value.get_values_for_type(x509.DNSName) # Verify hostname is in SAN entries if hostname not in san_dns_names: raise SSLError(f"Hostname '{hostname}' not found in certificate SAN entries: {san_dns_names}") except x509.ExtensionNotFound: # No SAN exists, fall back to CN validation cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value if cn != hostname: raise SSLError(f"Certificate CN '{cn}' does not match hostname '{hostname}'") return True # Use the custom adapter in your session session = requests.Session() session.mount("https://", StrictSANAdapter()) # Test the connection (should fail if SAN exists but doesn't match) try: response = session.get("https://your-test-server.com") print("Connection succeeded (unexpected!)") except SSLError as e: print(f"Connection failed as required: {str(e)}")
Both approaches ensure that your client adheres to FCS_TLSC_EXT.1.2 Test 2: if a server certificate has a SAN extension with no matching identifiers (even if the CN is correct), the connection will terminate with an error.
内容的提问来源于stack exchange,提问作者Muhammad Uzair Khan

