From d78a3cbb82eb7c08ed8505dafbca4ef5bee9adac Mon Sep 17 00:00:00 2001 From: metsw24-max Date: Fri, 16 Oct 2026 22:08:09 +0530 Subject: [PATCH] Carry the CA store and session verifier over to the redirect client (#2632) --- httplib.h | 57 ++++++++++++++++++++++++------ test/test.cc | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 11 deletions(-) diff --git a/httplib.h b/httplib.h index 1dca2c8f..2b365e16 100644 --- a/httplib.h +++ b/httplib.h @@ -10998,6 +10998,28 @@ inline bool load_client_ca_config(tls::ctx_t ctx, return ret; } +// Serializes the CA certificates installed in ctx as PEM, so they can be +// loaded into another context. +inline std::string ca_certs_to_pem(tls::ctx_t ctx) { + std::string pem; + std::vector certs; + tls::get_ca_certs(ctx, certs); + for (auto cert : certs) { + std::vector der; + if (tls::get_cert_der(cert, der)) { + auto b64 = base64_encode(std::string(der.begin(), der.end())); + pem += "-----BEGIN CERTIFICATE-----\n"; + for (size_t i = 0; i < b64.size(); i += 64) { + pem += b64.substr(i, 64); + pem += '\n'; + } + pem += "-----END CERTIFICATE-----\n"; + } + tls::free_cert(cert); + } + return pem; +} + // The parts of session setup that only SSLClient needs, plus the handful // WebSocketClient also exposes; everything else takes the defaults, which is // what keeps the two clients on one implementation. @@ -15903,6 +15925,27 @@ inline bool ClientImpl::create_redirect_client( redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); } + if (is_ssl()) { + auto &self = static_cast(*this); + + // A store handed to set_ca_cert_store() is owned by ctx_ and leaves no + // PEM behind. Without it the redirect client would fall back to the + // system CAs and trust roots this client was configured to exclude. + if (self.ca_cert_store_set_) { + std::string pem; + { + std::lock_guard guard(self.ctx_mutex_); + pem = detail::ca_certs_to_pem(self.ctx_); + } + if (!pem.empty()) { + redirect_client.load_ca_cert_store(pem.data(), pem.size()); + } + redirect_client.ca_cert_store_set_ = true; + } + + redirect_client.session_verifier_ = self.session_verifier_; + } + // Client certificates are set through constructor for SSLClient // NOTE: SSLClient constructor already takes client_cert_path and // client_key_path so we need to create it properly if client certs are @@ -21426,19 +21469,11 @@ inline size_t get_ca_certs(ctx_t ctx, std::vector &certs) { if (!ctx) { return 0; } auto *mbed_ctx = static_cast(ctx); - // Iterate through the CA chain + // Iterate through the CA chain. The certificates stay owned by the context, + // as free_cert() does not release anything on this backend. mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; while (cert != nullptr && cert->raw.len > 0) { - // Create a copy of the certificate for the caller - auto *copy = new mbedtls_x509_crt; - mbedtls_x509_crt_init(copy); - int ret = mbedtls_x509_crt_parse_der(copy, cert->raw.p, cert->raw.len); - if (ret == 0) { - certs.push_back(static_cast(copy)); - } else { - mbedtls_x509_crt_free(copy); - delete copy; - } + certs.push_back(static_cast(cert)); cert = cert->next; } return certs.size(); diff --git a/test/test.cc b/test/test.cc index d6efb505..b54771e9 100644 --- a/test/test.cc +++ b/test/test.cc @@ -15919,6 +15919,103 @@ TEST(SSLClientRedirectTest, CertFile) { ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_EQ(StatusCode::OK_200, res->status); } + +// A store set via set_ca_cert_store() must be carried over to the client that +// follows a cross-host redirect, rather than replaced by the system CAs. +TEST(SSLClientRedirectTest, SetCaCertStore) { + 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(); + + 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_follow_location(true); + cli.set_connection_timeout(30); + + auto res = cli.Get("/index"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + ASSERT_EQ(StatusCode::OK_200, res->status); +} + +// The session verifier must also run for the host a redirect leads to. +TEST(SSLClientRedirectTest, SessionVerifier) { + 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(); + + SSLClient cli("127.0.0.1", 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); + + // Accept the first host only. + std::atomic sessions{0}; + cli.set_session_verifier([&](tls::session_t) -> SSLVerifierResponse { + return sessions++ == 0 ? SSLVerifierResponse::CertificateAccepted + : SSLVerifierResponse::CertificateRejected; + }); + + auto res = cli.Get("/index"); + ASSERT_FALSE(res); + EXPECT_EQ(Error::SSLServerVerification, res.error()); + EXPECT_EQ(2, sessions.load()); +} #endif #ifdef CPPHTTPLIB_SSL_ENABLED