schannel: handle pkcs12 client certificates which contain CA certificates

The SChannel code uses the CertFindCertificateInStore function to
retrieve the client certificate from a pkcs12 certificate store.
However, when called with the CERT_FIND_ANY flag, this function does not
provide any guarantees on the order in which certificates are retrieved.
If a pkcs12 file contains an entire certificate chain instead of a
single client certificate, the CertFindCertificateInStore function may
return the CA or an intermediate certificate instead of the desired
client certificate. Since there is no associated private key for such a
certificate, the TLS handshake fails.

With this change, we now pass the CERT_FIND_HAS_PRIVATE_KEY flag. This
ensures that the CertFindCertificateInStore function will return a
certificate which has a corresponding private key. This will stop the CA
and intermediate certificates from being selected. I don't think there
would be much use in a client certificate which has no associated
private key, so this should ensure the client certificate is selected. I
suppose it may be possible for a pkcs12 file to contain multiple
certificates with private keys and the new behaviour may not guarantee
which is selected. However, this is no worse that the previous behaviour
in which any certificate may been selected.

The CERT_FIND_HAS_PRIVATE_KEY is only available in Windows 8 / Server
2012 (aka Windows NT6.2). For older versions, we will fall back to using
the CERT_FIND_ANY flag.

Closes #16825
This commit is contained in:
Joel Depooter 2025-03-24 16:06:24 -07:00 committed by Daniel Stenberg
parent fbdb1e1dbe
commit fe9898d26e
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2

View file

@ -151,6 +151,10 @@
#define PKCS12_NO_PERSIST_KEY 0x00008000
#endif
#ifndef CERT_FIND_HAS_PRIVATE_KEY
#define CERT_FIND_HAS_PRIVATE_KEY (21 << CERT_COMPARE_SHIFT)
#endif
/* ALPN requires version 8.1 of the Windows SDK, which was
shipped with Visual Studio 2013, aka _MSC_VER 1800:
https://technet.microsoft.com/en-us/library/hh831771%28v=ws.11%29.aspx
@ -611,6 +615,7 @@ schannel_acquire_credential_handle(struct Curl_cfilter *cf,
WCHAR* pszPassword;
size_t pwd_len = 0;
int str_w_len = 0;
int cert_find_flags;
const char *cert_showfilename_error = blob ?
"(memory blob)" : data->set.ssl.primary.clientcert;
curlx_unicodefree(cert_path);
@ -682,9 +687,17 @@ schannel_acquire_credential_handle(struct Curl_cfilter *cf,
return CURLE_SSL_CERTPROBLEM;
}
/* CERT_FIND_HAS_PRIVATE_KEY is only available in Windows 8 / Server
2012, (NT v6.2). For earlier versions we use CURL_FIND_ANY. */
if(curlx_verify_windows_version(6, 2, 0, PLATFORM_WINNT,
VERSION_GREATER_THAN_EQUAL))
cert_find_flags = CERT_FIND_HAS_PRIVATE_KEY;
else
cert_find_flags = CERT_FIND_ANY;
client_certs[0] = CertFindCertificateInStore(
cert_store, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0,
CERT_FIND_ANY, NULL, NULL);
cert_find_flags, NULL, NULL);
if(!client_certs[0]) {
failf(data, "schannel: Failed to get certificate from file %s"