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..39f58c4ec 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,8 +281,9 @@ 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) + 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 270ada7fc..fc35802d2 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,11 +570,11 @@ 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 ); -co_await secure.handshake( corosio::wolfssl_stream::client ); +secure.set_hostname( "api.example.com" ); +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 75dde6984..53f0ab407 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,7 +96,8 @@ 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); -if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec) +secure.set_hostname("api.example.com"); +if (auto [ec] = co_await secure.handshake(tls_role::client); ec) throw std::system_error(ec); // 4. Use encrypted I/O @@ -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: @@ -314,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: @@ -401,18 +408,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 @@ -464,7 +471,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"; @@ -476,7 +483,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 @@ -629,7 +636,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,7 +643,8 @@ capy::task https_get( // Wrap the connected socket (pointer form) and handshake corosio::wolfssl_stream secure(&sock, ctx); - if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec) + secure.set_hostname(hostname); + if (auto [ec] = co_await secure.handshake(tls_role::client); ec) throw std::system_error(ec); // Send HTTP request @@ -704,7 +711,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; @@ -746,7 +753,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..b4050b804 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,9 +85,10 @@ 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) + 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/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..212f982c0 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,7 +58,8 @@ namespace boost::corosio { // Reference mode - sock must outlive tls corosio::openssl_stream tls(&sock, ctx); - auto [ec] = co_await tls.handshake(openssl_stream::client); + tls.set_hostname("example.com"); + 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. @@ -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..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 @@ -244,11 +229,11 @@ 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 ); - co_await secure.handshake( corosio::tls_stream::client ); + secure.set_hostname( "example.com" ); + co_await secure.handshake( corosio::tls_role::client ); @endcode @see tls_role @@ -593,7 +578,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 +596,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 +820,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 +848,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..d653557b2 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,16 @@ 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). + 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)`. */ - virtual capy::io_task<> handshake(handshake_type type) = 0; + virtual capy::io_task<> handshake(tls_role role) = 0; /** Perform a graceful TLS shutdown asynchronously. @@ -145,6 +158,37 @@ 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. + + 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. + + @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; + /** 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..031e6b0cc 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,7 +58,8 @@ namespace boost::corosio { // Reference mode - sock must outlive tls corosio::wolfssl_stream tls(&sock, ctx); - auto [ec] = co_await tls.handshake(wolfssl_stream::client); + tls.set_hostname("example.com"); + 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. @@ -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 { @@ -269,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/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..c67ad787a 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,20 +77,42 @@ tls_method_compat() noexcept #endif } -inline void -apply_hostname_verification(SSL* ssl, std::string const& hostname) +// Whether the peer name is an IP literal rather than a DNS name. +inline bool +is_ip_literal(std::string const& s) noexcept { - if (hostname.empty()) - return; - - SSL_set_tlsext_host_name(ssl, hostname.c_str()); + ipv4_address v4; + ipv6_address v6; + return !parse_ipv4_address(s, v4) || !parse_ipv6_address(s, v6); +} -#if OPENSSL_VERSION_NUMBER >= 0x10100000L - SSL_set1_host(ssl, hostname.c_str()); -#else - if (auto* param = SSL_get0_param(ssl)) - X509_VERIFY_PARAM_set1_host(param, hostname.c_str(), 0); -#endif +inline bool +apply_hostname_verification(SSL* ssl, std::string const& hostname) +{ + // 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(); + + // 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; + + if (SSL_set_tlsext_host_name(ssl, dns_name) != 1) + return false; + + 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. @@ -654,7 +679,13 @@ 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_; // ALPN protocol negotiated during the handshake (empty if none). std::string alpn_selected_; @@ -686,15 +717,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; } @@ -904,7 +938,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/ @@ -918,12 +952,29 @@ struct openssl_stream::impl if (used_) reset(); + // 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; // 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 @@ -940,14 +991,13 @@ 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_); if (ret == 1) { - used_ = true; capture_alpn(); ec = co_await flush_output(); co_return {ec}; @@ -1077,8 +1127,6 @@ struct openssl_stream::impl SSL_set_bio(ssl_, int_bio, int_bio); - apply_hostname_verification(ssl_, cd.hostname); - return {}; } }; @@ -1142,9 +1190,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<> @@ -1159,6 +1207,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..4171c93aa 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 // @@ -660,7 +682,13 @@ 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_; // ALPN protocol negotiated during the handshake (empty if none). std::string alpn_selected_; @@ -1070,15 +1098,21 @@ 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(); + // 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) - ec = init_ssl_for_role(type); + ec = init_ssl_for_role(role); if (ec) co_return {ec}; @@ -1093,9 +1127,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_); @@ -1103,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) @@ -1336,7 +1369,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_) @@ -1359,7 +1392,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_; @@ -1449,16 +1482,63 @@ 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 (role == tls_role::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())); + 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. +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_IP_ALT_NAME) + // 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::invalid_argument); + } +#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 + 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_, cd.hostname.c_str()); + // Enable hostname verification (checks CN/SAN in peer cert) + 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); + } + } } return {}; @@ -1516,9 +1596,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<> @@ -1533,6 +1613,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..defcfb53c 100644 --- a/test/unit/openssl_stream.cpp +++ b/test/unit/openssl_stream.cpp @@ -166,6 +166,11 @@ 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::testHostnameRetryAfterFailure(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 e4b7aadef..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) @@ -1401,14 +1466,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 +1525,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 +1594,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 +1619,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 +1708,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 +1820,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 +2133,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 +2218,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 +2292,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 +2414,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 +2528,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 +2647,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_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_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 bacb0d5de..629bf45e6 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; @@ -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,370 @@ 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_role::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_role::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 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 + 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); + + // NOLINTBEGIN(bugprone-unused-return-value) + tls_context client_ctx; + 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); + server_ctx.use_private_key( + 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( + [&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 @@ -917,17 +1299,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()); @@ -1043,13 +1424,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)); @@ -1201,15 +1581,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()); @@ -1263,16 +1642,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()); @@ -1335,9 +1713,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; @@ -1349,14 +1726,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) @@ -1408,22 +1785,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) @@ -1499,11 +1875,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; }; @@ -1595,11 +1971,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; }; @@ -1689,11 +2065,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()); @@ -1729,11 +2105,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()); diff --git a/test/unit/wolfssl_stream.cpp b/test/unit/wolfssl_stream.cpp index 089e3c296..4e1ccb77c 100644 --- a/test/unit/wolfssl_stream.cpp +++ b/test/unit/wolfssl_stream.cpp @@ -158,6 +158,14 @@ 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::testHostnameRetryAfterFailure(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