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:
yhirose committed 2026-06-09 21:03:19 -04:00
1 parent 78ff94e604
commit e7e7bf7b44
2 files changed
+116 -3

No files matched your search

+103
View File
@@ -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);