Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* HTTPLIB_REQUIRE_ZSTD (default off)
* HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN (default on)
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
* HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE (default on)
* HTTPLIB_COMPILE (default off)
* HTTPLIB_INSTALL (default on)
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
Expand Down Expand Up @@ -110,6 +111,7 @@ option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system cer
option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON)
option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF)
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
option(HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE "Enable automatic root certificates update on Windows." ON)
# Defaults to static library but respects standard BUILD_SHARED_LIBS if set
include(CMakeDependentOption)
cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only."
Expand Down Expand Up @@ -296,6 +298,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:CPPHTTPLIB_OPENSSL_SUPPORT>
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN}>>:CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN>
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
$<$<AND:$<PLATFORM_ID:Windows>,$<NOT:$<BOOL:${HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE}>>>:CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE>
)

# CMake configuration files installation directory
Expand Down
77 changes: 73 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,26 @@ if (!res) {
break;

case httplib::Error::SSLServerVerification:
std::cout << "SSL verification failed, X509 error: "
<< res.ssl_openssl_error() << std::endl;
std::cout << "SSL verification failed" << std::endl;
#if defined(_WIN32) && !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
// On Windows with Schannel, check Windows certificate errors
std::cout << " Windows error: 0x" << std::hex << res.wincrypt_error()
<< ", chain status: 0x" << res.wincrypt_chain_error() << std::endl;
#else
// On other platforms, check OpenSSL errors
std::cout << " X509 error: " << res.ssl_openssl_error() << std::endl;
#endif
break;

case httplib::Error::SSLServerHostnameVerification:
std::cout << "SSL hostname verification failed, X509 error: "
<< res.ssl_openssl_error() << std::endl;
std::cout << "SSL hostname verification failed" << std::endl;
#if defined(_WIN32) && !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
// On Windows with Schannel, check Windows certificate errors
std::cout << " Windows error: 0x" << std::hex << res.wincrypt_error() << std::endl;
#else
// On other platforms, check OpenSSL errors
std::cout << " X509 error: " << res.ssl_openssl_error() << std::endl;
#endif
break;

default:
Expand All @@ -128,6 +141,62 @@ if (!res) {
}
```

For a simpler platform-agnostic approach, you can check which error field is non-zero:

```c++
auto res = cli.Get("/");
if (!res) {
if (res.error() == httplib::Error::SSLServerVerification) {
std::cout << "Certificate verification failed!" << std::endl;

// Check which backend reported the error
if (res.ssl_openssl_error() != 0) {
// OpenSSL reported the error (Linux, macOS, or Windows with Schannel disabled)
std::cout << "OpenSSL error: " << res.ssl_openssl_error() << std::endl;
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#if defined(_WIN32) && !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
else if (res.wincrypt_error() != 0) {
// Windows Schannel reported the error
std::cout << "Windows error: 0x" << std::hex << res.wincrypt_error() << std::endl;
std::cout << "Chain status: 0x" << std::hex << res.wincrypt_chain_error() << std::endl;
}
#endif
#endif
}
}
```

### Windows Certificate Verification

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.

**When certificate verification fails on Windows:**
- OpenSSL still handles the TLS handshake
- Windows Schannel performs certificate verification
- Check `wincrypt_error()` and `wincrypt_chain_error()` for diagnostic information
- The `ssl_openssl_error()` field will be 0 for certificate verification errors

**Windows-specific error codes** (`wincrypt_error()`) include:
- `CERT_E_EXPIRED` (0x800B0101) - Certificate has expired
- `CERT_E_UNTRUSTEDROOT` (0x800B0109) - Certificate chain to untrusted root
- `CERT_E_CN_NO_MATCH` (0x800B010F) - Certificate CN doesn't match hostname
- `CERT_E_REVOKED` (0x800B010C) - Certificate has been revoked
- `CERT_E_CHAINING` (0x800B010A) - Error building certificate chain

**Chain trust status** (`wincrypt_chain_error()`) provides additional details:
- `CERT_TRUST_IS_NOT_TIME_VALID` (0x00000001) - Certificate expired
- `CERT_TRUST_IS_REVOKED` (0x00000004) - Certificate revoked
- `CERT_TRUST_IS_NOT_SIGNATURE_VALID` (0x00000008) - Invalid signature
- `CERT_TRUST_IS_UNTRUSTED_ROOT` (0x00000020) - Untrusted root

To disable Windows automatic certificate updates and use OpenSSL verification:
```cmake
set(HTTPLIB_USE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE OFF)
```

Or define `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` before including httplib.h.

Server
------

Expand Down
166 changes: 164 additions & 2 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ using ssize_t = __int64;
#endif // NOMINMAX

#include <io.h>
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
#define CERT_CHAIN_PARA_HAS_EXTRA_FIELDS
#endif
#include <winsock2.h>
#include <ws2tcpip.h>

Expand Down Expand Up @@ -1348,6 +1352,17 @@ class Result {
: res_(std::move(res)), err_(err),
request_headers_(std::move(request_headers)), ssl_error_(ssl_error),
ssl_openssl_error_(ssl_openssl_error) {}

#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,
int ssl_error, unsigned long ssl_openssl_error,
unsigned long wincrypt_error, unsigned long wincrypt_chain_error)
: res_(std::move(res)), err_(err),
request_headers_(std::move(request_headers)), ssl_error_(ssl_error),
ssl_openssl_error_(ssl_openssl_error), wincrypt_error_(wincrypt_error),
wincrypt_chain_error_(wincrypt_chain_error) {}
#endif
#endif
// Response
operator bool() const { return res_ != nullptr; }
Expand All @@ -1368,6 +1383,14 @@ class Result {
int ssl_error() const { return ssl_error_; }
// OpenSSL Error
unsigned long ssl_openssl_error() const { return ssl_openssl_error_; }

#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
// Windows Certificate Error (from GetLastError or policy_status.dwError)
unsigned long wincrypt_error() const { return wincrypt_error_; }
// Windows Certificate Chain Trust Status (from TrustStatus.dwErrorStatus)
unsigned long wincrypt_chain_error() const { return wincrypt_chain_error_; }
#endif
#endif

// Request Headers
Expand All @@ -1386,6 +1409,12 @@ class Result {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
int ssl_error_ = 0;
unsigned long ssl_openssl_error_ = 0;

#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
unsigned long wincrypt_error_ = 0;
unsigned long wincrypt_chain_error_ = 0;
#endif
#endif
};

Expand Down Expand Up @@ -1705,6 +1734,12 @@ class ClientImpl {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
int last_ssl_error_ = 0;
unsigned long last_openssl_error_ = 0;

#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
unsigned long last_wincrypt_error_ = 0;
unsigned long last_wincrypt_chain_error_ = 0;
#endif
#endif

private:
Expand Down Expand Up @@ -6309,6 +6344,7 @@ inline bool is_ssl_peer_could_be_closed(SSL *ssl, socket_t sock) {
}

#ifdef _WIN32
#ifdef CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
// NOTE: This code came up with the following stackoverflow post:
// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store
inline bool load_system_certs_on_windows(X509_STORE *store) {
Expand All @@ -6335,6 +6371,7 @@ inline bool load_system_certs_on_windows(X509_STORE *store) {

return result;
}
#endif // CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC
template <typename T>
using CFObjectPtr =
Expand Down Expand Up @@ -8949,8 +8986,19 @@ inline Result ClientImpl::send_(Request &&req) {
auto error = Error::Success;
auto ret = send(req, *res, error);
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
return Result{ret ? std::move(res) : nullptr,
error,
std::move(req.headers),
last_ssl_error_,
last_openssl_error_,
last_wincrypt_error_,
last_wincrypt_chain_error_};
#else
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers),
last_ssl_error_, last_openssl_error_};
#endif
#else
return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)};
#endif
Expand Down Expand Up @@ -9536,8 +9584,19 @@ inline Result ClientImpl::send_with_content_provider_and_receiver(
std::move(content_receiver), error);

#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
return Result{std::move(res),
error,
std::move(req.headers),
last_ssl_error_,
last_openssl_error_,
last_wincrypt_error_,
last_wincrypt_chain_error_};
#else
return Result{std::move(res), error, std::move(req.headers), last_ssl_error_,
last_openssl_error_};
#endif
#else
return Result{std::move(res), error, std::move(req.headers)};
#endif
Expand Down Expand Up @@ -11343,8 +11402,10 @@ inline bool SSLClient::load_certs() {
} else {
auto loaded = false;
#ifdef _WIN32
#ifdef CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
loaded =
detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
#endif // CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC
loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_));
#endif // _WIN32
Expand Down Expand Up @@ -11391,6 +11452,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
}

if (verification_status == SSLVerifierResponse::NoDecisionMade) {
#if !defined(_WIN32) || \
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@solarispika why does this check need to be skipped when CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE is defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check and check from Windows are both checking certificate, so it is a either-or behavior.

So, if Windows automatic root certificate updates is disabled, then using old behavior; otherwise, use new one.

Am I missing something?

verify_result_ = SSL_get_verify_result(ssl2);

if (verify_result_ != X509_V_OK) {
Expand All @@ -11399,6 +11462,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
output_error_log(error, nullptr);
return false;
}
#endif // not _WIN32 ||
// CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE

auto server_cert = SSL_get1_peer_certificate(ssl2);
auto se = detail::scope_exit([&] { X509_free(server_cert); });
Expand All @@ -11410,6 +11475,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
return false;
}

#if !defined(_WIN32) || \
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
if (server_hostname_verification_) {
if (!verify_host(server_cert)) {
last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH;
Expand All @@ -11418,6 +11485,101 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
return false;
}
}
#else // _WIN32 && !
// CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
// Windows Schannel verification path - clear OpenSSL errors since
// we're not using OpenSSL for certificate verification
last_openssl_error_ = 0;

// Convert OpenSSL certificate to DER format
auto der_cert =
std::vector<unsigned char>(i2d_X509(server_cert, nullptr));
auto der_cert_data = der_cert.data();
if (i2d_X509(server_cert, &der_cert_data) < 0) {
error = Error::SSLServerVerification;
return false;
}

// Create a certificate context from the DER-encoded certificate
auto cert_context = CertCreateCertificateContext(
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(),
static_cast<DWORD>(der_cert.size()));

if (cert_context == nullptr) {
last_wincrypt_error_ = GetLastError();
error = Error::SSLServerVerification;
return false;
}

auto chain_para = CERT_CHAIN_PARA{};
chain_para.cbSize = sizeof(chain_para);
chain_para.dwUrlRetrievalTimeout = 10 * 1000;

auto chain_context = PCCERT_CHAIN_CONTEXT{};
auto result = CertGetCertificateChain(
nullptr, cert_context, nullptr, cert_context->hCertStore,
&chain_para,
CERT_CHAIN_CACHE_END_CERT |
CERT_CHAIN_REVOCATION_CHECK_END_CERT |
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
nullptr, &chain_context);

CertFreeCertificateContext(cert_context);

if (!result || chain_context == nullptr) {
if (!result) { last_wincrypt_error_ = GetLastError(); }
error = Error::SSLServerVerification;
return false;
}

// Capture detailed chain trust status before using the chain
last_wincrypt_chain_error_ =
chain_context->TrustStatus.dwErrorStatus;

// Verify chain policy
auto extra_policy_para = SSL_EXTRA_CERT_CHAIN_POLICY_PARA{};
extra_policy_para.cbSize = sizeof(extra_policy_para);
extra_policy_para.dwAuthType = AUTHTYPE_SERVER;
auto whost = detail::u8string_to_wstring(host_.c_str());
if (server_hostname_verification_) {
extra_policy_para.pwszServerName =
const_cast<wchar_t *>(whost.c_str());
}

auto policy_para = CERT_CHAIN_POLICY_PARA{};
policy_para.cbSize = sizeof(policy_para);
policy_para.dwFlags =
CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;
policy_para.pvExtraPolicyPara = &extra_policy_para;

auto policy_status = CERT_CHAIN_POLICY_STATUS{};
policy_status.cbSize = sizeof(policy_status);

result = CertVerifyCertificateChainPolicy(
CERT_CHAIN_POLICY_SSL, chain_context, &policy_para,
&policy_status);

CertFreeCertificateChain(chain_context);

if (!result) {
last_wincrypt_error_ = GetLastError();
error = Error::SSLServerVerification;
return false;
}

if (policy_status.dwError != 0) {
// Store the specific Windows certificate error code
last_wincrypt_error_ = policy_status.dwError;

if (policy_status.dwError == CERT_E_CN_NO_MATCH) {
error = Error::SSLServerHostnameVerification;
} else {
error = Error::SSLServerVerification;
}
return false;
}
#endif // not _WIN32 ||
// CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
}
}

Expand Down Expand Up @@ -12241,8 +12403,8 @@ inline void Client::set_follow_location(bool on) {

inline void Client::set_path_encode(bool on) { cli_->set_path_encode(on); }

[[deprecated("Use set_path_encode instead")]]
inline void Client::set_url_encode(bool on) {
[[deprecated("Use set_path_encode instead")]] inline void
Client::set_url_encode(bool on) {
cli_->set_path_encode(on);
}

Expand Down
Loading
Loading