schannel: Make CURLOPT_CAINFO work better on Windows 7

- Support hostname verification via alternative names (SAN) in the
  peer certificate when CURLOPT_CAINFO is used in Windows 7 and earlier.

CERT_NAME_SEARCH_ALL_NAMES_FLAG doesn't exist before Windows 8. As a
result CertGetNameString doesn't quite work on those versions of
Windows. This change provides an alternative solution for
CertGetNameString by iterating through CERT_ALT_NAME_INFO for earlier
versions of Windows.

Prior to this change many certificates failed the hostname validation
when CURLOPT_CAINFO was used in Windows 7 and earlier. Most certificates
now represent multiple hostnames and rely on the alternative names field
exclusively to represent their hostnames.

Reported-by: Jeroen Ooms

Fixes https://github.com/curl/curl/issues/3711
Closes https://github.com/curl/curl/pull/4761
This commit is contained in:
Faizur Rahman 2019-12-25 18:49:48 -05:00 committed by Jay Satiro
parent cbb5429001
commit 29e40a6d8a

View File

@ -293,6 +293,133 @@ cleanup:
return result;
}
/*
* Returns the number of characters necessary to populate all the host_names.
* If host_names is not NULL, populate it with all the host names. Each string
* in the host_names is null-terminated and the last string is double
* null-terminated. If no DNS names are found, a single null-terminated empty
* string is returned.
*/
static DWORD cert_get_name_string(struct Curl_easy *data,
CERT_CONTEXT *cert_context,
LPTSTR host_names,
DWORD length)
{
DWORD actual_length = 0;
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;
char *current_pos = NULL;
DWORD i;
/* CERT_NAME_SEARCH_ALL_NAMES_FLAG is available from Windows 8 onwards. */
if(Curl_verify_windows_version(6, 2, PLATFORM_WINNT,
VERSION_GREATER_THAN_EQUAL)) {
#ifdef CERT_NAME_SEARCH_ALL_NAMES_FLAG
/* CertGetNameString will provide the 8-bit character string without
* any decoding */
DWORD name_flags =
CERT_NAME_DISABLE_IE4_UTF8_FLAG | CERT_NAME_SEARCH_ALL_NAMES_FLAG;
actual_length = CertGetNameString(cert_context,
CERT_NAME_DNS_TYPE,
name_flags,
NULL,
host_names,
length);
return actual_length;
#endif
}
compute_content = host_names != NULL && length != 0;
/* Initialize default return values. */
actual_length = 1;
if(compute_content) {
*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;
/* Iterate over the alternate names and populate host_names. */
for(i = 0; i < alt_name_info->cAltEntry; i++) {
const CERT_ALT_NAME_ENTRY *entry = &alt_name_info->rgAltEntry[i];
wchar_t *dns_w = NULL;
size_t current_length = 0;
if(entry->dwAltNameChoice != CERT_ALT_NAME_DNS_NAME) {
continue;
}
if(entry->pwszDNSName == NULL) {
infof(data, "schannel: Empty DNS name.");
continue;
}
current_length = wcslen(entry->pwszDNSName) + 1;
if(!compute_content) {
actual_length += (DWORD)current_length;
continue;
}
/* Sanity check to prevent buffer overrun. */
if((actual_length + current_length) > length) {
failf(data, "schannel: Not enough memory to list all host names.");
break;
}
dns_w = entry->pwszDNSName;
/* pwszDNSName is in ia5 string format and hence doesn't contain any
* non-ascii characters. */
while(*dns_w != '\0') {
*current_pos++ = (char)(*dns_w++);
}
*current_pos++ = '\0';
actual_length += (DWORD)current_length;
}
if(compute_content) {
/* Last string has double null-terminator. */
*current_pos = '\0';
}
return actual_length;
}
static CURLcode verify_host(struct Curl_easy *data,
CERT_CONTEXT *pCertContextServer,
const char * const conn_hostname)
@ -303,21 +430,8 @@ static CURLcode verify_host(struct Curl_easy *data,
DWORD len = 0;
DWORD actual_len = 0;
/* CertGetNameString will provide the 8-bit character string without
* any decoding */
DWORD name_flags = CERT_NAME_DISABLE_IE4_UTF8_FLAG;
#ifdef CERT_NAME_SEARCH_ALL_NAMES_FLAG
name_flags |= CERT_NAME_SEARCH_ALL_NAMES_FLAG;
#endif
/* Determine the size of the string needed for the cert hostname */
len = CertGetNameString(pCertContextServer,
CERT_NAME_DNS_TYPE,
name_flags,
NULL,
NULL,
0);
len = cert_get_name_string(data, pCertContextServer, NULL, 0);
if(len == 0) {
failf(data,
"schannel: CertGetNameString() returned no "
@ -334,12 +448,8 @@ static CURLcode verify_host(struct Curl_easy *data,
result = CURLE_OUT_OF_MEMORY;
goto cleanup;
}
actual_len = CertGetNameString(pCertContextServer,
CERT_NAME_DNS_TYPE,
name_flags,
NULL,
(LPTSTR) cert_hostname_buff,
len);
actual_len = cert_get_name_string(
data, pCertContextServer, (LPTSTR)cert_hostname_buff, len);
/* Sanity check */
if(actual_len != len) {