mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-01-19 04:52:08 +00:00
Resolve #2116
This commit is contained in:
261
httplib.h
261
httplib.h
@@ -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) {
|
||||
|
||||
107
test/test.cc
107
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<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
|
||||
|
||||
Reference in New Issue
Block a user