schannel: fix TLS cert verification by IP SAN

Reported-by: elvinasp on github
Fixes #15149
Closes #15421
This commit is contained in:
edmcln 2024-10-27 08:01:52 -04:00 committed by Daniel Stenberg
parent fb711b5098
commit 9640a8ef6f
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
2 changed files with 212 additions and 125 deletions

View File

@ -176,6 +176,17 @@ struct schannel_cert_share {
struct curltime time; /* when the cached store was created */ struct curltime time; /* when the cached store was created */
}; };
/*
* size of the structure: 20 bytes.
*/
struct num_ip_data {
DWORD size; /* 04 bytes */
union {
struct in_addr ia; /* 04 bytes */
struct in6_addr ia6; /* 16 bytes */
} bData;
};
HCERTSTORE Curl_schannel_get_cached_cert_store(struct Curl_cfilter *cf, HCERTSTORE Curl_schannel_get_cached_cert_store(struct Curl_cfilter *cf,
const struct Curl_easy *data); const struct Curl_easy *data);

View File

@ -39,6 +39,7 @@
#include "schannel.h" #include "schannel.h"
#include "schannel_int.h" #include "schannel_int.h"
#include "inet_pton.h"
#include "vtls.h" #include "vtls.h"
#include "vtls_int.h" #include "vtls_int.h"
#include "sendf.h" #include "sendf.h"
@ -54,7 +55,6 @@
#define BACKEND ((struct schannel_ssl_backend_data *)connssl->backend) #define BACKEND ((struct schannel_ssl_backend_data *)connssl->backend)
#ifdef HAS_MANUAL_VERIFY_API #ifdef HAS_MANUAL_VERIFY_API
#define MAX_CAFILE_SIZE 1048576 /* 1 MiB */ #define MAX_CAFILE_SIZE 1048576 /* 1 MiB */
@ -343,7 +343,9 @@ cleanup:
static DWORD cert_get_name_string(struct Curl_easy *data, static DWORD cert_get_name_string(struct Curl_easy *data,
CERT_CONTEXT *cert_context, CERT_CONTEXT *cert_context,
LPTSTR host_names, LPTSTR host_names,
DWORD length) DWORD length,
PCERT_ALT_NAME_INFO alt_name_info,
BOOL Win8_compat)
{ {
DWORD actual_length = 0; DWORD actual_length = 0;
#if defined(CURL_WINDOWS_UWP) #if defined(CURL_WINDOWS_UWP)
@ -351,21 +353,16 @@ static DWORD cert_get_name_string(struct Curl_easy *data,
(void)cert_context; (void)cert_context;
(void)host_names; (void)host_names;
(void)length; (void)length;
(void)alt_name_info;
(void)Win8_compat;
#else #else
BOOL compute_content = FALSE; BOOL compute_content = FALSE;
CERT_INFO *cert_info = NULL;
CERT_EXTENSION *extension = NULL;
CRYPT_DECODE_PARA decode_para = {0, 0, 0};
CERT_ALT_NAME_INFO *alt_name_info = NULL;
DWORD alt_name_info_size = 0;
BOOL ret_val = FALSE;
LPTSTR current_pos = NULL; LPTSTR current_pos = NULL;
DWORD i; DWORD i;
#ifdef CERT_NAME_SEARCH_ALL_NAMES_FLAG #ifdef CERT_NAME_SEARCH_ALL_NAMES_FLAG
/* CERT_NAME_SEARCH_ALL_NAMES_FLAG is available from Windows 8 onwards. */ /* CERT_NAME_SEARCH_ALL_NAMES_FLAG is available from Windows 8 onwards. */
if(curlx_verify_windows_version(6, 2, 0, PLATFORM_WINNT, if(Win8_compat) {
VERSION_GREATER_THAN_EQUAL)) {
/* CertGetNameString will provide the 8-bit character string without /* CertGetNameString will provide the 8-bit character string without
* any decoding */ * any decoding */
DWORD name_flags = DWORD name_flags =
@ -378,6 +375,9 @@ static DWORD cert_get_name_string(struct Curl_easy *data,
length); length);
return actual_length; return actual_length;
} }
#else
(void)cert_context;
(void)Win8_compat;
#endif #endif
compute_content = host_names != NULL && length != 0; compute_content = host_names != NULL && length != 0;
@ -388,43 +388,6 @@ static DWORD cert_get_name_string(struct Curl_easy *data,
*host_names = '\0'; *host_names = '\0';
} }
if(!cert_context) {
failf(data, "schannel: Null certificate context.");
return actual_length;
}
cert_info = cert_context->pCertInfo;
if(!cert_info) {
failf(data, "schannel: Null certificate info.");
return actual_length;
}
extension = CertFindExtension(szOID_SUBJECT_ALT_NAME2,
cert_info->cExtension,
cert_info->rgExtension);
if(!extension) {
failf(data, "schannel: CertFindExtension() returned no extension.");
return actual_length;
}
decode_para.cbSize = sizeof(CRYPT_DECODE_PARA);
ret_val =
CryptDecodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
szOID_SUBJECT_ALT_NAME2,
extension->Value.pbData,
extension->Value.cbData,
CRYPT_DECODE_ALLOC_FLAG | CRYPT_DECODE_NOCOPY_FLAG,
&decode_para,
&alt_name_info,
&alt_name_info_size);
if(!ret_val) {
failf(data,
"schannel: CryptDecodeObjectEx() returned no alternate name "
"information.");
return actual_length;
}
current_pos = host_names; current_pos = host_names;
/* Iterate over the alternate names and populate host_names. */ /* Iterate over the alternate names and populate host_names. */
@ -467,6 +430,88 @@ static DWORD cert_get_name_string(struct Curl_easy *data,
return actual_length; return actual_length;
} }
/*
* Returns TRUE if the hostname is a numeric IPv4/IPv6 Address,
* and populates the buffer with IPv4/IPv6 info.
*/
static bool get_num_host_info(struct num_ip_data *ip_blob,
LPCSTR hostname)
{
struct in_addr ia;
struct in6_addr ia6;
bool result = FALSE;
int res = Curl_inet_pton(AF_INET, hostname, &ia);
if(res) {
ip_blob->size = sizeof(struct in_addr);
memcpy(&ip_blob->bData.ia, &ia, sizeof(struct in_addr));
result = TRUE;
}
else {
res = Curl_inet_pton(AF_INET6, hostname, &ia6);
if(res) {
ip_blob->size = sizeof(struct in6_addr);
memcpy(&ip_blob->bData.ia6, &ia6, sizeof(struct in6_addr));
result = TRUE;
}
}
return result;
}
static bool get_alt_name_info(struct Curl_easy *data,
PCCERT_CONTEXT ctx,
PCERT_ALT_NAME_INFO *alt_name_info,
LPDWORD alt_name_info_size)
{
bool result = FALSE;
#if defined(CURL_WINDOWS_UWP)
(void)data;
(void)ctx;
(void)alt_name_info;
(void)alt_name_info_size;
#else
PCERT_INFO cert_info = NULL;
PCERT_EXTENSION extension = NULL;
CRYPT_DECODE_PARA decode_para = { sizeof(CRYPT_DECODE_PARA), NULL, NULL };
if(!ctx) {
failf(data, "schannel: Null certificate context.");
return result;
}
cert_info = ctx->pCertInfo;
if(!cert_info) {
failf(data, "schannel: Null certificate info.");
return result;
}
extension = CertFindExtension(szOID_SUBJECT_ALT_NAME2,
cert_info->cExtension,
cert_info->rgExtension);
if(!extension) {
failf(data, "schannel: CertFindExtension() returned no extension.");
return result;
}
if(!CryptDecodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
szOID_SUBJECT_ALT_NAME2,
extension->Value.pbData,
extension->Value.cbData,
CRYPT_DECODE_ALLOC_FLAG | CRYPT_DECODE_NOCOPY_FLAG,
&decode_para,
alt_name_info,
alt_name_info_size)) {
failf(data,
"schannel: CryptDecodeObjectEx() returned no alternate name "
"information.");
return result;
}
result = TRUE;
#endif
return result;
}
/* Verify the server's hostname */ /* Verify the server's hostname */
CURLcode Curl_verify_host(struct Curl_cfilter *cf, CURLcode Curl_verify_host(struct Curl_cfilter *cf,
struct Curl_easy *data) struct Curl_easy *data)
@ -481,6 +526,12 @@ CURLcode Curl_verify_host(struct Curl_cfilter *cf,
size_t hostlen = strlen(conn_hostname); size_t hostlen = strlen(conn_hostname);
DWORD len = 0; DWORD len = 0;
DWORD actual_len = 0; DWORD actual_len = 0;
PCERT_ALT_NAME_INFO alt_name_info = NULL;
DWORD alt_name_info_size = 0;
struct num_ip_data ip_blob = { 0 };
bool Win8_compat;
struct num_ip_data *p = &ip_blob;
DWORD i;
sspi_status = sspi_status =
Curl_pSecFn->QueryContextAttributes(&BACKEND->ctxt->ctxt_handle, Curl_pSecFn->QueryContextAttributes(&BACKEND->ctxt->ctxt_handle,
@ -491,17 +542,44 @@ CURLcode Curl_verify_host(struct Curl_cfilter *cf,
char buffer[STRERROR_LEN]; char buffer[STRERROR_LEN];
failf(data, "schannel: Failed to read remote certificate context: %s", failf(data, "schannel: Failed to read remote certificate context: %s",
Curl_sspi_strerror(sspi_status, buffer, sizeof(buffer))); Curl_sspi_strerror(sspi_status, buffer, sizeof(buffer)));
result = CURLE_PEER_FAILED_VERIFICATION;
goto cleanup; goto cleanup;
} }
Win8_compat = curlx_verify_windows_version(6, 2, 0, PLATFORM_WINNT,
VERSION_GREATER_THAN_EQUAL);
if(get_num_host_info(p, conn_hostname) || !Win8_compat) {
if(!get_alt_name_info(data, pCertContextServer,
&alt_name_info, &alt_name_info_size)) {
goto cleanup;
}
}
if(p->size) {
for(i = 0; i < alt_name_info->cAltEntry; ++i) {
PCERT_ALT_NAME_ENTRY entry = &alt_name_info->rgAltEntry[i];
if(entry->dwAltNameChoice == CERT_ALT_NAME_IP_ADDRESS) {
if(entry->IPAddress.cbData == p->size) {
if(!memcmp(entry->IPAddress.pbData, &p->bData,
entry->IPAddress.cbData)) {
result = CURLE_OK;
infof(data,
"schannel: connection hostname (%s) matched cert's IP address!",
conn_hostname);
break;
}
}
}
}
}
else {
/* Determine the size of the string needed for the cert hostname */ /* Determine the size of the string needed for the cert hostname */
len = cert_get_name_string(data, pCertContextServer, NULL, 0); len = cert_get_name_string(data, pCertContextServer,
NULL, 0, alt_name_info, Win8_compat);
if(len == 0) { if(len == 0) {
failf(data, failf(data,
"schannel: CertGetNameString() returned no " "schannel: CertGetNameString() returned no "
"certificate name information"); "certificate name information");
result = CURLE_PEER_FAILED_VERIFICATION;
goto cleanup; goto cleanup;
} }
@ -513,15 +591,14 @@ CURLcode Curl_verify_host(struct Curl_cfilter *cf,
result = CURLE_OUT_OF_MEMORY; result = CURLE_OUT_OF_MEMORY;
goto cleanup; goto cleanup;
} }
actual_len = cert_get_name_string( actual_len = cert_get_name_string(data, pCertContextServer,
data, pCertContextServer, (LPTSTR)cert_hostname_buff, len); (LPTSTR)cert_hostname_buff, len, alt_name_info, Win8_compat);
/* Sanity check */ /* Sanity check */
if(actual_len != len) { if(actual_len != len) {
failf(data, failf(data,
"schannel: CertGetNameString() returned certificate " "schannel: CertGetNameString() returned certificate "
"name information of unexpected size"); "name information of unexpected size");
result = CURLE_PEER_FAILED_VERIFICATION;
goto cleanup; goto cleanup;
} }
@ -529,7 +606,6 @@ CURLcode Curl_verify_host(struct Curl_cfilter *cf,
* null-terminated and the last DNS name is double null-terminated. Due to * null-terminated and the last DNS name is double null-terminated. Due to
* this encoding, use the length of the buffer to iterate over all names. * this encoding, use the length of the buffer to iterate over all names.
*/ */
result = CURLE_PEER_FAILED_VERIFICATION;
while(cert_hostname_buff_index < len && while(cert_hostname_buff_index < len &&
cert_hostname_buff[cert_hostname_buff_index] != TEXT('\0') && cert_hostname_buff[cert_hostname_buff_index] != TEXT('\0') &&
result == CURLE_PEER_FAILED_VERIFICATION) { result == CURLE_PEER_FAILED_VERIFICATION) {
@ -582,6 +658,7 @@ CURLcode Curl_verify_host(struct Curl_cfilter *cf,
} }
else if(result != CURLE_OK) else if(result != CURLE_OK)
failf(data, "schannel: server certificate name verification failed"); failf(data, "schannel: server certificate name verification failed");
}
cleanup: cleanup:
Curl_safefree(cert_hostname_buff); Curl_safefree(cert_hostname_buff);
@ -592,7 +669,6 @@ cleanup:
return result; return result;
} }
#ifdef HAS_MANUAL_VERIFY_API #ifdef HAS_MANUAL_VERIFY_API
/* Verify the server's certificate and hostname */ /* Verify the server's certificate and hostname */
CURLcode Curl_verify_certificate(struct Curl_cfilter *cf, CURLcode Curl_verify_certificate(struct Curl_cfilter *cf,