Skip to content

Commit 104d033

Browse files
committed
Default using Windows Schannel for SSL/TLS certificate verification on Windows
This commit adds support for using Windows Schannel (Windows certificate store) for SSL/TLS certificate verification instead of OpenSSL's verification on Windows. This provides automatic root certificate updates from Windows Update. Changes: - Add CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE macro to optionally disable this feature and use OpenSSL verification - Add HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE CMake option (default ON) - Add wincrypt_error() and wincrypt_chain_error() methods to Result class for Windows-specific certificate error information - Modify SSLClient::initialize_ssl() to use Windows CertGetCertificateChain() and CertVerifyCertificateChainPolicy() APIs for certificate verification - Update tests to handle both Windows Schannel and OpenSSL verification paths - Update README.md with documentation for Windows certificate verification Closes #1978 Based on PR #2116 by @solarispika
1 parent adf58bf commit 104d033

File tree

4 files changed

+259
-10
lines changed

4 files changed

+259
-10
lines changed

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* HTTPLIB_REQUIRE_ZSTD (default off)
1212
* HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN (default on)
1313
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
14+
* HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE (default on)
1415
* HTTPLIB_COMPILE (default off)
1516
* HTTPLIB_INSTALL (default on)
1617
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
@@ -110,6 +111,7 @@ option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system cer
110111
option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON)
111112
option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF)
112113
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
114+
option(HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE "Enable automatic root certificates update on Windows." ON)
113115
# Defaults to static library but respects standard BUILD_SHARED_LIBS if set
114116
include(CMakeDependentOption)
115117
cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only."
@@ -296,6 +298,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
296298
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:CPPHTTPLIB_OPENSSL_SUPPORT>
297299
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN}>>:CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN>
298300
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
301+
$<$<AND:$<PLATFORM_ID:Windows>,$<NOT:$<BOOL:${HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE}>>>:CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE>
299302
)
300303

301304
# CMake configuration files installation directory

