From 6304fc6abba955ab7568cc77598cb4365e73937c Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 21 Oct 2026 17:11:52 -0400 Subject: [PATCH] Keep the system CAs out when a custom CA comes through empty Under the default system CA policy, a custom CA given to a client keeps the system CAs out. SSLClient and WebSocketClient only noted the custom CA when set_ca_cert_store() got a store or load_ca_cert_store() got some bytes, so a NULL store, such as the one tls::create_ca_store() returns for a PEM it cannot parse, or an empty PEM left the client trusting every system CA instead. Note the custom CA on every call, so that such a client fails verification. A redirect still passes a CA on only when the original client had one. --- httplib.h | 26 +++++++++++++++++--------- test/test.cc | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/httplib.h b/httplib.h index a776ea87..f16aa141 100644 --- a/httplib.h +++ b/httplib.h @@ -3527,9 +3527,13 @@ private: // 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; + // Set by set_ca_cert_store() and load_ca_cert_store() even when they bring + // no certificate, so that a CA that failed to come through fails + // verification instead of letting the system CAs in. + bool custom_ca_loaded_ = false; + std::function session_verifier_; // Like ca_cert_store_set_, keeps what ctx_ cannot report back: the verifier @@ -4793,6 +4797,9 @@ private: tls::session_t tls_session_ = nullptr; std::string ca_cert_file_path_; std::string ca_cert_dir_path_; + // Set by set_ca_cert_store() and load_ca_cert_store() even when they bring + // no certificate, so that a CA that failed to come through fails + // verification instead of letting the system CAs in. bool custom_ca_loaded_ = false; bool certs_loaded_ = false; SystemCAMode system_ca_mode_ = SystemCAMode::Auto; @@ -16357,7 +16364,7 @@ inline bool ClientImpl::create_redirect_client( } redirect_client.load_ca_cert_store(pem.data(), pem.size()); redirect_client.ca_cert_store_set_ = true; - } else { + } else if (self.custom_ca_loaded_) { redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(), ca_cert_pem_.size()); } @@ -19202,6 +19209,7 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) { } else if (ca_cert_store) { tls::free_ca_store(ca_cert_store); } + custom_ca_loaded_ = true; } inline void @@ -19234,6 +19242,7 @@ inline void SSLClient::load_ca_cert_store(const char *ca_cert, ca_cert_pem_.append(ca_cert, size); tls::load_ca_pem(ctx_, ca_cert, size); } + custom_ca_loaded_ = true; } inline bool SSLClient::load_certs() { @@ -19244,10 +19253,9 @@ inline bool SSLClient::load_certs() { std::call_once(initialize_cert_, [&]() { std::lock_guard guard(ctx_mutex_); - ret = detail::load_client_ca_config( - ctx_, ca_cert_file_path_, ca_cert_dir_path_, - !ca_cert_pem_.empty() || ca_cert_store_set_, system_ca_mode_, - last_backend_error_); + ret = detail::load_client_ca_config(ctx_, ca_cert_file_path_, + ca_cert_dir_path_, custom_ca_loaded_, + system_ca_mode_, last_backend_error_); }); return ret; @@ -19273,7 +19281,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { options.windows_cert_verification = enable_windows_cert_verification_ && system_ca_mode_ != SystemCAMode::Disabled && ca_cert_file_path_.empty() && - ca_cert_dir_path_.empty() && ca_cert_pem_.empty() && !ca_cert_store_set_; + ca_cert_dir_path_.empty() && !custom_ca_loaded_; // Only a verifier set through set_server_certificate_verifier() is seen // here, not one installed with tls::set_verify_callback() directly. options.server_certificate_verifier_set = @@ -23960,18 +23968,18 @@ inline void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) { if (store && tls_ctx_) { // set_ca_store takes ownership of store tls::set_ca_store(tls_ctx_, store); - custom_ca_loaded_ = true; } else if (store) { tls::free_ca_store(store); } + custom_ca_loaded_ = true; } inline void WebSocketClient::load_ca_cert_store(const char *ca_cert, std::size_t size) { if (tls_ctx_ && ca_cert && size > 0) { tls::load_ca_pem(tls_ctx_, ca_cert, size); - custom_ca_loaded_ = true; } + custom_ca_loaded_ = true; } inline void diff --git a/test/test.cc b/test/test.cc index a0208ae6..264e7a6c 100644 --- a/test/test.cc +++ b/test/test.cc @@ -70,6 +70,7 @@ namespace tls { // live below the split border; the definitions are linked from httplib.cc. ca_store_t create_ca_store(const char *pem, size_t len); bool set_verify_callback(ctx_t ctx, VerifyCallback callback); +std::vector get_ca_names(ctx_t ctx); } // namespace tls } // namespace httplib #endif @@ -18157,6 +18158,37 @@ TEST(SSLClientTest, IpHostUntrustedChainFails) { ASSERT_FALSE(res); } +#ifndef CPPHTTPLIB_WOLFSSL_SUPPORT +// A custom CA that brings no certificate still keeps the system CAs out, so a +// CA that failed to come through fails verification instead of trusting them. +// (wolfSSL does not list the system CAs it loaded.) +TEST(SSLClientTest, EmptyCustomCaKeepsSystemCasOut) { + SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + auto port = svr.bind_to_any_port("127.0.0.1"); + ASSERT_LT(0, port); + thread t = thread([&] { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + svr.wait_until_ready(); + + // The CAs a client trusts once its first request has loaded them + auto loaded_cas = [&](const std::function &setup) { + Client cli("https://127.0.0.1:" + std::to_string(port)); + setup(cli); + cli.Get("/"); + return tls::get_ca_names(cli.tls_context()).size(); + }; + + EXPECT_EQ(0u, loaded_cas([](Client &cli) { cli.load_ca_cert_store("", 0); })); + EXPECT_EQ(0u, + loaded_cas([](Client &cli) { cli.set_ca_cert_store(nullptr); })); +} +#endif + // enable_system_ca(false) prevents system CA loading entirely TEST(SSLClientTest, DisableSystemCa_Online) { SSLClient cli("google.com");