Repository: randombit/botan
Pull request #2947 (2022-04-04 12:54:46)

[TLS 1.3] Extract Handshake_Transitions from Handshake_State
Depends on: #2941 (requires rebase after merge)

This extracts the handshake state machine transition checker from `Handshake_State` into an extra class (`Handshake_Transitions`). Mainly, the TLS 1.3 implementation will need this functionality independent of the TLS 1.2 `Handshake_State` class.

Note that this PR is currently based on #2941 and contains the test utility code. I'll rebase once that PR has been merged.

randombit: LGTM, merged #2941 so this is ready for a rebase

Patch:
diff --git a/src/lib/tls/info.txt b/src/lib/tls/info.txt
index 3ed102e9c..2cebae346 100644
--- a/src/lib/tls/info.txt
+++ b/src/lib/tls/info.txt
@@ -27,6 +27,7 @@ tls_version.h
 tls_handshake_hash.h
 tls_handshake_io.h
 tls_handshake_state.h
+tls_handshake_transitions.h
 tls_reader.h
 tls_record.h
 tls_seq_numbers.h
diff --git a/src/lib/tls/tls_handshake_state.cpp b/src/lib/tls/tls_handshake_state.cpp
index 56c3b1b21..0fbce3750 100644
--- a/src/lib/tls/tls_handshake_state.cpp
+++ b/src/lib/tls/tls_handshake_state.cpp
@@ -76,104 +76,6 @@ const char* handshake_type_to_string(Handshake_Type type)
                        "Unknown TLS handshake message type " + std::to_string(type));
    }
 
