Fix Mbed TLS/wolfSSL hostname verification bugs in set_sni()

The stricter ws::Result error checks added in 6018c7f and 86d0210
exposed 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:
yhirose
2026-08-07 21:31:21 -04:00
parent 86d0210391
commit 19333f80d4
2 changed files with 113 additions and 30 deletions
+112 -24
View File
@@ -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
View File
@@ -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());