From 95173818148513265d77a3b7e97e7fae44b40157 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Mon, 20 Jul 2026 21:02:56 +0200 Subject: [PATCH 1/5] feat(tls)!: move hostname configuration to tls_stream The hostname used for SNI and peer certificate verification is per-connection state, not shared configuration. Remove tls_context::set_hostname and add tls_stream::set_hostname, which takes effect at the next handshake, persists across reset(), and disables SNI and hostname verification when given an empty name. A stream can now be reset and handshaked again against a different hostname. On the OpenSSL backend, reset() now also drops the cached TLS session so a post-reset handshake performs full certificate verification instead of silently resuming. --- .../pages/3.tutorials/3b.http-client.adoc | 2 +- .../pages/3.tutorials/3d.tls-context.adoc | 6 +- doc/modules/ROOT/pages/4.guide/4l.tls.adoc | 14 +- example/https-client/https_client.cpp | 2 +- example/tls_context_examples.cpp | 4 - include/boost/corosio/openssl_stream.hpp | 9 +- include/boost/corosio/tls_context.hpp | 29 +-- include/boost/corosio/tls_stream.hpp | 24 ++ include/boost/corosio/wolfssl_stream.hpp | 5 +- src/corosio/src/tls/context.cpp | 6 - src/corosio/src/tls/detail/context_impl.hpp | 1 - src/openssl/src/openssl_stream.cpp | 36 ++- src/wolfssl/src/wolfssl_stream.cpp | 19 +- test/unit/openssl_stream.cpp | 3 + test/unit/tls_context.cpp | 16 -- test/unit/tls_stream_tests.hpp | 246 +++++++++++++++++- test/unit/wolfssl_stream.cpp | 3 + 17 files changed, 328 insertions(+), 97 deletions(-) diff --git a/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc index 93493af82..686c06067 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc @@ -274,7 +274,6 @@ capy::task run_https_client( // Configure the TLS context corosio::tls_context ctx; - ctx.set_hostname(hostname); if (auto ec = ctx.set_default_verify_paths(); ec) throw std::system_error(ec); if (auto ec = ctx.set_verify_mode(corosio::tls_verify_mode::peer); ec) @@ -282,6 +281,7 @@ capy::task run_https_client( // Wrap the connected socket without taking ownership (pointer form) corosio::wolfssl_stream secure(&s, ctx); + secure.set_hostname(hostname); if (auto [ec] = co_await secure.handshake( corosio::wolfssl_stream::client); ec) throw std::system_error(ec); diff --git a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc index 270ada7fc..e3b1dd0d3 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc @@ -346,11 +346,11 @@ Typical usage: === Hostname Verification -For clients, set the expected server hostname: +For clients, set the expected server hostname on the stream before the handshake: [source,cpp] ---- -ctx.set_hostname( "api.example.com" ); +secure.set_hostname( "api.example.com" ); ---- This enables: @@ -570,10 +570,10 @@ tls_context make_https_client_context() // Usage with TLS stream tls_context ctx = make_https_client_context(); -ctx.set_hostname("api.example.com"); // Set before creating stream // Pointer form wraps the connected socket without taking ownership corosio::wolfssl_stream secure( &sock, ctx ); +secure.set_hostname( "api.example.com" ); co_await secure.handshake( corosio::wolfssl_stream::client ); ---- diff --git a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc index 75dde6984..1e25efabf 100644 --- a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc +++ b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc @@ -49,7 +49,10 @@ and checks the hostname: tls_context ctx; ctx.set_default_verify_paths(); // trust the system CAs ctx.set_verify_mode(tls_verify_mode::peer); // require + verify the peer -ctx.set_hostname("example.com"); // SNI + hostname check + +corosio::tcp_socket sock(ioc); +corosio::wolfssl_stream secure(&sock, ctx); +secure.set_hostname("example.com"); // SNI + hostname check ---- Trust anchors can also be supplied explicitly with @@ -80,7 +83,6 @@ The typical flow: ---- // 1. Configure a context tls_context ctx; -ctx.set_hostname("api.example.com"); if (auto ec = ctx.set_default_verify_paths(); ec) throw std::system_error(ec); if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec) @@ -94,6 +96,7 @@ if (auto [ec] = co_await sock.connect(endpoint); ec) // 3. Wrap the connected socket (pointer form; does not take ownership) corosio::wolfssl_stream secure(&sock, ctx); +secure.set_hostname("api.example.com"); if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec) throw std::system_error(ec); @@ -301,11 +304,11 @@ For HTTPS clients, use `peer`. For servers requiring client certificates ==== Hostname Verification (SNI) -For clients, set the expected server hostname: +For clients, set the expected server hostname on the stream: [source,cpp] ---- -ctx.set_hostname("api.example.com"); +secure.set_hostname("api.example.com"); ---- This does two things: @@ -629,7 +632,6 @@ capy::task https_get( // Configure TLS tls_context ctx; - ctx.set_hostname(hostname); if (auto ec = ctx.set_default_verify_paths(); ec) throw std::system_error(ec); if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec) @@ -637,6 +639,7 @@ capy::task https_get( // Wrap the connected socket (pointer form) and handshake corosio::wolfssl_stream secure(&sock, ctx); + secure.set_hostname(hostname); if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec) throw std::system_error(ec); @@ -746,7 +749,6 @@ server's CA explicitly with `load_verify_file()` instead. tls_context client_ctx; client_ctx.set_default_verify_paths(); client_ctx.set_verify_mode(tls_verify_mode::peer); -client_ctx.set_hostname("server.example.com"); // Provide client certificate client_ctx.use_certificate_file("client.crt", tls_file_format::pem); diff --git a/example/https-client/https_client.cpp b/example/https-client/https_client.cpp index a6971c145..cb9126fd4 100644 --- a/example/https-client/https_client.cpp +++ b/example/https-client/https_client.cpp @@ -78,7 +78,6 @@ run_client( // Configure TLS context corosio::tls_context ctx; - ctx.set_hostname(hostname); if (auto ec = ctx.set_default_verify_paths(); ec) throw std::system_error(ec); if (auto ec = ctx.set_verify_mode(corosio::tls_verify_mode::peer); ec) @@ -86,6 +85,7 @@ run_client( // Wrap socket in TLS stream corosio::wolfssl_stream secure(&s, ctx); + secure.set_hostname(hostname); // Perform TLS handshake if (auto [ec] = co_await secure.handshake(corosio::wolfssl_stream::client); ec) diff --git a/example/tls_context_examples.cpp b/example/tls_context_examples.cpp index dda62faa5..579c3e15a 100644 --- a/example/tls_context_examples.cpp +++ b/example/tls_context_examples.cpp @@ -50,9 +50,6 @@ tls_context make_https_client() // Verify the server certificate must(ctx.set_verify_mode( tls_verify_mode::peer )); - // Set the hostname for SNI and certificate verification - ctx.set_hostname( "api.example.com" ); - return ctx; } @@ -65,7 +62,6 @@ tls_context make_pinned_ca_client( std::string_view ca_pem ) must(ctx.add_certificate_authority( ca_pem )); must(ctx.set_verify_mode( tls_verify_mode::peer )); - ctx.set_hostname( "internal.example.com" ); return ctx; } diff --git a/include/boost/corosio/openssl_stream.hpp b/include/boost/corosio/openssl_stream.hpp index 6c474b631..f1395d7e8 100644 --- a/include/boost/corosio/openssl_stream.hpp +++ b/include/boost/corosio/openssl_stream.hpp @@ -51,7 +51,6 @@ namespace boost::corosio { @par Example @code tls_context ctx; - ctx.set_hostname("example.com"); ctx.set_verify_mode(tls_verify_mode::peer); corosio::tcp_socket sock(ioc); @@ -59,6 +58,7 @@ namespace boost::corosio { // Reference mode - sock must outlive tls corosio::openssl_stream tls(&sock, ctx); + tls.set_hostname("example.com"); auto [ec] = co_await tls.handshake(openssl_stream::client); // Or owning mode - tls owns the socket @@ -169,13 +169,18 @@ class BOOST_COROSIO_DECL openssl_stream final : public tls_stream Clears internal buffers and session data so the stream can perform a new handshake on the same underlying - connection. + connection. The previous session is discarded and never + resumed, so a handshake after `reset()` is always a full + handshake. @par Preconditions No TLS operation may be in progress on this stream. */ void reset() override; + /// Set the peer hostname for SNI and certificate verification. + void set_hostname(std::string_view hostname) override; + /// Return the underlying stream. capy::any_stream& next_layer() noexcept override { diff --git a/include/boost/corosio/tls_context.hpp b/include/boost/corosio/tls_context.hpp index 037b00829..ea0be5e2f 100644 --- a/include/boost/corosio/tls_context.hpp +++ b/include/boost/corosio/tls_context.hpp @@ -244,10 +244,10 @@ tls_context_data const& get_tls_context_data(tls_context const&) noexcept; corosio::tls_context ctx; ctx.set_default_verify_paths(); ctx.set_verify_mode( corosio::tls_verify_mode::peer ); - ctx.set_hostname( "example.com" ); // Use with a TLS stream corosio::openssl_stream secure( &sock, ctx ); + secure.set_hostname( "example.com" ); co_await secure.handshake( corosio::tls_stream::client ); @endcode @@ -593,7 +593,7 @@ class BOOST_COROSIO_DECL tls_context The system store is loaded when the native context is first built from this context. For a verified-safe client, combine this with `set_verify_mode( tls_verify_mode::peer )` and, when connecting by - name, `set_hostname()`. + name, `tls_stream::set_hostname()`. @return Success. The request is recorded and applied when the native context is built; if the system store cannot be loaded @@ -611,7 +611,6 @@ class BOOST_COROSIO_DECL tls_context // Trust the same CAs as the system ctx.set_default_verify_paths(); ctx.set_verify_mode( tls_verify_mode::peer ); - ctx.set_hostname( "example.com" ); @endcode @see load_verify_file @@ -836,28 +835,6 @@ class BOOST_COROSIO_DECL tls_context template std::error_code set_verify_callback(Callback callback); - /** Set the expected server hostname for verification. - - For client connections, sets the hostname that the server - certificate must match. This enables: - - 1. SNI (Server Name Indication) — tells the server which - certificate to present (for virtual hosting) - 2. Hostname verification — validates the certificate's - Subject Alternative Name or Common Name matches - - @param hostname The expected server hostname. - - @par Example - @code - ctx.set_hostname( "api.example.com" ); - @endcode - - @note This is typically required for HTTPS clients to ensure - they're connecting to the intended server. - */ - void set_hostname(std::string_view hostname); - /** Set a callback for Server Name Indication (SNI). For server connections, this callback is invoked during the TLS @@ -886,7 +863,7 @@ class BOOST_COROSIO_DECL tls_context create separate contexts and select the appropriate one before creating the TLS stream. - @see set_hostname + @see tls_stream::set_hostname */ template void set_servername_callback(Callback callback); diff --git a/include/boost/corosio/tls_stream.hpp b/include/boost/corosio/tls_stream.hpp index d03e610e7..e733334a5 100644 --- a/include/boost/corosio/tls_stream.hpp +++ b/include/boost/corosio/tls_stream.hpp @@ -145,6 +145,30 @@ class BOOST_COROSIO_DECL tls_stream */ virtual void reset() = 0; + /** Set the peer hostname for SNI and certificate verification. + + Configures the hostname sent in the TLS Server Name + Indication extension and matched against the peer + certificate during verification. The value takes effect + at the next `handshake()`; an established session is not + affected. It persists across `reset()`, so a stream reused + to reach a different host must set the new name before + handshaking again. + + An empty hostname (the default) disables SNI and hostname + verification. + + @note The hostname is meaningful for client handshakes; + backends may ignore it when handshaking as a server. + + @par Postconditions + The next `handshake()` uses `hostname` for SNI and + certificate verification, or neither if it is empty. + + @param hostname The peer hostname, or empty to disable. + */ + virtual void set_hostname(std::string_view hostname) = 0; + /** Returns a reference to the underlying stream. Provides access to the type-erased underlying stream for diff --git a/include/boost/corosio/wolfssl_stream.hpp b/include/boost/corosio/wolfssl_stream.hpp index aefa35a44..5f36ec27c 100644 --- a/include/boost/corosio/wolfssl_stream.hpp +++ b/include/boost/corosio/wolfssl_stream.hpp @@ -51,7 +51,6 @@ namespace boost::corosio { @par Example @code tls_context ctx; - ctx.set_hostname("example.com"); ctx.set_verify_mode(tls_verify_mode::peer); corosio::tcp_socket sock(ioc); @@ -59,6 +58,7 @@ namespace boost::corosio { // Reference mode - sock must outlive tls corosio::wolfssl_stream tls(&sock, ctx); + tls.set_hostname("example.com"); auto [ec] = co_await tls.handshake(wolfssl_stream::client); // Or owning mode - tls owns the socket @@ -176,6 +176,9 @@ class BOOST_COROSIO_DECL wolfssl_stream final : public tls_stream */ void reset() override; + /// Set the peer hostname for SNI and certificate verification. + void set_hostname(std::string_view hostname) override; + /// Return the underlying stream. capy::any_stream& next_layer() noexcept override { diff --git a/src/corosio/src/tls/context.cpp b/src/corosio/src/tls/context.cpp index ca5a7ff45..c59a0793f 100644 --- a/src/corosio/src/tls/context.cpp +++ b/src/corosio/src/tls/context.cpp @@ -221,12 +221,6 @@ tls_context::set_verify_depth(int depth) return {}; } -void -tls_context::set_hostname(std::string_view hostname) -{ - impl_->hostname = std::string(hostname); -} - void tls_context::set_servername_callback_impl( std::function callback) diff --git a/src/corosio/src/tls/detail/context_impl.hpp b/src/corosio/src/tls/detail/context_impl.hpp index 324f8a52d..ad14d1f40 100644 --- a/src/corosio/src/tls/detail/context_impl.hpp +++ b/src/corosio/src/tls/detail/context_impl.hpp @@ -69,7 +69,6 @@ struct tls_context_data tls_verify_mode verification_mode = tls_verify_mode::none; int verify_depth = 100; - std::string hostname; std::function verify_callback; // SNI (Server Name Indication) diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index 7d67175b3..5d03a22d1 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -77,16 +77,18 @@ tls_method_compat() noexcept inline void apply_hostname_verification(SSL* ssl, std::string const& hostname) { - if (hostname.empty()) - return; + // SSL_clear retains a previously applied name; an empty hostname + // must clear SNI and the verify-param host or a reset stream + // would leak the old peer's name into the next handshake + char const* name = hostname.empty() ? nullptr : hostname.c_str(); - SSL_set_tlsext_host_name(ssl, hostname.c_str()); + SSL_set_tlsext_host_name(ssl, name); #if OPENSSL_VERSION_NUMBER >= 0x10100000L - SSL_set1_host(ssl, hostname.c_str()); + SSL_set1_host(ssl, name); #else if (auto* param = SSL_get0_param(ssl)) - X509_VERIFY_PARAM_set1_host(param, hostname.c_str(), 0); + X509_VERIFY_PARAM_set1_host(param, name, 0); #endif } @@ -656,6 +658,9 @@ struct openssl_stream::impl BIO* ext_bio_ = nullptr; bool used_ = false; + // Per-stream SNI/verification hostname, set via set_hostname(). + std::string hostname_; + // ALPN protocol negotiated during the handshake (empty if none). std::string alpn_selected_; @@ -686,15 +691,18 @@ struct openssl_stream::impl // Preserves SSL* and BIO pair, releases session state SSL_clear(ssl_); + // SSL_clear() retains the negotiated session so a subsequent + // handshake on this SSL* can resume it. Resumed handshakes skip + // certificate/hostname re-verification, which would let a changed + // set_hostname() go unchecked after reset(); drop it to force a + // full handshake. + SSL_set_session(ssl_, nullptr); + // Drain stale data from the external BIO char drain[1024]; while (BIO_ctrl_pending(ext_bio_) > 0) BIO_read(ext_bio_, drain, sizeof(drain)); - // SSL_clear clears per-session settings; reapply hostname - auto& cd = detail::get_tls_context_data(ctx_); - apply_hostname_verification(ssl_, cd.hostname); - alpn_selected_.clear(); used_ = false; } @@ -918,6 +926,8 @@ struct openssl_stream::impl if (used_) reset(); + apply_hostname_verification(ssl_, hostname_); + std::error_code ec; // Client offers its ALPN protocol list; the server selects via the @@ -1077,8 +1087,6 @@ struct openssl_stream::impl SSL_set_bio(ssl_, int_bio, int_bio); - apply_hostname_verification(ssl_, cd.hostname); - return {}; } }; @@ -1159,6 +1167,12 @@ openssl_stream::reset() impl_->reset(); } +void +openssl_stream::set_hostname(std::string_view hostname) +{ + impl_->hostname_ = hostname; +} + std::string_view openssl_stream::name() const noexcept { diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index 773cc5c7a..43b50ad4c 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -662,6 +662,9 @@ struct wolfssl_stream::impl WOLFSSL* ssl_ = nullptr; bool used_ = false; + // Per-stream SNI/verification hostname, set via set_hostname(). + std::string hostname_; + // ALPN protocol negotiated during the handshake (empty if none). std::string alpn_selected_; @@ -1449,16 +1452,16 @@ struct wolfssl_stream::impl } #endif - // Apply per-session config (SNI + hostname verification) from context - if (type == wolfssl_stream::client && !cd.hostname.empty()) + // Apply per-session config (SNI + hostname verification) + if (type == wolfssl_stream::client && !hostname_.empty()) { // Set SNI extension so server knows which cert to present wolfSSL_UseSNI( - ssl_, WOLFSSL_SNI_HOST_NAME, cd.hostname.data(), - static_cast(cd.hostname.size())); + ssl_, WOLFSSL_SNI_HOST_NAME, hostname_.data(), + static_cast(hostname_.size())); // Enable hostname verification (checks CN/SAN in peer cert) - wolfSSL_check_domain_name(ssl_, cd.hostname.c_str()); + wolfSSL_check_domain_name(ssl_, hostname_.c_str()); } return {}; @@ -1533,6 +1536,12 @@ wolfssl_stream::reset() impl_->reset(); } +void +wolfssl_stream::set_hostname(std::string_view hostname) +{ + impl_->hostname_ = hostname; +} + std::string_view wolfssl_stream::name() const noexcept { diff --git a/test/unit/openssl_stream.cpp b/test/unit/openssl_stream.cpp index ea232f159..8104cd9dd 100644 --- a/test/unit/openssl_stream.cpp +++ b/test/unit/openssl_stream.cpp @@ -166,6 +166,9 @@ struct openssl_stream_test test::testCertificateValidation(make_stream); test::testSni(make_stream); test::testSniCallback(make_stream); + test::testHostnamePersistence(make_stream); + test::testHostnameRedirect(make_stream); + test::testHostnameClear(make_stream); test::testAlpnAccessorEmpty(make_stream); test::testAlpn(make_stream, /*alpn_supported=*/true); test::testAlpnNoOverlap(make_stream, /*alpn_supported=*/true); diff --git a/test/unit/tls_context.cpp b/test/unit/tls_context.cpp index b75bc42a7..a924b44ef 100644 --- a/test/unit/tls_context.cpp +++ b/test/unit/tls_context.cpp @@ -416,21 +416,6 @@ struct tls_context_test BOOST_TEST_EQ(detail::get_tls_context_data(ctx).verify_depth, 5); } - void testHostname() - { - tls_context ctx; - BOOST_TEST_EQ(detail::get_tls_context_data(ctx).hostname, std::string()); - - ctx.set_hostname("api.example.com"); - BOOST_TEST_EQ(detail::get_tls_context_data(ctx).hostname, - std::string("api.example.com")); - - // Overwrite - ctx.set_hostname("other.example.com"); - BOOST_TEST_EQ(detail::get_tls_context_data(ctx).hostname, - std::string("other.example.com")); - } - void testServernameCallback() { tls_context ctx; @@ -554,7 +539,6 @@ struct tls_context_test testAlpn(); testVerifyModeAndDepth(); - testHostname(); testServernameCallback(); testPasswordCallback(); diff --git a/test/unit/tls_stream_tests.hpp b/test/unit/tls_stream_tests.hpp index bacb0d5de..a70449b30 100644 --- a/test/unit/tls_stream_tests.hpp +++ b/test/unit/tls_stream_tests.hpp @@ -419,20 +419,29 @@ testSni(StreamFactory make_stream) // Correct hostname succeeds { io_context ioc; - auto client_ctx = make_client_context(); - client_ctx.set_hostname("www.example.com"); - auto server_ctx = make_server_context(); - run_tls_test(ioc, client_ctx, server_ctx, make_stream, make_stream); + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + auto with_hostname = [&](auto& s, tls_context const& ctx) { + auto tls = make_stream(s, ctx); + tls.set_hostname("www.example.com"); + return tls; + }; + run_tls_test( + ioc, client_ctx, server_ctx, with_hostname, make_stream); } // Wrong hostname fails { io_context ioc; - auto client_ctx = make_client_context(); - client_ctx.set_hostname("wrong.example.com"); - auto server_ctx = make_server_context(); + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + auto with_hostname = [&](auto& s, tls_context const& ctx) { + auto tls = make_stream(s, ctx); + tls.set_hostname("wrong.example.com"); + return tls; + }; run_tls_test_fail( - ioc, client_ctx, server_ctx, make_stream, make_stream); + ioc, client_ctx, server_ctx, with_hostname, make_stream); } } @@ -444,8 +453,12 @@ testSniCallback(StreamFactory make_stream) // SNI callback accepts hostname { io_context ioc; - auto client_ctx = make_client_context(); - client_ctx.set_hostname("www.example.com"); + auto client_ctx = make_client_context(); + auto with_hostname = [&](auto& s, tls_context const& ctx) { + auto tls = make_stream(s, ctx); + tls.set_hostname("www.example.com"); + return tls; + }; auto server_ctx = make_server_context(); server_ctx.set_servername_callback( @@ -453,14 +466,19 @@ testSniCallback(StreamFactory make_stream) return hostname == "www.example.com"; }); - run_tls_test(ioc, client_ctx, server_ctx, make_stream, make_stream); + run_tls_test( + ioc, client_ctx, server_ctx, with_hostname, make_stream); } // SNI callback rejects hostname { io_context ioc; - auto client_ctx = make_client_context(); - client_ctx.set_hostname("www.example.com"); + auto client_ctx = make_client_context(); + auto with_hostname = [&](auto& s, tls_context const& ctx) { + auto tls = make_stream(s, ctx); + tls.set_hostname("www.example.com"); + return tls; + }; auto server_ctx = make_server_context(); server_ctx.set_servername_callback( @@ -469,7 +487,7 @@ testSniCallback(StreamFactory make_stream) }); run_tls_test_fail( - ioc, client_ctx, server_ctx, make_stream, make_stream); + ioc, client_ctx, server_ctx, with_hostname, make_stream); } // Client sends no SNI: the server callback is never consulted and @@ -491,6 +509,206 @@ testSniCallback(StreamFactory make_stream) } } +namespace hostname_test_detail { + +// One handshake round with a clean close_notify exchange so the +// transport is empty for the next round. On handshake failure the +// mockets are closed to unblock the peer. +// +// m1 and m2 are independently typed: make_mocket_pair returns a +// (mocket, peer socket) pair, not two mockets of the same type. +template +void +run_hostname_round( + io_context& ioc, + Stream& client, + Stream& server, + Mocket1& m1, + Mocket2& m2, + bool expect_ok) +{ + std::error_code client_ec; + std::error_code server_ec; + + auto hs_client = [&]() -> capy::task<> { + auto [ec] = co_await client.handshake(tls_stream::client); + client_ec = ec; + if (ec) + { + m1.close(); // NOLINT(bugprone-unused-return-value) + m2.close(); // NOLINT(bugprone-unused-return-value) + } + }; + auto hs_server = [&]() -> capy::task<> { + auto [ec] = co_await server.handshake(tls_stream::server); + server_ec = ec; + }; + + capy::run_async(ioc.get_executor())(hs_client()); + capy::run_async(ioc.get_executor())(hs_server()); + ioc.run(); + ioc.restart(); + + if (expect_ok) + { + BOOST_TEST(!client_ec); + BOOST_TEST(!server_ec); + if (client_ec || server_ec) + return; + + auto sd_client = [&]() -> capy::task<> { + (void)co_await client.shutdown(); + }; + auto sd_server = [&]() -> capy::task<> { + char drain[32]; + (void)co_await server.read_some( + capy::mutable_buffer(drain, sizeof(drain))); + (void)co_await server.shutdown(); + }; + capy::run_async(ioc.get_executor())(sd_client()); + capy::run_async(ioc.get_executor())(sd_server()); + ioc.run(); + ioc.restart(); + } + else + { + // The server's error after a client-side verification abort is + // backend- and timing-dependent, so only the client is asserted. + BOOST_TEST(client_ec != std::error_code()); + } +} + +} // namespace hostname_test_detail + +/** A hostname set once persists across reset(): SNI is sent on + every subsequent handshake, not just the first. */ +template +void +testHostnamePersistence(StreamFactory make_stream) +{ + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair(ioc); + + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + + std::size_t sni_count = 0; + server_ctx.set_servername_callback( + [&sni_count](std::string_view hostname) -> bool { + if (hostname == "www.example.com") + ++sni_count; + return true; + }); + + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); + + client.set_hostname("www.example.com"); + + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, true); + client.reset(); + server.reset(); + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, true); + + BOOST_TEST_EQ(sni_count, 2u); + + m1.close(); // NOLINT(bugprone-unused-return-value) + m2.close(); // NOLINT(bugprone-unused-return-value) +} + +/** A new hostname set after reset() takes effect on the next + handshake: the server sees the new SNI name and verification + is enforced against it. */ +template +void +testHostnameRedirect(StreamFactory make_stream) +{ + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair(ioc); + + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + + std::vector seen; + server_ctx.set_servername_callback( + [&seen](std::string_view hostname) -> bool { + seen.emplace_back(hostname); + return true; + }); + + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); + + // Round 1: name matches the certificate + client.set_hostname("www.example.com"); + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, true); + + client.reset(); + server.reset(); + + // Round 2: new name is sent in SNI and fails verification + // against the old certificate + client.set_hostname("api.example.com"); + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, false); + + BOOST_TEST_EQ(seen.size(), 2u); + if (seen.size() == 2u) + { + BOOST_TEST_EQ(seen[0], "www.example.com"); + BOOST_TEST_EQ(seen[1], "api.example.com"); + } + + if (m1.is_open()) + m1.close(); // NOLINT(bugprone-unused-return-value) + if (m2.is_open()) + m2.close(); // NOLINT(bugprone-unused-return-value) +} + +/** set_hostname("") after reset() disables SNI and verification: + the next handshake sends no SNI, so the servername callback is + not consulted and the handshake still succeeds. */ +template +void +testHostnameClear(StreamFactory make_stream) +{ + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair(ioc); + + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + + std::size_t sni_count = 0; + server_ctx.set_servername_callback( + [&sni_count](std::string_view) -> bool { + ++sni_count; + return true; + }); + + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); + + client.set_hostname("www.example.com"); + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, true); + + client.reset(); + server.reset(); + + client.set_hostname(""); + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, true); + + // Only round 1 sent SNI + BOOST_TEST_EQ(sni_count, 1u); + + m1.close(); // NOLINT(bugprone-unused-return-value) + m2.close(); // NOLINT(bugprone-unused-return-value) +} + /** A freshly constructed stream reports no negotiated ALPN protocol. The accessor must return an empty view before any handshake, on diff --git a/test/unit/wolfssl_stream.cpp b/test/unit/wolfssl_stream.cpp index 089e3c296..43ee9bcfa 100644 --- a/test/unit/wolfssl_stream.cpp +++ b/test/unit/wolfssl_stream.cpp @@ -158,6 +158,9 @@ struct wolfssl_stream_test test::testCertificateValidation(make_stream); test::testSni(make_stream); test::testSniCallback(make_stream); + test::testHostnamePersistence(make_stream); + test::testHostnameRedirect(make_stream); + test::testHostnameClear(make_stream); test::testAlpnAccessorEmpty(make_stream); // Whether the linked WolfSSL can honor a verify callback on success // (WOLFSSL_ALWAYS_VERIFY_CB) is a build-time property, queried here From b503a42dc135f1ed03d65e8a22a9d1296db2271a Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Tue, 21 Jul 2026 16:12:08 +0200 Subject: [PATCH 2/5] refactor(tls)!: replace tls_stream::handshake_type with tls_role The scoped tls_role enum moves from tls_context.hpp to tls_stream.hpp, where its only consumer lives, and handshake() takes it in place of the nested unscoped handshake_type, which is removed. Backend plumbing that degraded the role to int (do_handshake, init_ssl_for_role) now carries tls_role end to end. --- .../pages/3.tutorials/3b.http-client.adoc | 2 +- .../pages/3.tutorials/3d.tls-context.adoc | 2 +- doc/modules/ROOT/pages/4.guide/4l.tls.adoc | 16 ++-- example/https-client/https_client.cpp | 2 +- include/boost/corosio/openssl_stream.hpp | 6 +- include/boost/corosio/tls_context.hpp | 17 +--- include/boost/corosio/tls_stream.hpp | 26 +++--- include/boost/corosio/wolfssl_stream.hpp | 6 +- src/openssl/src/openssl_stream.cpp | 10 +-- src/wolfssl/src/wolfssl_stream.cpp | 18 ++--- test/unit/test_utils.hpp | 57 +++++-------- test/unit/tls_stream_stress.cpp | 14 ++-- test/unit/tls_stream_tests.hpp | 80 +++++++++---------- 13 files changed, 112 insertions(+), 144 deletions(-) diff --git a/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc index 686c06067..39f58c4ec 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc @@ -283,7 +283,7 @@ capy::task run_https_client( corosio::wolfssl_stream secure(&s, ctx); secure.set_hostname(hostname); if (auto [ec] = co_await secure.handshake( - corosio::wolfssl_stream::client); ec) + corosio::tls_role::client); ec) throw std::system_error(ec); co_await do_request(secure, hostname); diff --git a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc index e3b1dd0d3..fc35802d2 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc @@ -574,7 +574,7 @@ tls_context ctx = make_https_client_context(); // Pointer form wraps the connected socket without taking ownership corosio::wolfssl_stream secure( &sock, ctx ); secure.set_hostname( "api.example.com" ); -co_await secure.handshake( corosio::wolfssl_stream::client ); +co_await secure.handshake( corosio::tls_role::client ); ---- === TLS Server Context diff --git a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc index 1e25efabf..4e9778578 100644 --- a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc +++ b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc @@ -97,7 +97,7 @@ if (auto [ec] = co_await sock.connect(endpoint); ec) // 3. Wrap the connected socket (pointer form; does not take ownership) corosio::wolfssl_stream secure(&sock, ctx); secure.set_hostname("api.example.com"); -if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec) +if (auto [ec] = co_await secure.handshake(tls_role::client); ec) throw std::system_error(ec); // 4. Use encrypted I/O @@ -404,18 +404,18 @@ the `capy::Stream` concept, so composed operations like `capy::read` and [source,cpp] ---- +enum class tls_role { client, server }; + class tls_stream { public: - enum handshake_type { client, server }; - template auto read_some(B const& buffers); // Decrypt and read template auto write_some(B const& buffers); // Encrypt and write - virtual capy::io_task<> handshake(handshake_type type) = 0; + virtual capy::io_task<> handshake(tls_role role) = 0; virtual capy::io_task<> shutdown() = 0; virtual capy::any_stream& next_layer() noexcept = 0; // Underlying stream @@ -467,7 +467,7 @@ Before encrypted communication, perform the TLS handshake: [source,cpp] ---- -auto [ec] = co_await secure.handshake(tls_stream::client); +auto [ec] = co_await secure.handshake(tls_role::client); if (ec) { std::cerr << "Handshake failed: " << ec.message() << "\n"; @@ -479,7 +479,7 @@ if (ec) [source,cpp] ---- -auto [ec] = co_await secure.handshake(tls_stream::server); +auto [ec] = co_await secure.handshake(tls_role::server); ---- === Handshake Errors @@ -640,7 +640,7 @@ capy::task https_get( // Wrap the connected socket (pointer form) and handshake corosio::wolfssl_stream secure(&sock, ctx); secure.set_hostname(hostname); - if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec) + if (auto [ec] = co_await secure.handshake(tls_role::client); ec) throw std::system_error(ec); // Send HTTP request @@ -707,7 +707,7 @@ capy::task handle_tls_client( // Owning form: the handler owns the socket, so move it in corosio::wolfssl_stream secure(std::move(sock), ctx); - auto [ec] = co_await secure.handshake(wolfssl_stream::server); + auto [ec] = co_await secure.handshake(tls_role::server); if (ec) co_return; diff --git a/example/https-client/https_client.cpp b/example/https-client/https_client.cpp index cb9126fd4..b4050b804 100644 --- a/example/https-client/https_client.cpp +++ b/example/https-client/https_client.cpp @@ -88,7 +88,7 @@ run_client( secure.set_hostname(hostname); // Perform TLS handshake - if (auto [ec] = co_await secure.handshake(corosio::wolfssl_stream::client); ec) + if (auto [ec] = co_await secure.handshake(corosio::tls_role::client); ec) throw std::system_error(ec); co_await do_request(secure, hostname); diff --git a/include/boost/corosio/openssl_stream.hpp b/include/boost/corosio/openssl_stream.hpp index f1395d7e8..212f982c0 100644 --- a/include/boost/corosio/openssl_stream.hpp +++ b/include/boost/corosio/openssl_stream.hpp @@ -59,7 +59,7 @@ namespace boost::corosio { // Reference mode - sock must outlive tls corosio::openssl_stream tls(&sock, ctx); tls.set_hostname("example.com"); - auto [ec] = co_await tls.handshake(openssl_stream::client); + auto [ec] = co_await tls.handshake(tls_role::client); // Or owning mode - tls owns the socket corosio::openssl_stream tls2(std::move(sock), ctx); @@ -145,11 +145,11 @@ class BOOST_COROSIO_DECL openssl_stream final : public tls_stream The underlying stream must be connected. No other TLS operation may be in progress on this stream. - @param type The handshake role (client or server). + @param role The handshake role, client or server. @return An awaitable yielding `(error_code)`. */ - capy::io_task<> handshake(handshake_type type) override; + capy::io_task<> handshake(tls_role role) override; /** Shut down the TLS session asynchronously. diff --git a/include/boost/corosio/tls_context.hpp b/include/boost/corosio/tls_context.hpp index ea0be5e2f..0e81cbe00 100644 --- a/include/boost/corosio/tls_context.hpp +++ b/include/boost/corosio/tls_context.hpp @@ -26,21 +26,6 @@ namespace boost::corosio { // Enumerations // -/** TLS handshake role. - - Specifies whether to perform the TLS handshake as a client or server. - - @see stream::handshake -*/ -enum class tls_role -{ - /// Perform handshake as the connecting client. - client, - - /// Perform handshake as the accepting server. - server -}; - /** TLS protocol version. Specifies the minimum or maximum TLS protocol version to use @@ -248,7 +233,7 @@ tls_context_data const& get_tls_context_data(tls_context const&) noexcept; // Use with a TLS stream corosio::openssl_stream secure( &sock, ctx ); secure.set_hostname( "example.com" ); - co_await secure.handshake( corosio::tls_stream::client ); + co_await secure.handshake( corosio::tls_role::client ); @endcode @see tls_role diff --git a/include/boost/corosio/tls_stream.hpp b/include/boost/corosio/tls_stream.hpp index e733334a5..eeaa63df3 100644 --- a/include/boost/corosio/tls_stream.hpp +++ b/include/boost/corosio/tls_stream.hpp @@ -22,6 +22,21 @@ namespace boost::corosio { +/** TLS handshake role. + + Specifies whether to perform the TLS handshake as a client or server. + + @see tls_stream::handshake +*/ +enum class tls_role +{ + /// Perform handshake as the connecting client. + client, + + /// Perform handshake as the accepting server. + server +}; + /** Abstract base class for TLS streams. This class provides a runtime-polymorphic interface for TLS @@ -47,13 +62,6 @@ namespace boost::corosio { class BOOST_COROSIO_DECL tls_stream { public: - /// Identify the TLS handshake role. - enum handshake_type - { - client, ///< Perform handshaking as a client. - server ///< Perform handshaking as a server. - }; - /// Destroy the TLS stream. virtual ~tls_stream() = default; @@ -105,11 +113,11 @@ class BOOST_COROSIO_DECL tls_stream For server connections, this waits for the ClientHello and sends the server's response. - @param type The type of handshaking to perform (client or server). + @param role The handshake role, client or server. @return An awaitable yielding `(error_code)`. */ - virtual capy::io_task<> handshake(handshake_type type) = 0; + virtual capy::io_task<> handshake(tls_role role) = 0; /** Perform a graceful TLS shutdown asynchronously. diff --git a/include/boost/corosio/wolfssl_stream.hpp b/include/boost/corosio/wolfssl_stream.hpp index 5f36ec27c..88808356d 100644 --- a/include/boost/corosio/wolfssl_stream.hpp +++ b/include/boost/corosio/wolfssl_stream.hpp @@ -59,7 +59,7 @@ namespace boost::corosio { // Reference mode - sock must outlive tls corosio::wolfssl_stream tls(&sock, ctx); tls.set_hostname("example.com"); - auto [ec] = co_await tls.handshake(wolfssl_stream::client); + auto [ec] = co_await tls.handshake(tls_role::client); // Or owning mode - tls owns the socket corosio::wolfssl_stream tls2(std::move(sock), ctx); @@ -145,11 +145,11 @@ class BOOST_COROSIO_DECL wolfssl_stream final : public tls_stream The underlying stream must be connected. No other TLS operation may be in progress on this stream. - @param type The handshake role (client or server). + @param role The handshake role, client or server. @return An awaitable yielding `(error_code)`. */ - capy::io_task<> handshake(handshake_type type) override; + capy::io_task<> handshake(tls_role role) override; /** Shut down the TLS session asynchronously. diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index 5d03a22d1..f2c283d8e 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -912,7 +912,7 @@ struct openssl_stream::impl co_return {std::error_code{}, total_written}; } - capy::io_task<> do_handshake(int type) + capy::io_task<> do_handshake(tls_role role) { // A requested configuration could not be applied when the native // context was built (inverted protocol window, rejected cipher/ @@ -933,7 +933,7 @@ struct openssl_stream::impl // Client offers its ALPN protocol list; the server selects via the // context callback. Role is only known here, so set the pre-encoded // wire offer per-SSL. - if (type == openssl_stream::client && !nc->alpn_wire_.empty()) + if (role == tls_role::client && !nc->alpn_wire_.empty()) { // SSL_set_alpn_protos uses the inverted convention: 0 = success. // A non-zero return (allocation failure) means the offer was not @@ -950,7 +950,7 @@ struct openssl_stream::impl { ERR_clear_error(); int ret; - if (type == openssl_stream::client) + if (role == tls_role::client) ret = SSL_connect(ssl_); else ret = SSL_accept(ssl_); @@ -1150,9 +1150,9 @@ openssl_stream::do_write_some( } capy::io_task<> -openssl_stream::handshake(handshake_type type) +openssl_stream::handshake(tls_role role) { - co_return co_await impl_->do_handshake(type); + co_return co_await impl_->do_handshake(role); } capy::io_task<> diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index 43b50ad4c..73bb84f1d 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -1073,7 +1073,7 @@ struct wolfssl_stream::impl co_return {std::error_code{}, total_written}; } - capy::io_task<> do_handshake(int type) + capy::io_task<> do_handshake(tls_role role) { if (used_) reset(); @@ -1081,7 +1081,7 @@ struct wolfssl_stream::impl std::error_code ec; // Initialize SSL object for the specified role (deferred from construction) - ec = init_ssl_for_role(type); + ec = init_ssl_for_role(role); if (ec) co_return {ec}; @@ -1096,9 +1096,9 @@ struct wolfssl_stream::impl op.want_read = false; op.want_write = false; - // Call appropriate handshake function based on type + // Call appropriate handshake function based on role int ret; - if (type == wolfssl_stream::client) + if (role == tls_role::client) ret = wolfSSL_connect(ssl_); else ret = wolfSSL_accept(ssl_); @@ -1339,7 +1339,7 @@ struct wolfssl_stream::impl // Initialization - std::error_code init_ssl_for_role(int type) + std::error_code init_ssl_for_role(tls_role role) { // Already initialized? if (ssl_) @@ -1362,7 +1362,7 @@ struct wolfssl_stream::impl return std::make_error_code(std::errc::invalid_argument); // Select appropriate context based on role - WOLFSSL_CTX* native_ctx = (type == wolfssl_stream::client) + WOLFSSL_CTX* native_ctx = (role == tls_role::client) ? native->client_ctx_ : native->server_ctx_; @@ -1453,7 +1453,7 @@ struct wolfssl_stream::impl #endif // Apply per-session config (SNI + hostname verification) - if (type == wolfssl_stream::client && !hostname_.empty()) + if (role == tls_role::client && !hostname_.empty()) { // Set SNI extension so server knows which cert to present wolfSSL_UseSNI( @@ -1519,9 +1519,9 @@ wolfssl_stream::do_write_some( } capy::io_task<> -wolfssl_stream::handshake(handshake_type type) +wolfssl_stream::handshake(tls_role role) { - co_return co_await impl_->do_handshake(type); + co_return co_await impl_->do_handshake(role); } capy::io_task<> diff --git a/test/unit/test_utils.hpp b/test/unit/test_utils.hpp index e4b7aadef..6968a8893 100644 --- a/test/unit/test_utils.hpp +++ b/test/unit/test_utils.hpp @@ -1401,14 +1401,12 @@ run_tls_test( // Store lambdas in named variables before invoking - anonymous lambda + immediate // invocation pattern [...](){}() can cause capture corruption with run_async auto client_task = [&client]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_task = [&server]() -> capy::task<> { - auto [ec] = co_await server.handshake( - std::remove_reference_t::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; @@ -1462,14 +1460,12 @@ run_tls_test_no_shutdown( // Store lambdas in named variables before invoking - anonymous lambda + immediate // invocation pattern [...](){}() can cause capture corruption with run_async auto client_task = [&client]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_task = [&server]() -> capy::task<> { - auto [ec] = co_await server.handshake( - std::remove_reference_t::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; @@ -1533,8 +1529,7 @@ run_tls_test_fail( auto client_task = [&client, &client_failed, &client_done, &server_done, &failsafe_stop, &s1, &s2, client_ec_out]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); if (client_ec_out) *client_ec_out = ec; if (ec) @@ -1559,8 +1554,7 @@ run_tls_test_fail( auto server_task = [&server, &server_failed, &server_done, &client_done, &failsafe_stop, &s1, &s2]() -> capy::task<> { - auto [ec] = co_await server.handshake( - std::remove_reference_t::server); + auto [ec] = co_await server.handshake(tls_role::server); if (ec) { server_failed = true; @@ -1649,14 +1643,12 @@ run_tls_shutdown_test( // Handshake phase auto client_hs = [&client]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_hs = [&server]() -> capy::task<> { - auto [ec] = co_await server.handshake( - std::remove_reference_t::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; @@ -1763,14 +1755,12 @@ run_tls_truncation_test( // Handshake phase auto client_hs = [&client]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_hs = [&server]() -> capy::task<> { - auto [ec] = co_await server.handshake( - std::remove_reference_t::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; @@ -2078,8 +2068,7 @@ run_connection_reset_test( auto client_task = [&client, &client_failed, &failsafe_stop]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); // Should fail because server closed socket if (ec) client_failed = true; @@ -2164,8 +2153,7 @@ run_stop_token_handshake_test( // Client handshake - will be cancelled while waiting for ServerHello auto client_task = [&client, &client_got_error, &failsafe_stop]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); if (ec) client_got_error = true; failsafe_stop.request_stop(); @@ -2239,14 +2227,12 @@ run_stop_token_read_test( // Handshake phase auto client_hs = [&client]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_hs = [&server]() -> capy::task<> { - auto [ec] = co_await server.handshake( - std::remove_reference_t::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; @@ -2363,14 +2349,12 @@ run_shutdown_cancel_test( // Handshake phase auto client_hs = [&client]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_hs = [&server]() -> capy::task<> { - auto [ec] = co_await server.handshake( - std::remove_reference_t::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; @@ -2479,14 +2463,12 @@ run_stop_token_write_test( // Handshake phase auto client_hs = [&client]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_hs = [&server]() -> capy::task<> { - auto [ec] = co_await server.handshake( - std::remove_reference_t::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; @@ -2600,8 +2582,7 @@ run_socket_cancel_test( // Client starts handshake - will be cancelled auto client_task = [&client, &client_got_error, &failsafe_stop]() -> capy::task<> { - auto [ec] = co_await client.handshake( - std::remove_reference_t::client); + auto [ec] = co_await client.handshake(tls_role::client); if (ec) client_got_error = true; failsafe_stop.request_stop(); diff --git a/test/unit/tls_stream_stress.cpp b/test/unit/tls_stream_stress.cpp index 13b6094b6..7449ddea9 100644 --- a/test/unit/tls_stream_stress.cpp +++ b/test/unit/tls_stream_stress.cpp @@ -98,12 +98,12 @@ struct tls_session_cycle_stress_impl std::error_code cec, sec; auto hs_client = [&client, &cec]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); cec = ec; }; auto hs_server = [&server, &sec]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); sec = ec; }; @@ -192,11 +192,11 @@ struct tls_concurrent_io_stress_impl { std::error_code cec, sec; auto hsc = [&client_a, &cec]() -> capy::task<> { - auto [ec] = co_await client_a.handshake(tls_stream::client); + auto [ec] = co_await client_a.handshake(tls_role::client); cec = ec; }; auto hss = [&server_a, &sec]() -> capy::task<> { - auto [ec] = co_await server_a.handshake(tls_stream::server); + auto [ec] = co_await server_a.handshake(tls_role::server); sec = ec; }; capy::run_async(ex)(hsc()); @@ -213,11 +213,11 @@ struct tls_concurrent_io_stress_impl { std::error_code cec, sec; auto hsc = [&client_b, &cec]() -> capy::task<> { - auto [ec] = co_await client_b.handshake(tls_stream::client); + auto [ec] = co_await client_b.handshake(tls_role::client); cec = ec; }; auto hss = [&server_b, &sec]() -> capy::task<> { - auto [ec] = co_await server_b.handshake(tls_stream::server); + auto [ec] = co_await server_b.handshake(tls_role::server); sec = ec; }; capy::run_async(ex)(hsc()); @@ -345,7 +345,7 @@ struct tls_cancel_handshake_stress_impl // Client handshake - will be cancelled mid-flight auto client_task = [&client, &client_got_error, &done, &failsafe_stop]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); if (ec) client_got_error = true; done = true; diff --git a/test/unit/tls_stream_tests.hpp b/test/unit/tls_stream_tests.hpp index a70449b30..85fdb37d3 100644 --- a/test/unit/tls_stream_tests.hpp +++ b/test/unit/tls_stream_tests.hpp @@ -63,12 +63,12 @@ testHandshakeFuse(StreamFactory make_stream) std::error_code server_ec; auto client_task = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); client_ec = ec; }; auto server_task = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); server_ec = ec; }; @@ -113,7 +113,7 @@ testReadWriteFuse(StreamFactory make_stream) auto test_data = corosio::test::scaled_test_data(max_size); auto client_task = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); if (ec) co_return; @@ -135,7 +135,7 @@ testReadWriteFuse(StreamFactory make_stream) }; auto server_task = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); if (ec) co_return; @@ -192,7 +192,7 @@ testShutdownFuse(StreamFactory make_stream) auto server = make_stream(m2, server_ctx); auto client_task = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); if (ec) co_return; @@ -202,7 +202,7 @@ testShutdownFuse(StreamFactory make_stream) }; auto server_task = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); if (ec) co_return; @@ -531,7 +531,7 @@ run_hostname_round( std::error_code server_ec; auto hs_client = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); client_ec = ec; if (ec) { @@ -540,7 +540,7 @@ run_hostname_round( } }; auto hs_server = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); server_ec = ec; }; @@ -1135,17 +1135,16 @@ testAlpn(StreamFactory make_stream, bool alpn_supported) // NOLINTNEXTLINE(bugprone-unused-return-value) server_ctx.set_alpn({"h2", "http/1.1"}); - auto client = make_stream(m1, client_ctx); - auto server = make_stream(m2, server_ctx); - using stream_type = std::remove_reference_t; + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); std::error_code cec, sec; auto hc = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(stream_type::client); + auto [ec] = co_await client.handshake(tls_role::client); cec = ec; }; auto hs = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(stream_type::server); + auto [ec] = co_await server.handshake(tls_role::server); sec = ec; }; capy::run_async(ioc.get_executor())(hc()); @@ -1261,13 +1260,12 @@ testVerifyCallback(StreamFactory make_stream, bool callback_supported = true) [](bool preverified, verify_context&) -> bool { return preverified; }); - auto client = make_stream(m1, client_ctx); - using stream_type = std::remove_reference_t; + auto client = make_stream(m1, client_ctx); std::error_code ec1; std::error_code ec2; auto attempt = [&](std::error_code& out) -> capy::task<> { - auto [ec] = co_await client.handshake(stream_type::client); + auto [ec] = co_await client.handshake(tls_role::client); out = ec; }; capy::run_async(ioc.get_executor())(attempt(ec1)); @@ -1419,15 +1417,14 @@ testMoveSemantics(StreamFactory make_stream) auto client_pre = make_stream(m1, client_ctx); auto client_orig{std::move(client_pre)}; - auto server = make_stream(m2, server_ctx); - using stream_type = std::remove_reference_t; + auto server = make_stream(m2, server_ctx); auto client_hs = [&]() -> capy::task<> { - auto [ec] = co_await client_orig.handshake(stream_type::client); + auto [ec] = co_await client_orig.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_hs = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(stream_type::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; capy::run_async(ioc.get_executor())(client_hs()); @@ -1481,16 +1478,15 @@ testAbruptClose(StreamFactory make_stream) auto client_ctx = make_client_context(); auto server_ctx = make_server_context(); - auto client = make_stream(m1, client_ctx); - auto server = make_stream(m2, server_ctx); - using stream_type = std::remove_reference_t; + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); auto client_hs = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(stream_type::client); + auto [ec] = co_await client.handshake(tls_role::client); BOOST_TEST(!ec); }; auto server_hs = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(stream_type::server); + auto [ec] = co_await server.handshake(tls_role::server); BOOST_TEST(!ec); }; capy::run_async(ioc.get_executor())(client_hs()); @@ -1553,9 +1549,8 @@ testEncryptedKey(StreamFactory make_stream, bool expect_success = true) bool callback_invoked = false; auto server_ctx = make_encrypted_key_server_context(callback_invoked); - auto client = make_stream(m1, client_ctx); - auto server = make_stream(m2, server_ctx); - using stream_type = std::remove_reference_t; + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); bool client_done = false, server_done = false; bool failsafe_hit = false; @@ -1567,14 +1562,14 @@ testEncryptedKey(StreamFactory make_stream, bool expect_success = true) std::stop_source failsafe_stop; auto client_hs = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(stream_type::client); + auto [ec] = co_await client.handshake(tls_role::client); client_ec = ec; client_done = true; if (server_done) failsafe_stop.request_stop(); }; auto server_hs = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(stream_type::server); + auto [ec] = co_await server.handshake(tls_role::server); server_ec = ec; server_done = true; if (client_done) @@ -1626,22 +1621,21 @@ testInvalidContextHandshake(StreamFactory make_stream) // NOLINTNEXTLINE(bugprone-unused-return-value) server_ctx.set_verify_mode(tls_verify_mode::none); - auto client = make_stream(m1, client_ctx); - auto server = make_stream(m2, server_ctx); - using stream_type = std::remove_reference_t; + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); bool client_done = false, server_done = false; std::error_code client_ec, server_ec; auto client_hs = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(stream_type::client); + auto [ec] = co_await client.handshake(tls_role::client); client_ec = ec; client_done = true; // Unblock the server if it is still waiting on the transport. m1.close(); // NOLINT(bugprone-unused-return-value) }; auto server_hs = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(stream_type::server); + auto [ec] = co_await server.handshake(tls_role::server); server_ec = ec; server_done = true; m2.close(); // NOLINT(bugprone-unused-return-value) @@ -1717,11 +1711,11 @@ testReset(StreamFactory make_stream, std::array const& modes) // Handshake auto hs_client = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); client_ec = ec; }; auto hs_server = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); server_ec = ec; }; @@ -1813,11 +1807,11 @@ testResetViaHandshake( std::error_code server_ec; auto hs_client = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); client_ec = ec; }; auto hs_server = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); server_ec = ec; }; @@ -1907,11 +1901,11 @@ testResetFuse(StreamFactory make_stream) { std::error_code cec, sec; auto hsc = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); cec = ec; }; auto hss = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); sec = ec; }; capy::run_async(ioc.get_executor())(hsc()); @@ -1947,11 +1941,11 @@ testResetFuse(StreamFactory make_stream) { std::error_code cec, sec; auto hsc = [&]() -> capy::task<> { - auto [ec] = co_await client.handshake(tls_stream::client); + auto [ec] = co_await client.handshake(tls_role::client); cec = ec; }; auto hss = [&]() -> capy::task<> { - auto [ec] = co_await server.handshake(tls_stream::server); + auto [ec] = co_await server.handshake(tls_role::server); sec = ec; }; capy::run_async(ioc.get_executor())(hsc()); From d7d9786168139ab2008fd34cdf8c4d714298345d Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Tue, 21 Jul 2026 19:07:01 +0200 Subject: [PATCH 3/5] feat(tls): verify IP-literal hostnames against iPAddress entries set_hostname with an IP literal (IPv4 or IPv6) now matches the certificate's iPAddress subjectAltName entries instead of its DNS names, and sends no SNI, which RFC 6066 restricts to DNS names. On OpenSSL the literal is applied via X509_VERIFY_PARAM_set1_ip_asc, with the unused host/ip field cleared so a reset stream cannot carry the previous target's matching rule. On WolfSSL the literal goes through wolfSSL_check_ip_address; enforcement requires a build with both OPENSSL_EXTRA and WOLFSSL_IP_ALT_NAME, and on builds lacking either the call reports success while verification silently checks nothing, so the handshake fails closed with function_not_supported, reported by the new wolfssl_supports_ip_alt_name() probe. --- doc/modules/ROOT/pages/4.guide/4l.tls.adoc | 4 ++ include/boost/corosio/tls_stream.hpp | 7 ++ include/boost/corosio/wolfssl_stream.hpp | 19 +++++ src/openssl/src/openssl_stream.cpp | 33 +++++++-- src/wolfssl/src/wolfssl_stream.cpp | 63 ++++++++++++++-- test/unit/openssl_stream.cpp | 1 + test/unit/test_utils.hpp | 65 +++++++++++++++++ test/unit/tls_stream_tests.hpp | 84 ++++++++++++++++++++++ test/unit/wolfssl_stream.cpp | 4 ++ 9 files changed, 268 insertions(+), 12 deletions(-) diff --git a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc index 4e9778578..53f0ab407 100644 --- a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc +++ b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc @@ -317,6 +317,10 @@ This does two things: to present (important for virtual hosting) 2. Verifies the server certificate matches this hostname +An IP literal (IPv4 or IPv6) is verified against the certificate's +iPAddress entries instead of its DNS names, and no SNI is sent +(RFC 6066 excludes literals). + ==== Verification Depth Limit the certificate chain depth: diff --git a/include/boost/corosio/tls_stream.hpp b/include/boost/corosio/tls_stream.hpp index eeaa63df3..9369f9ac4 100644 --- a/include/boost/corosio/tls_stream.hpp +++ b/include/boost/corosio/tls_stream.hpp @@ -166,6 +166,13 @@ class BOOST_COROSIO_DECL tls_stream An empty hostname (the default) disables SNI and hostname verification. + If `hostname` is an IP literal (IPv4 or IPv6), it is matched + against the certificate's iPAddress entries instead of its + DNS names, and no SNI is sent (RFC 6066 excludes literals). + A backend build that cannot match iPAddress entries fails the + handshake with `std::errc::function_not_supported` rather + than skip verification. + @note The hostname is meaningful for client handshakes; backends may ignore it when handshaking as a server. diff --git a/include/boost/corosio/wolfssl_stream.hpp b/include/boost/corosio/wolfssl_stream.hpp index 88808356d..031e6b0cc 100644 --- a/include/boost/corosio/wolfssl_stream.hpp +++ b/include/boost/corosio/wolfssl_stream.hpp @@ -272,6 +272,25 @@ wolfssl_supports_alpn() noexcept; BOOST_COROSIO_DECL bool wolfssl_supports_crl() noexcept; +/** Report whether this WolfSSL build can verify IP-literal hostnames. + + Matching an IP literal against a certificate's iPAddress entries + requires a WolfSSL built with both `OPENSSL_EXTRA` (routes the + address into the verify parameters the certificate check consults) + and `WOLFSSL_IP_ALT_NAME` (records iPAddress entries during + parsing). On a build lacking either, `wolfSSL_check_ip_address` + reports success but verification silently checks nothing, so a + handshake with an IP literal set via @ref tls_stream::set_hostname + fails with `std::errc::function_not_supported` rather than proceed + unverified. + + @return `true` if IP-literal verification is supported by this build. + + @see tls_stream::set_hostname +*/ +BOOST_COROSIO_DECL bool +wolfssl_supports_ip_alt_name() noexcept; + } // namespace boost::corosio #endif diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index f2c283d8e..27f1c5e1c 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -19,6 +19,9 @@ // Internal context implementation #include "src/tls/detail/context_impl.hpp" +#include +#include + #include #include #include @@ -74,6 +77,15 @@ tls_method_compat() noexcept #endif } +// Whether the peer name is an IP literal rather than a DNS name. +inline bool +is_ip_literal(std::string const& s) noexcept +{ + ipv4_address v4; + ipv6_address v6; + return !parse_ipv4_address(s, v4) || !parse_ipv6_address(s, v6); +} + inline void apply_hostname_verification(SSL* ssl, std::string const& hostname) { @@ -82,14 +94,23 @@ apply_hostname_verification(SSL* ssl, std::string const& hostname) // would leak the old peer's name into the next handshake char const* name = hostname.empty() ? nullptr : hostname.c_str(); - SSL_set_tlsext_host_name(ssl, name); + // RFC 6066 excludes IP literals from SNI, and a literal must match + // the certificate's iPAddress entries rather than its DNS names. + // The unused field is cleared so a reset stream cannot carry the + // previous target's matching rule. + bool const is_ip = name && is_ip_literal(hostname); + char const* dns_name = is_ip ? nullptr : name; + + SSL_set_tlsext_host_name(ssl, dns_name); -#if OPENSSL_VERSION_NUMBER >= 0x10100000L - SSL_set1_host(ssl, name); -#else if (auto* param = SSL_get0_param(ssl)) - X509_VERIFY_PARAM_set1_host(param, name, 0); -#endif + { + X509_VERIFY_PARAM_set1_host(param, dns_name, 0); + if (is_ip) + X509_VERIFY_PARAM_set1_ip_asc(param, name); + else + X509_VERIFY_PARAM_set1_ip(param, nullptr, 0); + } } // Map a portable protocol version to the OpenSSL version constant. diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index 73bb84f1d..6b6866923 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -19,6 +19,9 @@ // Internal context implementation #include "src/tls/detail/context_impl.hpp" +#include +#include + // Include WolfSSL options first to get proper feature detection #include #include @@ -93,6 +96,15 @@ is_zero_return_error(int err) noexcept return err == WOLFSSL_ERROR_ZERO_RETURN; } +// Whether the peer name is an IP literal rather than a DNS name. +inline bool +is_ip_literal(std::string const& s) noexcept +{ + ipv4_address v4; + ipv6_address v6; + return !parse_ipv4_address(s, v4) || !parse_ipv6_address(s, v6); +} + inline bool has_peer_shutdown(WOLFSSL* ssl) noexcept { @@ -156,6 +168,16 @@ wolfssl_supports_crl() noexcept #endif } +bool +wolfssl_supports_ip_alt_name() noexcept +{ +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_IP_ALT_NAME) + return true; +#else + return false; +#endif +} + // // Native context caching // @@ -1455,13 +1477,42 @@ struct wolfssl_stream::impl // Apply per-session config (SNI + hostname verification) if (role == tls_role::client && !hostname_.empty()) { - // Set SNI extension so server knows which cert to present - wolfSSL_UseSNI( - ssl_, WOLFSSL_SNI_HOST_NAME, hostname_.data(), - static_cast(hostname_.size())); + if (is_ip_literal(hostname_)) + { + // RFC 6066 excludes IP literals from SNI; the literal + // must match the certificate's iPAddress entries. + // Enforcement needs both flags: OPENSSL_EXTRA routes + // the address into the verify params that the cert + // check consults, and WOLFSSL_IP_ALT_NAME makes the + // parser record iPAddress entries at all. Without + // either, wolfSSL_check_ip_address still returns + // success and verification silently checks nothing. +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_IP_ALT_NAME) + if (wolfSSL_check_ip_address(ssl_, hostname_.c_str()) + != WOLFSSL_SUCCESS) + { + wolfSSL_free(ssl_); + ssl_ = nullptr; + return std::make_error_code( + std::errc::function_not_supported); + } +#else + wolfSSL_free(ssl_); + ssl_ = nullptr; + return std::make_error_code( + std::errc::function_not_supported); +#endif + } + else + { + // Set SNI extension so server knows which cert to present + wolfSSL_UseSNI( + ssl_, WOLFSSL_SNI_HOST_NAME, hostname_.data(), + static_cast(hostname_.size())); - // Enable hostname verification (checks CN/SAN in peer cert) - wolfSSL_check_domain_name(ssl_, hostname_.c_str()); + // Enable hostname verification (checks CN/SAN in peer cert) + wolfSSL_check_domain_name(ssl_, hostname_.c_str()); + } } return {}; diff --git a/test/unit/openssl_stream.cpp b/test/unit/openssl_stream.cpp index 8104cd9dd..e39f805bd 100644 --- a/test/unit/openssl_stream.cpp +++ b/test/unit/openssl_stream.cpp @@ -169,6 +169,7 @@ struct openssl_stream_test test::testHostnamePersistence(make_stream); test::testHostnameRedirect(make_stream); test::testHostnameClear(make_stream); + test::testHostnameIpLiteral(make_stream, /*ip_supported=*/true); test::testAlpnAccessorEmpty(make_stream); test::testAlpn(make_stream, /*alpn_supported=*/true); test::testAlpnNoOverlap(make_stream, /*alpn_supported=*/true); diff --git a/test/unit/test_utils.hpp b/test/unit/test_utils.hpp index 6968a8893..5f23b4bc6 100644 --- a/test/unit/test_utils.hpp +++ b/test/unit/test_utils.hpp @@ -108,6 +108,71 @@ inline constexpr char const* server_key_pem = "nOMOU6XI4lO9Xge/QDEN4Y2R\n" "-----END PRIVATE KEY-----\n"; +// Self-signed server certificate with an iPAddress SAN only. +// The CN is deliberately not an IP so hostname checks cannot +// succeed via CN fallback; only the IP-SAN path matches. +// Subject: C=US, ST=CA, L=Los Angeles, O=Corosio, CN=corosio-ip-test +// SAN: IP:127.0.0.1 +// Command: +// openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem +// -days 10000 -nodes +// -subj "/C=US/ST=CA/L=Los Angeles/O=Corosio/CN=corosio-ip-test" +// -addext "subjectAltName=IP:127.0.0.1" +inline constexpr char const* server_ip_cert_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDrDCCApSgAwIBAgIUL29ePiWtHFp225p0PpXCfX3Uic4wDQYJKoZIhvcNAQEL\n" + "BQAwXDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQHDAtMb3MgQW5n\n" + "ZWxlczEQMA4GA1UECgwHQ29yb3NpbzEYMBYGA1UEAwwPY29yb3Npby1pcC10ZXN0\n" + "MCAXDTI2MDcyMTE1NTQxNVoYDzIwNTMxMjA2MTU1NDE1WjBcMQswCQYDVQQGEwJV\n" + "UzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMRAwDgYDVQQKDAdD\n" + "b3Jvc2lvMRgwFgYDVQQDDA9jb3Jvc2lvLWlwLXRlc3QwggEiMA0GCSqGSIb3DQEB\n" + "AQUAA4IBDwAwggEKAoIBAQDTOzWcoMa44QpwSpJCqqZysMe1oUVRpocaRiTd436u\n" + "E79i9n3zBAJKrR0LL0J2TjcZcn6lcyyiAfdpmEgqht6KSIb7YzTuC3fsKi8b1bfk\n" + "AGPpw9cQrC4ESK+ppsa5QQbW1gpnUDsJ25o6wHUBJFYO60M54oDfi8+WGUNtzGk3\n" + "IHmvyFJQqghxhDCLRfDIAc3EdIFG6XcPiY6r/LelDCiVH3Odc2/Cn9iM25MVkG4a\n" + "SajfhOLejZkBqYKmygPm1RVc6buLEo5hd+Uw2LKc3rytEIeFgAMLhl66FeMt/sGy\n" + "xjTasrBs3qdd2lH6ddnZPgWKYc1BpNZ0LrYrFfJRqy2rAgMBAAGjZDBiMB0GA1Ud\n" + "DgQWBBTQwjj9zLFhE81MWLg5zi1QEhRokjAfBgNVHSMEGDAWgBTQwjj9zLFhE81M\n" + "WLg5zi1QEhRokjAPBgNVHRMBAf8EBTADAQH/MA8GA1UdEQQIMAaHBH8AAAEwDQYJ\n" + "KoZIhvcNAQELBQADggEBACw47fx00D/oBlGWT6JhWwpfY2kNUiBaZDny1qz8Tf0j\n" + "5uVnfuuCuZDwsqNwTPMmvG9C34F54GMXlQ/vb9v9YIk3RArTeNyCgJxEC+eY6+g+\n" + "lO/g0QBRxCSE2JnHTJkNvflgz8NazYrkHs+fu/i3ug6mWkFHx4fh7BaR0Zb602H5\n" + "NN8/mGbuBJ0TlvmuwKSgsgYDb3InP3g6Od+gOaSmdsrX7RnyAN9gYugbz0tG6DfW\n" + "oEYzCupdAcUNuYCFmVkiOTjAK8qds6422rwkVwF2gaIJ5dF+fH6U0qu75jI/D5lo\n" + "faUtSby6prrqx+YjlYaVZzH+OT8iVpnqqfqKjXUQTfU=\n" + "-----END CERTIFICATE-----\n"; + +// Matches server_ip_cert_pem above (RSA 2048-bit) +inline constexpr char const* server_ip_key_pem = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTOzWcoMa44Qpw\n" + "SpJCqqZysMe1oUVRpocaRiTd436uE79i9n3zBAJKrR0LL0J2TjcZcn6lcyyiAfdp\n" + "mEgqht6KSIb7YzTuC3fsKi8b1bfkAGPpw9cQrC4ESK+ppsa5QQbW1gpnUDsJ25o6\n" + "wHUBJFYO60M54oDfi8+WGUNtzGk3IHmvyFJQqghxhDCLRfDIAc3EdIFG6XcPiY6r\n" + "/LelDCiVH3Odc2/Cn9iM25MVkG4aSajfhOLejZkBqYKmygPm1RVc6buLEo5hd+Uw\n" + "2LKc3rytEIeFgAMLhl66FeMt/sGyxjTasrBs3qdd2lH6ddnZPgWKYc1BpNZ0LrYr\n" + "FfJRqy2rAgMBAAECggEAYxmlRm2brgNOnW4u/n4Hh0lu+MTHu83wFqCQDVX9CfiT\n" + "0v8oCgp4dMaRGL08Zjq92P+BcWf+qadYhz79pI4P/DqYsXpSy9evlKoZ3eo/0wVn\n" + "2rWZweW11Saw21w2YZWjesmCqgPXHwHbcvL2Men1QhyYNqEQq1BxvM7vdqTvO//y\n" + "4PW17k630gzTaVreM6t/Yf2KJXqeb0+42tRvgN+BhybCKj2uNhC/R08A/Rr+A461\n" + "p2rXo0YP+6NSJBP/00qz7EI6aAqfu37Q8MRNwqAb+pub1CQpBwGOSqIa3uNWuEuh\n" + "2/Cct/qMjuH39yt0anMevApvrNYsDZwtZotQsErb2QKBgQD21wVSKQdDXEnKQ7jA\n" + "M2gWbTqrnJm8awWYjm0sBpzF+yN0enCuOpsBflavJQ2WtV2k7VWKOhBBQxPtwgdI\n" + "FatDElO5yQz2XgShkYML+IJnufsMh8ZrKZ/gcW3rQyNDmkz36CheeWhdQyTpGAEv\n" + "dvq8pM0SOpAOOEAXkNl4Z9G7lwKBgQDbEegV6RofSfWwfN52tL3K2DJQqlR+BYEB\n" + "F8exeYaaY38csbHqOQFfot6kccOqr/EPKP2wf2h1fu3sJ74Hv3U2NDAOvoZQ3d7Q\n" + "5LR0dBIkWlrbhEYCmHfnmRcofc9QmvS6bq+D6RgNGJTDwOo/vrojGGjuBbEnaz83\n" + "HMg+PGFxDQKBgQDLMsrAjeHaw9hC12j5X9gpzhVkPHAaOYfLxEN+4JqiKFFRi5HC\n" + "+5+qpRQ67ie3jund4TpvpcjH0K5RJU7VOnFXr3iZEjbHgTISxzS34AWJ2gIemI7w\n" + "nL1uCDJSX1xiRF1kHwtMamlNjP6PnCEtr6ZNMOVYQjlgW1H3lFhR1DVFVQKBgDdZ\n" + "KNgQUudA2nBCvDolpCYRxXSX9Ez6uwM5rNxsJdPv+3eWdasFyBEPp0zI6XTAixkX\n" + "dDEZn5y/+wDFcb+nYcfWG6Y+ANWBmQASKH2brdG9emMn4kBZoUHEbhNu5egpnldU\n" + "C8g6Jjd41G042nZMi96+FhS9H2skL46PGRCQVNYpAoGBAMlKXlcaVlb6Jf5tcQOe\n" + "unY8LjSgAtwZJ9XQ0FAA82j+6oIndSB7AMkPu3mpKGV96cdhdxakGluXpKG2AC8J\n" + "CDPJTLBEuU/DxEXME1wQ9o3w1RPpi1rE1k8kMVOcS0WZHhGrnyM3vMJuUmo1/Zk/\n" + "43t2LygWhN+nrUbjLDRyTl7O\n" + "-----END PRIVATE KEY-----\n"; + // Different self-signed CA for "wrong CA" test scenarios // Subject: CN=localhost // Valid: 2023-01-01 to 2033-01-01 (self-signed) diff --git a/test/unit/tls_stream_tests.hpp b/test/unit/tls_stream_tests.hpp index 85fdb37d3..adc77112a 100644 --- a/test/unit/tls_stream_tests.hpp +++ b/test/unit/tls_stream_tests.hpp @@ -709,6 +709,90 @@ testHostnameClear(StreamFactory make_stream) m2.close(); // NOLINT(bugprone-unused-return-value) } +/** An IP-literal hostname is matched against the certificate's + iPAddress entries and is not sent as SNI; a mismatched literal + fails verification. On a build that cannot match iPAddress + entries the handshake fails closed instead. */ +template +void +testHostnameIpLiteral(StreamFactory make_stream, bool ip_supported) +{ + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair(ioc); + + tls_context client_ctx; + client_ctx.add_certificate_authority( + test::server_ip_cert_pem); // NOLINT(bugprone-unused-return-value) + client_ctx.set_verify_mode( + tls_verify_mode::peer); // NOLINT(bugprone-unused-return-value) + + tls_context server_ctx; + server_ctx.use_certificate( + test::server_ip_cert_pem, + tls_file_format::pem); // NOLINT(bugprone-unused-return-value) + server_ctx.use_private_key( + test::server_ip_key_pem, + tls_file_format::pem); // NOLINT(bugprone-unused-return-value) + server_ctx.set_verify_mode( + tls_verify_mode::none); // NOLINT(bugprone-unused-return-value) + + std::size_t sni_count = 0; + server_ctx.set_servername_callback( + [&sni_count](std::string_view) -> bool { + ++sni_count; + return true; + }); + + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); + + client.set_hostname("127.0.0.1"); + + if (!ip_supported) + { + // The build cannot parse iPAddress entries; the handshake + // must fail closed rather than skip verification. + std::error_code client_ec; + auto hs_client = [&]() -> capy::task<> { + auto [ec] = co_await client.handshake(tls_role::client); + client_ec = ec; + m1.close(); // NOLINT(bugprone-unused-return-value) + m2.close(); // NOLINT(bugprone-unused-return-value) + }; + auto hs_server = [&]() -> capy::task<> { + (void)co_await server.handshake(tls_role::server); + }; + capy::run_async(ioc.get_executor())(hs_client()); + capy::run_async(ioc.get_executor())(hs_server()); + ioc.run(); + + BOOST_TEST(client_ec == std::errc::function_not_supported); + BOOST_TEST_EQ(sni_count, 0u); + return; + } + + // Round 1: the literal matches the certificate's iPAddress entry. + // The CN is not an IP, so success proves the IP-SAN path. + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, true); + + // RFC 6066 excludes literals from SNI + BOOST_TEST_EQ(sni_count, 0u); + + client.reset(); + server.reset(); + + // Round 2: a different literal fails verification + client.set_hostname("192.0.2.1"); + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, false); + + if (m1.is_open()) + m1.close(); // NOLINT(bugprone-unused-return-value) + if (m2.is_open()) + m2.close(); // NOLINT(bugprone-unused-return-value) +} + /** A freshly constructed stream reports no negotiated ALPN protocol. The accessor must return an empty view before any handshake, on diff --git a/test/unit/wolfssl_stream.cpp b/test/unit/wolfssl_stream.cpp index 43ee9bcfa..250c14d3e 100644 --- a/test/unit/wolfssl_stream.cpp +++ b/test/unit/wolfssl_stream.cpp @@ -161,6 +161,10 @@ struct wolfssl_stream_test test::testHostnamePersistence(make_stream); test::testHostnameRedirect(make_stream); test::testHostnameClear(make_stream); + // IP-literal matching is build-gated (WOLFSSL_IP_ALT_NAME); + // when absent, an IP-literal hostname fails closed. + test::testHostnameIpLiteral( + make_stream, wolfssl_supports_ip_alt_name()); test::testAlpnAccessorEmpty(make_stream); // Whether the linked WolfSSL can honor a verify callback on success // (WOLFSSL_ALWAYS_VERIFY_CB) is a build-time property, queried here From 0ac8b76a891f64fa8aa0409e9bb30dadc5c192bf Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Tue, 21 Jul 2026 22:49:53 +0200 Subject: [PATCH 4/5] fix(tls): apply set_hostname on handshake retries and fail closed A failed handshake attempt now consumes the stream: the next handshake() behaves as if reset() had been called first, so a hostname changed after a failure is applied instead of being silently ignored on a dead session. The hostname now applies to client handshakes only; a server-role handshake clears any name a prior client-role handshake left in the verify params, so client certificates are never hostname-matched. Failures installing the SNI name or the verify-param host fail the handshake rather than proceed without the requested check. --- include/boost/corosio/tls_stream.hpp | 11 ++- src/openssl/src/openssl_stream.cpp | 45 ++++++++---- src/wolfssl/src/wolfssl_stream.cpp | 26 +++++-- test/unit/openssl_stream.cpp | 1 + test/unit/tls_stream_tests.hpp | 100 ++++++++++++++++++++++++--- test/unit/wolfssl_stream.cpp | 1 + 6 files changed, 154 insertions(+), 30 deletions(-) diff --git a/include/boost/corosio/tls_stream.hpp b/include/boost/corosio/tls_stream.hpp index 9369f9ac4..d653557b2 100644 --- a/include/boost/corosio/tls_stream.hpp +++ b/include/boost/corosio/tls_stream.hpp @@ -113,6 +113,11 @@ class BOOST_COROSIO_DECL tls_stream For server connections, this waits for the ClientHello and sends the server's response. + A handshake attempt, successful or not, consumes the stream + state: a subsequent call behaves as if `reset()` had been + called first and performs a fresh handshake using the + current configuration. + @param role The handshake role, client or server. @return An awaitable yielding `(error_code)`. @@ -173,13 +178,13 @@ class BOOST_COROSIO_DECL tls_stream handshake with `std::errc::function_not_supported` rather than skip verification. - @note The hostname is meaningful for client handshakes; - backends may ignore it when handshaking as a server. - @par Postconditions The next `handshake()` uses `hostname` for SNI and certificate verification, or neither if it is empty. + @note The hostname is used for client handshakes only; + it is ignored when handshaking as a server. + @param hostname The peer hostname, or empty to disable. */ virtual void set_hostname(std::string_view hostname) = 0; diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index 27f1c5e1c..c67ad787a 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -86,7 +86,7 @@ is_ip_literal(std::string const& s) noexcept return !parse_ipv4_address(s, v4) || !parse_ipv6_address(s, v6); } -inline void +inline bool apply_hostname_verification(SSL* ssl, std::string const& hostname) { // SSL_clear retains a previously applied name; an empty hostname @@ -101,16 +101,18 @@ apply_hostname_verification(SSL* ssl, std::string const& hostname) bool const is_ip = name && is_ip_literal(hostname); char const* dns_name = is_ip ? nullptr : name; - SSL_set_tlsext_host_name(ssl, dns_name); + if (SSL_set_tlsext_host_name(ssl, dns_name) != 1) + return false; - if (auto* param = SSL_get0_param(ssl)) - { - X509_VERIFY_PARAM_set1_host(param, dns_name, 0); - if (is_ip) - X509_VERIFY_PARAM_set1_ip_asc(param, name); - else - X509_VERIFY_PARAM_set1_ip(param, nullptr, 0); - } + auto* param = SSL_get0_param(ssl); + if (!param) + return name == nullptr; + + if (X509_VERIFY_PARAM_set1_host(param, dns_name, 0) != 1) + return false; + if (is_ip) + return X509_VERIFY_PARAM_set1_ip_asc(param, name) == 1; + return X509_VERIFY_PARAM_set1_ip(param, nullptr, 0) == 1; } // Map a portable protocol version to the OpenSSL version constant. @@ -677,7 +679,10 @@ struct openssl_stream::impl tls_context ctx_; SSL* ssl_ = nullptr; BIO* ext_bio_ = nullptr; - bool used_ = false; + + // A handshake was attempted (successfully or not); the stream + // must be reset before the next handshake. + bool used_ = false; // Per-stream SNI/verification hostname, set via set_hostname(). std::string hostname_; @@ -947,7 +952,22 @@ struct openssl_stream::impl if (used_) reset(); - apply_hostname_verification(ssl_, hostname_); + // A failed attempt leaves the SSL in a dead state; any attempt, + // not just a completed handshake, consumes the stream so the + // next handshake() starts fresh. + used_ = true; + + // The hostname applies to client handshakes only; a server + // handshake clears any name left by a prior client-role + // handshake so client certificates are never hostname-matched. + std::string const no_name; + if (!apply_hostname_verification( + ssl_, role == tls_role::client ? hostname_ : no_name)) + { + // Fail closed rather than handshake without the requested + // name check. + co_return std::make_error_code(std::errc::invalid_argument); + } std::error_code ec; @@ -978,7 +998,6 @@ struct openssl_stream::impl if (ret == 1) { - used_ = true; capture_alpn(); ec = co_await flush_output(); co_return {ec}; diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index 6b6866923..b364f2d27 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -682,7 +682,10 @@ struct wolfssl_stream::impl capy::any_stream* s_; tls_context ctx_; WOLFSSL* ssl_ = nullptr; - bool used_ = false; + + // A handshake was attempted (successfully or not); the stream + // must be reset before the next handshake. + bool used_ = false; // Per-stream SNI/verification hostname, set via set_hostname(). std::string hostname_; @@ -1100,6 +1103,12 @@ struct wolfssl_stream::impl if (used_) reset(); + // A failed attempt leaves ssl_ in a dead state that would also + // skip re-initialization (and a rechanged hostname) on retry; + // any attempt, not just a completed handshake, consumes the + // stream so the next handshake() starts fresh. + used_ = true; + std::error_code ec; // Initialize SSL object for the specified role (deferred from construction) @@ -1128,7 +1137,6 @@ struct wolfssl_stream::impl if (ret == WOLFSSL_SUCCESS) { // Handshake completed successfully - used_ = true; capture_alpn(); // Flush any remaining output if (read_out_len_ > 0) @@ -1506,12 +1514,22 @@ struct wolfssl_stream::impl else { // Set SNI extension so server knows which cert to present - wolfSSL_UseSNI( + int ret = wolfSSL_UseSNI( ssl_, WOLFSSL_SNI_HOST_NAME, hostname_.data(), static_cast(hostname_.size())); // Enable hostname verification (checks CN/SAN in peer cert) - wolfSSL_check_domain_name(ssl_, hostname_.c_str()); + if (ret == WOLFSSL_SUCCESS) + ret = wolfSSL_check_domain_name(ssl_, hostname_.c_str()); + + if (ret != WOLFSSL_SUCCESS) + { + // Fail closed rather than handshake without the + // requested name check. + wolfSSL_free(ssl_); + ssl_ = nullptr; + return std::make_error_code(std::errc::invalid_argument); + } } } diff --git a/test/unit/openssl_stream.cpp b/test/unit/openssl_stream.cpp index e39f805bd..defcfb53c 100644 --- a/test/unit/openssl_stream.cpp +++ b/test/unit/openssl_stream.cpp @@ -169,6 +169,7 @@ struct openssl_stream_test test::testHostnamePersistence(make_stream); test::testHostnameRedirect(make_stream); test::testHostnameClear(make_stream); + test::testHostnameRetryAfterFailure(make_stream); test::testHostnameIpLiteral(make_stream, /*ip_supported=*/true); test::testAlpnAccessorEmpty(make_stream); test::testAlpn(make_stream, /*alpn_supported=*/true); diff --git a/test/unit/tls_stream_tests.hpp b/test/unit/tls_stream_tests.hpp index adc77112a..629bf45e6 100644 --- a/test/unit/tls_stream_tests.hpp +++ b/test/unit/tls_stream_tests.hpp @@ -709,6 +709,89 @@ testHostnameClear(StreamFactory make_stream) m2.close(); // NOLINT(bugprone-unused-return-value) } +/** A hostname set after a failed handshake attempt takes effect on + the retry, without an intervening reset(): the failed attempt + must not pin the old name or the dead session into the next + handshake. */ +template +void +testHostnameRetryAfterFailure(StreamFactory make_stream) +{ + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair(ioc); + + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + + std::vector seen; + server_ctx.set_servername_callback( + [&seen](std::string_view hostname) -> bool { + seen.emplace_back(hostname); + return true; + }); + + auto client = make_stream(m1, client_ctx); + auto server = make_stream(m2, server_ctx); + + // Round 1: a staged fatal alert record fails the client handshake + // deterministically, after its hello is already on the wire. + client.set_hostname("stale.example.com"); + m1.provide(std::string("\x15\x03\x03\x00\x02\x02\x28", 7)); + + std::error_code hs_ec; + auto hs = [&]() -> capy::task<> { + auto [ec] = co_await client.handshake(tls_role::client); + hs_ec = ec; + }; + capy::run_async(ioc.get_executor())(hs()); + ioc.run(); + ioc.restart(); + BOOST_TEST(hs_ec != std::error_code()); + + // Drain the stale hello (one TLS record) from the raw peer socket + // so the server handshake below starts on a clean transport. + auto drain = [&]() -> capy::task<> { + unsigned char hdr[5]; + std::size_t got = 0; + while (got < sizeof(hdr)) + { + auto [ec, n] = co_await m2.read_some(capy::mutable_buffer( + hdr + got, sizeof(hdr) - got)); + if (ec) + co_return; + got += n; + } + std::vector body( + (std::size_t(hdr[3]) << 8) | std::size_t(hdr[4])); + got = 0; + while (got < body.size()) + { + auto [ec, n] = co_await m2.read_some(capy::mutable_buffer( + body.data() + got, body.size() - got)); + if (ec) + co_return; + got += n; + } + }; + capy::run_async(ioc.get_executor())(drain()); + ioc.run(); + ioc.restart(); + + // Round 2: the new name must take effect without reset() + client.set_hostname("www.example.com"); + hostname_test_detail::run_hostname_round( + ioc, client, server, m1, m2, true); + + // The server only ever saw the retry's SNI; the stale hello was + // consumed above, before the server stream touched the transport. + BOOST_TEST_EQ(seen.size(), 1u); + if (seen.size() == 1u) + BOOST_TEST_EQ(seen[0], "www.example.com"); + + m1.close(); // NOLINT(bugprone-unused-return-value) + m2.close(); // NOLINT(bugprone-unused-return-value) +} + /** An IP-literal hostname is matched against the certificate's iPAddress entries and is not sent as SNI; a mismatched literal fails verification. On a build that cannot match iPAddress @@ -720,21 +803,18 @@ testHostnameIpLiteral(StreamFactory make_stream, bool ip_supported) io_context ioc; auto [m1, m2] = corosio::test::make_mocket_pair(ioc); + // NOLINTBEGIN(bugprone-unused-return-value) tls_context client_ctx; - client_ctx.add_certificate_authority( - test::server_ip_cert_pem); // NOLINT(bugprone-unused-return-value) - client_ctx.set_verify_mode( - tls_verify_mode::peer); // NOLINT(bugprone-unused-return-value) + client_ctx.add_certificate_authority(test::server_ip_cert_pem); + client_ctx.set_verify_mode(tls_verify_mode::peer); tls_context server_ctx; server_ctx.use_certificate( - test::server_ip_cert_pem, - tls_file_format::pem); // NOLINT(bugprone-unused-return-value) + test::server_ip_cert_pem, tls_file_format::pem); server_ctx.use_private_key( - test::server_ip_key_pem, - tls_file_format::pem); // NOLINT(bugprone-unused-return-value) - server_ctx.set_verify_mode( - tls_verify_mode::none); // NOLINT(bugprone-unused-return-value) + test::server_ip_key_pem, tls_file_format::pem); + server_ctx.set_verify_mode(tls_verify_mode::none); + // NOLINTEND(bugprone-unused-return-value) std::size_t sni_count = 0; server_ctx.set_servername_callback( diff --git a/test/unit/wolfssl_stream.cpp b/test/unit/wolfssl_stream.cpp index 250c14d3e..4e1ccb77c 100644 --- a/test/unit/wolfssl_stream.cpp +++ b/test/unit/wolfssl_stream.cpp @@ -161,6 +161,7 @@ struct wolfssl_stream_test test::testHostnamePersistence(make_stream); test::testHostnameRedirect(make_stream); test::testHostnameClear(make_stream); + test::testHostnameRetryAfterFailure(make_stream); // IP-literal matching is build-gated (WOLFSSL_IP_ALT_NAME); // when absent, an IP-literal hostname fails closed. test::testHostnameIpLiteral( From 6a401d6091cff00e0a88b73d2f633bdb9aef18e9 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Tue, 21 Jul 2026 23:59:49 +0200 Subject: [PATCH 5/5] fix(tls): install IP literals via the WolfSSL verify params directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wolfSSL_check_ip_address reports success even on builds that compiled out enforcement, so its return check could never fail closed. Setting the literal through wolfSSL_get0_param and wolfSSL_X509_VERIFY_PARAM_set1_ip_asc installs it on the exact path the certificate check consults, returns a meaningful failure, and binds symbols exported only by OPENSSL_EXTRA builds — running against a WolfSSL downgraded below the build-time feature set now fails at load instead of silently skipping the name check. --- src/wolfssl/src/wolfssl_stream.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index b364f2d27..4171c93aa 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -1492,17 +1492,25 @@ struct wolfssl_stream::impl // Enforcement needs both flags: OPENSSL_EXTRA routes // the address into the verify params that the cert // check consults, and WOLFSSL_IP_ALT_NAME makes the - // parser record iPAddress entries at all. Without - // either, wolfSSL_check_ip_address still returns - // success and verification silently checks nothing. + // parser record iPAddress entries at all. #if defined(OPENSSL_EXTRA) && defined(WOLFSSL_IP_ALT_NAME) - if (wolfSSL_check_ip_address(ssl_, hostname_.c_str()) - != WOLFSSL_SUCCESS) + // Install via the verify params directly, not + // wolfSSL_check_ip_address: that wrapper reports + // success even when enforcement is compiled out, and + // binding these OPENSSL_EXTRA-only symbols makes a + // run against a downgraded WolfSSL fail at load + // rather than skip the check silently. + WOLFSSL_X509_VERIFY_PARAM* vp = wolfSSL_get0_param(ssl_); + if (!vp || + wolfSSL_X509_VERIFY_PARAM_set1_ip_asc( + vp, hostname_.c_str()) != WOLFSSL_SUCCESS) { + // Fail closed rather than handshake without the + // requested name check. wolfSSL_free(ssl_); ssl_ = nullptr; return std::make_error_code( - std::errc::function_not_supported); + std::errc::invalid_argument); } #else wolfSSL_free(ssl_);