Skip to content

Commit 1ffa731

Browse files
solarispikaclaude
andcommitted
Default using Windows Schannel for SSL/TLS on Windows
This commit enables Windows Schannel for certificate verification on Windows platforms, providing automatic root certificate updates from Windows Update. Key changes: - Added Windows Schannel certificate verification using CertGetCertificateChain and CertVerifyCertificateChainPolicy APIs - Implemented wincrypt_error() and wincrypt_chain_error() to expose Windows certificate errors alongside existing OpenSSL error fields - Clear ssl_openssl_error before Windows verification to provide unambiguous error source indication (0 means Windows error, non-zero means OpenSSL error) - Updated tests with platform-specific assertions for Windows vs OpenSSL errors - Added comprehensive Windows certificate error documentation to README Certificate verification flow on Windows: 1. OpenSSL performs TLS handshake (can set ssl_error) 2. Windows Schannel verifies certificate chain (sets wincrypt_error and wincrypt_chain_error) 3. Users check wincrypt_error() for Windows-specific error codes like CERT_E_EXPIRED, CERT_E_UNTRUSTEDROOT, CERT_E_REVOKED, etc. Feature can be disabled with CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE or by setting HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE=OFF in CMake. Fixes #1978 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 145fc8b commit 1ffa731

File tree

4 files changed

+255
-12
lines changed

4 files changed

+255
-12
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_TEST (default off)
@@ -109,6 +110,7 @@ option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system cer
109110
option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON)
110111
option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF)
111112
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
113+
option(HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE "Enable automatic root certificates update on Windows." ON)
112114
# Defaults to static library
113115
option(BUILD_SHARED_LIBS "Build the library as a shared library instead of static. Has no effect if using header-only." OFF)
114116
if (BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE)
@@ -274,6 +276,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
274276
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:CPPHTTPLIB_OPENSSL_SUPPORT>
275277
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN}>>:CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN>
276278
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
279+
$<$<AND:$<PLATFORM_ID:Windows>,$<NOT:$<BOOL:${HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE}>>>:CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE>
277280
)
278281

279282
# CMake configuration files installation directory

README.md

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

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

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

124137
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: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ using ssize_t = long;
197197
#endif // NOMINMAX
198198

199199
#include <io.h>
200+
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && \
201+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
202+
#define CERT_CHAIN_PARA_HAS_EXTRA_FIELDS
203+
#endif
200204
#include <winsock2.h>
201205
#include <ws2tcpip.h>
202206

