Merge origin/master

Both sides keep TLS verify callbacks per context. Keep this side's
storage, which lets a handshake keep the callback it is running and
takes an exception from it as a rejection, and take the other side's
apply_verify_mode(), which works out the verify mode in one place on
every backend, with Mbed TLS and wolfSSL client contexts verifying by
default. Mbed TLS installs its verify callback on every session, and
each session runs its own copy of the user's callback. The redirect
client setup that both sides added is kept once.
This commit is contained in:
yhirose committed 2026-10-17 00:08:39 -04:00
commit 0b382e353e
2 files changed
+465 -132

No files matched your search

+123 -91
View File
@@ -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<VerifyCallback> 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<std::chrono::steady_clock> 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<tls::cert_t> certs;
tls::get_ca_certs(ctx, certs);
std::string pem;
for (auto cert : certs) {
std::vector<unsigned char> 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<std::mutex> 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<Response>();
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<std::recursive_mutex> 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<std::mutex> 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<std::recursive_mutex> 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<int>(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<SSL_CTX *>(ctx),
require
? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT)
: SSL_VERIFY_NONE,
nullptr);
impl::apply_verify_mode(static_cast<SSL_CTX *>(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<std::mutex> guard(impl::verify_callback_mutex());
previous = static_cast<std::shared_ptr<VerifyCallback> *>(
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<uint32_t>(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<MbedTlsSession *>(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_t>(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<impl::MbedTlsContext *>(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_t>(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<bool>(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<char *>(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_t>(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<impl::WolfSSLContext *>(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;
}
+342 -41
View File
@@ -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(SSLServer &)>;
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<int> count1{0};
std::atomic<int> 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<int> 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<int> 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<bool> 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<int> 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