mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-09-30 12:42:26 +07:00
Fix Mbed TLS/wolfSSL hostname verification bugs in set_sni()
The stricter ws::Result error checks added in6018c7fand86d0210exposed two backend-parity bugs in setup_client_tls_session(), shared by SSLClient and WebSocketClient since their TLS setup was merged: - enable_server_hostname_verification(false) had no effect on Mbed TLS or wolfSSL for DNS hosts: mbedtls_ssl_set_hostname() and wolfSSL_check_domain_name() bind SNI and handshake-time identity checking together, so the identity check ran regardless of the option, failing the handshake before the post-handshake server_hostname_verification check was ever reached. - On a genuine wrong-hostname failure, Mbed TLS reported the generic Error::SSLServerVerification instead of Error::SSLServerHostnameVerification, because MBEDTLS_ERR_X509_CERT_VERIFY_FAILED was mapped without looking at which verify flag actually caused it. Fixes: - set_sni() now takes a verify_hostname flag. wolfSSL skips wolfSSL_check_domain_name() when it's false. Mbed TLS can't request SNI without also arming the CN/SAN check, so it installs a verify callback that masks the mismatch flag instead - a self-contained one when the session has no user verify callback of its own, so it never reads the process-wide set_verify_callback() slot another client may have populated (this was caught by ASAN as a stack-use-after-scope: VerifyCallbackTest.VerifyContextFields leaves a dangling lambda there because MbedTlsSession never had a reason to consult it before). - map_mbedtls_error() now takes the handshake's verify flags and reports HostnameMismatch when CN/SAN mismatch is the only one set, matching the wolfSSL mapping and the post-handshake identity check. - The duplicated verify-flags/error-mapping/backend_code logic in connect() and connect_nonblocking() is factored into fill_mbedtls_tls_error(); the duplicated flag-clearing in the two verify callbacks is factored into mbedtls_clear_cn_mismatch(); both use the existing hostname_mismatch_code() accessor instead of the raw Mbed TLS macro. Also tightens SSLClientTest.ServerHostnameVerificationError_Online to assert the specific error code now that all three backends agree, rather than accepting Mbed TLS's old fallback value. Verified full non-online suite green on OpenSSL (791), Mbed TLS (737), and wolfSSL (735), plus the split build, plus the Online hostname-mismatch test against badssl.com on all three backends.
This commit is contained in:
@@ -4830,7 +4830,7 @@ void set_verify_client(ctx_t ctx, bool require);
|
||||
// Session management
|
||||
session_t create_session(ctx_t ctx, socket_t sock);
|
||||
void free_session(session_t session);
|
||||
bool set_sni(session_t session, const char *hostname);
|
||||
bool set_sni(session_t session, const char *hostname, bool verify_hostname);
|
||||
|
||||
// Handshake (non-blocking capable)
|
||||
TlsError connect(session_t session);
|
||||
@@ -10206,12 +10206,12 @@ inline bool setup_client_tls_session(
|
||||
}
|
||||
if (!session) { return fail(Error::SSLConnection, 0, get_error()); }
|
||||
|
||||
// RFC 6066: SNI must not be set for IP addresses. On Mbed TLS and wolfSSL
|
||||
// set_sni also turns on hostname verification during the handshake, so it
|
||||
// must be skipped for IP hosts as well; their identity is checked
|
||||
// post-handshake below instead.
|
||||
// RFC 6066: SNI must not be set for IP addresses; skip it for IP hosts, so
|
||||
// their identity is checked post-handshake below instead. On Mbed TLS and
|
||||
// wolfSSL, set_sni also drives handshake-time hostname verification, so
|
||||
// options.server_hostname_verification is threaded through here.
|
||||
if (!is_ip_address(host)) {
|
||||
if (!set_sni(session, host.c_str())) {
|
||||
if (!set_sni(session, host.c_str(), options.server_hostname_verification)) {
|
||||
return fail(Error::SSLConnection, 0, get_error());
|
||||
}
|
||||
}
|
||||
@@ -18062,12 +18062,15 @@ inline void free_session(session_t session) {
|
||||
if (session) { SSL_free(static_cast<SSL *>(session)); }
|
||||
}
|
||||
|
||||
inline bool set_sni(session_t session, const char *hostname) {
|
||||
inline bool set_sni(session_t session, const char *hostname,
|
||||
bool /*verify_hostname*/) {
|
||||
if (!session || !hostname) return false;
|
||||
|
||||
auto ssl = static_cast<SSL *>(session);
|
||||
|
||||
// Set SNI (Server Name Indication) only - does not enable verification
|
||||
// Set SNI (Server Name Indication) only - does not enable verification.
|
||||
// OpenSSL never binds identity checking to SNI (that happens post-
|
||||
// handshake in setup_client_tls_session()), so verify_hostname is unused.
|
||||
#if defined(OPENSSL_IS_BORINGSSL)
|
||||
return SSL_set_tlsext_host_name(ssl, hostname) == 1;
|
||||
#else
|
||||
@@ -18705,6 +18708,21 @@ struct MbedTlsSession {
|
||||
unsigned char peeked_byte = 0;
|
||||
bool has_peeked_byte = false;
|
||||
|
||||
// Set by set_sni() when the caller disabled hostname verification, so the
|
||||
// verify callback can clear the CN/SAN mismatch flag while still enforcing
|
||||
// the rest of the chain (Mbed TLS ties SNI and identity checking together;
|
||||
// OpenSSL and wolfSSL keep them independent).
|
||||
bool suppress_hostname_mismatch = false;
|
||||
|
||||
// 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).
|
||||
bool has_verify_callback = false;
|
||||
|
||||
MbedTlsSession() { mbedtls_ssl_init(&ssl); }
|
||||
|
||||
~MbedTlsSession() { mbedtls_ssl_free(&ssl); }
|
||||
@@ -18721,7 +18739,8 @@ inline int &mbedtls_last_error() {
|
||||
}
|
||||
|
||||
// Helper to map Mbed TLS error to ErrorCode
|
||||
inline ErrorCode map_mbedtls_error(int ret, int &out_errno) {
|
||||
inline ErrorCode map_mbedtls_error(int ret, int &out_errno,
|
||||
uint32_t verify_flags) {
|
||||
if (ret == 0) { return ErrorCode::Success; }
|
||||
if (ret == MBEDTLS_ERR_SSL_WANT_READ) { return ErrorCode::WantRead; }
|
||||
if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return ErrorCode::WantWrite; }
|
||||
@@ -18734,11 +18753,34 @@ inline ErrorCode map_mbedtls_error(int ret, int &out_errno) {
|
||||
return ErrorCode::SyscallError;
|
||||
}
|
||||
if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
|
||||
// Unlike OpenSSL/wolfSSL, Mbed TLS folds the CN/SAN identity check into
|
||||
// the handshake's chain verification (see set_sni()); a mismatch there
|
||||
// is reported the same way as any other verify_flags bit. Report it as
|
||||
// HostnameMismatch, matching the other backends and the post-handshake
|
||||
// identity check below, but only when naming is the sole problem -
|
||||
// if the chain itself is also untrusted/expired/etc., that takes
|
||||
// priority over the naming detail.
|
||||
if (verify_flags == static_cast<uint32_t>(hostname_mismatch_code())) {
|
||||
return ErrorCode::HostnameMismatch;
|
||||
}
|
||||
return ErrorCode::CertVerifyFailed;
|
||||
}
|
||||
return ErrorCode::Fatal;
|
||||
}
|
||||
|
||||
// Populates a TlsError from a failed (non-zero) mbedtls_ssl_handshake()
|
||||
// return value, including the verify-flags-dependent HostnameMismatch
|
||||
// mapping; shared by connect() and connect_nonblocking() so the
|
||||
// backend_code policy for that mapping only lives in one place.
|
||||
inline void fill_mbedtls_tls_error(TlsError &err, mbedtls_ssl_context &ssl,
|
||||
int ret) {
|
||||
auto verify_flags = mbedtls_ssl_get_verify_result(&ssl);
|
||||
err.code = map_mbedtls_error(ret, err.sys_errno, verify_flags);
|
||||
err.backend_code = err.code == ErrorCode::HostnameMismatch
|
||||
? static_cast<uint64_t>(verify_flags)
|
||||
: static_cast<uint64_t>(-ret);
|
||||
}
|
||||
|
||||
// A TLS 1.3 NewSessionTicket (signaled by default on Mbed TLS 4.x) is a
|
||||
// non-fatal notification delivered between records, not an error and not
|
||||
// application data, so I/O calls that see it should just be retried. Kept in
|
||||
@@ -18848,18 +18890,44 @@ inline int mbedtls_sni_callback(void *p_ctx, mbedtls_ssl_context *ssl,
|
||||
return 0; // Accept any SNI
|
||||
}
|
||||
|
||||
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 (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.
|
||||
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) {
|
||||
auto &callback = get_verify_callback();
|
||||
if (!callback) { return 0; } // Continue with default verification
|
||||
|
||||
// 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).
|
||||
if (session && session->suppress_hostname_mismatch) {
|
||||
mbedtls_clear_cn_mismatch(flags);
|
||||
}
|
||||
|
||||
auto &callback = get_verify_callback();
|
||||
if (!callback) { return 0; } // Continue with default verification
|
||||
|
||||
// Build context
|
||||
VerifyContext verify_ctx;
|
||||
verify_ctx.session = static_cast<session_t>(session);
|
||||
@@ -19275,6 +19343,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->has_verify_callback = mctx->has_verify_callback;
|
||||
if (mctx->has_verify_callback) {
|
||||
mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback,
|
||||
session);
|
||||
@@ -19287,10 +19356,15 @@ inline void free_session(session_t session) {
|
||||
if (session) { delete static_cast<impl::MbedTlsSession *>(session); }
|
||||
}
|
||||
|
||||
inline bool set_sni(session_t session, const char *hostname) {
|
||||
inline bool set_sni(session_t session, const char *hostname,
|
||||
bool verify_hostname) {
|
||||
if (!session || !hostname) { return false; }
|
||||
auto msession = static_cast<impl::MbedTlsSession *>(session);
|
||||
|
||||
// mbedtls_ssl_set_hostname() both sends the SNI extension and binds the
|
||||
// handshake-time CN/SAN check to `hostname`; the two can't be requested
|
||||
// independently, so a disabled hostname check is handled below by masking
|
||||
// the resulting mismatch flag instead of skipping this call.
|
||||
int ret = mbedtls_ssl_set_hostname(&msession->ssl, hostname);
|
||||
if (ret != 0) {
|
||||
impl::mbedtls_last_error() = ret;
|
||||
@@ -19298,6 +19372,21 @@ 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, which never touches the process-wide callback slot.
|
||||
if (!msession->has_verify_callback) {
|
||||
mbedtls_ssl_set_verify(&msession->ssl,
|
||||
impl::mbedtls_mask_hostname_mismatch_callback,
|
||||
msession);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -19317,8 +19406,7 @@ inline TlsError connect(session_t session) {
|
||||
if (ret == 0) {
|
||||
err.code = ErrorCode::Success;
|
||||
} else {
|
||||
err.code = impl::map_mbedtls_error(ret, err.sys_errno);
|
||||
err.backend_code = static_cast<uint64_t>(-ret);
|
||||
impl::fill_mbedtls_tls_error(err, msession->ssl, ret);
|
||||
impl::mbedtls_last_error() = ret;
|
||||
}
|
||||
|
||||
@@ -19369,10 +19457,7 @@ inline bool connect_nonblocking(session_t session, socket_t sock,
|
||||
}
|
||||
|
||||
// TlsError or timeout
|
||||
if (err) {
|
||||
err->code = impl::map_mbedtls_error(ret, err->sys_errno);
|
||||
err->backend_code = static_cast<uint64_t>(-ret);
|
||||
}
|
||||
if (err) { impl::fill_mbedtls_tls_error(*err, msession->ssl, ret); }
|
||||
impl::mbedtls_last_error() = ret;
|
||||
return false;
|
||||
}
|
||||
@@ -19438,7 +19523,7 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
err.code = impl::map_mbedtls_error(ret, err.sys_errno);
|
||||
err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
|
||||
err.backend_code = static_cast<uint64_t>(-ret);
|
||||
impl::mbedtls_last_error() = ret;
|
||||
// mbedTLS signals a clean close_notify via a negative error code rather
|
||||
@@ -19471,7 +19556,7 @@ inline ssize_t write(session_t session, const void *buf, size_t len,
|
||||
return 0;
|
||||
}
|
||||
|
||||
err.code = impl::map_mbedtls_error(ret, err.sys_errno);
|
||||
err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
|
||||
err.backend_code = static_cast<uint64_t>(-ret);
|
||||
impl::mbedtls_last_error() = ret;
|
||||
return -1;
|
||||
@@ -20434,7 +20519,8 @@ inline void free_session(session_t session) {
|
||||
if (session) { delete static_cast<impl::WolfSSLSession *>(session); }
|
||||
}
|
||||
|
||||
inline bool set_sni(session_t session, const char *hostname) {
|
||||
inline bool set_sni(session_t session, const char *hostname,
|
||||
bool verify_hostname) {
|
||||
if (!session || !hostname) { return false; }
|
||||
auto wsession = static_cast<impl::WolfSSLSession *>(session);
|
||||
|
||||
@@ -20446,8 +20532,10 @@ inline bool set_sni(session_t session, const char *hostname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Also set hostname for verification
|
||||
wolfSSL_check_domain_name(wsession->ssl, hostname);
|
||||
// wolfSSL_check_domain_name binds identity checking to the handshake,
|
||||
// separately from the SNI extension sent above; skip it when hostname
|
||||
// verification is disabled so only the chain is checked, matching OpenSSL.
|
||||
if (verify_hostname) { wolfSSL_check_domain_name(wsession->ssl, hostname); }
|
||||
|
||||
wsession->hostname = hostname;
|
||||
return true;
|
||||
|
||||
+1
-6
@@ -11910,12 +11910,7 @@ TEST(SSLClientTest, ServerHostnameVerificationError_Online) {
|
||||
|
||||
auto res = cli.Get("/");
|
||||
ASSERT_TRUE(!res);
|
||||
|
||||
// The error type depends on when hostname verification occurs:
|
||||
// - OpenSSL: SSLServerHostnameVerification (post-handshake verification)
|
||||
// - Mbed TLS: SSLServerVerification (during handshake)
|
||||
EXPECT_TRUE(res.error() == Error::SSLServerHostnameVerification ||
|
||||
res.error() == Error::SSLServerVerification);
|
||||
EXPECT_EQ(Error::SSLServerHostnameVerification, res.error());
|
||||
|
||||
// Verify backend error is captured for hostname verification failure
|
||||
EXPECT_NE(0UL, res.ssl_backend_error());
|
||||
|
||||
Reference in New Issue
Block a user