@@ -1275,6 +1279,17 @@ class Result {
12751279
: res_(std::move(res)), err_(err),
12761280
request_headers_(std::move(request_headers)), ssl_error_(ssl_error),
12771281
ssl_openssl_error_(ssl_openssl_error) {}
1282+
1283+
#if defined(_WIN32) && \
1284+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1285+
Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,
1286+
int ssl_error, unsigned long ssl_openssl_error,
1287+
unsigned long wincrypt_error, unsigned long wincrypt_chain_error)
1288+
: res_(std::move(res)), err_(err),
1289+
request_headers_(std::move(request_headers)), ssl_error_(ssl_error),
1290+
ssl_openssl_error_(ssl_openssl_error), wincrypt_error_(wincrypt_error),
1291+
wincrypt_chain_error_(wincrypt_chain_error) {}
1292+
#endif
12781293
#endif
12791294
// Response
12801295
operator bool() const { return res_ != nullptr; }
@@ -1295,6 +1310,14 @@ class Result {
12951310
int ssl_error() const { return ssl_error_; }
12961311
// OpenSSL Error
12971312
unsigned long ssl_openssl_error() const { return ssl_openssl_error_; }
1313+
1314+
#if defined(_WIN32) && \
1315+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1316+
// Windows Certificate Error (from GetLastError or policy_status.dwError)
1317+
unsigned long wincrypt_error() const { return wincrypt_error_; }
1318+
// Windows Certificate Chain Trust Status (from TrustStatus.dwErrorStatus)
1319+
unsigned long wincrypt_chain_error() const { return wincrypt_chain_error_; }
1320+
#endif
12981321
#endif
12991322

13001323
// Request Headers
@@ -1313,6 +1336,12 @@ class Result {
13131336
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
13141337
int ssl_error_ = 0;
13151338
unsigned long ssl_openssl_error_ = 0;
1339+
1340+
#if defined(_WIN32) && \
1341+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1342+
unsigned long wincrypt_error_ = 0;
1343+
unsigned long wincrypt_chain_error_ = 0;
1344+
#endif
13161345
#endif
13171346
};
13181347

@@ -1614,6 +1643,12 @@ class ClientImpl {
16141643
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
16151644
int last_ssl_error_ = 0;
16161645
unsigned long last_openssl_error_ = 0;
1646+
1647+
#if defined(_WIN32) && \
1648+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1649+
unsigned long last_wincrypt_error_ = 0;
1650+
unsigned long last_wincrypt_chain_error_ = 0;
1651+
#endif
16171652
#endif
16181653

16191654
private:
@@ -6040,6 +6075,7 @@ inline bool is_ssl_peer_could_be_closed(SSL *ssl, socket_t sock) {
60406075
}
60416076

60426077
#ifdef _WIN32
6078+
#ifdef CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
60436079
// NOTE: This code came up with the following stackoverflow post:
60446080
// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store
60456081
inline bool load_system_certs_on_windows(X509_STORE *store) {
@@ -6066,6 +6102,7 @@ inline bool load_system_certs_on_windows(X509_STORE *store) {
60666102

60676103
return result;
60686104
}
6105+
#endif // CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
60696106
#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \
60706107
defined(TARGET_OS_OSX)
60716108
template <typename T>
@@ -8344,8 +8381,15 @@ inline Result ClientImpl::send_(Request &&req) {
83448381
auto error = Error::Success;
83458382
auto ret = send(req, *res, error);
83468383
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
8384+
#if defined(_WIN32) && \
8385+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
8386+
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers),
8387+
last_ssl_error_, last_openssl_error_, last_wincrypt_error_,
8388+
last_wincrypt_chain_error_};
8389+
#else
83478390
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers),
83488391
last_ssl_error_, last_openssl_error_};
8392+
#endif
83498393
#else
83508394
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)};
83518395
#endif
@@ -8899,8 +8943,15 @@ inline Result ClientImpl::send_with_content_provider(
88998943
std::move(content_provider_without_length), content_type, error);
89008944

89018945
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
8946+
#if defined(_WIN32) && \
8947+
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
8948+
return Result{std::move(res), error, std::move(req.headers), last_ssl_error_,
8949+
last_openssl_error_, last_wincrypt_error_,
8950+
last_wincrypt_chain_error_};
8951+
#else
89028952
return Result{std::move(res), error, std::move(req.headers), last_ssl_error_,
89038953
last_openssl_error_};
8954+
#endif
89048955
#else
89058956
return Result{std::move(res), error, std::move(req.headers)};
89068957
#endif
@@ -10483,8 +10534,10 @@ inline bool SSLClient::load_certs() {
1048310534
} else {
1048410535
auto loaded = false;
1048510536
#ifdef _WIN32
10537+
#ifdef CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1048610538
loaded =
1048710539
detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
10540+
#endif // CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1048810541
#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \
1048910542
defined(TARGET_OS_OSX)
1049010543
loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_));
@@ -10529,13 +10582,17 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
1052910582
}
1053010583

1053110584
if (verification_status == SSLVerifierResponse::NoDecisionMade) {
10585+
#if !defined(_WIN32) || \
10586+
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1053210587
verify_result_ = SSL_get_verify_result(ssl2);
1053310588

1053410589
if (verify_result_ != X509_V_OK) {
1053510590
last_openssl_error_ = static_cast<unsigned long>(verify_result_);
1053610591
error = Error::SSLServerVerification;
1053710592
return false;
1053810593
}
10594+
#endif // not _WIN32 ||
10595+
// CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1053910596

1054010597
auto server_cert = SSL_get1_peer_certificate(ssl2);
1054110598
auto se = detail::scope_exit([&] { X509_free(server_cert); });
@@ -10546,13 +10603,110 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
1054610603
return false;
1054710604
}
1054810605

