This commit is contained in:
yhirose
2026-01-02 21:43:26 -05:00
parent 8d03ef1615
commit 0dc7bab977
2 changed files with 357 additions and 11 deletions

261
httplib.h
View File

@@ -2083,6 +2083,11 @@ public:
void enable_server_hostname_verification(bool enabled);
void set_server_certificate_verifier(
std::function<SSLVerifierResponse(SSL *ssl)> 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<std::string, CertVerificationCache> 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<unsigned char> 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<DWORD>(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<DWORD>(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<wchar_t *>(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 <typename T>
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<std::mutex> 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<std::mutex> 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<SSLVerifierResponse(SSL *ssl)> verifier) {
cli_->set_server_certificate_verifier(verifier);
}
#ifdef _WIN32
inline void Client::enable_windows_certificate_verification(bool enabled) {
if (is_ssl_) {
static_cast<SSLClient &>(*cli_).enable_windows_certificate_verification(
enabled);
}
}
inline void Client::set_windows_certificate_verification_timeout(time_t sec) {
if (is_ssl_) {
static_cast<SSLClient &>(*cli_)
.set_windows_certificate_verification_timeout(sec);
}
}
#endif
#endif
inline void Client::set_logger(Logger logger) {

View File

@@ -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<unsigned long>(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<unsigned long>(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<unsigned long>(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<std::chrono::milliseconds>(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