diff --git a/httplib.h b/httplib.h index 9f911a01..4cf7157e 100644 --- a/httplib.h +++ b/httplib.h @@ -4079,7 +4079,9 @@ struct MbedTlsContext { mbedtls_x509_crt own_cert; mbedtls_pk_context own_key; bool is_server = false; + // Peer certificate required (set_verify_client()); a client defaults to true bool verify_client = false; + // Set by tls::set_verify_callback(). VerifyCallback verify_callback; MbedTlsContext(); @@ -4103,6 +4105,7 @@ namespace impl { struct WolfSSLContext { WOLFSSL_CTX *ctx = nullptr; bool is_server = false; + // Peer certificate required (set_verify_client()); a client defaults to true bool verify_client = false; // Replaced under verify_callback_mutex() std::shared_ptr verify_callback; @@ -10655,6 +10658,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(); @@ -10667,6 +10674,7 @@ private: time_t max_timeout_msec_; 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 @@ -11188,18 +11196,20 @@ 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); - - std::string pem; 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) + "\n"; + pem += b64.substr(i, 64); + pem += '\n'; } pem += "-----END CERTIFICATE-----\n"; } @@ -13078,6 +13088,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 @@ -13130,6 +13141,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()) { @@ -13145,7 +13158,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); @@ -13155,6 +13170,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; } @@ -13190,6 +13206,7 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { } } assert(ret < 0); + tls_error_ = err; } return ret; } @@ -15478,6 +15495,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_); @@ -15576,6 +15602,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), @@ -16198,7 +16228,11 @@ inline bool ClientImpl::create_redirect_client( // A store given to set_ca_cert_store() lives only in this client's // context, which then holds all that is trusted. if (self.ca_cert_store_set_) { - auto pem = detail::ca_certs_to_pem(self.ctx_); + std::string pem; + { + std::lock_guard guard(self.ctx_mutex_); + pem = detail::ca_certs_to_pem(self.ctx_); + } redirect_client.load_ca_cert_store(pem.data(), pem.size()); redirect_client.ca_cert_store_set_ = true; } else { @@ -16652,6 +16686,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, @@ -18854,7 +18892,18 @@ inline bool SSLClient::process_socket( 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)); + [&](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; } @@ -19643,6 +19692,17 @@ inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { return run_verify_callback(*callback, verify_ctx) ? 1 : 0; } +// 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 has_callback = + SSL_CTX_get_ex_data(ssl_ctx, verify_callback_index()) != nullptr; + 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 @@ -19877,11 +19937,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) { @@ -20546,7 +20602,10 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { std::lock_guard guard(impl::verify_callback_mutex()); previous = static_cast *>( SSL_CTX_get_ex_data(ssl_ctx, index)); - SSL_CTX_set_ex_data(ssl_ctx, index, stored); + if (SSL_CTX_set_ex_data(ssl_ctx, index, stored) != 1) { + delete stored; + return false; + } } // A handshake running the previous callback keeps it until it returns. delete previous; @@ -20561,11 +20620,10 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { #endif } - // Keeps the other mode bits, so a server that requires a client - // certificate goes on requiring one. - SSL_CTX_set_verify(ssl_ctx, - SSL_CTX_get_verify_mode(ssl_ctx) | SSL_VERIFY_PEER, - stored ? impl::openssl_verify_callback : 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; } @@ -20793,32 +20851,21 @@ 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); - // 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). + // 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); } + const auto &callback = session->verify_callback; + if (!callback) { return 0; } // Continue with default verification + // Build context VerifyContext verify_ctx; verify_ctx.session = static_cast(session); @@ -20836,7 +20883,7 @@ inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, verify_ctx.error_string = nullptr; } - bool accepted = run_verify_callback(session->verify_callback, verify_ctx); + bool accepted = run_verify_callback(callback, verify_ctx); if (accepted) { *flags = 0; // Clear all error flags @@ -20845,6 +20892,19 @@ inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED; } +// 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; + } 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() { @@ -20889,7 +20949,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_verify_mode(*ctx); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 @@ -20944,7 +21005,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_verify_mode(*ctx); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 @@ -21193,15 +21254,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_verify_mode(*mctx); } inline session_t create_session(ctx_t ctx, socket_t sock) { @@ -21232,13 +21285,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 + // Installed always, as it also masks the hostname mismatch that set_sni() + // may ask for. The session runs its own copy of the user's callback, which + // set_verify_callback() cannot replace under a handshake. session->verify_callback = mctx->verify_callback; - if (session->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); } @@ -21264,18 +21315,7 @@ 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 masking callback alone. - if (!msession->verify_callback) { - mbedtls_ssl_set_verify(&msession->ssl, - impl::mbedtls_mask_hostname_mismatch_callback, - msession); - } - } + if (!verify_hostname) { msession->suppress_hostname_mismatch = true; } return true; } @@ -21950,12 +21990,7 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { // create_session() hands each session its own copy to run. mbed_ctx->verify_callback = std::move(callback); - - // OPTIONAL runs the callback even when verification is disabled, as - // SSL_VERIFY_PEER does on OpenSSL, unless a client certificate is required. - if (mbed_ctx->verify_callback && !mbed_ctx->verify_client) { - mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); - } + impl::apply_verify_mode(*mbed_ctx); return true; } @@ -22146,6 +22181,18 @@ inline int wolfssl_verify_callback(int preverify_ok, return accepted ? 1 : 0; } +// 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); + if (wctx.verify_client || has_callback) { disable_server_resumption(&wctx); } + 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( @@ -22182,7 +22229,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); } @@ -22208,7 +22256,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, @@ -22381,21 +22429,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 || wctx->verify_callback) { - impl::disable_server_resumption(wctx); - } - 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) { @@ -23125,9 +23159,7 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { // A handshake running the previous callback keeps it until it returns. wctx->verify_callback.swap(stored); } - - // A server that requires a client certificate goes on requiring one. - set_verify_client(ctx, wctx->verify_client); + impl::apply_verify_mode(*wctx); return true; } diff --git a/test/test.cc b/test/test.cc index 0ee82450..1078e258 100644 --- a/test/test.cc +++ b/test/test.cc @@ -16056,17 +16056,19 @@ 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; + 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()) { - using namespace httplib::tls; - + const std::string &client_encrypted_private_key_pass = std::string(), + 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()); + if (setup) { setup(svr); } svr.Get("/test", [&](const Request &req, Response &res) { res.set_content("test", "text/plain"); @@ -16078,7 +16080,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(); @@ -16087,7 +16091,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); @@ -16107,6 +16111,104 @@ TEST(SSLClientServerTest, ClientEncryptedCertPresent) { CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); } +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()); + if (setup) { setup(svr); } + + svr.Get("/test", [](const Request &, Response &) { FAIL(); }); + + 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); + // 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); + + 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 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) { + 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(); } + +// 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); +} + +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, @@ -16175,41 +16277,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()); @@ -17104,6 +17171,88 @@ TEST(VerifyCallbackTest, SessionMatchesSessionVerifier) { EXPECT_EQ(session_verifier_session, verifier_session); } +// 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; @@ -17709,6 +17858,158 @@ TEST(SSLClientRedirectTest, CaCertStoreReplacesEarlierPem) { EXPECT_EQ(Error::SSLServerVerification, res.error()); } #endif + +// 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()); +} + +// 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