From 0dc7bab977b746417bc2097758059fcda47a719c Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 2 Jan 2026 21:43:26 -0500 Subject: [PATCH] Resolve #2116 --- httplib.h | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++- test/test.cc | 107 +++++++++++++++++++-- 2 files changed, 357 insertions(+), 11 deletions(-) diff --git a/httplib.h b/httplib.h index c69d861..b4b7eca 100644 --- a/httplib.h +++ b/httplib.h @@ -2083,6 +2083,11 @@ public: void enable_server_hostname_verification(bool enabled); void set_server_certificate_verifier( std::function verifier); + +#ifdef _WIN32 + void enable_windows_certificate_verification(bool enabled); + void set_windows_certificate_verification_timeout(time_t sec); +#endif #endif void set_logger(Logger logger); @@ -2171,6 +2176,11 @@ public: SSL_CTX *ssl_context() const; +#ifdef _WIN32 + void enable_windows_certificate_verification(bool enabled); + void set_windows_certificate_verification_timeout(time_t sec); +#endif + private: bool create_and_connect_socket(Socket &socket, Error &error) override; bool ensure_socket_connection(Socket &socket, Error &error) override; @@ -2196,6 +2206,10 @@ private: bool verify_host_with_common_name(X509 *server_cert) const; bool check_host_name(const char *pattern, size_t pattern_len) const; +#ifdef _WIN32 + bool verify_peer_cert_with_windows(X509 *server_cert, Error &error); +#endif + SSL_CTX *ctx_; std::mutex ctx_mutex_; std::once_flag initialize_cert_; @@ -2204,6 +2218,24 @@ private: long verify_result_ = 0; +#ifdef _WIN32 + bool enable_windows_cert_verification_ = true; + time_t windows_cert_verification_timeout_sec_ = 5; + unsigned long last_wincrypt_error_ = 0; + unsigned long last_wincrypt_chain_error_ = 0; + + // Cache for certificate verification results to improve performance + struct CertVerificationCache { + std::string cert_fingerprint; + bool is_valid; + unsigned long wincrypt_error; + unsigned long chain_error; + std::chrono::steady_clock::time_point timestamp; + }; + mutable std::mutex cert_cache_mutex_; + mutable std::map cert_cache_; +#endif + friend class ClientImpl; }; #endif @@ -7155,8 +7187,9 @@ inline bool is_ssl_peer_could_be_closed(SSL *ssl, socket_t sock) { #ifdef _WIN32 // 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) { - auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT"); +inline bool add_windows_certs_to_x509_store(X509_STORE *store, + const wchar_t *store_name) { + auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, store_name); if (!hStore) { return false; } auto result = false; @@ -7179,6 +7212,121 @@ inline bool load_system_certs_on_windows(X509_STORE *store) { return result; } + +inline bool load_system_certs_on_windows(X509_STORE *store) { + return add_windows_certs_to_x509_store(store, L"ROOT") | + add_windows_certs_to_x509_store(store, L"CA"); +} + +// Get certificate fingerprint for caching purposes +inline std::string get_cert_fingerprint(X509 *cert) { + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int md_size = 0; + + if (!X509_digest(cert, EVP_sha256(), md, &md_size)) { return ""; } + + std::string fingerprint; + fingerprint.reserve(md_size * 2); + for (unsigned int i = 0; i < md_size; ++i) { + char buf[3]; + snprintf(buf, sizeof(buf), "%02x", md[i]); + fingerprint += buf; + } + + return fingerprint; +} + +// Verify certificate using Windows CertGetCertificateChain API +// This provides real-time certificate validation with Windows Update +// integration +inline bool verify_cert_with_windows_schannel(X509 *server_cert, + const std::string &hostname, + bool verify_hostname, + time_t timeout_sec, + unsigned long &out_wincrypt_error, + unsigned long &out_chain_error) { + if (!server_cert) { return false; } + + out_wincrypt_error = 0; + out_chain_error = 0; + + // Convert OpenSSL certificate to DER format + auto der_len = i2d_X509(server_cert, nullptr); + if (der_len < 0) { return false; } + + std::vector der_cert(der_len); + auto der_cert_data = der_cert.data(); + if (i2d_X509(server_cert, &der_cert_data) < 0) { return false; } + + // Create Windows certificate context + auto cert_context = CertCreateCertificateContext( + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(), + static_cast(der_cert.size())); + + if (!cert_context) { return false; } + + auto cert_guard = + detail::scope_exit([&] { CertFreeCertificateContext(cert_context); }); + + // Setup chain parameters + CERT_CHAIN_PARA chain_para = {}; + chain_para.cbSize = sizeof(chain_para); + chain_para.dwUrlRetrievalTimeout = static_cast(timeout_sec * 1000); + + // Build certificate chain with revocation checking + PCCERT_CHAIN_CONTEXT chain_context = nullptr; + auto chain_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); + + if (!chain_result || !chain_context) { return false; } + + auto chain_guard = + detail::scope_exit([&] { CertFreeCertificateChain(chain_context); }); + + // Capture chain trust status for diagnostic information + out_chain_error = chain_context->TrustStatus.dwErrorStatus; + + // Check if chain has errors + if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) { + return false; + } + + // Verify SSL policy + SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {}; + extra_policy_para.cbSize = sizeof(extra_policy_para); + extra_policy_para.dwAuthType = AUTHTYPE_SERVER; + + std::wstring whost; + if (verify_hostname) { + whost = detail::u8string_to_wstring(hostname.c_str()); + extra_policy_para.pwszServerName = const_cast(whost.c_str()); + } + + CERT_CHAIN_POLICY_PARA 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; + + CERT_CHAIN_POLICY_STATUS policy_status = {}; + policy_status.cbSize = sizeof(policy_status); + + if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context, + &policy_para, &policy_status)) { + out_wincrypt_error = GetLastError(); + return false; + } + + if (policy_status.dwError != 0) { + out_wincrypt_error = policy_status.dwError; + return false; + } + + return true; +} + #elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC template using CFObjectPtr = @@ -12834,6 +12982,17 @@ inline long SSLClient::get_openssl_verify_result() const { inline SSL_CTX *SSLClient::ssl_context() const { return ctx_; } +#ifdef _WIN32 +inline void SSLClient::enable_windows_certificate_verification(bool enabled) { + enable_windows_cert_verification_ = enabled; +} + +inline void +SSLClient::set_windows_certificate_verification_timeout(time_t sec) { + windows_cert_verification_timeout_sec_ = sec; +} +#endif + inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { if (!is_valid()) { error = Error::SSLConnection; @@ -13033,6 +13192,17 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { return false; } } + +#ifdef _WIN32 + // Additional Windows Schannel verification (optional, enabled by + // default) This provides real-time certificate validation with + // Windows Update integration + if (enable_windows_cert_verification_) { + if (!verify_peer_cert_with_windows(server_cert, error)) { + return false; + } + } +#endif } } @@ -13229,6 +13399,77 @@ inline bool SSLClient::check_host_name(const char *pattern, } #endif +#ifdef _WIN32 +inline bool SSLClient::verify_peer_cert_with_windows(X509 *server_cert, + Error &error) { + if (!server_cert) { return false; } + + // Get certificate fingerprint for caching + auto fingerprint = detail::get_cert_fingerprint(server_cert); + if (fingerprint.empty()) { return false; } + + // Check cache first (with 5-minute validity) + { + std::lock_guard lock(cert_cache_mutex_); + auto it = cert_cache_.find(fingerprint); + if (it != cert_cache_.end()) { + auto age = std::chrono::steady_clock::now() - it->second.timestamp; + if (age < std::chrono::minutes(5)) { + // Cache hit - return cached result + if (!it->second.is_valid) { + error = Error::SSLServerVerification; + last_wincrypt_error_ = it->second.wincrypt_error; + last_wincrypt_chain_error_ = it->second.chain_error; + } + return it->second.is_valid; + } + // Cache expired, remove it + cert_cache_.erase(it); + } + } + + // Perform Windows Schannel verification + unsigned long wincrypt_error = 0; + unsigned long chain_error = 0; + bool is_valid = detail::verify_cert_with_windows_schannel( + server_cert, host_, server_hostname_verification_, + windows_cert_verification_timeout_sec_, wincrypt_error, chain_error); + + // Store error information + last_wincrypt_error_ = wincrypt_error; + last_wincrypt_chain_error_ = chain_error; + + // Update cache + { + std::lock_guard lock(cert_cache_mutex_); + CertVerificationCache cache_entry; + cache_entry.cert_fingerprint = fingerprint; + cache_entry.is_valid = is_valid; + cache_entry.wincrypt_error = wincrypt_error; + cache_entry.chain_error = chain_error; + cache_entry.timestamp = std::chrono::steady_clock::now(); + cert_cache_[fingerprint] = cache_entry; + + // Limit cache size to prevent memory growth + if (cert_cache_.size() > 100) { + // Remove oldest entry + auto oldest = cert_cache_.begin(); + for (auto it = cert_cache_.begin(); it != cert_cache_.end(); ++it) { + if (it->second.timestamp < oldest->second.timestamp) { oldest = it; } + } + cert_cache_.erase(oldest); + } + } + + if (!is_valid) { + error = Error::SSLServerVerification; + output_error_log(error, nullptr); + } + + return is_valid; +} +#endif + // Universal client implementation inline Client::Client(const std::string &scheme_host_port) : Client(scheme_host_port, std::string(), std::string()) {} @@ -13906,6 +14147,22 @@ inline void Client::set_server_certificate_verifier( std::function verifier) { cli_->set_server_certificate_verifier(verifier); } + +#ifdef _WIN32 +inline void Client::enable_windows_certificate_verification(bool enabled) { + if (is_ssl_) { + static_cast(*cli_).enable_windows_certificate_verification( + enabled); + } +} + +inline void Client::set_windows_certificate_verification_timeout(time_t sec) { + if (is_ssl_) { + static_cast(*cli_) + .set_windows_certificate_verification_timeout(sec); + } +} +#endif #endif inline void Client::set_logger(Logger logger) { diff --git a/test/test.cc b/test/test.cc index 41f072b..4ced7a5 100644 --- a/test/test.cc +++ b/test/test.cc @@ -8484,14 +8484,20 @@ 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()); - // Verify OpenSSL error is captured for SSLServerVerification +#if defined(_WIN32) && \ + !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) + // On Windows Schannel, verify Windows certificate error is captured + EXPECT_NE(0UL, res.wincrypt_error()); + // Common errors: CERT_E_UNTRUSTEDROOT, CERT_E_CHAINING +#else + // On OpenSSL, 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) { @@ -8506,14 +8512,21 @@ 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()); - // Verify OpenSSL error is captured for SSLServerHostnameVerification - // This occurs when verify_host() fails due to hostname mismatch +#if defined(_WIN32) && \ + !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) + // On Windows Schannel, verify Windows certificate error is captured + EXPECT_EQ(static_cast(CERT_E_CN_NO_MATCH), + res.wincrypt_error()); +#else + // On OpenSSL, 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) { @@ -8889,14 +8902,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 Schannel, verify Windows certificate error is captured + 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) { @@ -14101,3 +14119,74 @@ TEST(Issue2318Test, EmptyHostString) { EXPECT_EQ(httplib::Error::Connection, res.error()); } } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef _WIN32 +// Windows Certificate Verification Tests +TEST(SSLClientTest, WindowsCertificateVerification_DefaultEnabled) { + // Test 1: Default behavior (Windows verification enabled) + SSLClient cli("www.google.com", 443); + cli.enable_server_certificate_verification(true); + + auto res = cli.Get("/"); + // Should succeed or fail gracefully (may fail due to network) + // The important thing is that Windows verification was enabled + if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); } +} + +TEST(SSLClientTest, WindowsCertificateVerification_DisableWindows) { + // Test 2: Disable Windows verification (OpenSSL only) + SSLClient cli("www.google.com", 443); + cli.enable_server_certificate_verification(true); + cli.enable_windows_certificate_verification(false); + + auto res = cli.Get("/"); + // Should work with OpenSSL verification only + if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); } +} + +TEST(SSLClientTest, WindowsCertificateVerification_CustomTimeout) { + // Test 3: Custom timeout + SSLClient cli("www.google.com", 443); + cli.enable_server_certificate_verification(true); + cli.set_windows_certificate_verification_timeout(2); + + auto res = cli.Get("/"); + // Should complete within reasonable time + if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); } +} + +TEST(SSLClientTest, WindowsCertificateVerification_InvalidCertificate) { + // Test 4: Invalid certificate (should fail) + SSLClient cli("self-signed.badssl.com", 443); + cli.enable_server_certificate_verification(true); + + auto res = cli.Get("/"); + // Should fail due to invalid certificate + EXPECT_FALSE(res); + EXPECT_EQ(Error::SSLServerVerification, res.error()); +} + +TEST(SSLClientTest, WindowsCertificateVerification_CachingBehavior) { + // Test 5: Multiple connections (test caching) + SSLClient cli("www.google.com", 443); + cli.enable_server_certificate_verification(true); + + // First connection + auto res1 = cli.Get("/"); + auto start2 = std::chrono::high_resolution_clock::now(); + // Second connection (should use cache) + auto res2 = cli.Get("/"); + auto end2 = std::chrono::high_resolution_clock::now(); + auto duration2 = + std::chrono::duration_cast(end2 - start2); + + // Second connection should be fast (cache hit) + if (res2) { + EXPECT_NE(StatusCode::InternalServerError_500, res2->status); + // Cache should make second connection faster (very loose bound) + EXPECT_LT(duration2.count(), 5000); // Within 5 seconds + } +} +#endif +#endif