From d78a3cbb82eb7c08ed8505dafbca4ef5bee9adac Mon Sep 17 00:00:00 2001 From: metsw24-max Date: Fri, 16 Oct 2026 22:08:09 +0530 Subject: [PATCH 01/16] 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 From e225d1876b9ad0dd560ed1104ae8d3ce6c19b6c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:25:43 +0000 Subject: [PATCH 02/16] Store the TLS verify callback per context instead of thread_local tls::set_verify_callback() kept the callback in a thread_local slot, so two SSLClients configured on one thread overwrote each other's verifier, and a client used from another thread ran none. Keep it with the context instead (OpenSSL SSL_CTX ex_data, MbedTlsContext/WolfSSLContext member) and have each backend trampoline look it up from the session's context. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wxevjmRqzpy8qDAD3vdwr --- httplib.h | 108 ++++++++++++++++++++++++++++++++------------------- test/test.cc | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 39 deletions(-) diff --git a/httplib.h b/httplib.h index 2b365e16..dcc43e3a 100644 --- a/httplib.h +++ b/httplib.h @@ -4013,6 +4013,9 @@ struct MbedTlsContext { bool is_server = false; bool verify_client = false; bool has_verify_callback = false; + // Installed by tls::set_verify_callback(); run by the sessions created from + // this context. + VerifyCallback verify_callback; MbedTlsContext(); ~MbedTlsContext(); @@ -4037,6 +4040,10 @@ struct WolfSSLContext { bool is_server = false; bool verify_client = false; bool has_verify_callback = false; + // Installed by tls::set_verify_callback(); run by the sessions created from + // this context (each one points its verify-callback ctx back at this + // struct in create_session()). + VerifyCallback verify_callback; std::string ca_pem_data_; // accumulated PEM for get_ca_names/get_ca_certs WolfSSLContext(); @@ -18933,16 +18940,6 @@ inline PeerCert get_peer_cert_from_session(const_session_t session) { namespace impl { -inline VerifyCallback &get_verify_callback() { - static thread_local VerifyCallback callback; - return callback; -} - -inline VerifyCallback &get_mbedtls_verify_callback() { - static thread_local VerifyCallback callback; - return callback; -} - // Check if a string is an IPv4 address inline bool is_ipv4_address(const std::string &str) { int dots = 0; @@ -19268,16 +19265,29 @@ inline STACK_OF(X509_NAME) * return ca_list; } +// SSL_CTX ex_data slot holding the heap-allocated VerifyCallback installed by +// set_verify_callback(); freed by set_verify_callback() and free_context(). +inline int openssl_verify_callback_index() { + static const int index = + SSL_CTX_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr); + return index; +} + +inline VerifyCallback *get_openssl_verify_callback(const SSL_CTX *ssl_ctx) { + return static_cast( + SSL_CTX_get_ex_data(ssl_ctx, openssl_verify_callback_index())); +} + // OpenSSL verify callback wrapper inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { - auto &callback = get_verify_callback(); - if (!callback) { return preverify_ok; } - // Get SSL object from X509_STORE_CTX auto ssl = static_cast( X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); if (!ssl) { return preverify_ok; } + auto callback = get_openssl_verify_callback(SSL_get_SSL_CTX(ssl)); + if (!callback || !*callback) { return preverify_ok; } + // Get current certificate and depth auto cert = X509_STORE_CTX_get_current_cert(ctx); int depth = X509_STORE_CTX_get_error_depth(ctx); @@ -19293,7 +19303,7 @@ inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { verify_ctx.error_string = (error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr; - return callback(verify_ctx) ? 1 : 0; + return (*callback)(verify_ctx) ? 1 : 0; } // X509_STORE_get0_objects is deprecated since OpenSSL 4.0 because it is not @@ -19334,7 +19344,10 @@ inline ctx_t create_client_context() { } inline void free_context(ctx_t ctx) { - if (ctx) { SSL_CTX_free(static_cast(ctx)); } + if (!ctx) { return; } + auto ssl_ctx = static_cast(ctx); + delete impl::get_openssl_verify_callback(ssl_ctx); + SSL_CTX_free(ssl_ctx); } inline bool set_min_version(ctx_t ctx, Version version) { @@ -20165,9 +20178,20 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto ssl_ctx = static_cast(ctx); - impl::get_verify_callback() = std::move(callback); + VerifyCallback *stored = nullptr; + if (callback) { + stored = new (std::nothrow) VerifyCallback(std::move(callback)); + if (!stored) { return false; } + } + auto old = impl::get_openssl_verify_callback(ssl_ctx); + if (SSL_CTX_set_ex_data(ssl_ctx, impl::openssl_verify_callback_index(), + stored) != 1) { + delete stored; + return false; + } + delete old; - if (impl::get_verify_callback()) { + if (stored) { SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, impl::openssl_verify_callback); } else { SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr); @@ -20224,13 +20248,14 @@ struct MbedTlsSession { // OpenSSL and wolfSSL keep them independent). bool suppress_hostname_mismatch = false; + // The context this session was created from; mbedtls_verify_callback() + // runs its verify_callback. + const MbedTlsContext *ctx = nullptr; + // Copied from the owning MbedTlsContext at creation. set_sni() uses this to // decide which verify callback to install when hostname verification is - // disabled: mbedtls_verify_callback() when a user callback is genuinely - // wired for this context, or a self-contained one otherwise, so a session - // that never opted into a callback never consults the process-wide - // set_verify_callback() slot (which some other, unrelated client may have - // populated). + // disabled: mbedtls_verify_callback() when a user callback is wired for this + // context, or a self-contained one otherwise. bool has_verify_callback = false; MbedTlsSession() { mbedtls_ssl_init(&ssl); } @@ -20406,9 +20431,7 @@ inline void mbedtls_clear_cn_mismatch(uint32_t *flags) { // Verify callback used when hostname verification is disabled for a session // that has no user-supplied verify callback of its own (MbedTlsSession:: -// has_verify_callback is false). Deliberately does not consult -// get_verify_callback(): that slot is process-wide, so reading it here would -// pick up whatever another, unrelated client last installed there. +// has_verify_callback is false). inline int mbedtls_mask_hostname_mismatch_callback(void *data, mbedtls_x509_crt *, int, uint32_t *flags) { @@ -20435,7 +20458,8 @@ inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, mbedtls_clear_cn_mismatch(flags); } - auto &callback = get_verify_callback(); + if (!session || !session->ctx) { return 0; } + const auto &callback = session->ctx->verify_callback; if (!callback) { return 0; } // Continue with default verification // Build context @@ -20853,6 +20877,7 @@ inline session_t create_session(ctx_t ctx, socket_t sock) { // Set per-session verify callback with session pointer if callback is // registered + session->ctx = mctx; session->has_verify_callback = mctx->has_verify_callback; if (mctx->has_verify_callback) { mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback, @@ -20889,7 +20914,7 @@ inline bool set_sni(session_t session, const char *hostname, // mbedtls_verify_callback() masks the mismatch flag itself before // consulting it (see suppress_hostname_mismatch above) - reinstalling it // here would be redundant. Otherwise install the self-contained masking - // callback, which never touches the process-wide callback slot. + // callback. if (!msession->has_verify_callback) { mbedtls_ssl_set_verify(&msession->ssl, impl::mbedtls_mask_hostname_mismatch_callback, @@ -21572,19 +21597,17 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto *mbed_ctx = static_cast(ctx); - impl::get_verify_callback() = std::move(callback); - mbed_ctx->has_verify_callback = - static_cast(impl::get_verify_callback()); + mbed_ctx->verify_callback = std::move(callback); + mbed_ctx->has_verify_callback = static_cast(mbed_ctx->verify_callback); if (mbed_ctx->has_verify_callback) { // Set OPTIONAL mode to ensure callback is called even when verification - // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set) + // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set). + // The callback itself is wired per session in create_session(), which + // passes the session so mbedtls_verify_callback() can reach this context. mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); - mbedtls_ssl_conf_verify(&mbed_ctx->conf, impl::mbedtls_verify_callback, - nullptr); - } else { - mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr); } + mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr); return true; } @@ -21710,8 +21733,12 @@ inline int wolfssl_sni_callback(WOLFSSL *ssl, int *ret, void *exArg) { // wolfSSL verify callback wrapper inline int wolfssl_verify_callback(int preverify_ok, WOLFSSL_X509_STORE_CTX *x509_ctx) { - auto &callback = get_verify_callback(); - if (!callback) { return preverify_ok; } + // userCtx carries the pointer create_session() passed to + // wolfSSL_SetCertCbCtx() + auto wctx = x509_ctx ? static_cast(x509_ctx->userCtx) + : nullptr; + if (!wctx || !wctx->verify_callback) { return preverify_ok; } + const auto &callback = wctx->verify_callback; WOLFSSL_X509 *cert = wolfSSL_X509_STORE_CTX_get_current_cert(x509_ctx); int depth = wolfSSL_X509_STORE_CTX_get_error_depth(x509_ctx); @@ -22001,6 +22028,9 @@ inline session_t create_session(ctx_t ctx, socket_t sock) { wolfSSL_set_fd(session->ssl, static_cast(sock)); + // Lets wolfssl_verify_callback() find this context's verify_callback + wolfSSL_SetCertCbCtx(session->ssl, wctx); + return static_cast(session); } @@ -22728,8 +22758,8 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto *wctx = static_cast(ctx); - impl::get_verify_callback() = std::move(callback); - wctx->has_verify_callback = static_cast(impl::get_verify_callback()); + wctx->verify_callback = std::move(callback); + wctx->has_verify_callback = static_cast(wctx->verify_callback); if (wctx->has_verify_callback) { wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, diff --git a/test/test.cc b/test/test.cc index b54771e9..2b57479f 100644 --- a/test/test.cc +++ b/test/test.cc @@ -15429,6 +15429,88 @@ TEST(VerifyCallbackTest, VerifyContextFields) { EXPECT_TRUE(saw_leaf_cert); } +// Each client's verifier is stored with its own TLS context, so configuring a +// second client on the same thread must not replace the first one's. +TEST(VerifyCallbackTest, PerClientVerifiersOnSameThread) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + }); + + svr.wait_until_ready(); + + std::atomic count1{0}; + std::atomic count2{0}; + + SSLClient cli1(HOST, PORT); + cli1.enable_server_certificate_verification(false); + cli1.set_server_certificate_verifier([&](const tls::VerifyContext &) { + count1++; + return true; + }); + + SSLClient cli2(HOST, PORT); + cli2.enable_server_certificate_verification(false); + cli2.set_server_certificate_verifier([&](const tls::VerifyContext &) { + count2++; + return true; + }); + + auto res1 = cli1.Get("/test"); + ASSERT_TRUE(res1) << "Error: " << to_string(res1.error()); + EXPECT_GT(count1.load(), 0); + EXPECT_EQ(0, count2.load()); + + count1 = 0; + auto res2 = cli2.Get("/test"); + ASSERT_TRUE(res2) << "Error: " << to_string(res2.error()); + EXPECT_EQ(0, count1.load()); + EXPECT_GT(count2.load(), 0); +} + +// A verifier set on one thread must still run when the request is made from +// another thread. +TEST(VerifyCallbackTest, VerifierRunsOnAnotherThread) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + }); + + svr.wait_until_ready(); + + std::atomic count{0}; + + SSLClient cli(HOST, PORT); + cli.enable_server_certificate_verification(false); + cli.set_server_certificate_verifier([&](const tls::VerifyContext &) { + count++; + return true; + }); + + Result res; + thread worker([&]() { res = cli.Get("/test"); }); + worker.join(); + + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_GT(count.load(), 0); +} + TEST(TlsVerifyErrorTest, GetVerifyErrorString) { using httplib::tls::TlsError; From 9f6dcf1230ceadfe98b13c6e0548353ea46843cd Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 14:12:01 -0400 Subject: [PATCH 03/16] Carry the server certificate verifier over to the redirect client (#2634) * Carry the server certificate verifier over to the redirect client create_redirect_client() now copies the CA store and the session verifier to the SSLClient it builds for a cross-host https redirect (#2632), but a verifier given to set_server_certificate_verifier() was still dropped: the redirect client's ctx never had the verify callback wired in, so a pinning verifier never ran on the host the server redirects to. SSLClient now keeps its own copy of the verifier, since ctx_ cannot report it back and the backends hold it in a slot shared by every client, and installs it on the redirect client. The copy also replaces the Windows-only server_certificate_verifier_set_ flag. SSLClientRedirectTest.ServerCertificateVerifier fails before this change and passes after (OpenSSL 3, Mbed TLS 2.28, wolfSSL 5.6). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 * Clarify the server_certificate_verifier_ comment The backends keep the verify callback in a per-thread slot, not one shared by every client. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --------- Co-authored-by: Claude --- httplib.h | 22 +++++++++++++-------- test/test.cc | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/httplib.h b/httplib.h index 2b365e16..fa589b26 100644 --- a/httplib.h +++ b/httplib.h @@ -3454,11 +3454,13 @@ private: std::function session_verifier_; + // The verifier given to set_server_certificate_verifier(), kept for the + // redirect client: ctx_ cannot report it back, and the backends keep it in + // a per-thread slot that any other client may overwrite. + tls::VerifyCallback server_certificate_verifier_; + #ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE bool enable_windows_cert_verification_ = true; - // Like ca_cert_store_set_, tracks what ctx_ cannot report back: whether - // set_server_certificate_verifier() installed a verifier. - bool server_certificate_verifier_set_ = false; #endif friend class ClientImpl; @@ -15944,6 +15946,11 @@ inline bool ClientImpl::create_redirect_client( } redirect_client.session_verifier_ = self.session_verifier_; + + if (self.server_certificate_verifier_) { + redirect_client.set_server_certificate_verifier( + self.server_certificate_verifier_); + } } // Client certificates are set through constructor for SSLClient @@ -18751,10 +18758,8 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) { inline void SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) { if (!ctx_) { return; } -#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE - server_certificate_verifier_set_ = static_cast(verifier); -#endif - tls::set_verify_callback(ctx_, verifier); + server_certificate_verifier_ = verifier; + tls::set_verify_callback(ctx_, std::move(verifier)); } inline void SSLClient::set_session_verifier( @@ -18817,7 +18822,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { ca_cert_dir_path_.empty() && ca_cert_pem_.empty() && !ca_cert_store_set_; // 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 = server_certificate_verifier_set_; + options.server_certificate_verifier_set = + static_cast(server_certificate_verifier_); #endif tls::session_t session = nullptr; diff --git a/test/test.cc b/test/test.cc index b54771e9..1d8ea604 100644 --- a/test/test.cc +++ b/test/test.cc @@ -16016,6 +16016,61 @@ TEST(SSLClientRedirectTest, SessionVerifier) { EXPECT_EQ(Error::SSLServerVerification, res.error()); EXPECT_EQ(2, sessions.load()); } + +// So must the verifier given to set_server_certificate_verifier(). +TEST(SSLClientRedirectTest, ServerCertificateVerifier) { + auto ssl_port = PORT + 1; + std::atomic redirected{false}; + + SSLServer ssl_svr1(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(ssl_svr1.is_valid()); + ssl_svr1.Get("/index", [&](const Request &, Response &res) { + redirected = true; + 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 host that redirects only; the verifier runs once per + // certificate in the chain, so count the calls made after the redirect. + std::atomic calls_after_redirect{0}; + cli.set_server_certificate_verifier([&](const tls::VerifyContext &) { + if (!redirected) { return true; } + calls_after_redirect++; + return false; + }); + + auto res = cli.Get("/index"); + ASSERT_FALSE(res); + EXPECT_LT(0, calls_after_redirect.load()); +} #endif #ifdef CPPHTTPLIB_SSL_ENABLED From 0c583ac9b55fef052011622c9ef606cd3ebd602e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:15:10 +0000 Subject: [PATCH 04/16] Drop no-op mbedtls_ssl_conf_verify reset in set_verify_callback Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wxevjmRqzpy8qDAD3vdwr --- httplib.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/httplib.h b/httplib.h index dcc43e3a..d9a1c2aa 100644 --- a/httplib.h +++ b/httplib.h @@ -21603,11 +21603,9 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (mbed_ctx->has_verify_callback) { // Set OPTIONAL mode to ensure callback is called even when verification // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set). - // The callback itself is wired per session in create_session(), which - // passes the session so mbedtls_verify_callback() can reach this context. + // The callback itself is wired per session in create_session(). mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); } - mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr); return true; } From 5056242a4b48479a49c9bbba592b795819a9cae3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:21:18 +0000 Subject: [PATCH 05/16] Simplify per-context verify callback bookkeeping Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wxevjmRqzpy8qDAD3vdwr --- httplib.h | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/httplib.h b/httplib.h index d9a1c2aa..9c6b8c32 100644 --- a/httplib.h +++ b/httplib.h @@ -4013,8 +4013,7 @@ struct MbedTlsContext { bool is_server = false; bool verify_client = false; bool has_verify_callback = false; - // Installed by tls::set_verify_callback(); run by the sessions created from - // this context. + // Set by tls::set_verify_callback(). VerifyCallback verify_callback; MbedTlsContext(); @@ -4040,9 +4039,7 @@ struct WolfSSLContext { bool is_server = false; bool verify_client = false; bool has_verify_callback = false; - // Installed by tls::set_verify_callback(); run by the sessions created from - // this context (each one points its verify-callback ctx back at this - // struct in create_session()). + // Set by tls::set_verify_callback(). VerifyCallback verify_callback; std::string ca_pem_data_; // accumulated PEM for get_ca_names/get_ca_certs @@ -20248,16 +20245,9 @@ struct MbedTlsSession { // OpenSSL and wolfSSL keep them independent). bool suppress_hostname_mismatch = false; - // The context this session was created from; mbedtls_verify_callback() - // runs its verify_callback. + // The context this session was created from. const MbedTlsContext *ctx = nullptr; - // Copied from the owning MbedTlsContext at creation. set_sni() uses this to - // decide which verify callback to install when hostname verification is - // disabled: mbedtls_verify_callback() when a user callback is wired for this - // context, or a self-contained one otherwise. - bool has_verify_callback = false; - MbedTlsSession() { mbedtls_ssl_init(&ssl); } ~MbedTlsSession() { mbedtls_ssl_free(&ssl); } @@ -20430,8 +20420,7 @@ inline void mbedtls_clear_cn_mismatch(uint32_t *flags) { } // Verify callback used when hostname verification is disabled for a session -// that has no user-supplied verify callback of its own (MbedTlsSession:: -// has_verify_callback is false). +// that has no user-supplied verify callback of its own. inline int mbedtls_mask_hostname_mismatch_callback(void *data, mbedtls_x509_crt *, int, uint32_t *flags) { @@ -20878,7 +20867,6 @@ inline session_t create_session(ctx_t ctx, socket_t sock) { // Set per-session verify callback with session pointer if callback is // registered session->ctx = mctx; - session->has_verify_callback = mctx->has_verify_callback; if (mctx->has_verify_callback) { mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback, session); @@ -20915,7 +20903,7 @@ inline bool set_sni(session_t session, const char *hostname, // consulting it (see suppress_hostname_mismatch above) - reinstalling it // here would be redundant. Otherwise install the self-contained masking // callback. - if (!msession->has_verify_callback) { + if (!msession->ctx->has_verify_callback) { mbedtls_ssl_set_verify(&msession->ssl, impl::mbedtls_mask_hostname_mismatch_callback, msession); @@ -21731,8 +21719,7 @@ inline int wolfssl_sni_callback(WOLFSSL *ssl, int *ret, void *exArg) { // wolfSSL verify callback wrapper inline int wolfssl_verify_callback(int preverify_ok, WOLFSSL_X509_STORE_CTX *x509_ctx) { - // userCtx carries the pointer create_session() passed to - // wolfSSL_SetCertCbCtx() + // userCtx is the context passed to wolfSSL_SetCertCbCtx() auto wctx = x509_ctx ? static_cast(x509_ctx->userCtx) : nullptr; if (!wctx || !wctx->verify_callback) { return preverify_ok; } @@ -22026,7 +22013,7 @@ inline session_t create_session(ctx_t ctx, socket_t sock) { wolfSSL_set_fd(session->ssl, static_cast(sock)); - // Lets wolfssl_verify_callback() find this context's verify_callback + // Lets wolfssl_verify_callback() find the context wolfSSL_SetCertCbCtx(session->ssl, wctx); return static_cast(session); From 5d1009594422607444b3d64f556efe5de23c9042 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:57:00 +0000 Subject: [PATCH 06/16] Install one verify callback on every Mbed TLS session Every session now gets mbedtls_verify_callback(), which reads the user's callback from the owning context when the handshake runs. The has_verify_callback flag and the separate hostname-masking callback are no longer needed, and a callback set after a session was created is still honoured. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- httplib.h | 41 ++++++++--------------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/httplib.h b/httplib.h index c0c363fe..8c37dea2 100644 --- a/httplib.h +++ b/httplib.h @@ -4014,7 +4014,6 @@ struct MbedTlsContext { mbedtls_pk_context own_key; bool is_server = false; bool verify_client = false; - bool has_verify_callback = false; // Set by tls::set_verify_callback(). VerifyCallback verify_callback; @@ -20425,16 +20424,6 @@ inline void mbedtls_clear_cn_mismatch(uint32_t *flags) { *flags &= ~static_cast(hostname_mismatch_code()); } -// Verify callback used when hostname verification is disabled for a session -// that has no user-supplied verify callback of its own. -inline int mbedtls_mask_hostname_mismatch_callback(void *data, - mbedtls_x509_crt *, int, - uint32_t *flags) { - (void)data; - mbedtls_clear_cn_mismatch(flags); - return 0; -} - inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, int cert_depth, uint32_t *flags); @@ -20836,7 +20825,7 @@ inline void set_verify_client(ctx_t ctx, bool require) { } else { // If a verify callback is set, use OPTIONAL mode to ensure the callback // is called (matching OpenSSL behavior). Otherwise use NONE. - mbedtls_ssl_conf_authmode(&mctx->conf, mctx->has_verify_callback + mbedtls_ssl_conf_authmode(&mctx->conf, mctx->verify_callback ? MBEDTLS_SSL_VERIFY_OPTIONAL : MBEDTLS_SSL_VERIFY_NONE); } @@ -20870,13 +20859,11 @@ inline session_t create_session(ctx_t ctx, socket_t sock) { mbedtls_ssl_set_bio(&session->ssl, &session->sock, impl::mbedtls_net_send_cb, impl::mbedtls_net_recv_cb, nullptr); - // Set per-session verify callback with session pointer if callback is - // registered + // Set the per-session verify callback. It looks up the context's user + // callback, if any, when the handshake runs, so it does not matter whether + // set_verify_callback() is called before or after this session is created. session->ctx = mctx; - if (mctx->has_verify_callback) { - mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback, - session); - } + mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback, session); return static_cast(session); } @@ -20902,19 +20889,8 @@ inline bool set_sni(session_t session, const char *hostname, msession->hostname = hostname; - if (!verify_hostname) { - msession->suppress_hostname_mismatch = true; - // If a user verify callback is already wired for this session, - // mbedtls_verify_callback() masks the mismatch flag itself before - // consulting it (see suppress_hostname_mismatch above) - reinstalling it - // here would be redundant. Otherwise install the self-contained masking - // callback. - if (!msession->ctx->has_verify_callback) { - mbedtls_ssl_set_verify(&msession->ssl, - impl::mbedtls_mask_hostname_mismatch_callback, - msession); - } - } + // mbedtls_verify_callback() masks the mismatch flag for this session + if (!verify_hostname) { msession->suppress_hostname_mismatch = true; } return true; } @@ -21592,9 +21568,8 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { auto *mbed_ctx = static_cast(ctx); mbed_ctx->verify_callback = std::move(callback); - mbed_ctx->has_verify_callback = static_cast(mbed_ctx->verify_callback); - if (mbed_ctx->has_verify_callback) { + if (mbed_ctx->verify_callback) { // Set OPTIONAL mode to ensure callback is called even when verification // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set). // The callback itself is wired per session in create_session(). From f6420979b286429b6d64bcee8c597b4a39050960 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:39:00 +0000 Subject: [PATCH 07/16] Drop the redundant has_verify_callback flag from the wolfSSL context It only mirrored whether verify_callback was set, so test verify_callback directly, as the Mbed TLS context now does. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- httplib.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/httplib.h b/httplib.h index 8c37dea2..ee9e8bc6 100644 --- a/httplib.h +++ b/httplib.h @@ -4039,7 +4039,6 @@ struct WolfSSLContext { WOLFSSL_CTX *ctx = nullptr; bool is_server = false; bool verify_client = false; - bool has_verify_callback = false; // Set by tls::set_verify_callback(). VerifyCallback verify_callback; std::string ca_pem_data_; // accumulated PEM for get_ca_names/get_ca_certs @@ -21965,9 +21964,9 @@ inline void set_verify_client(ctx_t ctx, bool require) { if (require) { wolfSSL_CTX_set_verify( wctx->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, - wctx->has_verify_callback ? impl::wolfssl_verify_callback : nullptr); + wctx->verify_callback ? impl::wolfssl_verify_callback : nullptr); } else { - if (wctx->has_verify_callback) { + if (wctx->verify_callback) { wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, impl::wolfssl_verify_callback); } else { @@ -22725,9 +22724,8 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { auto *wctx = static_cast(ctx); wctx->verify_callback = std::move(callback); - wctx->has_verify_callback = static_cast(wctx->verify_callback); - if (wctx->has_verify_callback) { + if (wctx->verify_callback) { wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, impl::wolfssl_verify_callback); } else { From a445ed23bf921628527e4b767165e7f6bb65bf13 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:57:26 +0000 Subject: [PATCH 08/16] Tidy the Mbed TLS verify callback after the per-session change Remove the now-unused forward declaration and the null checks that can no longer fire, since create_session() always sets the session and its context before installing the callback, and shorten the comments. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- httplib.h | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/httplib.h b/httplib.h index ee9e8bc6..888f78d4 100644 --- a/httplib.h +++ b/httplib.h @@ -20423,25 +20423,18 @@ inline void mbedtls_clear_cn_mismatch(uint32_t *flags) { *flags &= ~static_cast(hostname_mismatch_code()); } -inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, - int cert_depth, uint32_t *flags); - // MbedTLS verify callback wrapper inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, int cert_depth, uint32_t *flags) { // data points to the MbedTlsSession auto *session = static_cast(data); - // set_sni() disabled hostname verification for this session: drop the - // CN/SAN mismatch flag so it doesn't fail the chain check below, mirroring - // the OpenSSL/wolfSSL backends where identity checking is independent of - // SNI. The final pass/fail decision still comes from the remaining flags - // (or, below, from the user's own verify callback). - if (session && session->suppress_hostname_mismatch) { - mbedtls_clear_cn_mismatch(flags); - } + // set_sni() disabled hostname verification for this session: ignore the + // CN/SAN mismatch flag, as the OpenSSL/wolfSSL backends do. The final + // pass/fail decision still comes from the remaining flags (or, below, from + // the user's own verify callback). + if (session->suppress_hostname_mismatch) { mbedtls_clear_cn_mismatch(flags); } - if (!session || !session->ctx) { return 0; } const auto &callback = session->ctx->verify_callback; if (!callback) { return 0; } // Continue with default verification @@ -20858,9 +20851,9 @@ inline session_t create_session(ctx_t ctx, socket_t sock) { mbedtls_ssl_set_bio(&session->ssl, &session->sock, impl::mbedtls_net_send_cb, impl::mbedtls_net_recv_cb, nullptr); - // Set the per-session verify callback. It looks up the context's user - // callback, if any, when the handshake runs, so it does not matter whether - // set_verify_callback() is called before or after this session is created. + // Always install the verify callback: it reads the context's user callback + // when the handshake runs, so the order of set_verify_callback() and this + // call does not matter. session->ctx = mctx; mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback, session); @@ -20888,7 +20881,6 @@ inline bool set_sni(session_t session, const char *hostname, msession->hostname = hostname; - // mbedtls_verify_callback() masks the mismatch flag for this session if (!verify_hostname) { msession->suppress_hostname_mismatch = true; } return true; From 300c084d6f11b9ad177d9046a308455eb0118e62 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 21:12:47 +0000 Subject: [PATCH 09/16] Derive the TLS verify mode from one place per backend set_verify_client() and set_verify_callback() each set the verify mode on their own, and set_verify_callback() ignored whether the peer's certificate was required. A server that required client certificates therefore stopped requiring them as soon as a verify callback was set on its context (OpenSSL, Mbed TLS, wolfSSL) or set and then cleared again (OpenSSL, Mbed TLS). Compute the mode from "peer certificate required" and "callback set" in one helper per backend and call it from set_verify_client(), set_verify_callback() and the context constructors. The context now records that a client verifies the server by default, so clearing a callback on a client context restores that default. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- httplib.h | 110 ++++++++++++++++++++++++++------------------------- test/test.cc | 53 +++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 54 deletions(-) diff --git a/httplib.h b/httplib.h index 888f78d4..e0d85e26 100644 --- a/httplib.h +++ b/httplib.h @@ -4013,6 +4013,8 @@ struct MbedTlsContext { mbedtls_x509_crt own_cert; mbedtls_pk_context own_key; bool is_server = false; + // Whether the peer's certificate is required; set by + // tls::set_verify_client(). A client verifies the server by default. bool verify_client = false; // Set by tls::set_verify_callback(). VerifyCallback verify_callback; @@ -4038,6 +4040,8 @@ namespace impl { struct WolfSSLContext { WOLFSSL_CTX *ctx = nullptr; bool is_server = false; + // Whether the peer's certificate is required; set by + // tls::set_verify_client(). A client verifies the server by default. bool verify_client = false; // Set by tls::set_verify_callback(). VerifyCallback verify_callback; @@ -19307,6 +19311,17 @@ inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { return (*callback)(verify_ctx) ? 1 : 0; } +// The verify mode follows from whether the peer's certificate is required and +// whether a verify callback is set, however the two were last changed. +inline void apply_verify_mode(SSL_CTX *ssl_ctx, bool require) { + auto callback = get_openssl_verify_callback(ssl_ctx); + auto has_callback = callback && *callback; + auto mode = require ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) + : (has_callback ? SSL_VERIFY_PEER : SSL_VERIFY_NONE); + SSL_CTX_set_verify(ssl_ctx, mode, + has_callback ? openssl_verify_callback : nullptr); +} + // X509_STORE_get0_objects is deprecated since OpenSSL 4.0 because it is not // thread-safe; X509_STORE_get1_objects (OpenSSL 3.3+) returns a snapshot // that must be released with release_store_objects @@ -19537,11 +19552,7 @@ inline ctx_t create_server_context() { inline void set_verify_client(ctx_t ctx, bool require) { if (!ctx) return; - SSL_CTX_set_verify(static_cast(ctx), - require - ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) - : SSL_VERIFY_NONE, - nullptr); + impl::apply_verify_mode(static_cast(ctx), require); } inline session_t create_session(ctx_t ctx, socket_t sock) { @@ -20192,11 +20203,10 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { } delete old; - if (stored) { - SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, impl::openssl_verify_callback); - } else { - SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr); - } + // Keep requiring the peer's certificate if set_verify_client() asked for it + auto require = + (SSL_CTX_get_verify_mode(ssl_ctx) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) != 0; + impl::apply_verify_mode(ssl_ctx, require); return true; } @@ -20464,6 +20474,20 @@ inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED; } +// The authentication mode follows from whether the peer's certificate is +// required and whether a verify callback is set, however the two were last +// changed. OPTIONAL makes sure the callback runs even if verification is not +// required (matching OpenSSL, where SSL_VERIFY_PEER is set). +inline void apply_authmode(MbedTlsContext &mctx) { + auto mode = MBEDTLS_SSL_VERIFY_NONE; + if (mctx.verify_client) { + mode = MBEDTLS_SSL_VERIFY_REQUIRED; + } else if (mctx.verify_callback) { + mode = MBEDTLS_SSL_VERIFY_OPTIONAL; + } + mbedtls_ssl_conf_authmode(&mctx.conf, mode); +} + } // namespace impl inline ctx_t create_client_context() { @@ -20508,7 +20532,8 @@ inline ctx_t create_client_context() { #endif // Default: verify peer certificate - mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); + ctx->verify_client = true; + impl::apply_authmode(*ctx); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 @@ -20563,7 +20588,7 @@ inline ctx_t create_server_context() { #endif // Default: don't verify client - mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_NONE); + impl::apply_authmode(*ctx); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 @@ -20812,15 +20837,7 @@ inline void set_verify_client(ctx_t ctx, bool require) { if (!ctx) { return; } auto mctx = static_cast(ctx); mctx->verify_client = require; - if (require) { - mbedtls_ssl_conf_authmode(&mctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); - } else { - // If a verify callback is set, use OPTIONAL mode to ensure the callback - // is called (matching OpenSSL behavior). Otherwise use NONE. - mbedtls_ssl_conf_authmode(&mctx->conf, mctx->verify_callback - ? MBEDTLS_SSL_VERIFY_OPTIONAL - : MBEDTLS_SSL_VERIFY_NONE); - } + impl::apply_authmode(*mctx); } inline session_t create_session(ctx_t ctx, socket_t sock) { @@ -21558,14 +21575,9 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto *mbed_ctx = static_cast(ctx); + // The callback itself is wired per session in create_session() mbed_ctx->verify_callback = std::move(callback); - - if (mbed_ctx->verify_callback) { - // Set OPTIONAL mode to ensure callback is called even when verification - // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set). - // The callback itself is wired per session in create_session(). - mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); - } + impl::apply_authmode(*mbed_ctx); return true; } @@ -21722,6 +21734,17 @@ inline int wolfssl_verify_callback(int preverify_ok, return accepted ? 1 : 0; } +// The verify mode follows from whether the peer's certificate is required and +// whether a verify callback is set, however the two were last changed. +inline void apply_verify_mode(WolfSSLContext &wctx) { + auto has_callback = static_cast(wctx.verify_callback); + auto mode = wctx.verify_client + ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) + : (has_callback ? SSL_VERIFY_PEER : SSL_VERIFY_NONE); + wolfSSL_CTX_set_verify(wctx.ctx, mode, + has_callback ? wolfssl_verify_callback : nullptr); +} + inline void set_wolfssl_password_cb(WOLFSSL_CTX *ctx, const char *password) { wolfSSL_CTX_set_default_passwd_cb_userdata(ctx, const_cast(password)); wolfSSL_CTX_set_default_passwd_cb( @@ -21756,7 +21779,8 @@ inline ctx_t create_client_context() { } // Default: verify peer certificate - wolfSSL_CTX_set_verify(ctx->ctx, SSL_VERIFY_PEER, nullptr); + ctx->verify_client = true; + impl::apply_verify_mode(*ctx); return static_cast(ctx); } @@ -21780,7 +21804,7 @@ inline ctx_t create_server_context() { } // Default: don't verify client - wolfSSL_CTX_set_verify(ctx->ctx, SSL_VERIFY_NONE, nullptr); + impl::apply_verify_mode(*ctx); // Enable SNI on server wolfSSL_CTX_SNI_SetOptions(ctx->ctx, WOLFSSL_SNI_HOST_NAME, @@ -21953,18 +21977,7 @@ inline void set_verify_client(ctx_t ctx, bool require) { if (!ctx) { return; } auto wctx = static_cast(ctx); wctx->verify_client = require; - if (require) { - wolfSSL_CTX_set_verify( - wctx->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, - wctx->verify_callback ? impl::wolfssl_verify_callback : nullptr); - } else { - if (wctx->verify_callback) { - wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, - impl::wolfssl_verify_callback); - } else { - wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_NONE, nullptr); - } - } + impl::apply_verify_mode(*wctx); } inline session_t create_session(ctx_t ctx, socket_t sock) { @@ -22716,18 +22729,7 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { auto *wctx = static_cast(ctx); wctx->verify_callback = std::move(callback); - - if (wctx->verify_callback) { - wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, - impl::wolfssl_verify_callback); - } else { - wolfSSL_CTX_set_verify( - wctx->ctx, - wctx->verify_client - ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) - : SSL_VERIFY_NONE, - nullptr); - } + impl::apply_verify_mode(*wctx); return true; } diff --git a/test/test.cc b/test/test.cc index 090b9cb1..8e55411b 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14571,6 +14571,59 @@ TEST(SSLClientServerTest, ClientEncryptedCertPresent) { CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); } +// A server that requires client certificates must keep requiring them after a +// verify callback is set on its context, or set and then cleared again. +void ClientCertStillRequired(bool clear_callback) { + using namespace httplib::tls; + + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, + CLIENT_CA_CERT_DIR); + ASSERT_TRUE(svr.is_valid()); + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("test", "text/plain"); + }); + + ASSERT_TRUE(set_verify_callback(svr.tls_context(), + [](const VerifyContext &) { return true; })); + if (clear_callback) { + ASSERT_TRUE(set_verify_callback(svr.tls_context(), nullptr)); + } + + auto port = svr.bind_to_any_port(HOST); + 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(); + + { + SSLClient cli(HOST, port); + cli.enable_server_certificate_verification(false); + cli.set_connection_timeout(30); + EXPECT_FALSE(cli.Get("/test")); + } + + { + SSLClient cli(HOST, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); + cli.enable_server_certificate_verification(false); + cli.set_connection_timeout(30); + auto res = cli.Get("/test"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status); + } +} + +TEST(SSLClientServerTest, ClientCertStillRequiredAfterVerifyCallbackSet) { + ClientCertStillRequired(false); +} + +TEST(SSLClientServerTest, ClientCertStillRequiredAfterVerifyCallbackCleared) { + ClientCertStillRequired(true); +} + // PEM memory-based constructor tests (works with all TLS backends) void PemMemoryClientCertPresent( const std::string &client_cert_file, From aa3aeb8ec368a167bfd716e9beb2d6507e4a57b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 22:25:51 +0000 Subject: [PATCH 10/16] Name the Mbed TLS verify-mode helper like the other backends Rename apply_authmode() to apply_verify_mode() to match the OpenSSL and wolfSSL helpers, and shorten the comments on the helpers and on the verify_client fields. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- httplib.h | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/httplib.h b/httplib.h index e0d85e26..d36ab8e7 100644 --- a/httplib.h +++ b/httplib.h @@ -4013,8 +4013,7 @@ struct MbedTlsContext { mbedtls_x509_crt own_cert; mbedtls_pk_context own_key; bool is_server = false; - // Whether the peer's certificate is required; set by - // tls::set_verify_client(). A client verifies the server by default. + // Peer certificate required (set_verify_client()); a client defaults to true bool verify_client = false; // Set by tls::set_verify_callback(). VerifyCallback verify_callback; @@ -4040,8 +4039,7 @@ namespace impl { struct WolfSSLContext { WOLFSSL_CTX *ctx = nullptr; bool is_server = false; - // Whether the peer's certificate is required; set by - // tls::set_verify_client(). A client verifies the server by default. + // Peer certificate required (set_verify_client()); a client defaults to true bool verify_client = false; // Set by tls::set_verify_callback(). VerifyCallback verify_callback; @@ -19311,8 +19309,8 @@ inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { return (*callback)(verify_ctx) ? 1 : 0; } -// The verify mode follows from whether the peer's certificate is required and -// whether a verify callback is set, however the two were last changed. +// Sets the verify mode from whether the peer's certificate is required and +// whether a verify callback is set. inline void apply_verify_mode(SSL_CTX *ssl_ctx, bool require) { auto callback = get_openssl_verify_callback(ssl_ctx); auto has_callback = callback && *callback; @@ -20474,11 +20472,10 @@ inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED; } -// The authentication mode follows from whether the peer's certificate is -// required and whether a verify callback is set, however the two were last -// changed. OPTIONAL makes sure the callback runs even if verification is not -// required (matching OpenSSL, where SSL_VERIFY_PEER is set). -inline void apply_authmode(MbedTlsContext &mctx) { +// Sets the authentication mode from whether the peer's certificate is required +// and whether a verify callback is set. OPTIONAL lets the callback run even if +// verification is not required (matching OpenSSL's SSL_VERIFY_PEER). +inline void apply_verify_mode(MbedTlsContext &mctx) { auto mode = MBEDTLS_SSL_VERIFY_NONE; if (mctx.verify_client) { mode = MBEDTLS_SSL_VERIFY_REQUIRED; @@ -20533,7 +20530,7 @@ inline ctx_t create_client_context() { // Default: verify peer certificate ctx->verify_client = true; - impl::apply_authmode(*ctx); + impl::apply_verify_mode(*ctx); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 @@ -20588,7 +20585,7 @@ inline ctx_t create_server_context() { #endif // Default: don't verify client - impl::apply_authmode(*ctx); + impl::apply_verify_mode(*ctx); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 @@ -20837,7 +20834,7 @@ inline void set_verify_client(ctx_t ctx, bool require) { if (!ctx) { return; } auto mctx = static_cast(ctx); mctx->verify_client = require; - impl::apply_authmode(*mctx); + impl::apply_verify_mode(*mctx); } inline session_t create_session(ctx_t ctx, socket_t sock) { @@ -21577,7 +21574,7 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { // The callback itself is wired per session in create_session() mbed_ctx->verify_callback = std::move(callback); - impl::apply_authmode(*mbed_ctx); + impl::apply_verify_mode(*mbed_ctx); return true; } @@ -21734,8 +21731,8 @@ inline int wolfssl_verify_callback(int preverify_ok, return accepted ? 1 : 0; } -// The verify mode follows from whether the peer's certificate is required and -// whether a verify callback is set, however the two were last changed. +// Sets the verify mode from whether the peer's certificate is required and +// whether a verify callback is set. inline void apply_verify_mode(WolfSSLContext &wctx) { auto has_callback = static_cast(wctx.verify_callback); auto mode = wctx.verify_client From 589b028022af5c14edb9ef9c54f9f0b0b631a69b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 22:48:39 +0000 Subject: [PATCH 11/16] Build the client-cert-still-required tests on the existing helpers ClientCertPresent() and ClientCertMissing() now take a setup step for the server, bind to any port instead of the legacy PORT, and the two new tests are just calls to them. ClientCertMissing also no longer checks the server certificate: with it checked, the request failed because cert.pem is not trusted, whether or not the server asked for a client certificate, so the test never exercised the client certificate requirement. A rejected client is reported as SSLConnection, or as Read on TLS 1.3, where the client only notices on its first read. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- test/test.cc | 109 +++++++++++++++++++++------------------------------ 1 file changed, 45 insertions(+), 64 deletions(-) diff --git a/test/test.cc b/test/test.cc index 8e55411b..4da2ced4 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14522,15 +14522,19 @@ TEST(SSLClientTest, SetInterfaceWithINET6) { // ClientCertPresent uses get_peer_cert() - works with all TLS backends #ifdef CPPHTTPLIB_SSL_ENABLED +using ClientCertServerSetup = std::function; + void ClientCertPresent( const std::string &client_cert_file, const std::string &client_private_key_file, - const std::string &client_encrypted_private_key_pass = std::string()) { + const std::string &client_encrypted_private_key_pass = std::string(), + const ClientCertServerSetup &setup = nullptr) { using namespace httplib::tls; SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, CLIENT_CA_CERT_DIR); ASSERT_TRUE(svr.is_valid()); + if (setup) { setup(svr); } svr.Get("/test", [&](const Request &req, Response &res) { res.set_content("test", "text/plain"); @@ -14542,7 +14546,9 @@ void ClientCertPresent( EXPECT_EQ("Common Name", common_name); }); - thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto port = svr.bind_to_any_port(HOST); + ASSERT_LT(0, port); + thread t = thread([&]() { svr.listen_after_bind(); }); auto se = detail::scope_exit([&] { svr.stop(); t.join(); @@ -14551,7 +14557,7 @@ void ClientCertPresent( svr.wait_until_ready(); - SSLClient cli(HOST, PORT, client_cert_file, client_private_key_file, + SSLClient cli(HOST, port, client_cert_file, client_private_key_file, client_encrypted_private_key_pass); cli.enable_server_certificate_verification(false); cli.set_connection_timeout(30); @@ -14571,23 +14577,13 @@ TEST(SSLClientServerTest, ClientEncryptedCertPresent) { CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); } -// A server that requires client certificates must keep requiring them after a -// verify callback is set on its context, or set and then cleared again. -void ClientCertStillRequired(bool clear_callback) { - using namespace httplib::tls; - +void ClientCertMissing(const ClientCertServerSetup &setup = nullptr) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, CLIENT_CA_CERT_DIR); ASSERT_TRUE(svr.is_valid()); - svr.Get("/test", [](const Request &, Response &res) { - res.set_content("test", "text/plain"); - }); + if (setup) { setup(svr); } - ASSERT_TRUE(set_verify_callback(svr.tls_context(), - [](const VerifyContext &) { return true; })); - if (clear_callback) { - ASSERT_TRUE(set_verify_callback(svr.tls_context(), nullptr)); - } + svr.Get("/test", [&](const Request &, Response &) { ASSERT_TRUE(false); }); auto port = svr.bind_to_any_port(HOST); ASSERT_LT(0, port); @@ -14597,25 +14593,45 @@ void ClientCertStillRequired(bool clear_callback) { t.join(); ASSERT_FALSE(svr.is_running()); }); + svr.wait_until_ready(); - { - SSLClient cli(HOST, port); - cli.enable_server_certificate_verification(false); - cli.set_connection_timeout(30); - EXPECT_FALSE(cli.Get("/test")); - } + SSLClient cli(HOST, port); + cli.set_connection_timeout(30); + // Skip the server certificate check, so that the request fails because the + // client certificate is missing rather than because cert.pem is not trusted + cli.enable_server_certificate_verification(false); - { - SSLClient cli(HOST, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); - cli.enable_server_certificate_verification(false); - cli.set_connection_timeout(30); - auto res = cli.Get("/test"); - ASSERT_TRUE(res) << "Error: " << to_string(res.error()); - EXPECT_EQ(StatusCode::OK_200, res->status); + auto res = cli.Get("/test"); + ASSERT_TRUE(!res); + // The server rejects the client during the handshake, or on TLS 1.3 right + // after it, which the client only notices on its first read + EXPECT_TRUE(res.error() == Error::SSLConnection || res.error() == Error::Read) + << to_string(res.error()); + + // Verify backend error is captured + if (res.error() == Error::SSLConnection) { + EXPECT_NE(0UL, res.ssl_backend_error()); } } +TEST(SSLClientServerTest, ClientCertMissing) { ClientCertMissing(); } + +// A server that requires client certificates must keep requiring them after a +// verify callback is set on its context, or set and then cleared again. +void ClientCertStillRequired(bool clear_callback) { + auto setup = [clear_callback](SSLServer &svr) { + ASSERT_TRUE(tls::set_verify_callback( + svr.tls_context(), [](const tls::VerifyContext &) { return true; })); + if (clear_callback) { + ASSERT_TRUE(tls::set_verify_callback(svr.tls_context(), nullptr)); + } + }; + ClientCertMissing(setup); + ClientCertPresent(CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE, std::string(), + setup); +} + TEST(SSLClientServerTest, ClientCertStillRequiredAfterVerifyCallbackSet) { ClientCertStillRequired(false); } @@ -14692,41 +14708,6 @@ TEST(SSLClientServerTest, PemMemoryClientEncryptedCertPresent) { CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); } -TEST(SSLClientServerTest, ClientCertMissing) { - SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, - CLIENT_CA_CERT_DIR); - ASSERT_TRUE(svr.is_valid()); - - svr.Get("/test", [&](const Request &, Response &) { ASSERT_TRUE(false); }); - - thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); - auto se = detail::scope_exit([&] { - svr.stop(); - t.join(); - ASSERT_FALSE(svr.is_running()); - }); - - svr.wait_until_ready(); - - SSLClient cli(HOST, PORT); - cli.set_connection_timeout(30); - // cert.pem does not match HOST. Skip the hostname check, which runs before - // the chain check on Windows, so the result does not depend on the order. - cli.enable_server_hostname_verification(false); - - auto res = cli.Get("/test"); - ASSERT_TRUE(!res); - // When client cert is missing and server requires it, connection fails - // Error type depends on backend implementation - EXPECT_TRUE(res.error() == Error::SSLServerVerification || - res.error() == Error::SSLConnection); - - // Verify backend error is captured - // Note: This test may have different error codes depending on the exact - // verification failure - EXPECT_NE(0UL, res.ssl_backend_error()); -} - TEST(SSLClientServerTest, TrustDirOptional) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE); ASSERT_TRUE(svr.is_valid()); From 8f21292d16aa8c29f05f24dc3a92c7a58a60628a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 23:02:50 +0000 Subject: [PATCH 12/16] Tidy the client certificate test helpers Use ASSERT_FALSE and FAIL() instead of their hand-written forms, drop an unused using-directive and a comment that restated the code, and fix the section comment that still referred to get_peer_cert(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- test/test.cc | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/test.cc b/test/test.cc index 4da2ced4..56595e89 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14520,7 +14520,7 @@ TEST(SSLClientTest, SetInterfaceWithINET6) { } #endif -// ClientCertPresent uses get_peer_cert() - works with all TLS backends +// Client certificate tests - work with all TLS backends #ifdef CPPHTTPLIB_SSL_ENABLED using ClientCertServerSetup = std::function; @@ -14529,8 +14529,6 @@ void ClientCertPresent( const std::string &client_private_key_file, const std::string &client_encrypted_private_key_pass = std::string(), const ClientCertServerSetup &setup = nullptr) { - using namespace httplib::tls; - SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, CLIENT_CA_CERT_DIR); ASSERT_TRUE(svr.is_valid()); @@ -14583,7 +14581,7 @@ void ClientCertMissing(const ClientCertServerSetup &setup = nullptr) { ASSERT_TRUE(svr.is_valid()); if (setup) { setup(svr); } - svr.Get("/test", [&](const Request &, Response &) { ASSERT_TRUE(false); }); + svr.Get("/test", [](const Request &, Response &) { FAIL(); }); auto port = svr.bind_to_any_port(HOST); ASSERT_LT(0, port); @@ -14603,13 +14601,12 @@ void ClientCertMissing(const ClientCertServerSetup &setup = nullptr) { cli.enable_server_certificate_verification(false); auto res = cli.Get("/test"); - ASSERT_TRUE(!res); + ASSERT_FALSE(res); // The server rejects the client during the handshake, or on TLS 1.3 right // after it, which the client only notices on its first read EXPECT_TRUE(res.error() == Error::SSLConnection || res.error() == Error::Read) << to_string(res.error()); - // Verify backend error is captured if (res.error() == Error::SSLConnection) { EXPECT_NE(0UL, res.ssl_backend_error()); } From 6529de1a914b4f26261a70e7d89f80f1705f0fbf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 23:29:35 +0000 Subject: [PATCH 13/16] Declare tls::set_verify_callback() for the split test build The ClientCertStillRequired tests call tls::set_verify_callback(), whose declaration lives below the split border, so test_split failed to compile. Declare it next to create_ca_store(), which the split build needs for the same reason. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- test/test.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test.cc b/test/test.cc index 56595e89..edf1aec3 100644 --- a/test/test.cc +++ b/test/test.cc @@ -69,6 +69,7 @@ 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); +bool set_verify_callback(ctx_t ctx, VerifyCallback callback); } // namespace tls } // namespace httplib #endif From b4ef313cc7a7be9399a33568453b302012bf4fab Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 01:04:13 +0000 Subject: [PATCH 14/16] Accept a write error for a client rejected after the TLS 1.3 handshake On TLS 1.3 the server rejects a client without a certificate only after the client has finished its side of the handshake, so the client notices either when it writes the request or when it reads the response. CI saw the write fail on Windows (OpenSSL) and once on macOS (Mbed TLS), which ClientCertMissing did not accept. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- test/test.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test.cc b/test/test.cc index edf1aec3..65e69c1e 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14604,8 +14604,9 @@ void ClientCertMissing(const ClientCertServerSetup &setup = nullptr) { auto res = cli.Get("/test"); ASSERT_FALSE(res); // The server rejects the client during the handshake, or on TLS 1.3 right - // after it, which the client only notices on its first read - EXPECT_TRUE(res.error() == Error::SSLConnection || res.error() == Error::Read) + // after it, which the client notices on its first write or read + EXPECT_TRUE(res.error() == Error::SSLConnection || + res.error() == Error::Write || res.error() == Error::Read) << to_string(res.error()); if (res.error() == Error::SSLConnection) { From 28741cb9d1058c41d39451cc6f9a3437f9d81243 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 01:54:23 +0000 Subject: [PATCH 15/16] Keep the TLS error behind a failed read or write in the client's Result A request that fails after the TLS handshake reports Error::Read or Error::Write, and SSLSocketStream dropped the TLS error behind it. On TLS 1.3, where a server rejects a client without a certificate only after the handshake, ssl_error() and ssl_backend_error() were 0 instead of showing, for example, OpenSSL's "tlsv13 alert certificate required". SSLSocketStream now keeps the error from its last failed TLS read or write, and SSLClient copies it into the values Result reports. Error itself stays Read or Write. Those values are also reset at the start of each request, so a later successful request no longer reports the TLS error of an earlier one. A client whose TLS context failed to set up keeps reporting that error. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- httplib.h | 43 ++++++++++++++++++++++++++++++++++++++----- test/test.cc | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/httplib.h b/httplib.h index d36ab8e7..107b0988 100644 --- a/httplib.h +++ b/httplib.h @@ -10494,6 +10494,10 @@ public: // See SocketStream::set_readable_hint(). void set_readable_hint() { readable_hint_ = true; } + // The error from the last read() or write() that failed in the TLS layer, + // with code Success if none did + const tls::TlsError &tls_error() const { return tls_error_; } + private: bool ensure_readable(); @@ -10506,6 +10510,7 @@ private: time_t max_timeout_msec_; const std::chrono::time_point start_time_; bool readable_hint_ = false; + tls::TlsError tls_error_; }; // A TLS stream for WebSocket connections, where the receive path and the @@ -10720,11 +10725,14 @@ inline bool process_client_socket_ssl( tls::session_t session, socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, - std::chrono::time_point start_time, T callback) { + std::chrono::time_point start_time, T callback, + tls::TlsError *tls_error = nullptr) { SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec, max_timeout_msec, start_time); - return callback(strm); + auto ret = callback(strm); + if (tls_error) { *tls_error = strm.tls_error(); } + return ret; } inline std::pair make_digest_authentication_header( @@ -12791,6 +12799,7 @@ inline SSLSocketStream::SSLSocketStream( write_timeout_sec_(write_timeout_sec), write_timeout_usec_(write_timeout_usec), max_timeout_msec_(max_timeout_msec), start_time_(start_time) { + tls_error_.code = tls::ErrorCode::Success; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Clear AUTO_RETRY for proper non-blocking I/O timeout handling // Note: create_session() also clears this, but SSLClient currently @@ -12843,6 +12852,8 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { auto ret = tls::read(session_, ptr, size, err); if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { error_ = Error::ConnectionClosed; + } else if (ret < 0) { + tls_error_ = err; } return ret; } else if (ensure_readable()) { @@ -12858,7 +12869,9 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { while (--n >= 0 && err.code == tls::ErrorCode::WantRead) { #endif if (tls::pending(session_) > 0) { - return tls::read(session_, ptr, size, err); + ret = tls::read(session_, ptr, size, err); + if (ret >= 0) { return ret; } + break; } else if (wait_readable()) { std::this_thread::sleep_for(std::chrono::microseconds{10}); ret = tls::read(session_, ptr, size, err); @@ -12868,6 +12881,7 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { } } assert(ret < 0); + tls_error_ = err; } else if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { error_ = Error::ConnectionClosed; } @@ -12903,6 +12917,7 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { } } assert(ret < 0); + tls_error_ = err; } return ret; } @@ -15209,6 +15224,14 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, inline bool ClientImpl::send(Request &req, Response &res, Error &error) { std::lock_guard request_mutex_guard(request_mutex_); +#ifdef CPPHTTPLIB_SSL_ENABLED + // Don't let an earlier request's TLS error show up in this one's Result. A + // client that failed to set up its TLS context keeps reporting why. + if (is_valid()) { + last_ssl_error_ = 0; + last_backend_error_ = 0; + } +#endif auto ret = send_(req, res, error); if (error == Error::SSLPeerCouldBeClosed_) { assert(!ret); @@ -18555,10 +18578,20 @@ inline bool SSLClient::process_socket( std::chrono::time_point start_time, std::function callback) { assert(socket.ssl); - return detail::process_client_socket_ssl( + tls::TlsError tls_error; + auto ret = detail::process_client_socket_ssl( socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, - std::move(callback)); + std::move(callback), &tls_error); + + // A request that failed after the handshake reports Error::Read or + // Error::Write; keep the TLS error behind it, such as a fatal alert from + // the server, for Result::ssl_error() and ssl_backend_error() + if (!ret && tls_error.code != tls::ErrorCode::Success) { + last_ssl_error_ = static_cast(tls_error.code); + last_backend_error_ = tls_error.backend_code; + } + return ret; } inline bool SSLClient::is_ssl() const { return true; } diff --git a/test/test.cc b/test/test.cc index 65e69c1e..d37ab081 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14612,6 +14612,8 @@ void ClientCertMissing(const ClientCertServerSetup &setup = nullptr) { if (res.error() == Error::SSLConnection) { EXPECT_NE(0UL, res.ssl_backend_error()); } + // A rejection noticed on the write keeps the TLS error the write hit + if (res.error() == Error::Write) { EXPECT_NE(0, res.ssl_error()); } } TEST(SSLClientServerTest, ClientCertMissing) { ClientCertMissing(); } @@ -14639,6 +14641,39 @@ TEST(SSLClientServerTest, ClientCertStillRequiredAfterVerifyCallbackCleared) { ClientCertStillRequired(true); } +// A TLS error from one request must not show up in a later request's Result +TEST(SSLClientServerTest, SSLErrorNotCarriedToLaterRequest) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("test", "text/plain"); + }); + + auto port = svr.bind_to_any_port(HOST); + 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(); + + SSLClient cli(HOST, port); + cli.set_connection_timeout(30); + cli.enable_server_hostname_verification(false); + + // cert.pem is not trusted, so the handshake fails + ASSERT_FALSE(cli.Get("/test")); + + cli.enable_server_certificate_verification(false); + auto res = cli.Get("/test"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(0, res.ssl_error()); + EXPECT_EQ(0UL, res.ssl_backend_error()); +} + // PEM memory-based constructor tests (works with all TLS backends) void PemMemoryClientCertPresent( const std::string &client_cert_file, From a1d4ccedeaf68371635e93aebf7bcb82d3d812f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 02:40:23 +0000 Subject: [PATCH 16/16] Reset the client's TLS error per attempt and read it under the request lock Reset last_ssl_error_ and last_backend_error_ at the start of each attempt in send_() rather than once in send(), so a request that succeeds on the SSLPeerCouldBeClosed_ retry does not report the TLS error of the attempt before it. Now that every request writes those members, hold request_mutex_ until the Result has read them, so another thread's request on the same client cannot reset or replace them in between. Also pick the TLS error up in a lambda in SSLClient::process_socket() instead of through an out-parameter on process_client_socket_ssl(), which has no other caller. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- httplib.h | 57 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/httplib.h b/httplib.h index 107b0988..9bd653bd 100644 --- a/httplib.h +++ b/httplib.h @@ -10725,14 +10725,11 @@ inline bool process_client_socket_ssl( tls::session_t session, socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, - std::chrono::time_point start_time, T callback, - tls::TlsError *tls_error = nullptr) { + std::chrono::time_point start_time, T callback) { SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec, max_timeout_msec, start_time); - auto ret = callback(strm); - if (tls_error) { *tls_error = strm.tls_error(); } - return ret; + return callback(strm); } inline std::pair make_digest_authentication_header( @@ -15224,14 +15221,6 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, inline bool ClientImpl::send(Request &req, Response &res, Error &error) { std::lock_guard request_mutex_guard(request_mutex_); -#ifdef CPPHTTPLIB_SSL_ENABLED - // Don't let an earlier request's TLS error show up in this one's Result. A - // client that failed to set up its TLS context keeps reporting why. - if (is_valid()) { - last_ssl_error_ = 0; - last_backend_error_ = 0; - } -#endif auto ret = send_(req, res, error); if (error == Error::SSLPeerCouldBeClosed_) { assert(!ret); @@ -15243,6 +15232,15 @@ inline bool ClientImpl::send(Request &req, Response &res, Error &error) { } inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { +#ifdef CPPHTTPLIB_SSL_ENABLED + // Don't let an earlier attempt's TLS error show up in this one's Result. A + // client that failed to set up its TLS context keeps reporting why. + if (is_valid()) { + last_ssl_error_ = 0; + last_backend_error_ = 0; + } +#endif + { std::lock_guard guard(socket_mutex_); @@ -15341,6 +15339,10 @@ inline Result ClientImpl::send(const Request &req) { inline Result ClientImpl::send_(Request &&req) { auto res = detail::make_unique(); auto error = Error::Success; +#ifdef CPPHTTPLIB_SSL_ENABLED + // Hold the request lock until the Result has read this request's TLS error + std::lock_guard request_mutex_guard(request_mutex_); +#endif auto ret = send(req, *res, error); #ifdef CPPHTTPLIB_SSL_ENABLED return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers), @@ -16413,6 +16415,10 @@ inline Result ClientImpl::send_with_content_provider_and_receiver( auto error = Error::Success; +#ifdef CPPHTTPLIB_SSL_ENABLED + // Hold the request lock until the Result has read this request's TLS error + std::lock_guard request_mutex_guard(request_mutex_); +#endif auto res = send_with_content_provider_and_receiver( req, body, content_length, std::move(content_provider), std::move(content_provider_without_length), content_type, @@ -18578,20 +18584,21 @@ inline bool SSLClient::process_socket( std::chrono::time_point start_time, std::function callback) { assert(socket.ssl); - tls::TlsError tls_error; - auto ret = detail::process_client_socket_ssl( + return detail::process_client_socket_ssl( socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, - std::move(callback), &tls_error); - - // A request that failed after the handshake reports Error::Read or - // Error::Write; keep the TLS error behind it, such as a fatal alert from - // the server, for Result::ssl_error() and ssl_backend_error() - if (!ret && tls_error.code != tls::ErrorCode::Success) { - last_ssl_error_ = static_cast(tls_error.code); - last_backend_error_ = tls_error.backend_code; - } - return ret; + [&](detail::SSLSocketStream &strm) { + auto ret = callback(strm); + // A request that failed after the handshake reports Error::Read or + // Error::Write; keep the TLS error behind it, such as a fatal alert + // from the server, for Result::ssl_error() and ssl_backend_error() + const auto &tls_error = strm.tls_error(); + if (!ret && tls_error.code != tls::ErrorCode::Success) { + last_ssl_error_ = static_cast(tls_error.code); + last_backend_error_ = tls_error.backend_code; + } + return ret; + }); } inline bool SSLClient::is_ssl() const { return true; }