mirror of
https://github.com/curl/curl.git
synced 2026-04-14 22:21:41 +03:00
Backtrack on previous change that aimed to solve the wrong `share.h` being included. It turns out it did not fix this issue. At the same time it introduced relative header filenames and the need to include the same headers differently depending on the source files' location, reducing readability and editability. Replace this method by re-adding curl's lib source directory to the header path and addressing headers by the their full, relative name to that base directory. Aligning with this method already used in src and tests. With these advantages: - makes includes easier to read, recognize, grep, sort, write, and copy between sources, - syncs the way these headers are included across curl components, - avoids the ambiguity between system `schannel.h`, `rustls.h` vs. local headers using the same names in `lib/vtls`, - silences clang-tidy `readability-duplicate-include` checker, which detects the above issue, Ref: https://clang.llvm.org/extra/clang-tidy/checks/readability/duplicate-include.html - possibly silences TIOBE coding standard warnings: `6.10.2.a: Don't use relative paths in #include statements.` - long shot: it works well with concatenated test sources, for clang-tidy-friendly custom unity builds. Ref: #20667 Slight downside: it's not enforced. If there happens to be a collision between a local `lib/*.h` header and a system one, the solution is to rename (possibly with its `.c` counterpart) into the `curl_` namespace. This is also the method used by curl in the past. Also: - curlx/inet_pton: reduce scope of an include. - toolx/tool_time: apply this to an include, and update VS project files accordingly. Also dropping unnecessary lib/curlx header path. - clang-tidy: enable `readability-duplicate-include`. Follow-up to3887069c66#19676 Follow-up to625f2c1644#16991 #16949 Closes #20623
864 lines
30 KiB
C
864 lines
30 KiB
C
/***************************************************************************
|
|
* _ _ ____ _
|
|
* Project ___| | | | _ \| |
|
|
* / __| | | | |_) | |
|
|
* | (__| |_| | _ <| |___
|
|
* \___|\___/|_| \_\_____|
|
|
*
|
|
* Copyright (C) Marc Hoersken, <info@marc-hoersken.de>
|
|
* Copyright (C) Mark Salisbury, <mark.salisbury@hp.com>
|
|
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
|
|
*
|
|
* This software is licensed as described in the file COPYING, which
|
|
* you should have received as part of this distribution. The terms
|
|
* are also available at https://curl.se/docs/copyright.html.
|
|
*
|
|
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
|
|
* copies of the Software, and permit persons to whom the Software is
|
|
* furnished to do so, under the terms of the COPYING file.
|
|
*
|
|
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
|
* KIND, either express or implied.
|
|
*
|
|
* SPDX-License-Identifier: curl
|
|
*
|
|
***************************************************************************/
|
|
/*
|
|
* Source file for Schannel-specific certificate verification. This code should
|
|
* only be invoked by code in schannel.c.
|
|
*/
|
|
#include "curl_setup.h"
|
|
|
|
#ifdef USE_SCHANNEL
|
|
|
|
#ifndef USE_WINDOWS_SSPI
|
|
#error "cannot compile Schannel support without SSPI."
|
|
#endif
|
|
|
|
#include "vtls/schannel.h"
|
|
#include "vtls/schannel_int.h"
|
|
|
|
#include "curlx/fopen.h"
|
|
#include "curlx/inet_pton.h"
|
|
#include "vtls/vtls.h"
|
|
#include "vtls/vtls_int.h"
|
|
#include "curl_trc.h"
|
|
#include "strerror.h"
|
|
#include "curlx/winapi.h"
|
|
#include "curlx/multibyte.h"
|
|
#include "vtls/hostcheck.h"
|
|
#include "curlx/version_win32.h"
|
|
|
|
#define BACKEND ((struct schannel_ssl_backend_data *)connssl->backend)
|
|
|
|
#define MAX_CAFILE_SIZE 1048576 /* 1 MiB */
|
|
#define BEGIN_CERT "-----BEGIN CERTIFICATE-----"
|
|
#define END_CERT "\n-----END CERTIFICATE-----"
|
|
|
|
struct cert_chain_engine_config_win8 {
|
|
DWORD cbSize;
|
|
HCERTSTORE hRestrictedRoot;
|
|
HCERTSTORE hRestrictedTrust;
|
|
HCERTSTORE hRestrictedOther;
|
|
DWORD cAdditionalStore;
|
|
HCERTSTORE *rghAdditionalStore;
|
|
DWORD dwFlags;
|
|
DWORD dwUrlRetrievalTimeout;
|
|
DWORD MaximumCachedCertificates;
|
|
DWORD CycleDetectionModulus;
|
|
HCERTSTORE hExclusiveRoot;
|
|
HCERTSTORE hExclusiveTrustedPeople;
|
|
DWORD dwExclusiveFlags;
|
|
};
|
|
|
|
/* Offered by mingw-w64 v4+. MS SDK ~10+/~VS2017+. */
|
|
#ifndef CERT_CHAIN_EXCLUSIVE_ENABLE_CA_FLAG
|
|
#define CERT_CHAIN_EXCLUSIVE_ENABLE_CA_FLAG 0x00000001
|
|
#endif
|
|
|
|
/* Legacy structure to supply size to Win7 clients */
|
|
struct cert_chain_engine_config_win7 {
|
|
DWORD cbSize;
|
|
HCERTSTORE hRestrictedRoot;
|
|
HCERTSTORE hRestrictedTrust;
|
|
HCERTSTORE hRestrictedOther;
|
|
DWORD cAdditionalStore;
|
|
HCERTSTORE *rghAdditionalStore;
|
|
DWORD dwFlags;
|
|
DWORD dwUrlRetrievalTimeout;
|
|
DWORD MaximumCachedCertificates;
|
|
DWORD CycleDetectionModulus;
|
|
HCERTSTORE hExclusiveRoot;
|
|
HCERTSTORE hExclusiveTrustedPeople;
|
|
};
|
|
|
|
static int is_cr_or_lf(char c)
|
|
{
|
|
return c == '\r' || c == '\n';
|
|
}
|
|
|
|
/* Search the substring needle,needlelen into string haystack,haystacklen
|
|
* Strings do not need to be terminated by a '\0'.
|
|
* Similar of macOS/Linux memmem (not available on Visual Studio).
|
|
* Return position of beginning of first occurrence or NULL if not found
|
|
*/
|
|
static const char *c_memmem(const void *haystack, size_t haystacklen,
|
|
const void *needle, size_t needlelen)
|
|
{
|
|
const char *p;
|
|
char first;
|
|
const char *str_limit = (const char *)haystack + haystacklen;
|
|
if(!needlelen || needlelen > haystacklen)
|
|
return NULL;
|
|
first = *(const char *)needle;
|
|
for(p = (const char *)haystack; p <= (str_limit - needlelen); p++)
|
|
if(((*p) == first) && (memcmp(p, needle, needlelen) == 0))
|
|
return p;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static CURLcode add_certs_data_to_store(HCERTSTORE trust_store,
|
|
const char *ca_buffer,
|
|
size_t ca_buffer_size,
|
|
const char *ca_file_text,
|
|
struct Curl_easy *data)
|
|
{
|
|
const size_t begin_cert_len = strlen(BEGIN_CERT);
|
|
const size_t end_cert_len = strlen(END_CERT);
|
|
CURLcode result = CURLE_OK;
|
|
int num_certs = 0;
|
|
bool more_certs = 1;
|
|
const char *current_ca_file_ptr = ca_buffer;
|
|
const char *ca_buffer_limit = ca_buffer + ca_buffer_size;
|
|
|
|
while(more_certs && (current_ca_file_ptr < ca_buffer_limit)) {
|
|
const char *begin_cert_ptr = c_memmem(current_ca_file_ptr,
|
|
ca_buffer_limit-current_ca_file_ptr,
|
|
BEGIN_CERT,
|
|
begin_cert_len);
|
|
if(!begin_cert_ptr || !is_cr_or_lf(begin_cert_ptr[begin_cert_len])) {
|
|
more_certs = 0;
|
|
}
|
|
else {
|
|
const char *end_cert_ptr = c_memmem(begin_cert_ptr,
|
|
ca_buffer_limit-begin_cert_ptr,
|
|
END_CERT,
|
|
end_cert_len);
|
|
if(!end_cert_ptr) {
|
|
failf(data,
|
|
"schannel: CA file '%s' is not correctly formatted",
|
|
ca_file_text);
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
more_certs = 0;
|
|
}
|
|
else {
|
|
CERT_BLOB cert_blob;
|
|
const CERT_CONTEXT *cert_context = NULL;
|
|
BOOL add_cert_result = FALSE;
|
|
DWORD actual_content_type = 0;
|
|
DWORD cert_size =
|
|
(DWORD)((end_cert_ptr + end_cert_len) - begin_cert_ptr);
|
|
|
|
cert_blob.pbData = (BYTE *)CURL_UNCONST(begin_cert_ptr);
|
|
cert_blob.cbData = cert_size;
|
|
/* Caution: CryptQueryObject() is deprecated */
|
|
if(!CryptQueryObject(CERT_QUERY_OBJECT_BLOB,
|
|
&cert_blob,
|
|
CERT_QUERY_CONTENT_FLAG_CERT,
|
|
CERT_QUERY_FORMAT_FLAG_ALL,
|
|
0,
|
|
NULL,
|
|
&actual_content_type,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
(const void **)&cert_context)) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data,
|
|
"schannel: failed to extract certificate from CA file "
|
|
"'%s': %s",
|
|
ca_file_text,
|
|
curlx_winapi_strerror(GetLastError(), buffer, sizeof(buffer)));
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
more_certs = 0;
|
|
}
|
|
else {
|
|
current_ca_file_ptr = begin_cert_ptr + cert_size;
|
|
|
|
/* Sanity check that the cert_context object is the right type */
|
|
if(CERT_QUERY_CONTENT_CERT != actual_content_type) {
|
|
failf(data,
|
|
"schannel: unexpected content type '%lu' when extracting "
|
|
"certificate from CA file '%s'",
|
|
actual_content_type, ca_file_text);
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
more_certs = 0;
|
|
}
|
|
else {
|
|
add_cert_result =
|
|
CertAddCertificateContextToStore(trust_store,
|
|
cert_context,
|
|
CERT_STORE_ADD_ALWAYS,
|
|
NULL);
|
|
if(!add_cert_result) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data,
|
|
"schannel: failed to add certificate from CA file '%s' "
|
|
"to certificate store: %s",
|
|
ca_file_text,
|
|
curlx_winapi_strerror(GetLastError(), buffer,
|
|
sizeof(buffer)));
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
more_certs = 0;
|
|
}
|
|
else {
|
|
num_certs++;
|
|
}
|
|
}
|
|
|
|
switch(actual_content_type) {
|
|
case CERT_QUERY_CONTENT_CERT:
|
|
case CERT_QUERY_CONTENT_SERIALIZED_CERT:
|
|
CertFreeCertificateContext(cert_context);
|
|
break;
|
|
case CERT_QUERY_CONTENT_CRL:
|
|
case CERT_QUERY_CONTENT_SERIALIZED_CRL:
|
|
CertFreeCRLContext((PCCRL_CONTEXT)cert_context);
|
|
break;
|
|
case CERT_QUERY_CONTENT_CTL:
|
|
case CERT_QUERY_CONTENT_SERIALIZED_CTL:
|
|
CertFreeCTLContext((PCCTL_CONTEXT)cert_context);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(result == CURLE_OK) {
|
|
if(!num_certs) {
|
|
infof(data, "schannel: did not add any certificates from CA file '%s'",
|
|
ca_file_text);
|
|
}
|
|
else {
|
|
infof(data, "schannel: added %d certificate(s) from CA file '%s'",
|
|
num_certs, ca_file_text);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static CURLcode add_certs_file_to_store(HCERTSTORE trust_store,
|
|
const char *ca_file,
|
|
struct Curl_easy *data)
|
|
{
|
|
CURLcode result;
|
|
HANDLE ca_file_handle;
|
|
LARGE_INTEGER file_size;
|
|
char *ca_file_buffer = NULL;
|
|
size_t ca_file_bufsize = 0;
|
|
DWORD total_bytes_read = 0;
|
|
|
|
/*
|
|
* Read the CA file completely into memory before parsing it. This
|
|
* optimizes for the common case where the CA file will be relatively
|
|
* small ( < 1 MiB ).
|
|
*/
|
|
ca_file_handle = curlx_CreateFile(ca_file,
|
|
GENERIC_READ,
|
|
FILE_SHARE_READ,
|
|
NULL,
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
NULL);
|
|
if(ca_file_handle == INVALID_HANDLE_VALUE) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data, "schannel: failed to open CA file '%s': %s", ca_file,
|
|
curlx_winapi_strerror(GetLastError(), buffer, sizeof(buffer)));
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
goto cleanup;
|
|
}
|
|
|
|
if(!GetFileSizeEx(ca_file_handle, &file_size)) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data, "schannel: failed to determine size of CA file '%s': %s",
|
|
ca_file,
|
|
curlx_winapi_strerror(GetLastError(), buffer, sizeof(buffer)));
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
goto cleanup;
|
|
}
|
|
|
|
if(file_size.QuadPart > MAX_CAFILE_SIZE) {
|
|
failf(data, "schannel: CA file exceeds max size of %u bytes",
|
|
MAX_CAFILE_SIZE);
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
goto cleanup;
|
|
}
|
|
|
|
ca_file_bufsize = (size_t)file_size.QuadPart;
|
|
ca_file_buffer = (char *)curlx_malloc(ca_file_bufsize + 1);
|
|
if(!ca_file_buffer) {
|
|
result = CURLE_OUT_OF_MEMORY;
|
|
goto cleanup;
|
|
}
|
|
|
|
while(total_bytes_read < ca_file_bufsize) {
|
|
DWORD bytes_to_read = (DWORD)(ca_file_bufsize - total_bytes_read);
|
|
DWORD bytes_read = 0;
|
|
|
|
if(!ReadFile(ca_file_handle, ca_file_buffer + total_bytes_read,
|
|
bytes_to_read, &bytes_read, NULL)) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data, "schannel: failed to read from CA file '%s': %s", ca_file,
|
|
curlx_winapi_strerror(GetLastError(), buffer, sizeof(buffer)));
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
goto cleanup;
|
|
}
|
|
if(bytes_read == 0) {
|
|
/* Premature EOF -- adjust the bufsize to the new value */
|
|
ca_file_bufsize = total_bytes_read;
|
|
}
|
|
else {
|
|
total_bytes_read += bytes_read;
|
|
}
|
|
}
|
|
|
|
/* null-terminate the buffer */
|
|
ca_file_buffer[ca_file_bufsize] = '\0';
|
|
|
|
result = add_certs_data_to_store(trust_store,
|
|
ca_file_buffer, ca_file_bufsize,
|
|
ca_file,
|
|
data);
|
|
|
|
cleanup:
|
|
if(ca_file_handle != INVALID_HANDLE_VALUE) {
|
|
CloseHandle(ca_file_handle);
|
|
}
|
|
Curl_safefree(ca_file_buffer);
|
|
|
|
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 hostnames. 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,
|
|
PCERT_ALT_NAME_INFO alt_name_info,
|
|
BOOL Win8_compat)
|
|
{
|
|
DWORD actual_length = 0;
|
|
BOOL compute_content = FALSE;
|
|
LPTSTR current_pos = NULL;
|
|
DWORD i;
|
|
|
|
/* CERT_NAME_SEARCH_ALL_NAMES_FLAG is available from Windows 8 onwards. */
|
|
if(Win8_compat) {
|
|
/* Offered by mingw-w64 v4+. MS SDK ~10+/~VS2017+. */
|
|
#ifndef CERT_NAME_SEARCH_ALL_NAMES_FLAG
|
|
#define CERT_NAME_SEARCH_ALL_NAMES_FLAG 0x2
|
|
#endif
|
|
/* 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;
|
|
}
|
|
|
|
if(!alt_name_info)
|
|
return 0;
|
|
|
|
compute_content = host_names != NULL && length != 0;
|
|
|
|
/* Initialize default return values. */
|
|
actual_length = 1;
|
|
if(compute_content) {
|
|
*host_names = '\0';
|
|
}
|
|
|
|
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) {
|
|
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 hostnames.");
|
|
break;
|
|
}
|
|
dns_w = entry->pwszDNSName;
|
|
/* pwszDNSName is in ia5 string format and hence does not contain any
|
|
* non-ASCII characters. */
|
|
while(*dns_w != '\0') {
|
|
*current_pos++ = (TCHAR)(*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;
|
|
}
|
|
|
|
/*
|
|
* 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 = curlx_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 = curlx_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;
|
|
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;
|
|
return result;
|
|
}
|
|
|
|
/* Verify the server's hostname */
|
|
CURLcode Curl_verify_host(struct Curl_cfilter *cf, struct Curl_easy *data)
|
|
{
|
|
CURLcode result = CURLE_PEER_FAILED_VERIFICATION;
|
|
struct ssl_connect_data *connssl = cf->ctx;
|
|
CERT_CONTEXT *pCertContextServer = NULL;
|
|
SECURITY_STATUS sspi_status;
|
|
TCHAR *cert_hostname_buff = NULL;
|
|
size_t cert_hostname_buff_index = 0;
|
|
const char *conn_hostname = connssl->peer.hostname;
|
|
size_t hostlen = strlen(conn_hostname);
|
|
DWORD 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 =
|
|
Curl_pSecFn->QueryContextAttributes(&BACKEND->ctxt->ctxt_handle,
|
|
SECPKG_ATTR_REMOTE_CERT_CONTEXT,
|
|
&pCertContextServer);
|
|
|
|
if((sspi_status != SEC_E_OK) || !pCertContextServer) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data, "schannel: Failed to read remote certificate context: %s",
|
|
Curl_sspi_strerror(sspi_status, buffer, sizeof(buffer)));
|
|
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 && alt_name_info) {
|
|
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 */
|
|
len = cert_get_name_string(data, pCertContextServer,
|
|
NULL, 0, alt_name_info, Win8_compat);
|
|
if(len == 0) {
|
|
failf(data,
|
|
"schannel: CertGetNameString() returned no "
|
|
"certificate name information");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* CertGetNameString guarantees that the returned name will not contain
|
|
* embedded null bytes. This appears to be undocumented behavior.
|
|
*/
|
|
cert_hostname_buff = (LPTSTR)curlx_malloc(len * sizeof(TCHAR));
|
|
if(!cert_hostname_buff) {
|
|
result = CURLE_OUT_OF_MEMORY;
|
|
goto cleanup;
|
|
}
|
|
actual_len = cert_get_name_string(data, pCertContextServer,
|
|
(LPTSTR)cert_hostname_buff, len,
|
|
alt_name_info, Win8_compat);
|
|
|
|
/* Sanity check */
|
|
if(actual_len != len) {
|
|
failf(data,
|
|
"schannel: CertGetNameString() returned certificate "
|
|
"name information of unexpected size");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* cert_hostname_buff contains all DNS names, where each name is
|
|
* 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.
|
|
*/
|
|
while(cert_hostname_buff_index < len &&
|
|
cert_hostname_buff[cert_hostname_buff_index] != TEXT('\0') &&
|
|
result == CURLE_PEER_FAILED_VERIFICATION) {
|
|
char *cert_hostname;
|
|
|
|
/* Comparing the cert name and the connection hostname encoded as UTF-8
|
|
* is acceptable since both values are assumed to use ASCII
|
|
* (or some equivalent) encoding
|
|
*/
|
|
cert_hostname = curlx_convert_tchar_to_UTF8(
|
|
&cert_hostname_buff[cert_hostname_buff_index]);
|
|
if(!cert_hostname) {
|
|
result = CURLE_OUT_OF_MEMORY;
|
|
}
|
|
else {
|
|
if(Curl_cert_hostcheck(cert_hostname, strlen(cert_hostname),
|
|
conn_hostname, hostlen)) {
|
|
infof(data, "schannel: connection hostname (%s) validated "
|
|
"against certificate name (%s)",
|
|
conn_hostname, cert_hostname);
|
|
result = CURLE_OK;
|
|
}
|
|
else {
|
|
size_t cert_hostname_len;
|
|
|
|
infof(data,
|
|
"schannel: connection hostname (%s) did not match "
|
|
"against certificate name (%s)",
|
|
conn_hostname, cert_hostname);
|
|
|
|
cert_hostname_len =
|
|
_tcslen(&cert_hostname_buff[cert_hostname_buff_index]);
|
|
|
|
/* Move on to next cert name */
|
|
cert_hostname_buff_index += cert_hostname_len + 1;
|
|
|
|
result = CURLE_PEER_FAILED_VERIFICATION;
|
|
}
|
|
curlx_free(cert_hostname);
|
|
}
|
|
}
|
|
|
|
if(result == CURLE_PEER_FAILED_VERIFICATION) {
|
|
failf(data,
|
|
"schannel: CertGetNameString() failed to match "
|
|
"connection hostname (%s) against server certificate names",
|
|
conn_hostname);
|
|
}
|
|
else if(result != CURLE_OK)
|
|
failf(data, "schannel: server certificate name verification failed");
|
|
}
|
|
|
|
cleanup:
|
|
LocalFree(alt_name_info);
|
|
Curl_safefree(cert_hostname_buff);
|
|
|
|
if(pCertContextServer)
|
|
CertFreeCertificateContext(pCertContextServer);
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Verify the server's certificate and hostname */
|
|
CURLcode Curl_verify_certificate(struct Curl_cfilter *cf,
|
|
struct Curl_easy *data)
|
|
{
|
|
struct ssl_connect_data *connssl = cf->ctx;
|
|
struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
|
|
struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
|
|
SECURITY_STATUS sspi_status;
|
|
CURLcode result = CURLE_OK;
|
|
CERT_CONTEXT *pCertContextServer = NULL;
|
|
const CERT_CHAIN_CONTEXT *pChainContext = NULL;
|
|
HCERTCHAINENGINE cert_chain_engine = NULL;
|
|
HCERTSTORE trust_store = NULL;
|
|
HCERTSTORE own_trust_store = NULL;
|
|
|
|
DEBUGASSERT(BACKEND);
|
|
|
|
sspi_status =
|
|
Curl_pSecFn->QueryContextAttributes(&BACKEND->ctxt->ctxt_handle,
|
|
SECPKG_ATTR_REMOTE_CERT_CONTEXT,
|
|
&pCertContextServer);
|
|
|
|
if((sspi_status != SEC_E_OK) || !pCertContextServer) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data, "schannel: Failed to read remote certificate context: %s",
|
|
Curl_sspi_strerror(sspi_status, buffer, sizeof(buffer)));
|
|
result = CURLE_PEER_FAILED_VERIFICATION;
|
|
}
|
|
|
|
if(result == CURLE_OK &&
|
|
(conn_config->CAfile || conn_config->ca_info_blob) &&
|
|
BACKEND->use_manual_cred_validation) {
|
|
/*
|
|
* Create a chain engine that uses the certificates in the CA file as
|
|
* trusted certificates. This is only supported on Windows 7+.
|
|
*/
|
|
|
|
if(curlx_verify_windows_version(6, 1, 0, PLATFORM_WINNT,
|
|
VERSION_LESS_THAN)) {
|
|
failf(data, "schannel: this version of Windows is too old to support "
|
|
"certificate verification via CA bundle file.");
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
}
|
|
else {
|
|
/* try cache */
|
|
trust_store = Curl_schannel_get_cached_cert_store(cf, data);
|
|
|
|
if(trust_store) {
|
|
infof(data, "schannel: reusing certificate store from cache");
|
|
}
|
|
else {
|
|
/* Open the certificate store */
|
|
trust_store = CertOpenStore(CERT_STORE_PROV_MEMORY,
|
|
0,
|
|
(HCRYPTPROV)NULL,
|
|
CERT_STORE_CREATE_NEW_FLAG,
|
|
NULL);
|
|
if(!trust_store) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data, "schannel: failed to create certificate store: %s",
|
|
curlx_winapi_strerror(GetLastError(), buffer, sizeof(buffer)));
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
}
|
|
else {
|
|
const struct curl_blob *ca_info_blob = conn_config->ca_info_blob;
|
|
own_trust_store = trust_store;
|
|
|
|
if(ca_info_blob) {
|
|
result = add_certs_data_to_store(trust_store,
|
|
(const char *)ca_info_blob->data,
|
|
ca_info_blob->len,
|
|
"(memory blob)",
|
|
data);
|
|
}
|
|
else {
|
|
result = add_certs_file_to_store(trust_store,
|
|
conn_config->CAfile,
|
|
data);
|
|
}
|
|
if(result == CURLE_OK) {
|
|
if(Curl_schannel_set_cached_cert_store(cf, data, trust_store)) {
|
|
own_trust_store = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(result == CURLE_OK) {
|
|
struct cert_chain_engine_config_win8 engine_config;
|
|
BOOL create_engine_result;
|
|
|
|
memset(&engine_config, 0, sizeof(engine_config));
|
|
engine_config.hExclusiveRoot = trust_store;
|
|
|
|
/* Win8/Server2012 allows us to match partial chains */
|
|
if(curlx_verify_windows_version(6, 2, 0, PLATFORM_WINNT,
|
|
VERSION_GREATER_THAN_EQUAL) &&
|
|
!ssl_config->no_partialchain) {
|
|
engine_config.cbSize = sizeof(engine_config);
|
|
engine_config.dwExclusiveFlags = CERT_CHAIN_EXCLUSIVE_ENABLE_CA_FLAG;
|
|
}
|
|
else
|
|
engine_config.cbSize = sizeof(struct cert_chain_engine_config_win7);
|
|
|
|
/* CertCreateCertificateChainEngine will check the expected size of the
|
|
* CERT_CHAIN_ENGINE_CONFIG structure and fail if the specified size
|
|
* does not match the expected size. When this occurs, it indicates that
|
|
* CAINFO is not supported on the version of Windows in use.
|
|
*/
|
|
create_engine_result =
|
|
CertCreateCertificateChainEngine(
|
|
(CERT_CHAIN_ENGINE_CONFIG *)&engine_config, &cert_chain_engine);
|
|
if(!create_engine_result) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data,
|
|
"schannel: failed to create certificate chain engine: %s",
|
|
curlx_winapi_strerror(GetLastError(), buffer, sizeof(buffer)));
|
|
result = CURLE_SSL_CACERT_BADFILE;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(result == CURLE_OK) {
|
|
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)) {
|
|
char buffer[WINAPI_ERROR_LEN];
|
|
failf(data, "schannel: failed to get the certificate chain: %s",
|
|
curlx_winapi_strerror(GetLastError(), buffer, sizeof(buffer)));
|
|
pChainContext = NULL;
|
|
result = CURLE_PEER_FAILED_VERIFICATION;
|
|
}
|
|
|
|
if(result == CURLE_OK) {
|
|
CERT_SIMPLE_CHAIN *pSimpleChain = pChainContext->rgpChain[0];
|
|
DWORD dwTrustErrorMask = ~(DWORD)(CERT_TRUST_IS_NOT_TIME_NESTED);
|
|
dwTrustErrorMask &= pSimpleChain->TrustStatus.dwErrorStatus;
|
|
|
|
if(data->set.ssl.revoke_best_effort) {
|
|
/* Ignore errors when root certificates are missing the revocation
|
|
* list URL, or when the list could not be downloaded because the
|
|
* server is currently unreachable. */
|
|
dwTrustErrorMask &= ~(DWORD)(CERT_TRUST_REVOCATION_STATUS_UNKNOWN |
|
|
CERT_TRUST_IS_OFFLINE_REVOCATION);
|
|
}
|
|
|
|
if(dwTrustErrorMask) {
|
|
if(dwTrustErrorMask & CERT_TRUST_IS_REVOKED)
|
|
failf(data, "schannel: trust for this certificate or one of "
|
|
"the certificates in the certificate chain has been revoked");
|
|
else if(dwTrustErrorMask & CERT_TRUST_IS_PARTIAL_CHAIN)
|
|
failf(data, "schannel: the certificate chain is incomplete");
|
|
else if(dwTrustErrorMask & CERT_TRUST_IS_UNTRUSTED_ROOT)
|
|
failf(data, "schannel: the certificate or certificate chain is "
|
|
"based on an untrusted root");
|
|
else if(dwTrustErrorMask & CERT_TRUST_IS_NOT_TIME_VALID)
|
|
failf(data, "schannel: this certificate or one of the certificates "
|
|
"in the certificate chain is not time valid");
|
|
else if(dwTrustErrorMask & CERT_TRUST_REVOCATION_STATUS_UNKNOWN)
|
|
failf(data, "schannel: the revocation status is unknown");
|
|
else
|
|
failf(data, "schannel: error 0x%08lx", dwTrustErrorMask);
|
|
result = CURLE_PEER_FAILED_VERIFICATION;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(result == CURLE_OK) {
|
|
if(conn_config->verifyhost) {
|
|
result = Curl_verify_host(cf, data);
|
|
}
|
|
}
|
|
|
|
if(cert_chain_engine) {
|
|
CertFreeCertificateChainEngine(cert_chain_engine);
|
|
}
|
|
|
|
if(own_trust_store) {
|
|
CertCloseStore(own_trust_store, 0);
|
|
}
|
|
|
|
if(pChainContext)
|
|
CertFreeCertificateChain(pChainContext);
|
|
|
|
if(pCertContextServer)
|
|
CertFreeCertificateContext(pCertContextServer);
|
|
|
|
return result;
|
|
}
|
|
|
|
#endif /* USE_SCHANNEL */
|