From 9a08b01ed7fe666998c225a67f26102ae3f468cb Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 21:38:40 -0400 Subject: [PATCH] Turn off session resumption on wolfSSL servers verifying clients wolfSSL shares its session cache between contexts, so a session resumed on a server that checks client certificates could carry a certificate checked against another server's CA, or against a client CA replaced since. Such a server no longer resumes sessions. --- README.md | 2 ++ httplib.h | 14 +++++++++ test/test.cc | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/README.md b/README.md index 24f9c1ed..34b7632c 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ cpp-httplib supports multiple TLS backends through an abstraction layer: > [!NOTE] > **wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable. +> +> A wolfSSL server that verifies client certificates does not resume TLS sessions: wolfSSL shares its session cache between contexts, so a resumed session could carry a certificate checked against another server's client CA. > [!NOTE] > **BoringSSL (best-effort):** BoringSSL builds under `CPPHTTPLIB_OPENSSL_SUPPORT` and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. Two known behavioral differences vs OpenSSL: (1) BoringSSL's public headers require C++14 or later, so consumers must compile accordingly; (2) hostname verification is SAN-only per RFC 6125 §6.4.4 (no CN fallback). diff --git a/httplib.h b/httplib.h index d020b099..f22845a1 100644 --- a/httplib.h +++ b/httplib.h @@ -22010,6 +22010,18 @@ inline int session_ex_data_index() { return index; } +// A resumed session skips the client's certificate, so a server that checks +// one would let in a certificate checked against another server's CA, as +// wolfSSL shares its session cache between contexts, or against a CA since +// replaced by update_server_client_ca(). +inline void disable_server_resumption(WolfSSLContext *wctx) { + if (!wctx->is_server) { return; } + wolfSSL_CTX_set_session_cache_mode(wctx->ctx, WOLFSSL_SESS_CACHE_OFF); +#ifdef HAVE_SESSION_TICKET + wolfSSL_CTX_NoTicketTLSv12(wctx->ctx); +#endif +} + // wolfSSL verify callback wrapper inline int wolfssl_verify_callback(int preverify_ok, WOLFSSL_X509_STORE_CTX *x509_ctx) { @@ -22279,6 +22291,7 @@ inline void set_verify_client(ctx_t ctx, bool require) { auto wctx = static_cast(ctx); wctx->verify_client = require; if (require) { + impl::disable_server_resumption(wctx); wolfSSL_CTX_set_verify( wctx->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, wctx->verify_callback ? impl::wolfssl_verify_callback : nullptr); @@ -23015,6 +23028,7 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { wctx->verify_callback = std::move(callback); if (wctx->verify_callback) { + impl::disable_server_resumption(wctx); wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, impl::wolfssl_verify_callback); } else { diff --git a/test/test.cc b/test/test.cc index b83585cf..abd97c12 100644 --- a/test/test.cc +++ b/test/test.cc @@ -4978,6 +4978,90 @@ TEST(SSLClientServerTest, ClientResumesSessionUntilClientCAChanges) { } #endif +#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT +// wolfSSL shares its session cache between contexts, so a resumed session +// could carry a client certificate checked against another server's CA, or +// against a client CA since replaced. +TEST(SSLClientServerTest, ClientSessionIsNotResumedPastItsCA) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE); + ASSERT_TRUE(svr.is_valid()); + // Trusts a CA that did not sign the client's certificate. + SSLServer other(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, SERVER_CERT2_FILE); + ASSERT_TRUE(other.is_valid()); + for (auto s : {&svr, &other}) { + s->Get("/test", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + } + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + thread t2 = thread([&]() { ASSERT_TRUE(other.listen(HOST, PORT + 1)); }); + auto se = detail::scope_exit([&] { + other.stop(); + svr.stop(); + t2.join(); + t.join(); + }); + svr.wait_until_ready(); + other.wait_until_ready(); + + auto ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()); + auto ctx_se = detail::scope_exit([&] { wolfSSL_CTX_free(ctx); }); + wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_NONE, nullptr); + ASSERT_EQ(WOLFSSL_SUCCESS, wolfSSL_CTX_use_certificate_file( + ctx, CLIENT_CERT_FILE, WOLFSSL_FILETYPE_PEM)); + ASSERT_EQ(WOLFSSL_SUCCESS, + wolfSSL_CTX_use_PrivateKey_file(ctx, CLIENT_PRIVATE_KEY_FILE, + WOLFSSL_FILETYPE_PEM)); + + WOLFSSL_SESSION *session = nullptr; + auto session_se = detail::scope_exit([&] { + if (session) { wolfSSL_SESSION_free(session); } + }); + + // A request over a new connection, resuming `session` when there is one and + // keeping the connection's own in its place. Tells whether it was answered. + auto get = [&](int port) { + auto error = Error::Success; + auto sock = detail::create_client_socket( + HOST, "", port, AF_UNSPEC, false, false, nullptr, + /*connection_timeout_sec=*/5, 0, + /*read_timeout_sec=*/5, 0, + /*write_timeout_sec=*/5, 0, std::string(), error); + if (sock == INVALID_SOCKET) { return false; } + auto sock_se = detail::scope_exit([&] { detail::close_socket(sock); }); + + auto ssl = wolfSSL_new(ctx); + auto ssl_se = detail::scope_exit([&] { wolfSSL_free(ssl); }); + wolfSSL_set_fd(ssl, static_cast(sock)); + if (session) { wolfSSL_set_session(ssl, session); } + if (wolfSSL_connect(ssl) != WOLFSSL_SUCCESS) { return false; } + + std::string req = "GET /test HTTP/1.1\r\nHost: x\r\n" + "Connection: close\r\n\r\n"; + wolfSSL_write(ssl, req.data(), static_cast(req.size())); + std::string res; + char buf[256]; + int n; + while ((n = wolfSSL_read(ssl, buf, sizeof(buf))) > 0) { + res.append(buf, static_cast(n)); + } + + wolfSSL_shutdown(ssl); + if (session) { wolfSSL_SESSION_free(session); } + session = wolfSSL_get1_session(ssl); + return res.find("200 OK") != std::string::npos; + }; + + ASSERT_TRUE(get(PORT)); + EXPECT_FALSE(get(PORT + 1)); + + ASSERT_TRUE(get(PORT)); + ASSERT_TRUE(replace_client_ca(svr)); + EXPECT_FALSE(get(PORT)); +} +#endif + TEST(SSLClientServerTest, UpdateCertsPemWaitsForHandshakeInProgress) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); ASSERT_TRUE(svr.is_valid());