10606+
#if !defined(_WIN32) || \
10607+
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1054910608
if (server_hostname_verification_) {
1055010609
if (!verify_host(server_cert)) {
1055110610
last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH;
1055210611
error = Error::SSLServerHostnameVerification;
1055310612
return false;
1055410613
}
1055510614
}
10615+
#else // _WIN32 && !
10616+
// CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
10617+
// Windows Schannel verification path - clear OpenSSL errors since
10618+
// we're not using OpenSSL for certificate verification
10619+
last_openssl_error_ = 0;
10620+
10621+
// Convert OpenSSL certificate to DER format
10622+
auto der_cert =
10623+
std::vector<unsigned char>(i2d_X509(server_cert, nullptr));
10624+
auto der_cert_data = der_cert.data();
10625+
if (i2d_X509(server_cert, &der_cert_data) < 0) {
10626+
error = Error::SSLServerVerification;
10627+
return false;
10628+
}
10629+
10630+
// Create a certificate context from the DER-encoded certificate
10631+
auto cert_context = CertCreateCertificateContext(
10632+
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(),
10633+
static_cast<DWORD>(der_cert.size()));
10634+
10635+
if (cert_context == nullptr) {
10636+
last_wincrypt_error_ = GetLastError();
10637+
error = Error::SSLServerVerification;
10638+
return false;
10639+
}
10640+
10641+
auto chain_para = CERT_CHAIN_PARA{};
10642+
chain_para.cbSize = sizeof(chain_para);
10643+
chain_para.dwUrlRetrievalTimeout = 10 * 1000;
10644+
10645+
auto chain_context = PCCERT_CHAIN_CONTEXT{};
10646+
auto result = CertGetCertificateChain(
10647+
nullptr, cert_context, nullptr, cert_context->hCertStore,
10648+
&chain_para,
10649+
CERT_CHAIN_CACHE_END_CERT |
10650+
CERT_CHAIN_REVOCATION_CHECK_END_CERT |
10651+
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
10652+
nullptr, &chain_context);
10653+
10654+
CertFreeCertificateContext(cert_context);
10655+
10656+
if (!result || chain_context == nullptr) {
10657+
if (!result) {
10658+
last_wincrypt_error_ = GetLastError();
10659+
}
10660+
error = Error::SSLServerVerification;
10661+
return false;
10662+
}
10663+
10664+
// Capture detailed chain trust status before using the chain
10665+
last_wincrypt_chain_error_ = chain_context->TrustStatus.dwErrorStatus;
10666+
10667+
// Verify chain policy
10668+
auto extra_policy_para = SSL_EXTRA_CERT_CHAIN_POLICY_PARA{};
10669+
extra_policy_para.cbSize = sizeof(extra_policy_para);
10670+
extra_policy_para.dwAuthType = AUTHTYPE_SERVER;
10671+
auto whost = detail::u8string_to_wstring(host_.c_str());
10672+
if (server_hostname_verification_) {
10673+
extra_policy_para.pwszServerName =
10674+
const_cast<wchar_t *>(whost.c_str());
10675+
}
10676+
10677+
auto policy_para = CERT_CHAIN_POLICY_PARA{};
10678+
policy_para.cbSize = sizeof(policy_para);
10679+
policy_para.dwFlags =
10680+
CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;
10681+
policy_para.pvExtraPolicyPara = &extra_policy_para;
10682+
10683+
auto policy_status = CERT_CHAIN_POLICY_STATUS{};
10684+
policy_status.cbSize = sizeof(policy_status);
10685+
10686+
result = CertVerifyCertificateChainPolicy(
10687+
CERT_CHAIN_POLICY_SSL, chain_context, &policy_para,
10688+
&policy_status);
10689+
10690+
CertFreeCertificateChain(chain_context);
10691+
10692+
if (!result) {
10693+
last_wincrypt_error_ = GetLastError();
10694+
error = Error::SSLServerVerification;
10695+
return false;
10696+
}
10697+
10698+
if (policy_status.dwError != 0) {
10699+
// Store the specific Windows certificate error code
10700+
last_wincrypt_error_ = policy_status.dwError;
10701+
10702+
if (policy_status.dwError == CERT_E_CN_NO_MATCH) {
10703+
error = Error::SSLServerHostnameVerification;
10704+
} else {
10705+
error = Error::SSLServerVerification;
10706+
}
10707+
return false;
10708+
}
10709+
#endif // not _WIN32 || CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1055610710
}
1055710711
}
1055810712

0 commit comments

Comments
 (0)