curl · Report #3734992
Schannel custom-CA path skips Extended Key Usage enforcement
How it was found
Hi all,
We believe the Schannel custom-CA verification path in lib/vtls/schannel_verify.c may skip Extended Key Usage enforcement. In particular, a certificate that chains to the trusted custom CA but contains only id-kp-clientAuth, rather than id-kp-serverAuth, may pass peer verification on Windows when curl is used with CURLOPT_CAINFO or CURLOPT_CAINFO_BLOB set.
Note: We have not actually tested this on a Windows machine (due to lack of ability), so this report is currently based on source review and Win32 API documentation.
The relevant code is in Curl_verify_certificate(). It allocates a CERT_CHAIN_PARA, zeroes it, sets only cbSize, and passes it to CertGetCertificateChain:
CERT_CHAIN_PARA ChainPara;
memset(&ChainPara, 0, sizeof(ChainPara));
ChainPara.cbSize = sizeof(ChainPara);
if(!CertGetCertificateChain(cert_chain_engine,
pCertContextServer,
NULL,
pCertContextServer->hCertStore,
&ChainPara,
(ssl_config->no_revoke ? 0 :
CERT_CHAIN_REVOCATION_CHECK_CHAIN),
NULL,
&pChainContext)) {
Because RequestedUsage is left unset, chain building appears to run without an EKU constraint. Microsoft documents that server/client authorization distinctions should be expressed through the RequestedUsage member passed to CertGetCertificateChain, and then enforced with CertVerifyCertificateChainPolicy as appropriate. In the current Schannel verification path, we do not see either RequestedUsage populated for serverAuth or a subsequent CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, ...) call. As a result, Windows may have no reason to set CERT_TRUST_IS_NOT_VALID_FOR_USAGE for a leaf that lacks id-kp-serverAuth.
This also explains why the existing trust-mask logic does not help. The mask does not suppress CERT_TRUST_IS_NOT_VALID_FOR_USAGE (0x10), so if Windows set that bit, curl would already fail correctly. The issue seems to be that this path never asks Windows to evaluate server-auth EKU in the first place.
The affected path is entered when CURLOPT_CAINFO or CURLOPT_CAINFO_BLOB is supplied on Windows 7 or later, which sets backend->use_manual_cred_validation = TRUE in schannel.c. If CURLOPT_SSL_VERIFYPEER is enabled, curl then dispatches to Curl_verify_certificate(). When no custom CA is provided, curl stays on Schannel's native validation path instead. That distinction matters because the issue appears specific to the manual custom-CA detour.
The practical consequence is a possible MitM in environments where an attacker can obtain a certificate for the target hostname from a CA that is present in the victim's custom CA bundle, but where that certificate is not valid for server authentication. This does not appear to be exploitable with a self-signed certificate. The main risk seems to be private PKI environments, such as enterprise, IoT, or embedded deployments, where non-server certificates may be issued more broadly.
Below is a reproducer. The certificate-generation steps are cross-platform with OpenSSL. The curl step is Windows-only, and the expected result is still a prediction based on source analysis and Microsoft API behavior.
Step 1: generate a CA and issue a certificate for server.example.com with only id-kp-clientAuth and no id-kp-serverAuth:
# CA key and self-signed certificate
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 3650 -key ca.key -out ca.pem \
-subj "/CN=Test CA"
# Server key and CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/CN=server.example.com"
# Extension file: SAN + clientAuth EKU only
cat > server_exts.cnf << 'EOF'
subjectAltName = DNS:server.example.com
extendedKeyUsage = clientAuth
EOF
# Sign the server cert with the CA
openssl x509 -req -days 365 -in server.csr \
-CA ca.pem -CAkey ca.key -CAcreateserial \
-extfile server_exts.cnf \
-out server.pem
# Confirm: should show clientAuth and not serverAuth
openssl x509 -in server.pem -noout -text | grep -A3 "Extended Key"
Step 2: on Windows, serve server.pem and server.key from any TLS server, add a hosts-file entry for server.example.com, then run:
curl --cacert ca.pem https://server.example.com/
Expected behavior: curl should reject the connection with an error such as (60) SSL certificate problem: certificate is not valid for the requested usage.
Predicted actual behavior on the unpatched Schannel --cacert path: the connection succeeds, because CertGetCertificateChain is called with no requested EKU and therefore may not set CERT_TRUST_IS_NOT_VALID_FOR_USAGE, allowing the existing trust-mask check to pass.
As a control, importing ca.pem into the Windows root store and running without --cacert should exercise Schannel's native path instead. If that path rejects the certificate, it would further indicate that the EKU gap is specific to the manual custom-CA path.
Two possible fixes stand out.
Option A, minimal: populate RequestedUsage before calling CertGetCertificateChain:
- CERT_CHAIN_PARA ChainPara;
-
- memset(&ChainPara, 0, sizeof(ChainPara));
- ChainPara.cbSize = sizeof(ChainPara);
+ CERT_CHAIN_PARA ChainPara;
+ LPSTR serverAuthOID = (LPSTR)szOID_PKIX_KP_SERVER_AUTH;
+
+ memset(&ChainPara, 0, sizeof(ChainPara));
+ ChainPara.cbSize = sizeof(ChainPara);
+ ChainPara.RequestedUsage.dwType = USAGE_MATCH_TYPE_AND;
+ ChainPara.RequestedUsage.Usage.cUsageIdentifier = 1;
+ ChainPara.RequestedUsage.Usage.rgpszUsageIdentifier = &serverAuthOID;
Option B, more complete: call CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, ...) after chain building. That would align this path more closely with normal Schannel TLS policy evaluation and should cover EKU and related SSL policy checks in a more standard way.
Either approach may be sufficient. Option B seems more robust, while Option A is the smallest targeted change.
Thank you, AISLE Research Team
Impact
An attacker who can obtain a hostname-valid certificate from a CA in the victim's custom CURLOPT_CAINFO or CURLOPT_CAINFO_BLOB bundle may be able to bypass server-auth EKU checks on Windows and intercept libcurl connections on the Schannel manual-validation path. This appears limited to custom-CA configurations and is most relevant in private PKI environments where non-server certificates may be issued more broadly; it does not appear to be exploitable with a self-signed certificate.