openssl: make CURLSSLOPT_NATIVE_CA import Windows intermediate CAs

- If CURLSSLOPT_NATIVE_CA on Windows then import from intermediate CA
  "CA" store after importing from root CA "ROOT" store.

This change allows curl to work in situations where a server does not
send all intermediate certs and they are present in the "CA" store (the
store with intermediate CAs). This is already allowed by the Schannel
backend.

Also this change makes partial chain verification possible for those
certs since we allow partial chain verification by default for OpenSSL
(unless CURLSSLOPT_NO_PARTIALCHAIN). This is not allowed by the Schannel
backend.

Prior to this change CURLSSLOPT_NATIVE_CA only imported "ROOT" certs.

Fixes https://github.com/curl/curl/issues/12155
Closes https://github.com/curl/curl/pull/12185
This commit is contained in:
Nico Rieck 2023-10-23 12:38:19 +02:00 committed by Jay Satiro
parent c0d4fbb1f5
commit 2d6333101a

View File

@ -98,6 +98,9 @@
#include "curl_memory.h"
#include "memdebug.h"
#ifndef ARRAYSIZE
#define ARRAYSIZE(A) (sizeof(A)/sizeof((A)[0]))
#endif
/* Uncomment the ALLOW_RENEG line to a real #define if you want to allow TLS
renegotiations when built with BoringSSL. Renegotiating is non-compliant
@ -3034,6 +3037,151 @@ static CURLcode load_cacert_from_memory(X509_STORE *store,
return (count > 0) ? CURLE_OK : CURLE_SSL_CACERT_BADFILE;
}
#if defined(USE_WIN32_CRYPTO)
static CURLcode import_windows_cert_store(struct Curl_easy *data,
const char *name,
X509_STORE *store,
bool *imported)
{
CURLcode result = CURLE_OK;
HCERTSTORE hStore;
*imported = false;
hStore = CertOpenSystemStoreA(0, name);
if(hStore) {
PCCERT_CONTEXT pContext = NULL;
/* The array of enhanced key usage OIDs will vary per certificate and
is declared outside of the loop so that rather than malloc/free each
iteration we can grow it with realloc, when necessary. */
CERT_ENHKEY_USAGE *enhkey_usage = NULL;
DWORD enhkey_usage_size = 0;
/* This loop makes a best effort to import all valid certificates from
the MS root store. If a certificate cannot be imported it is
skipped. 'result' is used to store only hard-fail conditions (such
as out of memory) that cause an early break. */
result = CURLE_OK;
for(;;) {
X509 *x509;
FILETIME now;
BYTE key_usage[2];
DWORD req_size;
const unsigned char *encoded_cert;
#if defined(DEBUGBUILD) && !defined(CURL_DISABLE_VERBOSE_STRINGS)
char cert_name[256];
#endif
pContext = CertEnumCertificatesInStore(hStore, pContext);
if(!pContext)
break;
#if defined(DEBUGBUILD) && !defined(CURL_DISABLE_VERBOSE_STRINGS)
if(!CertGetNameStringA(pContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0,
NULL, cert_name, sizeof(cert_name))) {
strcpy(cert_name, "Unknown");
}
infof(data, "SSL: Checking cert \"%s\"", cert_name);
#endif
encoded_cert = (const unsigned char *)pContext->pbCertEncoded;
if(!encoded_cert)
continue;
GetSystemTimeAsFileTime(&now);
if(CompareFileTime(&pContext->pCertInfo->NotBefore, &now) > 0 ||
CompareFileTime(&now, &pContext->pCertInfo->NotAfter) > 0)
continue;
/* If key usage exists check for signing attribute */
if(CertGetIntendedKeyUsage(pContext->dwCertEncodingType,
pContext->pCertInfo,
key_usage, sizeof(key_usage))) {
if(!(key_usage[0] & CERT_KEY_CERT_SIGN_KEY_USAGE))
continue;
}
else if(GetLastError())
continue;
/* If enhanced key usage exists check for server auth attribute.
*
* Note "In a Microsoft environment, a certificate might also have
* EKU extended properties that specify valid uses for the
* certificate." The call below checks both, and behavior varies
* depending on what is found. For more details see
* CertGetEnhancedKeyUsage doc.
*/
if(CertGetEnhancedKeyUsage(pContext, 0, NULL, &req_size)) {
if(req_size && req_size > enhkey_usage_size) {
void *tmp = realloc(enhkey_usage, req_size);
if(!tmp) {
failf(data, "SSL: Out of memory allocating for OID list");
result = CURLE_OUT_OF_MEMORY;
break;
}
enhkey_usage = (CERT_ENHKEY_USAGE *)tmp;
enhkey_usage_size = req_size;
}
if(CertGetEnhancedKeyUsage(pContext, 0, enhkey_usage, &req_size)) {
if(!enhkey_usage->cUsageIdentifier) {
/* "If GetLastError returns CRYPT_E_NOT_FOUND, the certificate
is good for all uses. If it returns zero, the certificate
has no valid uses." */
if((HRESULT)GetLastError() != CRYPT_E_NOT_FOUND)
continue;
}
else {
DWORD i;
bool found = false;
for(i = 0; i < enhkey_usage->cUsageIdentifier; ++i) {
if(!strcmp("1.3.6.1.5.5.7.3.1" /* OID server auth */,
enhkey_usage->rgpszUsageIdentifier[i])) {
found = true;
break;
}
}
if(!found)
continue;
}
}
else
continue;
}
else
continue;
x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded);
if(!x509)
continue;
/* Try to import the certificate. This may fail for legitimate
reasons such as duplicate certificate, which is allowed by MS but
not OpenSSL. */
if(X509_STORE_add_cert(store, x509) == 1) {
#if defined(DEBUGBUILD) && !defined(CURL_DISABLE_VERBOSE_STRINGS)
infof(data, "SSL: Imported cert \"%s\"", cert_name);
#endif
*imported = true;
}
X509_free(x509);
}
free(enhkey_usage);
CertFreeCertificateContext(pContext);
CertCloseStore(hStore, 0);
if(result)
return result;
}
return result;
}
#endif
static CURLcode populate_x509_store(struct Curl_cfilter *cf,
struct Curl_easy *data,
X509_STORE *store)
@ -3063,140 +3211,25 @@ static CURLcode populate_x509_store(struct Curl_cfilter *cf,
https://github.com/d3x0r/SACK/blob/master/src/netlib/ssl_layer.c#L1037
https://datatracker.ietf.org/doc/html/rfc5280 */
if(ssl_config->native_ca_store) {
HCERTSTORE hStore = CertOpenSystemStore(0, TEXT("ROOT"));
if(hStore) {
PCCERT_CONTEXT pContext = NULL;
/* The array of enhanced key usage OIDs will vary per certificate and
is declared outside of the loop so that rather than malloc/free each
iteration we can grow it with realloc, when necessary. */
CERT_ENHKEY_USAGE *enhkey_usage = NULL;
DWORD enhkey_usage_size = 0;
/* This loop makes a best effort to import all valid certificates from
the MS root store. If a certificate cannot be imported it is
skipped. 'result' is used to store only hard-fail conditions (such
as out of memory) that cause an early break. */
result = CURLE_OK;
for(;;) {
X509 *x509;
FILETIME now;
BYTE key_usage[2];
DWORD req_size;
const unsigned char *encoded_cert;
#if defined(DEBUGBUILD) && !defined(CURL_DISABLE_VERBOSE_STRINGS)
char cert_name[256];
#endif
pContext = CertEnumCertificatesInStore(hStore, pContext);
if(!pContext)
break;
#if defined(DEBUGBUILD) && !defined(CURL_DISABLE_VERBOSE_STRINGS)
if(!CertGetNameStringA(pContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0,
NULL, cert_name, sizeof(cert_name))) {
strcpy(cert_name, "Unknown");
}
infof(data, "SSL: Checking cert \"%s\"", cert_name);
#endif
encoded_cert = (const unsigned char *)pContext->pbCertEncoded;
if(!encoded_cert)
continue;
GetSystemTimeAsFileTime(&now);
if(CompareFileTime(&pContext->pCertInfo->NotBefore, &now) > 0 ||
CompareFileTime(&now, &pContext->pCertInfo->NotAfter) > 0)
continue;
/* If key usage exists check for signing attribute */
if(CertGetIntendedKeyUsage(pContext->dwCertEncodingType,
pContext->pCertInfo,
key_usage, sizeof(key_usage))) {
if(!(key_usage[0] & CERT_KEY_CERT_SIGN_KEY_USAGE))
continue;
}
else if(GetLastError())
continue;
/* If enhanced key usage exists check for server auth attribute.
*
* Note "In a Microsoft environment, a certificate might also have
* EKU extended properties that specify valid uses for the
* certificate." The call below checks both, and behavior varies
* depending on what is found. For more details see
* CertGetEnhancedKeyUsage doc.
*/
if(CertGetEnhancedKeyUsage(pContext, 0, NULL, &req_size)) {
if(req_size && req_size > enhkey_usage_size) {
void *tmp = realloc(enhkey_usage, req_size);
if(!tmp) {
failf(data, "SSL: Out of memory allocating for OID list");
result = CURLE_OUT_OF_MEMORY;
break;
}
enhkey_usage = (CERT_ENHKEY_USAGE *)tmp;
enhkey_usage_size = req_size;
}
if(CertGetEnhancedKeyUsage(pContext, 0, enhkey_usage, &req_size)) {
if(!enhkey_usage->cUsageIdentifier) {
/* "If GetLastError returns CRYPT_E_NOT_FOUND, the certificate
is good for all uses. If it returns zero, the certificate
has no valid uses." */
if((HRESULT)GetLastError() != CRYPT_E_NOT_FOUND)
continue;
}
else {
DWORD i;
bool found = false;
for(i = 0; i < enhkey_usage->cUsageIdentifier; ++i) {
if(!strcmp("1.3.6.1.5.5.7.3.1" /* OID server auth */,
enhkey_usage->rgpszUsageIdentifier[i])) {
found = true;
break;
}
}
if(!found)
continue;
}
}
else
continue;
}
else
continue;
x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded);
if(!x509)
continue;
/* Try to import the certificate. This may fail for legitimate
reasons such as duplicate certificate, which is allowed by MS but
not OpenSSL. */
if(X509_STORE_add_cert(store, x509) == 1) {
#if defined(DEBUGBUILD) && !defined(CURL_DISABLE_VERBOSE_STRINGS)
infof(data, "SSL: Imported cert \"%s\"", cert_name);
#endif
imported_native_ca = true;
}
X509_free(x509);
}
free(enhkey_usage);
CertFreeCertificateContext(pContext);
CertCloseStore(hStore, 0);
const char *storeNames[] = {
"ROOT", /* Trusted Root Certification Authorities */
"CA" /* Intermediate Certification Authorities */
};
size_t i;
for(i = 0; i < ARRAYSIZE(storeNames); ++i) {
bool imported = false;
result = import_windows_cert_store(data, storeNames[i], store,
&imported);
if(result)
return result;
if(imported) {
infof(data, "successfully imported Windows %s store", storeNames[i]);
imported_native_ca = true;
}
else
infof(data, "error importing Windows %s store, continuing anyway",
storeNames[i]);
}
if(imported_native_ca)
infof(data, "successfully imported Windows CA store");
else
infof(data, "error importing Windows CA store, continuing anyway");
}
#endif
if(ca_info_blob) {