-namespace {
-
-uint32_t bitmask_for_handshake_type(Handshake_Type type)
-   {
-   switch(type)
-      {
-      case HELLO_VERIFY_REQUEST:
-         return (1 << 0);
-
-      case HELLO_REQUEST:
-         return (1 << 1);
-
-      case CLIENT_HELLO:
-         return (1 << 2);
-
-      case SERVER_HELLO:
-         return (1 << 3);
-
-      case CERTIFICATE:
-         return (1 << 4);
-
-      case CERTIFICATE_URL:
-         return (1 << 5);
-
-      case CERTIFICATE_STATUS:
-         return (1 << 6);
-
-      case SERVER_KEX:
-         return (1 << 7);
-
-      case CERTIFICATE_REQUEST:
-         return (1 << 8);
-
-      case SERVER_HELLO_DONE:
-         return (1 << 9);
-
-      case CERTIFICATE_VERIFY:
-         return (1 << 10);
-
-      case CLIENT_KEX:
-         return (1 << 11);
-
-      case NEW_SESSION_TICKET:
-         return (1 << 12);
-
-      case HANDSHAKE_CCS:
-         return (1 << 13);
-
-      case FINISHED:
-         return (1 << 14);
-
-      // allow explicitly disabling new handshakes
-      case HANDSHAKE_NONE:
-         return 0;
-      }
-
-   throw TLS_Exception(Alert::UNEXPECTED_MESSAGE,
-                       "Unknown TLS handshake message type " + std::to_string(type));
-   }
-
-std::string handshake_mask_to_string(uint32_t mask, char combiner)
-   {
-   const Handshake_Type types[] = {
-      HELLO_VERIFY_REQUEST,
-      HELLO_REQUEST,
-      CLIENT_HELLO,
-      SERVER_HELLO,
-      CERTIFICATE,
-      CERTIFICATE_URL,
-      CERTIFICATE_STATUS,
-      SERVER_KEX,
-      CERTIFICATE_REQUEST,
-      SERVER_HELLO_DONE,
-      CERTIFICATE_VERIFY,
-      CLIENT_KEX,
-      NEW_SESSION_TICKET,
-      HANDSHAKE_CCS,
-      FINISHED
-   };
-
-   std::ostringstream o;
-   bool empty = true;
-
-   for(auto&& t : types)
-      {
-      if(mask & bitmask_for_handshake_type(t))
-         {
-         if(!empty)
-            o << combiner;
-         o << handshake_type_to_string(t);
-         empty = false;
-         }
-      }
-
-   return o.str();
-   }
-
-}
 
 /*
 * Initialize the SSL/TLS Handshake State
@@ -313,57 +215,23 @@ void Handshake_State::compute_session_keys(const secure_vector<uint8_t>& resume_
 
 void Handshake_State::confirm_transition_to(Handshake_Type handshake_msg)
    {
-   const uint32_t mask = bitmask_for_handshake_type(handshake_msg);
-
-   m_hand_received_mask |= mask;
-
-   const bool ok = (m_hand_expecting_mask & mask) != 0; // overlap?
-
-   if(!ok)
-      {
-      const uint32_t seen_so_far = m_hand_received_mask & ~mask;
-
-      std::ostringstream msg;
-
-      msg << "Unexpected state transition in handshake got a " << handshake_type_to_string(handshake_msg);
-
-      if(m_hand_expecting_mask == 0)
-         msg << " not expecting messages";
-      else
-         msg << " expected " << handshake_mask_to_string(m_hand_expecting_mask, '|');
-
-      if(seen_so_far != 0)
-         msg << " seen " << handshake_mask_to_string(seen_so_far, '+');
-
-      throw Unexpected_Message(msg.str());
-      }
-
-   /* We don't know what to expect next, so force a call to
-      set_expected_next; if it doesn't happen, the next transition
-      check will always fail which is what we want.
-   */
-   m_hand_expecting_mask = 0;
+   m_transitions.confirm_transition_to(handshake_msg);
    }
 
 void Handshake_State::set_expected_next(Handshake_Type handshake_msg)
    {
-   m_hand_expecting_mask |= bitmask_for_handshake_type(handshake_msg);
+   m_transitions.set_expected_next(handshake_msg);
    }
 
 bool Handshake_State::received_handshake_msg(Handshake_Type handshake_msg) const
    {
-   const uint32_t mask = bitmask_for_handshake_type(handshake_msg);
-
-   return (m_hand_received_mask & mask) != 0;
+   return m_transitions.received_handshake_msg(handshake_msg);
    }
 
 std::pair<Handshake_Type, std::vector<uint8_t>>
 Handshake_State::get_next_handshake_msg()
    {
-   const bool expecting_ccs =
-      (bitmask_for_handshake_type(HANDSHAKE_CCS) & m_hand_expecting_mask) != 0;
-
-   return m_handshake_io->get_next_record(expecting_ccs);
+   return m_handshake_io->get_next_record(m_transitions.change_cipher_spec_expected());
    }
 
 std::vector<uint8_t> Handshake_State::session_ticket() const
diff --git a/src/lib/tls/tls_handshake_state.h b/src/lib/tls/tls_handshake_state.h
index 1cc22d029..ebd267677 100644
--- a/src/lib/tls/tls_handshake_state.h
+++ b/src/lib/tls/tls_handshake_state.h
@@ -11,6 +11,7 @@
 
 #include <botan/internal/tls_handshake_hash.h>
 #include <botan/internal/tls_handshake_io.h>
+#include <botan/internal/tls_handshake_transitions.h>
 #include <botan/internal/tls_session_key.h>
 #include <botan/tls_ciphersuite.h>
 #include <botan/tls_exceptn.h>
@@ -176,8 +177,7 @@ class Handshake_State
 
       std::unique_ptr<Handshake_IO> m_handshake_io;
 
-      uint32_t m_hand_expecting_mask = 0;
-      uint32_t m_hand_received_mask = 0;
+      Handshake_Transitions m_transitions;
       Protocol_Version m_version;
       std::optional<Ciphersuite> m_ciphersuite;
       Session_Keys m_session_keys;
