Repository: randombit/botan
Pull request #3762 (2023-10-16 08:33:45)

Introduce TLS::Channel::is_handshake complete()
... and fix a minor issue in the ASIO stream using it. Namely, that `async_handshake` did prematurely notify "completion" in the TLS 1.3 server use case. For a typical application, nothing bad will happen except that it'll get the handshake completion callback prematurely.

Background: Technically, RFC 8446 Section 4.4.4 allows the server to send application data as soon as it sent its `Finished` message -- without waiting for the client's `Finished`:

> Servers MAY send data after sending their first flight, but because the handshake is not yet complete, they have no assurance of either the peer's identity or its liveness [...]

The semantics of `TLS::Channel::is_active() == true` is that "we can write". Therefore, the predicate method `TLS::Channel::is_active()` becomes `true` _before_ the TLS 1.3 server receives the client's `Finished`.

The ASIO stream's `AsyncHandshakeOperation` used to rely on `is_active()` to detect success. Now it relies on `is_handshake_complete()`, fixing the above-described discrepancy.


Patch:
diff --git a/src/lib/tls/asio/asio_async_ops.h b/src/lib/tls/asio/asio_async_ops.h
index 254a1a7f4..9911dbe16 100644
--- a/src/lib/tls/asio/asio_async_ops.h
+++ b/src/lib/tls/asio/asio_async_ops.h
@@ -278,7 +278,7 @@ class AsyncHandshakeOperation : public AsyncBase<Handler, typename Stream::execu
                return;
             }
 
