diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a8ab0a..bfed9f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -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." @@ -296,6 +298,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} $<$:CPPHTTPLIB_OPENSSL_SUPPORT> $<$,$,$>:CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN> $<$:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO> + $<$,$>>:CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE> ) # CMake configuration files installation directory diff --git a/README.md b/README.md index edecbb6..581183b 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 ------ diff --git a/httplib.h b/httplib.h index ed30e55..995d2d0 100644 --- a/httplib.h +++ b/httplib.h @@ -221,6 +221,10 @@ using ssize_t = __int64; #endif // NOMINMAX #include +#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && \ + !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) +#define CERT_CHAIN_PARA_HAS_EXTRA_FIELDS +#endif #include #include @@ -1349,6 +1353,17 @@ public: : 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 &&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; } @@ -1369,6 +1384,14 @@ public: 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 @@ -1387,6 +1410,12 @@ private: #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 }; @@ -1706,6 +1735,12 @@ protected: #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: @@ -6310,6 +6345,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) { @@ -6336,6 +6372,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 using CFObjectPtr = @@ -8954,8 +8991,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 @@ -9541,8 +9589,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 @@ -11348,8 +11407,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 @@ -11396,6 +11457,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) verify_result_ = SSL_get_verify_result(ssl2); if (verify_result_ != X509_V_OK) { @@ -11404,6 +11467,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { output_error_log(error, nullptr); return false; } +#endif // !_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); }); @@ -11415,6 +11480,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; @@ -11423,6 +11490,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(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(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(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 // !_WIN32 || + // CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE } } diff --git a/test/test.cc b/test/test.cc index 72e0436..35e3a48 100644 --- a/test/test.cc +++ b/test/test.cc @@ -8394,14 +8394,19 @@ TEST(SSLClientTest, ServerCertificateVerificationError_Online) { ASSERT_TRUE(!res); EXPECT_EQ(Error::SSLServerVerification, res.error()); - // For SSL server verification errors, ssl_error should be 0, only - // ssl_openssl_error should be set + // For SSL server verification errors, ssl_error should be 0 EXPECT_EQ(0, res.ssl_error()); +#if defined(_WIN32) && \ + !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) + // On Windows with Schannel, check wincrypt_error instead of ssl_openssl_error + EXPECT_NE(0UL, res.wincrypt_error()); +#else // Verify OpenSSL error is captured for SSLServerVerification // This occurs when SSL_get_verify_result() returns a verification failure EXPECT_EQ(static_cast(X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT), res.ssl_openssl_error()); +#endif } TEST(SSLClientTest, ServerHostnameVerificationError_Online) { @@ -8416,14 +8421,19 @@ TEST(SSLClientTest, ServerHostnameVerificationError_Online) { EXPECT_EQ(Error::SSLServerHostnameVerification, res.error()); - // For SSL hostname verification errors, ssl_error should be 0, only - // ssl_openssl_error should be set + // For SSL hostname verification errors, ssl_error should be 0 EXPECT_EQ(0, res.ssl_error()); +#if defined(_WIN32) && \ + !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) + // On Windows with Schannel, check wincrypt_error instead of ssl_openssl_error + EXPECT_NE(0UL, res.wincrypt_error()); +#else // Verify OpenSSL error is captured for SSLServerHostnameVerification // This occurs when verify_host() fails due to hostname mismatch EXPECT_EQ(static_cast(X509_V_ERR_HOSTNAME_MISMATCH), res.ssl_openssl_error()); +#endif } TEST(SSLClientTest, ServerCertificateVerification1_Online) { @@ -8799,14 +8809,19 @@ TEST(SSLClientServerTest, ClientCertMissing) { ASSERT_TRUE(!res); EXPECT_EQ(Error::SSLServerVerification, res.error()); - // For SSL server verification errors, ssl_error should be 0, only - // ssl_openssl_error should be set + // For SSL server verification errors, ssl_error should be 0 EXPECT_EQ(0, res.ssl_error()); +#if defined(_WIN32) && \ + !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) + // On Windows with Schannel, check wincrypt_error instead of ssl_openssl_error + EXPECT_NE(0UL, res.wincrypt_error()); +#else // Verify OpenSSL error is captured for SSLServerVerification // Note: This test may have different error codes depending on the exact // verification failure EXPECT_NE(0UL, res.ssl_openssl_error()); +#endif } TEST(SSLClientServerTest, TrustDirOptional) {