mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-09-30 20:52:31 +07:00
Fix set_ca_cert_store() breaking CA exclusivity and redirect CA transfer
Since the TLS abstraction layer was introduced, SSLClient::set_ca_cert_store() handed the store to the TLS context without leaving any trace on the client. As a result: - load_certs() merged system CA certs into the user-provided store, silently broadening the trust set (a custom store used to suppress system CA loading) - Client::load_ca_cert_store() went through the native store path, bypassing the PEM retention used for redirect transfer, so CA certs were not carried over to clients created for HTTPS redirects - The Windows Schannel verification skip for custom CA certs did not trigger Track custom store assignment with a flag checked by load_certs() and the Schannel path, and route Client::load_ca_cert_store() through the PEM-based SSLClient path so the CA data survives redirects.
This commit is contained in:
@@ -2804,6 +2804,11 @@ private:
|
||||
std::mutex ctx_mutex_;
|
||||
std::once_flag initialize_cert_;
|
||||
|
||||
// Tracks whether a custom CA store was applied via set_ca_cert_store(),
|
||||
// since the store handle itself is owned by ctx_ and leaves no other trace.
|
||||
// Used to keep custom CA configuration exclusive with system CA loading.
|
||||
bool ca_cert_store_set_ = false;
|
||||
|
||||
long verify_result_ = 0;
|
||||
|
||||
std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
|
||||
@@ -16092,6 +16097,7 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
|
||||
if (ca_cert_store && ctx_) {
|
||||
// set_ca_store takes ownership of ca_cert_store
|
||||
tls::set_ca_store(ctx_, ca_cert_store);
|
||||
ca_cert_store_set_ = true;
|
||||
} else if (ca_cert_store) {
|
||||
tls::free_ca_store(ca_cert_store);
|
||||
}
|
||||
@@ -16138,7 +16144,7 @@ inline bool SSLClient::load_certs() {
|
||||
last_backend_error_ = tls::get_error();
|
||||
ret = false;
|
||||
}
|
||||
} else if (ca_cert_pem_.empty()) {
|
||||
} else if (ca_cert_pem_.empty() && !ca_cert_store_set_) {
|
||||
if (!tls::load_system_certs(ctx_)) {
|
||||
last_backend_error_ = tls::get_error();
|
||||
}
|
||||
@@ -16273,7 +16279,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
|
||||
// Skip when a custom CA cert is specified, as the Windows certificate
|
||||
// store would not know about user-provided CA certificates.
|
||||
if (enable_windows_cert_verification_ && ca_cert_file_path_.empty() &&
|
||||
ca_cert_dir_path_.empty() && ca_cert_pem_.empty()) {
|
||||
ca_cert_dir_path_.empty() && ca_cert_pem_.empty() &&
|
||||
!ca_cert_store_set_) {
|
||||
std::vector<unsigned char> der;
|
||||
if (get_cert_der(server_cert, der)) {
|
||||
uint64_t wincrypt_error = 0;
|
||||
@@ -16335,7 +16342,10 @@ inline void Client::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
|
||||
}
|
||||
|
||||
inline void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) {
|
||||
set_ca_cert_store(tls::create_ca_store(ca_cert, size));
|
||||
if (is_ssl_) {
|
||||
// Use the PEM-based path so the CA data is retained for redirect transfer
|
||||
static_cast<SSLClient &>(*cli_).load_ca_cert_store(ca_cert, size);
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
|
||||
+103
@@ -51,6 +51,16 @@ inline std::string u8_to_string(const char8_t *s) {
|
||||
#define SERVER_ENCRYPTED_PRIVATE_KEY_FILE "./key_encrypted.pem"
|
||||
#define SERVER_ENCRYPTED_PRIVATE_KEY_PASS "test123!"
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
namespace httplib {
|
||||
namespace tls {
|
||||
// Declared here for the split build, where the TLS abstraction declarations
|
||||
// live below the split border; the definition is linked from httplib.cc.
|
||||
ca_store_t create_ca_store(const char *pem, size_t len);
|
||||
} // namespace tls
|
||||
} // namespace httplib
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
using namespace httplib;
|
||||
|
||||
@@ -11625,6 +11635,99 @@ TEST(SSLClientTest, SetCaCertStoreSkipsSystemCerts_Online) {
|
||||
EXPECT_EQ(Error::SSLServerVerification, res.error());
|
||||
}
|
||||
|
||||
// Same as above, but through the native store handle path. Regression test:
|
||||
// set_ca_cert_store() used to leave no trace on the client, so load_certs()
|
||||
// merged system certs into the user-provided store.
|
||||
TEST(SSLClientTest, SetCaCertStoreNativeSkipsSystemCerts_Online) {
|
||||
std::string cert;
|
||||
read_file(SERVER_CERT2_FILE, cert);
|
||||
|
||||
SSLClient cli("google.com");
|
||||
cli.set_ca_cert_store(tls::create_ca_store(cert.c_str(), cert.size()));
|
||||
cli.enable_server_certificate_verification(true);
|
||||
|
||||
auto res = cli.Get("/");
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(Error::SSLServerVerification, res.error());
|
||||
}
|
||||
|
||||
// A custom store set via the native handle must still verify a server whose
|
||||
// cert it does contain.
|
||||
TEST(SSLClientTest, SetCaCertStoreNativeVerifiesLocalServer) {
|
||||
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
svr.Get("/test", [&](const Request &, Response &res) {
|
||||
res.set_content("test", "text/plain");
|
||||
});
|
||||
|
||||
thread t = thread([&]() { ASSERT_TRUE(svr.listen("127.0.0.1", PORT)); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
SSLClient cli("127.0.0.1", PORT);
|
||||
std::string cert;
|
||||
read_file(SERVER_CERT2_FILE, cert);
|
||||
cli.set_ca_cert_store(tls::create_ca_store(cert.c_str(), cert.size()));
|
||||
cli.enable_server_certificate_verification(true);
|
||||
cli.set_connection_timeout(30);
|
||||
|
||||
auto res = cli.Get("/test");
|
||||
ASSERT_TRUE(res);
|
||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
// CA certs loaded through the universal Client must be transferred to the
|
||||
// client created internally for following an HTTPS redirect. Regression test:
|
||||
// Client::load_ca_cert_store() used to bypass the PEM-based path that stores
|
||||
// the CA data for redirect transfer.
|
||||
TEST(UniversalClientRedirectTest, LoadCaCertStore) {
|
||||
auto ssl_port = PORT + 1;
|
||||
|
||||
SSLServer ssl_svr1(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(ssl_svr1.is_valid());
|
||||
ssl_svr1.Get("/index", [&](const Request &, Response &res) {
|
||||
res.set_redirect("https://127.0.0.1:" + std::to_string(ssl_port) +
|
||||
"/index");
|
||||
});
|
||||
|
||||
SSLServer ssl_svr2(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(ssl_svr2.is_valid());
|
||||
ssl_svr2.Get("/index", [&](const Request &, Response &res) {
|
||||
res.set_content("test", "text/plain");
|
||||
});
|
||||
|
||||
thread t = thread([&]() { ASSERT_TRUE(ssl_svr1.listen("127.0.0.1", PORT)); });
|
||||
thread t2 =
|
||||
thread([&]() { ASSERT_TRUE(ssl_svr2.listen("127.0.0.1", ssl_port)); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
ssl_svr2.stop();
|
||||
ssl_svr1.stop();
|
||||
t2.join();
|
||||
t.join();
|
||||
ASSERT_FALSE(ssl_svr1.is_running());
|
||||
});
|
||||
|
||||
ssl_svr1.wait_until_ready();
|
||||
ssl_svr2.wait_until_ready();
|
||||
|
||||
Client cli("https://127.0.0.1:" + std::to_string(PORT));
|
||||
std::string cert;
|
||||
read_file(SERVER_CERT2_FILE, cert);
|
||||
cli.load_ca_cert_store(cert.c_str(), cert.size());
|
||||
cli.enable_server_certificate_verification(true);
|
||||
cli.set_follow_location(true);
|
||||
cli.set_connection_timeout(30);
|
||||
|
||||
auto res = cli.Get("/index");
|
||||
ASSERT_TRUE(res);
|
||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(MultipartFormDataTest, LargeData) {
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user