diff --git a/src/lib/tls/tls_handshake_transitions.cpp b/src/lib/tls/tls_handshake_transitions.cpp
new file mode 100644
index 000000000..9bf3611c1
--- /dev/null
+++ b/src/lib/tls/tls_handshake_transitions.cpp
@@ -0,0 +1,176 @@
+/*
+* TLS Handshake State Transitions
+* (C) 2004-2006,2011,2012 Jack Lloyd
+*     2017 Harry Reimann, Rohde & Schwarz Cybersecurity
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/internal/tls_handshake_transitions.h>
+
+#include <botan/tls_exceptn.h>
+
+#include <sstream>
+
+namespace Botan::TLS {
+
+namespace {
+
+uint32_t bitmask_for_handshake_type(Handshake_Type type)
+   {
+   switch(type)
+      {
+      case HELLO_VERIFY_REQUEST:
+         return (1 << 0);
+
+      case HELLO_REQUEST:
+         return (1 << 1);
+
+      case CLIENT_HELLO:
+         return (1 << 2);
+
+      case SERVER_HELLO:
+         return (1 << 3);
+
+      case CERTIFICATE:
+         return (1 << 4);
+
+      case CERTIFICATE_URL:
+         return (1 << 5);
+
+      case CERTIFICATE_STATUS:
+         return (1 << 6);
+
+      case SERVER_KEX:
+         return (1 << 7);
+
+      case CERTIFICATE_REQUEST:
+         return (1 << 8);
+
+      case SERVER_HELLO_DONE:
+         return (1 << 9);
+
+      case CERTIFICATE_VERIFY:
+         return (1 << 10);
+
+      case CLIENT_KEX:
+         return (1 << 11);
+
+      case NEW_SESSION_TICKET:
+         return (1 << 12);
+
+      case HANDSHAKE_CCS:
+         return (1 << 13);
+
+      case FINISHED:
+         return (1 << 14);
+
+      // allow explicitly disabling new handshakes
+      case HANDSHAKE_NONE:
+         return 0;
+      }
+
+   throw TLS_Exception(Alert::UNEXPECTED_MESSAGE,
+                       "Unknown TLS handshake message type " + std::to_string(type));
+   }
+
+std::string handshake_mask_to_string(uint32_t mask, char combiner)
+   {
+   const Handshake_Type types[] =
+      {
+      HELLO_VERIFY_REQUEST,
+      HELLO_REQUEST,
+      CLIENT_HELLO,
+      SERVER_HELLO,
+      CERTIFICATE,
+      CERTIFICATE_URL,
+      CERTIFICATE_STATUS,
+      SERVER_KEX,
+      CERTIFICATE_REQUEST,
+      SERVER_HELLO_DONE,
+      CERTIFICATE_VERIFY,
+      CLIENT_KEX,
+      NEW_SESSION_TICKET,
+      HANDSHAKE_CCS,
+      FINISHED
+      };
+
+   std::ostringstream o;
+   bool empty = true;
+
+   for(auto&& t : types)
+      {
+      if(mask & bitmask_for_handshake_type(t))
+         {
+         if(!empty)
+            { o << combiner; }
+         o << handshake_type_to_string(t);
+         empty = false;
+         }
+      }
+
+   return o.str();
+   }
+
+}
+
+bool Handshake_Transitions::received_handshake_msg(Handshake_Type msg_type) const
+   {
+   const uint32_t mask = bitmask_for_handshake_type(msg_type);
+
+   return (m_hand_received_mask & mask) != 0;
+   }
+
+void Handshake_Transitions::confirm_transition_to(Handshake_Type msg_type)
+   {
+   const uint32_t mask = bitmask_for_handshake_type(msg_type);
+
+   m_hand_received_mask |= mask;
+
+   const bool ok = (m_hand_expecting_mask & mask) != 0; // overlap?
+
+   if(!ok)
+      {
+      const uint32_t seen_so_far = m_hand_received_mask & ~mask;
+
+      std::ostringstream msg;
+
+      msg << "Unexpected state transition in handshake got a " << handshake_type_to_string(msg_type);
+
+      if(m_hand_expecting_mask == 0)
+         { msg << " not expecting messages"; }
+      else
+         { msg << " expected " << handshake_mask_to_string(m_hand_expecting_mask, '|'); }
+
+      if(seen_so_far != 0)
+         { msg << " seen " << handshake_mask_to_string(seen_so_far, '+'); }
+
+      throw Unexpected_Message(msg.str());
+      }
+
+   /* We don't know what to expect next, so force a call to
+      set_expected_next; if it doesn't happen, the next transition
+      check will always fail which is what we want.
+   */
+   m_hand_expecting_mask = 0;
+   }
+
+void Handshake_Transitions::set_expected_next(Handshake_Type msg_type)
+   {
+   m_hand_expecting_mask |= bitmask_for_handshake_type(msg_type);
+   }
+
+void Handshake_Transitions::set_expected_next(const std::vector<Handshake_Type>& msg_types)
+   {
+   for (const auto type : msg_types)
+      {
+      set_expected_next(type);
+      }
+   }
+
+bool Handshake_Transitions::change_cipher_spec_expected() const
+   {
+   return (bitmask_for_handshake_type(HANDSHAKE_CCS) & m_hand_expecting_mask) != 0;
+   }
+
+}
diff --git a/src/lib/tls/tls_handshake_transitions.h b/src/lib/tls/tls_handshake_transitions.h
new file mode 100644
index 000000000..019ec3ca5
--- /dev/null
+++ b/src/lib/tls/tls_handshake_transitions.h
@@ -0,0 +1,66 @@
+/*
+* TLS Handshake State Transitions
+* (C) 2004-2006,2011,2012 Jack Lloyd
+*     2017 Harry Reimann, Rohde & Schwarz Cybersecurity
+*     2022 René Meusel, Hannes Rantzsch - neXenio GmbH
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_TLS_HANDSHAKE_TRANSITIONS_H_
+#define BOTAN_TLS_HANDSHAKE_TRANSITIONS_H_
+
+#include <vector>
+
+#include <botan/tls_magic.h>
+
+namespace Botan::TLS {
+
+/**
+ * Manages the expectations for incoming handshake messages in both TLS 1.2 and 1.3.
+ * This does not bear any knowledge about the actual state machine but is a mere
+ * helper to implement state transition validation.
+ */
+class BOTAN_TEST_API Handshake_Transitions
+   {
+   public:
+      /**
+       * Return true iff we have received a particular message already
+       * @param msg_type the message type
+       */
+      bool received_handshake_msg(Handshake_Type msg_type) const;
+
+      /**
+       * Confirm that we were expecting this message type
+       * @param msg_type the message type
+       */
+      void confirm_transition_to(Handshake_Type msg_type);
+
+      /**
+       * Record that we are expecting a particular message type next
+       * @param msg_type the message type
+       */
+      void set_expected_next(Handshake_Type msg_type);
+
+      /**
+       * Record that we are expecting one of the enumerated message types next.
+       * Note that receiving any of the expected messages in `confirm_transition_to`
+       * resets _all_ the expectations.
+       *
+       * @param msg_types the message types
+       */
+      void set_expected_next(const std::vector<Handshake_Type>& msg_types);
+
+      /**
+       * Check whether a Change Cipher Spec must be expected
+       */
+      bool change_cipher_spec_expected() const;
+
+   private:
+      uint32_t m_hand_expecting_mask = 0;
+      uint32_t m_hand_received_mask = 0;
+   };
+
+}
+
+#endif