-            if(!m_stream.native_handle()->is_active() && !ec) {
+            if(!m_stream.native_handle()->is_handshake_complete() && !ec) {
                // Read more encrypted TLS data from the network
                m_stream.next_layer().async_read_some(m_stream.input_buffer(), std::move(*this));
                return;
diff --git a/src/lib/tls/asio/asio_stream.h b/src/lib/tls/asio/asio_stream.h
index 9da6c1a23..cd6532ae6 100644
--- a/src/lib/tls/asio/asio_stream.h
+++ b/src/lib/tls/asio/asio_stream.h
@@ -229,7 +229,7 @@ class Stream {
             send_pending_encrypted_data(ec);
          }
 
-         while(!native_handle()->is_active() && !ec) {
+         while(!native_handle()->is_handshake_complete() && !ec) {
             boost::asio::const_buffer read_buffer{input_buffer().data(), m_nextLayer.read_some(input_buffer(), ec)};
             if(ec) {
                return;
diff --git a/src/lib/tls/tls12/tls_channel_impl_12.cpp b/src/lib/tls/tls12/tls_channel_impl_12.cpp
index 93030abfe..5adcc7de7 100644
--- a/src/lib/tls/tls12/tls_channel_impl_12.cpp
+++ b/src/lib/tls/tls12/tls_channel_impl_12.cpp
@@ -237,13 +237,14 @@ void Channel_Impl_12::change_cipher_spec_writer(Connection_Side side) {
    m_write_cipher_states[epoch] = write_state;
 }
 
-bool Channel_Impl_12::is_active() const {
-   if(is_closed()) {
-      return false;
-   }
+bool Channel_Impl_12::is_handshake_complete() const {
    return (active_state() != nullptr);
 }
 
+bool Channel_Impl_12::is_active() const {
+   return !is_closed() && is_handshake_complete();
+}
+
 bool Channel_Impl_12::is_closed() const {
    return m_has_been_closed;
 }
diff --git a/src/lib/tls/tls12/tls_channel_impl_12.h b/src/lib/tls/tls12/tls_channel_impl_12.h
index 5b9f9ee6e..ccfe0780c 100644
--- a/src/lib/tls/tls12/tls_channel_impl_12.h
+++ b/src/lib/tls/tls12/tls_channel_impl_12.h
@@ -83,6 +83,11 @@ class Channel_Impl_12 : public Channel_Impl {
       */
       void send_alert(const Alert& alert) override;
 
+      /**
+      * @return true iff the TLS handshake completed successfully
+      */
+      bool is_handshake_complete() const override;
+
       /**
       * @return true iff the connection is active for sending application data
       */
diff --git a/src/lib/tls/tls13/tls_channel_impl_13.cpp b/src/lib/tls/tls13/tls_channel_impl_13.cpp
index 6cb234375..aa49fe817 100644
--- a/src/lib/tls/tls13/tls_channel_impl_13.cpp
+++ b/src/lib/tls/tls13/tls_channel_impl_13.cpp
@@ -107,7 +107,7 @@ size_t Channel_Impl_13::from_peer(std::span<const uint8_t> data) {
          if(record.type == Record_Type::Handshake) {
             m_handshake_layer.copy_data(record.fragment);
 
-            if(!handshake_finished()) {
+            if(!is_handshake_complete()) {
                while(auto handshake_msg = m_handshake_layer.next_message(policy(), m_transcript_hash)) {
                   // RFC 8446 5.1
                   //    Handshake messages MUST NOT span key changes.  Implementations
@@ -316,7 +316,7 @@ SymmetricKey Channel_Impl_13::key_material_export(std::string_view label,
 
 void Channel_Impl_13::update_traffic_keys(bool request_peer_update) {
    BOTAN_STATE_CHECK(!is_downgrading());
-   BOTAN_STATE_CHECK(handshake_finished());
+   BOTAN_STATE_CHECK(is_handshake_complete());
    BOTAN_ASSERT_NONNULL(m_cipher_state);
    send_post_handshake_message(Key_Update(request_peer_update));
    m_cipher_state->update_write_keys();
diff --git a/src/lib/tls/tls13/tls_channel_impl_13.h b/src/lib/tls/tls13/tls_channel_impl_13.h
index 660869ba3..b3f0e35c1 100644
--- a/src/lib/tls/tls13/tls_channel_impl_13.h
+++ b/src/lib/tls/tls13/tls_channel_impl_13.h
@@ -193,7 +193,6 @@ class Channel_Impl_13 : public Channel_Impl {
       virtual void process_handshake_msg(Handshake_Message_13 msg) = 0;
       virtual void process_post_handshake_msg(Post_Handshake_Message_13 msg) = 0;
       virtual void process_dummy_change_cipher_spec() = 0;
-      virtual bool handshake_finished() const = 0;
 
       /**
        * @return whether a change cipher spec record should be prepended _now_
diff --git a/src/lib/tls/tls13/tls_client_impl_13.cpp b/src/lib/tls/tls13/tls_client_impl_13.cpp
index ce4c9a0b9..3d097fddb 100644
--- a/src/lib/tls/tls13/tls_client_impl_13.cpp
+++ b/src/lib/tls/tls13/tls_client_impl_13.cpp
@@ -91,7 +91,7 @@ void Client_Impl_13::process_handshake_msg(Handshake_Message_13 message) {
 }
 
 void Client_Impl_13::process_post_handshake_msg(Post_Handshake_Message_13 message) {
-   BOTAN_STATE_CHECK(handshake_finished());
+   BOTAN_STATE_CHECK(is_handshake_complete());
 
    std::visit([&](auto msg) { handle(msg); }, m_handshake_state.received(std::move(message)));
 }
@@ -114,7 +114,7 @@ void Client_Impl_13::process_dummy_change_cipher_spec() {
    // ... no further processing.
 }
 
-bool Client_Impl_13::handshake_finished() const {
+bool Client_Impl_13::is_handshake_complete() const {
    return m_handshake_state.handshake_finished();
 }
 
@@ -418,7 +418,7 @@ void Client_Impl_13::handle(const Certificate_Request_13& certificate_request_ms
    // RFC 8446 4.3.2
    //    [The 'context' field] SHALL be zero length unless used for the
    //    post-handshake authentication exchanges described in Section 4.6.2.
-   if(!m_handshake_state.handshake_finished() && !certificate_request_msg.context().empty()) {
+   if(!is_handshake_complete() && !certificate_request_msg.context().empty()) {
       throw TLS_Exception(Alert::DecodeError, "Certificate_Request context must be empty in the main handshake");
    }
 
@@ -595,7 +595,7 @@ bool Client_Impl_13::prepend_ccs() {
 }
 
 std::string Client_Impl_13::application_protocol() const {
-   if(m_handshake_state.handshake_finished()) {
+   if(is_handshake_complete()) {
       const auto& eee = m_handshake_state.encrypted_extensions().extensions();
       if(eee.has<Application_Layer_Protocol_Notification>()) {
          return eee.get<Application_Layer_Protocol_Notification>()->single_protocol();
diff --git a/src/lib/tls/tls13/tls_client_impl_13.h b/src/lib/tls/tls13/tls_client_impl_13.h
index 97aca1166..33c7a9037 100644
--- a/src/lib/tls/tls13/tls_client_impl_13.h
+++ b/src/lib/tls/tls13/tls_client_impl_13.h
@@ -68,12 +68,16 @@ class Client_Impl_13 : public Channel_Impl_13 {
        */
       std::optional<std::string> external_psk_identity() const override;
 
+      /**
+       * @return true if the TLS handshake finished successfully
+       */
+      bool is_handshake_complete() const override;
+
    private:
       void process_handshake_msg(Handshake_Message_13 msg) override;
       void process_post_handshake_msg(Post_Handshake_Message_13 msg) override;
       void process_dummy_change_cipher_spec() override;
 
-      bool handshake_finished() const override;
       bool prepend_ccs() override;
 
       using Channel_Impl_13::handle;
diff --git a/src/lib/tls/tls13/tls_server_impl_13.cpp b/src/lib/tls/tls13/tls_server_impl_13.cpp
index e2711876d..f9d0c5ed1 100644
--- a/src/lib/tls/tls13/tls_server_impl_13.cpp
+++ b/src/lib/tls/tls13/tls_server_impl_13.cpp
@@ -32,7 +32,7 @@ Server_Impl_13::Server_Impl_13(const std::shared_ptr<Callbacks>& callbacks,
 }
 
 std::string Server_Impl_13::application_protocol() const {
-   if(m_handshake_state.handshake_finished()) {
+   if(is_handshake_complete()) {
       const auto& eee = m_handshake_state.encrypted_extensions().extensions();
       if(const auto alpn = eee.get<Application_Layer_Protocol_Notification>()) {
          return alpn->single_protocol();
@@ -67,14 +67,13 @@ bool Server_Impl_13::new_session_ticket_supported() const {
    //       regardless of this method indicating no support for tickets.
    //
    // TODO: Implement other PSK KE modes than PSK_DHE_KE
-   return m_handshake_state.handshake_finished() &&
-          m_handshake_state.client_hello().extensions().has<PSK_Key_Exchange_Modes>() &&
+   return is_handshake_complete() && m_handshake_state.client_hello().extensions().has<PSK_Key_Exchange_Modes>() &&
           value_exists(m_handshake_state.client_hello().extensions().get<PSK_Key_Exchange_Modes>()->modes(),
                        PSK_Key_Exchange_Mode::PSK_DHE_KE);
 }
 
 size_t Server_Impl_13::send_new_session_tickets(const size_t tickets) {
-   BOTAN_STATE_CHECK(handshake_finished());
+   BOTAN_STATE_CHECK(is_handshake_complete());
 
    if(tickets == 0) {
       return 0;
@@ -125,7 +124,7 @@ void Server_Impl_13::process_handshake_msg(Handshake_Message_13 message) {
 }
 
 void Server_Impl_13::process_post_handshake_msg(Post_Handshake_Message_13 message) {
-   BOTAN_STATE_CHECK(handshake_finished());
+   BOTAN_STATE_CHECK(is_handshake_complete());
 
    std::visit([&](auto msg) { handle(msg); }, m_handshake_state.received(std::move(message)));
 }
@@ -148,7 +147,7 @@ void Server_Impl_13::process_dummy_change_cipher_spec() {
    // ... no further processing.
 }
 
-bool Server_Impl_13::handshake_finished() const {
+bool Server_Impl_13::is_handshake_complete() const {
    return m_handshake_state.handshake_finished();
 }
 
@@ -440,7 +439,7 @@ void Server_Impl_13::handle(const Certificate_13& certificate_msg) {
    // RFC 8446 4.3.2
    //    certificate_request_context:  [...] This field SHALL be zero length
    //    unless used for the post-handshake authentication exchanges [...].
-   if(!handshake_finished() && !certificate_msg.request_context().empty()) {
+   if(!is_handshake_complete() && !certificate_msg.request_context().empty()) {
       throw TLS_Exception(Alert::DecodeError, "Received a client certificate message with non-empty request context");
    }
 
diff --git a/src/lib/tls/tls13/tls_server_impl_13.h b/src/lib/tls/tls13/tls_server_impl_13.h
index d8b5f3cae..dd848e340 100644
--- a/src/lib/tls/tls13/tls_server_impl_13.h
+++ b/src/lib/tls/tls13/tls_server_impl_13.h
@@ -33,6 +33,8 @@ class Server_Impl_13 : public Channel_Impl_13 {
       bool new_session_ticket_supported() const override;
       size_t send_new_session_tickets(size_t tickets) override;
 
+      bool is_handshake_complete() const override;
+
    private:
       void process_handshake_msg(Handshake_Message_13 msg) override;
       void process_post_handshake_msg(Post_Handshake_Message_13 msg) override;
@@ -50,8 +52,6 @@ class Server_Impl_13 : public Channel_Impl_13 {
 
       void maybe_handle_compatibility_mode();
 
-      bool handshake_finished() const override;
-
       void downgrade();
 
    private:
diff --git a/src/lib/tls/tls_channel.h b/src/lib/tls/tls_channel.h
index 6819cfb9c..dc74ba468 100644
--- a/src/lib/tls/tls_channel.h
+++ b/src/lib/tls/tls_channel.h
@@ -89,6 +89,22 @@ class BOTAN_PUBLIC_API(2, 0) Channel {
       virtual void close() = 0;
 
       /**
+      * Becomes true as soon as the TLS handshake is fully complete and all
+      * security assurances TLS provides can be guaranteed.
+      *
+      * @returns true once the TLS handshake has finished successfully
+      */
+      virtual bool is_handshake_complete() const = 0;
+
+      /**
+      * Check whether the connection is ready to send application data. Note
+      * that a TLS 1.3 server MAY send data _before_ receiving the client's
+      * Finished message. Only _after_ receiving the client's Finished, can the
+      * server be sure about the client's liveness and (optional) identity.
+      *
+      * Consider using is_handshake_complete() if you need to wait until the
+      * handshake if fully complete.
+      *
       * @return true iff the connection is active for sending application data
       */
       virtual bool is_active() const = 0;
diff --git a/src/lib/tls/tls_channel_impl.h b/src/lib/tls/tls_channel_impl.h
index b9bf8bc50..e39b88c10 100644
--- a/src/lib/tls/tls_channel_impl.h
+++ b/src/lib/tls/tls_channel_impl.h
@@ -79,6 +79,11 @@ class Channel_Impl {
       */
       void close() { send_warning_alert(Alert::CloseNotify); }
 
+      /**
+      * @return true iff the TLS handshake has finished successfully
+      */
+      virtual bool is_handshake_complete() const = 0;
+
       /**
       * @return true iff the connection is active for sending application data
       */
diff --git a/src/lib/tls/tls_client.cpp b/src/lib/tls/tls_client.cpp
index 6fbb75040..c28cc2a65 100644
--- a/src/lib/tls/tls_client.cpp
+++ b/src/lib/tls/tls_client.cpp
@@ -97,6 +97,10 @@ size_t Client::from_peer(std::span<const uint8_t> data) {
    return read;
 }
 
+bool Client::is_handshake_complete() const {
+   return m_impl->is_handshake_complete();
+}
+
 bool Client::is_active() const {
    return m_impl->is_active();
 }
diff --git a/src/lib/tls/tls_client.h b/src/lib/tls/tls_client.h
index 918e87eb8..fce64d16e 100644
--- a/src/lib/tls/tls_client.h
+++ b/src/lib/tls/tls_client.h
@@ -70,6 +70,9 @@ class BOTAN_PUBLIC_API(2, 0) Client final : public Channel {
       std::string application_protocol() const override;
 
       size_t from_peer(std::span<const uint8_t> data) override;
+
+      bool is_handshake_complete() const override;
+
       bool is_active() const override;
 
       bool is_closed() const override;
diff --git a/src/lib/tls/tls_server.cpp b/src/lib/tls/tls_server.cpp
index 1993e418f..428d0b55a 100644
--- a/src/lib/tls/tls_server.cpp
+++ b/src/lib/tls/tls_server.cpp
@@ -65,6 +65,10 @@ size_t Server::from_peer(std::span<const uint8_t> data) {
    return read;
 }
 
+bool Server::is_handshake_complete() const {
+   return m_impl->is_handshake_complete();
+}
+
 bool Server::is_active() const {
    return m_impl->is_active();
 }
diff --git a/src/lib/tls/tls_server.h b/src/lib/tls/tls_server.h
index 9e29bdfaa..e7dbf35f5 100644
--- a/src/lib/tls/tls_server.h
+++ b/src/lib/tls/tls_server.h
@@ -66,6 +66,8 @@ class BOTAN_PUBLIC_API(2, 0) Server final : public Channel {
 
       size_t from_peer(std::span<const uint8_t> data) override;
 
+      bool is_handshake_complete() const override;
+
       bool is_active() const override;
 
       bool is_closed() const override;


Test patch:
diff --git a/src/tests/test_tls_rfc8448.cpp b/src/tests/test_tls_rfc8448.cpp
index 2f0acbcac..a3dff9f71 100644
--- a/src/tests/test_tls_rfc8448.cpp
+++ b/src/tests/test_tls_rfc8448.cpp
@@ -977,6 +977,7 @@ class Test_TLS_RFC8448_Client : public Test_TLS_RFC8448 {
                                                    "tls_ephemeral_key_agreement"});
 
                   result.confirm("client is not yet active", !ctx->client.is_active());
+                  result.confirm("handshake is not yet complete", !ctx->client.is_handshake_complete());
                }),
 
             Botan_Tests::CHECK(
@@ -1002,6 +1003,7 @@ class Test_TLS_RFC8448_Client : public Test_TLS_RFC8448 {
                   result.require("certificate exists", !ctx->certs_verified().empty());
                   result.require("correct certificate", ctx->certs_verified().front() == server_certificate());
                   result.require("client is active", ctx->client.is_active());
+                  result.confirm("handshake is complete", ctx->client.is_handshake_complete());
 
                   result.test_eq(
                      "correct handshake finished", ctx->pull_send_buffer(), vars.get_req_bin("Record_ClientFinished"));
@@ -1452,6 +1454,7 @@ class Test_TLS_RFC8448_Client : public Test_TLS_RFC8448 {
 
                                   result.confirm("client is ready to send application traffic",
                                                  ctx->client.is_active());
+                                  result.confirm("handshake is complete", ctx->client.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK(
@@ -1465,6 +1468,7 @@ class Test_TLS_RFC8448_Client : public Test_TLS_RFC8448 {
 
                   result.require("client cannot send application traffic anymore", !ctx->client.is_active());
                   result.require("client is not fully closed yet", !ctx->client.is_closed());
+                  result.confirm("handshake stays completed", ctx->client.is_handshake_complete());
 
                   ctx->client.received_data(vars.get_req_bin("Record_Server_CloseNotify"));
 
@@ -1541,6 +1545,7 @@ class Test_TLS_RFC8448_Client : public Test_TLS_RFC8448 {
                                                                    "tls_ephemeral_key_agreement"});
 
                                   result.confirm("client is not yet active", !ctx->client.is_active());
+                                  result.confirm("handshake is not yet complete", !ctx->client.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK(
@@ -1560,6 +1565,7 @@ class Test_TLS_RFC8448_Client : public Test_TLS_RFC8448 {
                                                    "tls_session_activated"});
                   result.require("PSK negotiated", ctx->psk_identity_negotiated() == vars.get_req_str("PskIdentity"));
                   result.require("client is active", ctx->client.is_active());
+                  result.confirm("handshake is complete", ctx->client.is_handshake_complete());
 
                   result.test_eq(
                      "correct handshake finished", ctx->pull_send_buffer(), vars.get_req_bin("Record_ClientFinished"));
@@ -1691,7 +1697,13 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                  concat(vars.get_req_bin("Record_ServerHello"),
                                                         vars.get_req_bin("Record_ServerHandshakeMessages")));
 
+                                  // Note: is_active() defines that we can send application data.
+                                  //       RFC 8446 Section 4.4.4 explicitly allows that for servers
+                                  //       that did not receive the client's Finished message, yet.
+                                  //       However, before receiving and validating this message,
+                                  //       the handshake is not yet finished.
                                   result.confirm("Server can now send application data", ctx->server.is_active());
+                                  result.confirm("handshake is not yet complete", !ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Send Client Finished",
@@ -1765,6 +1777,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is not yet closed", !ctx->server.is_closed());
                                   result.confirm("connection is still active", ctx->server.is_active());
+                                  result.confirm("handshake is still finished", ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Expect Server close_notify",
@@ -1774,6 +1787,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is now inactive", !ctx->server.is_active());
                                   result.confirm("connection is now closed", ctx->server.is_closed());
+                                  result.confirm("handshake is still finished", ctx->server.is_handshake_complete());
                                   result.test_eq("Server's close notify",
                                                  ctx->pull_send_buffer(),
                                                  vars.get_req_bin("Record_Server_CloseNotify"));
@@ -1849,7 +1863,13 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                  concat(vars.get_req_bin("Record_ServerHello"),
                                                         vars.get_req_bin("Record_ServerHandshakeMessages")));
 
+                                  // Note: is_active() defines that we can send application data.
+                                  //       RFC 8446 Section 4.4.4 explicitly allows that for servers
+                                  //       that did not receive the client's Finished message, yet.
+                                  //       However, before receiving and validating this message,
+                                  //       the handshake is not yet finished.
                                   result.confirm("Server can now send application data", ctx->server.is_active());
+                                  result.confirm("handshake is not yet complete", !ctx->server.is_handshake_complete());
                                }),
 
             // TODO: The rest of this test vector requires 0-RTT which is not
@@ -1908,6 +1928,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                  ctx->pull_send_buffer(),
                                                  vars.get_req_bin("Record_HelloRetryRequest"));
                                   result.confirm("TLS handshake not yet finished", !ctx->server.is_active());
+                                  result.confirm("handshake is not yet complete", !ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Receive updated Client Hello message",
@@ -1960,6 +1981,8 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                  concat(vars.get_req_bin("Record_ServerHello"),
                                                         vars.get_req_bin("Record_ServerHandshakeMessages")));
                                   result.confirm("Server could now send application data", ctx->server.is_active());
+                                  result.confirm("handshake is not yet complete",
+                                                 !ctx->server.is_handshake_complete());  // See RFC 8446 4.4.4
                                }),
 
             Botan_Tests::CHECK("Receive Client Finished",
@@ -1975,6 +1998,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                                    "tls_session_activated"});
 
                                   result.confirm("TLS handshake finished", ctx->server.is_active());
+                                  result.confirm("handshake is complete", ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Receive Client close_notify",
@@ -1987,6 +2011,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is not yet closed", !ctx->server.is_closed());
                                   result.confirm("connection is still active", ctx->server.is_active());
+                                  result.confirm("handshake is still complete", ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Expect Server close_notify",
@@ -1996,6 +2021,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is now inactive", !ctx->server.is_active());
                                   result.confirm("connection is now closed", ctx->server.is_closed());
+                                  result.confirm("handshake is still complete", ctx->server.is_handshake_complete());
                                   result.test_eq("Server's close notify",
                                                  ctx->pull_send_buffer(),
                                                  vars.get_req_bin("Record_Server_CloseNotify"));
@@ -2078,6 +2104,8 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                   result.confirm("Not yet aware of client's cert chain", ctx->server.peer_cert_chain().empty());
                   result.confirm("Server could now send application data", ctx->server.is_active());
+                  result.confirm("handshake is not yet complete",
+                                 !ctx->server.is_handshake_complete());  // See RFC 8446 4.4.4
                }),
 
             Botan_Tests::CHECK("Receive Client's second flight",
@@ -2106,6 +2134,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                  !cert_chain.empty() && cert_chain.front() == client_certificate());
 
                                   result.confirm("TLS handshake finished", ctx->server.is_active());
+                                  result.confirm("handshake is complete", ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Receive Client close_notify",
@@ -2118,6 +2147,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is not yet closed", !ctx->server.is_closed());
                                   result.confirm("connection is still active", ctx->server.is_active());
+                                  result.confirm("handshake is still complete", ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Expect Server close_notify",
@@ -2127,6 +2157,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is now inactive", !ctx->server.is_active());
                                   result.confirm("connection is now closed", ctx->server.is_closed());
+                                  result.confirm("handshake is still complete", ctx->server.is_handshake_complete());
                                   result.test_eq("Server's close notify",
                                                  ctx->pull_send_buffer(),
                                                  vars.get_req_bin("Record_Server_CloseNotify"));
@@ -2204,6 +2235,8 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                         vars.get_req_bin("Record_ServerHandshakeMessages")));
 
                                   result.confirm("Server could now send application data", ctx->server.is_active());
+                                  result.confirm("handshake is not yet complete",
+                                                 !ctx->server.is_handshake_complete());  // See RFC 8446 4.4.4
                                }),
 
             Botan_Tests::CHECK("Receive Client Finished",
@@ -2218,7 +2251,8 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                                    "tls_session_established",
                                                                    "tls_session_activated"});
 
-                                  result.confirm("TLS handshake finished", ctx->server.is_active());
+                                  result.confirm("TLS handshake fully finished", ctx->server.is_active());
+                                  result.confirm("handshake is complete", ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Receive Client close_notify",
@@ -2231,6 +2265,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is not yet closed", !ctx->server.is_closed());
                                   result.confirm("connection is still active", ctx->server.is_active());
+                                  result.confirm("handshake is still complete", ctx->server.is_handshake_complete());
                                }),
 
             Botan_Tests::CHECK("Expect Server close_notify",
@@ -2240,6 +2275,7 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is now inactive", !ctx->server.is_active());
                                   result.confirm("connection is now closed", ctx->server.is_closed());
+                                  result.confirm("handshake is still complete", ctx->server.is_handshake_complete());
                                   result.test_eq("Server's close notify",
                                                  ctx->pull_send_buffer(),
                                                  vars.get_req_bin("Record_Server_CloseNotify"));
@@ -2333,6 +2369,8 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
                                                         vars.get_req_bin("Record_ServerHandshakeMessages")));
 
                                   result.confirm("Server can now send application data", ctx->server.is_active());
+                                  result.confirm("handshake is not yet complete",
+                                                 !ctx->server.is_handshake_complete());  // See RFC 8446 4.4.4
                                }),
 
             Botan_Tests::CHECK("Send Client Finished",
@@ -2378,11 +2416,13 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448 {
 
                                   result.confirm("connection is not yet closed", !ctx->server.is_closed());
                                   result.confirm("connection is still active", ctx->server.is_active());
+                                  result.confirm("handshake is still complete", ctx->server.is_handshake_complete());
 
                                   ctx->server.close();
 
                                   result.confirm("connection is now inactive", !ctx->server.is_active());
                                   result.confirm("connection is now closed", ctx->server.is_closed());
+                                  result.confirm("handshake is still complete", ctx->server.is_handshake_complete());
                                   result.test_eq("Server's close notify",
                                                  ctx->pull_send_buffer(),
                                                  vars.get_req_bin("Record_Server_CloseNotify"));
diff --git a/src/tests/unit_asio_stream.cpp b/src/tests/unit_asio_stream.cpp
index 79a32f574..03f7f6e90 100644
--- a/src/tests/unit_asio_stream.cpp
+++ b/src/tests/unit_asio_stream.cpp
@@ -64,6 +64,8 @@ class MockChannel {
 
       bool is_active() const { return m_active; }
 
+      bool is_handshake_complete() const { return m_active; }
+
    private:
       std::shared_ptr<Botan::TLS::Callbacks> m_callbacks;
       std::size_t m_bytes_till_complete_record;  // number of bytes still to read before tls record is completed