README.md

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,26 @@ if (!res) {
113113
break;
114114

115115
case httplib::Error::SSLServerVerification:
116-
std::cout << "SSL verification failed, X509 error: "
117-
<< res.ssl_openssl_error() << std::endl;
116+
std::cout << "SSL verification failed" << std::endl;
117+
#if defined(_WIN32) && !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
118+
// On Windows with Schannel, check Windows certificate errors
119+
std::cout << " Windows error: 0x" << std::hex << res.wincrypt_error()
120+
<< ", chain status: 0x" << res.wincrypt_chain_error() << std::endl;
121+
#else
122+
// On other platforms, check OpenSSL errors
123+
std::cout << " X509 error: " << res.ssl_openssl_error() << std::endl;
124+
#endif
118125
break;
119126

120127
case httplib::Error::SSLServerHostnameVerification:
121-
std::cout << "SSL hostname verification failed, X509 error: "
122-
<< res.ssl_openssl_error() << std::endl;
128+
std::cout << "SSL hostname verification failed" << std::endl;
129+
#if defined(_WIN32) && !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
130+
// On Windows with Schannel, check Windows certificate errors
131+
std::cout << " Windows error: 0x" << std::hex << res.wincrypt_error() << std::endl;
132+
#else
133+
// On other platforms, check OpenSSL errors
134+
std::cout << " X509 error: " << res.ssl_openssl_error() << std::endl;
135+
#endif
123136
break;
124137

125138
default:
@@ -128,6 +141,62 @@ if (!res) {
128141
}
129142
```
130143
144+
For a simpler platform-agnostic approach, you can check which error field is non-zero:
145+
146+
```c++
147+
auto res = cli.Get("/");
148+
if (!res) {
149+
if (res.error() == httplib::Error::SSLServerVerification) {
150+
std::cout << "Certificate verification failed!" << std::endl;
151+
152+
// Check which backend reported the error
153+
if (res.ssl_openssl_error() != 0) {
154+
// OpenSSL reported the error (Linux, macOS, or Windows with Schannel disabled)
155+
std::cout << "OpenSSL error: " << res.ssl_openssl_error() << std::endl;
156+
}
157+
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
158+
#if defined(_WIN32) && !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
159+
else if (res.wincrypt_error() != 0) {
160+
// Windows Schannel reported the error
161+
std::cout << "Windows error: 0x" << std::hex << res.wincrypt_error() << std::endl;
162+
std::cout << "Chain status: 0x" << std::hex << res.wincrypt_chain_error() << std::endl;
163+
}
164+
#endif
165+
#endif
166+
}
167+
}
168+
```
169+
170+
### Windows Certificate Verification
171+
172+
On Windows, by default, cpp-httplib uses Windows Schannel for certificate verification instead of OpenSSL's verification. This provides automatic root certificate updates from Windows Update.
173+
174+
**When certificate verification fails on Windows:**
175+
- OpenSSL still handles the TLS handshake
176+
- Windows Schannel performs certificate verification
177+
- Check `wincrypt_error()` and `wincrypt_chain_error()` for diagnostic information
178+
- The `ssl_openssl_error()` field will be 0 for certificate verification errors
179+
180+
**Windows-specific error codes** (`wincrypt_error()`) include:
181+
- `CERT_E_EXPIRED` (0x800B0101) - Certificate has expired
182+
- `CERT_E_UNTRUSTEDROOT` (0x800B0109) - Certificate chain to untrusted root
183+
- `CERT_E_CN_NO_MATCH` (0x800B010F) - Certificate CN doesn't match hostname
184+
- `CERT_E_REVOKED` (0x800B010C) - Certificate has been revoked
185+
- `CERT_E_CHAINING` (0x800B010A) - Error building certificate chain
186+
187+
**Chain trust status** (`wincrypt_chain_error()`) provides additional details:
188+
- `CERT_TRUST_IS_NOT_TIME_VALID` (0x00000001) - Certificate expired
189+
- `CERT_TRUST_IS_REVOKED` (0x00000004) - Certificate revoked
190+
- `CERT_TRUST_IS_NOT_SIGNATURE_VALID` (0x00000008) - Invalid signature
191+
- `CERT_TRUST_IS_UNTRUSTED_ROOT` (0x00000020) - Untrusted root
192+
193+
To disable Windows automatic certificate updates and use OpenSSL verification:
194+
```cmake
195+
set(HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE OFF)
196+
```
197+
198+
Or define `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` before including httplib.h.
199+
131200
Server
132201
------
133202

httplib.h

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ using ssize_t = __int64;
221221
#endif // NOMINMAX
222222

223223
#include <io.h>
224+
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && \
225+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
226+
#define CERT_CHAIN_PARA_HAS_EXTRA_FIELDS
227+
#endif
224228
#include <winsock2.h>
225229
#include <ws2tcpip.h>
226230

@@ -1349,6 +1353,17 @@ class Result {
13491353
: res_(std::move(res)), err_(err),
13501354
request_headers_(std::move(request_headers)), ssl_error_(ssl_error),
13511355
ssl_openssl_error_(ssl_openssl_error) {}
1356+
1357+
#if defined(_WIN32) && \
1358+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1359+
Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,
1360+
int ssl_error, unsigned long ssl_openssl_error,
1361+
unsigned long wincrypt_error, unsigned long wincrypt_chain_error)
1362+
: res_(std::move(res)), err_(err),
1363+
request_headers_(std::move(request_headers)), ssl_error_(ssl_error),
1364+
ssl_openssl_error_(ssl_openssl_error), wincrypt_error_(wincrypt_error),
1365+
wincrypt_chain_error_(wincrypt_chain_error) {}
1366+
#endif
13521367
#endif
13531368
// Response
13541369
operator bool() const { return res_ != nullptr; }
@@ -1369,6 +1384,14 @@ class Result {
13691384
int ssl_error() const { return ssl_error_; }
13701385
// OpenSSL Error
13711386
unsigned long ssl_openssl_error() const { return ssl_openssl_error_; }
1387+
1388+
#if defined(_WIN32) && \
1389+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1390+
// Windows Certificate Error (from GetLastError or policy_status.dwError)
1391+
unsigned long wincrypt_error() const { return wincrypt_error_; }
1392+
// Windows Certificate Chain Trust Status (from TrustStatus.dwErrorStatus)
1393+
unsigned long wincrypt_chain_error() const { return wincrypt_chain_error_; }
1394+
#endif
13721395
#endif
13731396

13741397
// Request Headers
@@ -1387,6 +1410,12 @@ class Result {
13871410
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
13881411
int ssl_error_ = 0;
13891412
unsigned long ssl_openssl_error_ = 0;
1413+
1414+
#if defined(_WIN32) && \
1415+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1416+
unsigned long wincrypt_error_ = 0;
1417+
unsigned long wincrypt_chain_error_ = 0;
1418+
#endif
13901419
#endif
13911420
};
13921421

@@ -1706,6 +1735,12 @@ class ClientImpl {
17061735
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
17071736
int last_ssl_error_ = 0;
17081737
unsigned long last_openssl_error_ = 0;
1738+
1739+
#if defined(_WIN32) && \
1740+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1741+
unsigned long last_wincrypt_error_ = 0;
1742+
unsigned long last_wincrypt_chain_error_ = 0;
1743+
#endif
17091744
#endif
17101745

17111746
private:
@@ -6310,6 +6345,7 @@ inline bool is_ssl_peer_could_be_closed(SSL *ssl, socket_t sock) {
63106345
}
63116346

63126347
#ifdef _WIN32
6348+
#ifdef CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
63136349
// NOTE: This code came up with the following stackoverflow post:
63146350
// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store
63156351
inline bool load_system_certs_on_windows(X509_STORE *store) {
@@ -6336,6 +6372,7 @@ inline bool load_system_certs_on_windows(X509_STORE *store) {
63366372

63376373
return result;
63386374
}
6375+
#endif // CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
63396376
#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC
63406377
template <typename T>
63416378
using CFObjectPtr =
@@ -8954,8 +8991,19 @@ inline Result ClientImpl::send_(Request &&req) {
89548991
auto error = Error::Success;
89558992
auto ret = send(req, *res, error);
89568993
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
8994+
#if defined(_WIN32) && \
8995+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
8996+
return Result{ret ? std::move(res) : nullptr,
8997+
error,
8998+
std::move(req.headers),
8999+
last_ssl_error_,
9000+
last_openssl_error_,
9001+
last_wincrypt_error_,
9002+
last_wincrypt_chain_error_};
9003+
#else
89579004
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers),
89589005
last_ssl_error_, last_openssl_error_};
9006+
#endif
89599007
#else
89609008
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)};
89619009
#endif
@@ -9541,8 +9589,19 @@ inline Result ClientImpl::send_with_content_provider_and_receiver(
95419589
std::move(content_receiver), error);
95429590

95439591
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
9592+
#if defined(_WIN32) && \
9593+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
9594+
return Result{std::move(res),
9595+
error,
9596+
std::move(req.headers),
9597+
last_ssl_error_,
9598+
last_openssl_error_,
9599+
last_wincrypt_error_,
9600+
last_wincrypt_chain_error_};
9601+
#else
95449602
return Result{std::move(res), error, std::move(req.headers), last_ssl_error_,
95459603
last_openssl_error_};
9604+
#endif
95469605
#else
95479606
return Result{std::move(res), error, std::move(req.headers)};
95489607
#endif
@@ -11348,8 +11407,10 @@ inline bool SSLClient::load_certs() {
1134811407
} else {
1134911408
auto loaded = false;
1135011409
#ifdef _WIN32
11410+
#ifdef CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1135111411
loaded =
1135211412
detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
11413+
#endif // CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1135311414
#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC
1135411415
loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_));
1135511416
#endif // _WIN32
@@ -11396,6 +11457,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
1139611457
}
1139711458

1139811459
if (verification_status == SSLVerifierResponse::NoDecisionMade) {
11460+
#if !defined(_WIN32) || \
11461+
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1139911462
verify_result_ = SSL_get_verify_result(ssl2);
1140011463

1140111464
if (verify_result_ != X509_V_OK) {
@@ -11404,6 +11467,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
1140411467
output_error_log(error, nullptr);
1140511468
return false;
1140611469
}
11470+
#endif // !_WIN32 ||
11471+
// CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1140711472

1140811473
auto server_cert = SSL_get1_peer_certificate(ssl2);
1140911474
auto se = detail::scope_exit([&] { X509_free(server_cert); });
@@ -11415,6 +11480,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
1141511480
return false;
1141611481
}
1141711482

11483+
#if !defined(_WIN32) || \
11484+
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1141811485
if (server_hostname_verification_) {
1141911486
if (!verify_host(server_cert)) {
1142011487
last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH;
@@ -11423,6 +11490,101 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
1142311490
return false;
1142411491
}
1142511492
}
11493+
#else // _WIN32 &&
11494+
// !CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
11495+
// Windows Schannel verification path - clear OpenSSL errors since
11496+
// we're not using OpenSSL for certificate verification
11497+
last_openssl_error_ = 0;
11498+
11499+
// Convert OpenSSL certificate to DER format
11500+
auto der_cert =
11501+
std::vector<unsigned char>(i2d_X509(server_cert, nullptr));
11502+
auto der_cert_data = der_cert.data();
11503+
if (i2d_X509(server_cert, &der_cert_data) < 0) {
11504+
error = Error::SSLServerVerification;
11505+
return false;
11506+
}
11507+
11508+
// Create a certificate context from the DER-encoded certificate
11509+
auto cert_context = CertCreateCertificateContext(
11510+
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(),
11511+
static_cast<DWORD>(der_cert.size()));
11512+
11513+
if (cert_context == nullptr) {
11514+
last_wincrypt_error_ = GetLastError();
11515+
error = Error::SSLServerVerification;
11516+
return false;
11517+
}
11518+
11519+
auto chain_para = CERT_CHAIN_PARA{};
11520+
chain_para.cbSize = sizeof(chain_para);
11521+
chain_para.dwUrlRetrievalTimeout = 10 * 1000;
11522+
11523+
auto chain_context = PCCERT_CHAIN_CONTEXT{};
11524+
auto result = CertGetCertificateChain(
11525+
nullptr, cert_context, nullptr, cert_context->hCertStore,
11526+
&chain_para,
11527+
CERT_CHAIN_CACHE_END_CERT |
11528+
CERT_CHAIN_REVOCATION_CHECK_END_CERT |
11529+
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
11530+
nullptr, &chain_context);
11531+
11532+
CertFreeCertificateContext(cert_context);
11533+
11534+
if (!result || chain_context == nullptr) {
11535+
if (!result) { last_wincrypt_error_ = GetLastError(); }
11536+
error = Error::SSLServerVerification;
11537+
return false;
11538+
}
11539+
11540+
// Capture detailed chain trust status before using the chain
11541+
last_wincrypt_chain_error_ =
11542+
chain_context->TrustStatus.dwErrorStatus;
11543+
11544+
// Verify chain policy
11545+
auto extra_policy_para = SSL_EXTRA_CERT_CHAIN_POLICY_PARA{};
11546+
extra_policy_para.cbSize = sizeof(extra_policy_para);
11547+
extra_policy_para.dwAuthType = AUTHTYPE_SERVER;
11548+
auto whost = detail::u8string_to_wstring(host_.c_str());
11549+
if (server_hostname_verification_) {
11550+
extra_policy_para.pwszServerName =
11551+
const_cast<wchar_t *>(whost.c_str());
11552+
}
11553+
11554+
auto policy_para = CERT_CHAIN_POLICY_PARA{};
11555+
policy_para.cbSize = sizeof(policy_para);
11556+
policy_para.dwFlags =
11557+
CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;
11558+
policy_para.pvExtraPolicyPara = &extra_policy_para;
11559+
11560+
auto policy_status = CERT_CHAIN_POLICY_STATUS{};
11561+
policy_status.cbSize = sizeof(policy_status);
11562+
11563+
result = CertVerifyCertificateChainPolicy(
11564+
CERT_CHAIN_POLICY_SSL, chain_context, &policy_para,
11565+
&policy_status);
11566+
11567+
CertFreeCertificateChain(chain_context);
11568+
11569+
if (!result) {
11570+
last_wincrypt_error_ = GetLastError();
11571+
error = Error::SSLServerVerification;
11572+
return false;
11573+
}
11574+
11575+
if (policy_status.dwError != 0) {
11576+
// Store the specific Windows certificate error code
11577+
last_wincrypt_error_ = policy_status.dwError;
11578+
11579+
if (policy_status.dwError == CERT_E_CN_NO_MATCH) {
11580+
error = Error::SSLServerHostnameVerification;
11581+
} else {
11582+
error = Error::SSLServerVerification;
11583+
}
11584+
return false;
11585+
}
11586+
#endif // !_WIN32 ||
11587+
// CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1142611588
}
1142711589
}
1142811590

0 commit comments

Comments
 (0)