Test patch:
diff --git a/src/tests/test_tls_handshake_transitions.cpp b/src/tests/test_tls_handshake_transitions.cpp
new file mode 100644
index 000000000..17415f159
--- /dev/null
+++ b/src/tests/test_tls_handshake_transitions.cpp
@@ -0,0 +1,89 @@
+/*
+* (C) 2022 Jack Lloyd
+* (C) 2022 Hannes Rantzsch, René Meusel - neXenio
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include "tests.h"
+
+#if defined(BOTAN_HAS_TLS)
+
+#include <botan/internal/tls_handshake_transitions.h>
+
+namespace Botan_Tests {
+
+namespace {
+
+std::vector<Test::Result> test_handshake_state_transitions()
+   {
+   return {
+      CHECK("uninitialized expects nothing", [](Test::Result& result) {
+         Botan::TLS::Handshake_Transitions ht;
+         result.confirm("CCS is not expected by default", !ht.change_cipher_spec_expected());
+
+         result.confirm("no messages were received", !ht.received_handshake_msg(Botan::TLS::Handshake_Type::CLIENT_HELLO));
+         result.test_throws("no expectations set, always throws", [&] {
+            ht.confirm_transition_to(Botan::TLS::Handshake_Type::CLIENT_HELLO);
+         });
+      }),
+
+      CHECK("expect exactly one message", [](Test::Result& result) {
+         Botan::TLS::Handshake_Transitions ht;
+         ht.set_expected_next(Botan::TLS::Handshake_Type::CLIENT_HELLO);
+
+         result.test_no_throw("client hello met expectation", [&] {
+            ht.confirm_transition_to(Botan::TLS::Handshake_Type::CLIENT_HELLO);
+         });
+
+         result.confirm("received client hello", ht.received_handshake_msg(Botan::TLS::Handshake_Type::CLIENT_HELLO));
+
+         result.test_throws("confirmation resets expectations", [&] {
+            ht.confirm_transition_to(Botan::TLS::Handshake_Type::CLIENT_HELLO);
+         });
+      }),
+
+      CHECK("expect exactly one message but don't satisfy it", [](Test::Result& result)
+         {
+         Botan::TLS::Handshake_Transitions ht;
+         ht.set_expected_next(Botan::TLS::Handshake_Type::CLIENT_HELLO);
+
+         result.test_throws("server hello does not meet expectation", [&]{
+            ht.confirm_transition_to(Botan::TLS::Handshake_Type::SERVER_HELLO);
+         });
+         }),
+
+      CHECK("two expectations can be fulfilled", [](Test::Result& result)
+         {
+         Botan::TLS::Handshake_Transitions ht;
+         ht.set_expected_next({Botan::TLS::Handshake_Type::CERTIFICATE_REQUEST,Botan::TLS::Handshake_Type::CERTIFICATE});
+
+         auto ht2 = ht;  // copying, as confirmation reset the object's superposition
+
+         result.test_no_throw("CERTIFICATE", [&] {
+            ht.confirm_transition_to(Botan::TLS::Handshake_Type::CERTIFICATE);
+         });
+         result.confirm("received CERTIFICATE", ht.received_handshake_msg(Botan::TLS::Handshake_Type::CERTIFICATE));
+
+         result.test_no_throw("CERTIFICATE_REQUEST", [&] {
+            ht2.confirm_transition_to(Botan::TLS::Handshake_Type::CERTIFICATE_REQUEST);
+         });
+         result.confirm("received CERTIFICATE_REQUEST", ht2.received_handshake_msg(Botan::TLS::Handshake_Type::CERTIFICATE_REQUEST));
+         }),
+
+      CHECK("expect CCS", [](Test::Result& result)
+         {
+         Botan::TLS::Handshake_Transitions ht;
+         ht.set_expected_next(Botan::TLS::Handshake_Type::HANDSHAKE_CCS);
+         result.confirm("CCS expected", ht.change_cipher_spec_expected());
+         }),
+      };
+   }
+
+}  // namespace
+
+BOTAN_REGISTER_TEST_FN("tls", "tls_handshake_transitions",
+                       test_handshake_state_transitions);
+}
+
+#endif