Repository: randombit/botan
Pull request #2957 (2022-04-13 08:35:32)

[TLS 1.3] Client Authentication in the main Handshake
### Pull Request Dependencies

Before merging this, we should first review merge:

* https://github.com/randombit/botan/pull/2968
* https://github.com/randombit/botan/pull/2922

### Description

This implements:
 * client authentication during the main handshake
 * coalescing of multiple handshake messages (which is required for the RFC 8448 client auth test) into a single (encrypted) record

Post-handshake authentication is left for future work. Rationale: A server may always ask for client authentication during the handshake. Post-handshake auth can be disabled by the client, by not negotiating the "post_handshake_auth" extension in the Client Hello.

#### TODO

* ~We should explicitly support the "signature_algorithm_cert" extension~
  The client can choose to not use this extension. Hence its support becomes crucial when implementing the server only.

lgtm-com[bot]: This pull request **introduces 1 alert** when merging 348a5bbdd107b00915482dabc9516c9fe54fbe8f into 8976bf250a3e7c299a4b36485f0328a843f2627e - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-47b299ab7a807a54ba3539e3a6c7ceba2ad0787d)

**new alerts:**

* 1 for Comparison result is always the same
lgtm-com[bot]: This pull request **introduces 1 alert** when merging c22912f94b5798e771b11f94ad27710a066ee30b into 8976bf250a3e7c299a4b36485f0328a843f2627e - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-0f6cdaaae9e7aba9ea7dff95b1740e73e35c65c1)

**new alerts:**

* 1 for Comparison result is always the same
lgtm-com[bot]: This pull request **introduces 1 alert** when merging 3abfc13636bdc118e2eae62e5738c7256d7e6a54 into 45b74cc41d57d374c58dff4f1a5edccb7d17c362 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-a0646a6f5a6b2d2ce59333177951d263adfa0d6a)

**new alerts:**

* 1 for Comparison result is always the same
lgtm-com[bot]: This pull request **introduces 1 alert** when merging 9549e06689ba5d59411120e70d267b378e986348 into bae64dea8a46cabfe9011bde903598a5c907f0c6 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-75aab00f71815d62077eafe27478cdc493f7a6f8)

**new alerts:**

* 1 for Comparison result is always the same
lgtm-com[bot]: This pull request **introduces 1 alert** when merging 3ed31d46b717dda42b1764f3d5cc075b1664c482 into bae64dea8a46cabfe9011bde903598a5c907f0c6 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-83714c0d90771cc3a7502d486884c85ac5102e7d)

**new alerts:**

* 1 for Comparison result is always the same
lgtm-com[bot]: This pull request **introduces 1 alert** when merging 86bacc1d299f4a9b0e5bef5a83395bee463f40a0 into bae64dea8a46cabfe9011bde903598a5c907f0c6 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-3af3996be7e343806cbcf9e35884022c81f22fc9)

**new alerts:**

* 1 for Comparison result is always the same
reneme: Rebased and retargeted to master.

Patch:
diff --git a/src/bogo_shim/bogo_shim.cpp b/src/bogo_shim/bogo_shim.cpp
index b80bf4976..869df18b4 100644
--- a/src/bogo_shim/bogo_shim.cpp
+++ b/src/bogo_shim/bogo_shim.cpp
@@ -82,6 +82,7 @@ std::string map_to_bogo_error(const std::string& e)
          { "Application data before handshake done", ":APPLICATION_DATA_INSTEAD_OF_HANDSHAKE:" },
          { "Bad Hello_Request, has non-zero size", ":BAD_HELLO_REQUEST:" },
          { "Bad code for TLS alert level", ":UNKNOWN_ALERT_TYPE:" },
+         { "Bad encoding on signature algorithms extension", ":DECODE_ERROR:" },
          { "Bad extension size", ":DECODE_ERROR:" },
          { "Bad length in hello verify request", ":DECODE_ERROR:" },
          { "Bad lengths in DTLS header", ":BAD_HANDSHAKE_RECORD:" },
@@ -99,6 +100,8 @@ std::string map_to_bogo_error(const std::string& e)
          { "Certificate key type did not match ciphersuite", ":WRONG_CERTIFICATE_TYPE:" },
          { "Certificate usage constraints do not allow this ciphersuite", ":KEY_USAGE_BIT_INCORRECT:" },
          { "Certificate: Message malformed", ":DECODE_ERROR:" },
+         { "Certificate_Request context must be empty in the main handshake", ":DECODE_ERROR:" },
+         { "Certificate_Request message did not provide a signature_algorithms extension", ":DECODE_ERROR:" },
          { "Channel_Impl_12::key_material_export cannot export during renegotiation", "failed to export keying material" },
          { "Client cert verify failed", ":BAD_SIGNATURE:" },
          { "Client certificate does not support signing", ":KEY_USAGE_BIT_INCORRECT:" },
@@ -123,6 +126,8 @@ std::string map_to_bogo_error(const std::string& e)
          { "Empty ALPN protocol not allowed", ":PARSE_TLSEXT:" },
          { "Encoding error: Cannot encode PSS string, output length too small", ":NO_COMMON_SIGNATURE_ALGORITHMS:" },
          { "Expected TLS but got a record with DTLS version", ":WRONG_VERSION_NUMBER:" },
+         { "Failed to agree on a signature algorithm", ":NO_COMMON_SIGNATURE_ALGORITHMS:" },
+         { "Failed to negotiate a common signature algorithm for client authentication", ":NO_COMMON_SIGNATURE_ALGORITHMS:" },
          { "Finished message didn't verify", ":DIGEST_CHECK_FAILED:" },
          { "Have data remaining in buffer after ClientHello", ":EXCESS_HANDSHAKE_DATA:" },
          { "Have data remaining in buffer after Finished", ":EXCESS_HANDSHAKE_DATA:" },
@@ -824,7 +829,10 @@ class Shim_Policy final : public Botan::TLS::Policy
                {
                const Botan::TLS::Signature_Scheme scheme(pref);
                if(!scheme.is_available())
+                  {
+                  shim_log("skipping inavailable but preferred signature scheme: " + std::to_string(pref));
                   continue;
+                  }
                pref_hash.push_back(scheme.hash_function_name());
                }
 
@@ -876,10 +884,17 @@ class Shim_Policy final : public Botan::TLS::Policy
                schemes.emplace_back(static_cast<uint16_t>(pref));
                }
 
-            // BoGo gets sad if these are not included in our signature_algorithms extension
+            // The relevant tests (*-Sign-Negotiate-*) want to configure a preference
+            // for the scheme of our signing operation (-signing-prefs). However, this
+            // policy method (`allowed_signature_schemes`) also restricts the peer's
+            // signing operation. If we weren't to add a few 'common' algorithms, initial
+            // security parameter negotiation would fail.
+            // By placing the BoGo-configured scheme first we make sure our implementation
+            // meets BoGo's expectation when it is our turn to sign.
             if(!m_args.flag_set("server"))
                {
                schemes.emplace_back(Botan::TLS::Signature_Scheme::RSA_PKCS1_SHA256);
+               schemes.emplace_back(Botan::TLS::Signature_Scheme::RSA_PSS_SHA256);
                schemes.emplace_back(Botan::TLS::Signature_Scheme::ECDSA_SHA256);
                }
 
diff --git a/src/bogo_shim/config.json b/src/bogo_shim/config.json
index c00f8a309..7111d39c4 100644
--- a/src/bogo_shim/config.json
+++ b/src/bogo_shim/config.json
@@ -12,7 +12,8 @@
         "PartialFinishedWithServerHelloDone": "Unexpected record vs excess handshake data",
         "HelloRetryRequest-DuplicateCurve-TLS13": "expects 'illegal parameter' but we want to stick with 'decode error'",
         "HelloRetryRequest-DuplicateCookie-TLS13": "expects 'illegal parameter' but we want to stick with 'decode error'",
-        "EncryptedExtensionsWithKeyShare-TLS13": "expects 'unsupported extension' but RFC requires 'illegal parameter'"
+        "EncryptedExtensionsWithKeyShare-TLS13": "expects 'unsupported extension' but RFC requires 'illegal parameter'",
+        "ClientSkipCertificateVerify-TLS13": "would require ambiguous error mapping"
     },
 
     "DisabledTests": {
@@ -113,23 +114,7 @@
         "*EarlyData*": "No TLS 1.3 Early Data, yet",
         "TLS13-1RTT-Client-*": "No TLS 1.3 Early Data, yet",
 
-        "FailCertCallback-Client-TLS13": "No client auth in TLS 1.3, yet",
-        "Client-Sign*-TLS13": "No client auth in TLS 1.3, yet",
-        "TLS13-Client-ClientAuth-": "No client auth in TLS 1.3, yet",
-        "ClientAuth-*-TLS13": "No client auth in TLS 1.3, yet",
-        "TLS13-Client-ClientAuth-*": "No client auth in TLS 1.3, yet",
-        "NoClientCertificate-TLS13": "No client auth in TLS 1.3, yet",
-        "NoCommonAlgorithms-TLS13": "No client auth in TLS 1.3, yet",
-        "ClientAuth-*-TLS13-*": "No client auth in TLS 1.3, yet",
-        "TrailingMessageData-TLS13-CertificateRequest-TLS": "No client auth in TLS 1.3, yet",
-        "RequestContextInHandshake-TLS13": "No client auth in TLS 1.3, yet",
-        "UnknownExtensionInCertificateRequest-TLS13": "No client auth in TLS 1.3, yet",
-        "MissingSignatureAlgorithmsInCertificateRequest-TLS13": "No client auth in TLS 1.3, yet",
-        "ClientSkipCertificateVerify-TLS13": "No client auth in TLS 1.3, yet",
-        "SendReceiveIntermediate-Client-TLS13": "No client auth in TLS 1.3, yet",
-        "TLS13-Client-CertReq-CA-List": "No client auth in TLS 1.3, yet",
-        "SendNoClientCertificateExtensions-TLS13": "No client auth in TLS 1.3, yet",
-
+        "SendNoClientCertificateExtensions-TLS13": "-signed-cert-timestamps currently not supported in the shim",
         "KeyUpdate-RequestACK-UnfinishedWrite": "-read-with-unfinished-write currently not supported in the shim",
 
         "*Binder*": "No TLS 1.3",
diff --git a/src/lib/tls/msg_cert_req.cpp b/src/lib/tls/msg_cert_req.cpp
index 241bfe1e5..b613c0f80 100644
--- a/src/lib/tls/msg_cert_req.cpp
+++ b/src/lib/tls/msg_cert_req.cpp
@@ -17,7 +17,7 @@
 
 namespace Botan::TLS {
 
-Handshake_Type Certificate_Req::type() const
+Handshake_Type Certificate_Request_12::type() const
    {
    return CERTIFICATE_REQUEST;
    }
@@ -56,7 +56,7 @@ uint8_t cert_type_name_to_code(const std::string& name)
 /**
 * Create a new Certificate Request message
 */
-Certificate_Req::Certificate_Req(Handshake_IO& io,
+Certificate_Request_12::Certificate_Request_12(Handshake_IO& io,
                                  Handshake_Hash& hash,
                                  const Policy& policy,
                                  const std::vector<X509_DN>& ca_certs) :
@@ -70,7 +70,7 @@ Certificate_Req::Certificate_Req(Handshake_IO& io,
 /**
 * Deserialize a Certificate Request message
 */
-Certificate_Req::Certificate_Req(const std::vector<uint8_t>& buf)
+Certificate_Request_12::Certificate_Request_12(const std::vector<uint8_t>& buf)
    {
    if(buf.size() < 4)
       throw Decoding_Error("Certificate_Req: Bad certificate request");
@@ -115,17 +115,17 @@ Certificate_Req::Certificate_Req(const std::vector<uint8_t>& buf)
       }
    }
 
-const std::vector<std::string>& Certificate_Req::acceptable_cert_types() const
+const std::vector<std::string>& Certificate_Request_12::acceptable_cert_types() const
    {
    return m_cert_key_types;
    }
 
-const std::vector<X509_DN>& Certificate_Req::acceptable_CAs() const
+const std::vector<X509_DN>& Certificate_Request_12::acceptable_CAs() const
    {
    return m_names;
    }
 
-const std::vector<Signature_Scheme>& Certificate_Req::signature_schemes() const
+const std::vector<Signature_Scheme>& Certificate_Request_12::signature_schemes() const
    {
    return m_schemes;
    }
@@ -133,7 +133,7 @@ const std::vector<Signature_Scheme>& Certificate_Req::signature_schemes() const
 /**
 * Serialize a Certificate Request message
 */
-std::vector<uint8_t> Certificate_Req::serialize() const
+std::vector<uint8_t> Certificate_Request_12::serialize() const
    {
    std::vector<uint8_t> buf;
 
diff --git a/src/lib/tls/msg_cert_verify.cpp b/src/lib/tls/msg_cert_verify.cpp
index 17404e90e..61defac14 100644
--- a/src/lib/tls/msg_cert_verify.cpp
+++ b/src/lib/tls/msg_cert_verify.cpp
@@ -13,6 +13,7 @@
 #include <botan/internal/tls_handshake_io.h>
 #include <botan/internal/tls_handshake_state.h>
 #include <botan/internal/tls_reader.h>
+#include <botan/internal/stl_util.h>
 #include <botan/pk_keys.h>
 #include <botan/tls_algos.h>
 #include <botan/tls_extensions.h>
@@ -20,13 +21,13 @@
 namespace Botan::TLS {
 
 /*
-* Create a new Certificate Verify message
+* Create a new Certificate Verify message for TLS 1.2
 */
-Certificate_Verify::Certificate_Verify(Handshake_IO& io,
-                                       Handshake_State& state,
-                                       const Policy& policy,
-                                       RandomNumberGenerator& rng,
-                                       const Private_Key* priv_key)
+Certificate_Verify_12::Certificate_Verify_12(Handshake_IO& io,
+                                             Handshake_State& state,
+                                             const Policy& policy,
+                                             RandomNumberGenerator& rng,
+                                             const Private_Key* priv_key)
    {
    BOTAN_ASSERT_NONNULL(priv_key);
 
@@ -106,8 +107,73 @@ bool Certificate_Verify_12::verify(const X509_Certificate& cert,
 
 #if defined(BOTAN_HAS_TLS_13)
 
+namespace {
+
+std::vector<uint8_t> message(Connection_Side side, const Transcript_Hash& hash)
+   {
+   std::vector<uint8_t> msg(64, 0x20);
+   msg.reserve(64 + 33 + 1 + hash.size());
+
+   const std::string context_string = (side == Botan::TLS::Connection_Side::SERVER)
+                                      ? "TLS 1.3, server CertificateVerify"
+                                      : "TLS 1.3, client CertificateVerify";
+
+   msg.insert(msg.end(), context_string.cbegin(), context_string.cend());
+   msg.push_back(0x00);
+
+   msg.insert(msg.end(), hash.cbegin(), hash.cend());
+   return msg;
+   }
+
+Signature_Scheme choose_signature_scheme(
+      const Private_Key& key,
+      const std::vector<Signature_Scheme>& allowed_schemes,
+      const std::vector<Signature_Scheme>& peer_allowed_schemes)
+   {
+   for(Signature_Scheme scheme : allowed_schemes)
+      {
+      if(scheme.is_available()
+         && scheme.is_suitable_for(key)
+         && value_exists(peer_allowed_schemes, scheme))
+         {
+         return scheme;
+         }
+      }
+
+   throw TLS_Exception(Alert::HANDSHAKE_FAILURE, "Failed to agree on a signature algorithm");
+   }
+
+}
+
+/*
+* Create a new Certificate Verify message for TLS 1.3
+*/
+Certificate_Verify_13::Certificate_Verify_13(
+      const std::vector<Signature_Scheme>& peer_allowed_schemes,
+      Connection_Side whoami,
+      const Private_Key& key,
+      const Policy& policy,
+      const Transcript_Hash& hash,
+      Callbacks& callbacks,
+      RandomNumberGenerator& rng)
+   : m_side(whoami)
+   {
+   m_scheme = choose_signature_scheme(key, policy.allowed_signature_schemes(), peer_allowed_schemes);
+   BOTAN_ASSERT_NOMSG(m_scheme.is_available());
+
+   // TODO: we need to verify that the provided private key is strong enough for TLS 1.3
+
+   m_signature =
+      callbacks.tls_sign_message(key,
+                                 rng,
+                                 m_scheme.padding_string(),
+                                 m_scheme.format().value(),
+                                 message(m_side, hash));
+   }
+
+
 Certificate_Verify_13::Certificate_Verify_13(const std::vector<uint8_t>& buf,
-      const Connection_Side side)
+                                             const Connection_Side side)
    : Certificate_Verify(buf)
    , m_side(side)
    {
@@ -133,24 +199,12 @@ bool Certificate_Verify_13::verify(const X509_Certificate& cert,
    if(m_scheme.algorithm_identifier() != cert.subject_public_key_algo())
       { throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "Signature algorithm does not match certificate's public key"); }
 
-   std::vector<uint8_t> msg(64, 0x20);
-   msg.reserve(64 + 32 + 1 + transcript_hash.size());
-
-   const std::string context_string = (m_side == Botan::TLS::Connection_Side::SERVER)
-                                      ? "TLS 1.3, server CertificateVerify"
-                                      : "TLS 1.3, client CertificateVerify";
-
-   msg.insert(msg.end(), context_string.cbegin(), context_string.cend());
-   msg.push_back(0x00);
-
-   msg.insert(msg.end(), transcript_hash.cbegin(), transcript_hash.cend());
-
    const auto key = cert.load_subject_public_key();
    const bool signature_valid =
       callbacks.tls_verify_message(*key,
                                    m_scheme.padding_string(),
                                    m_scheme.format().value(),
-                                   msg,
+                                   message(m_side, transcript_hash),
                                    m_signature);
 
 #if defined(BOTAN_UNSAFE_FUZZER_MODE)
diff --git a/src/lib/tls/tls12/tls_client_impl_12.cpp b/src/lib/tls/tls12/tls_client_impl_12.cpp
index bb5916e55..65c34e51d 100644
--- a/src/lib/tls/tls12/tls_client_impl_12.cpp
+++ b/src/lib/tls/tls12/tls_client_impl_12.cpp
@@ -599,7 +599,7 @@ void Client_Impl_12::process_handshake_msg(const Handshake_State* active_state,
    else if(type == CERTIFICATE_REQUEST)
       {
       state.set_expected_next(SERVER_HELLO_DONE);
-      state.cert_req(new Certificate_Req(contents));
+      state.cert_req(new Certificate_Request_12(contents));
       }
    else if(type == SERVER_HELLO_DONE)
       {
diff --git a/src/lib/tls/tls12/tls_handshake_state.cpp b/src/lib/tls/tls12/tls_handshake_state.cpp
index 54a27c63d..9eeb52c53 100644
--- a/src/lib/tls/tls12/tls_handshake_state.cpp
+++ b/src/lib/tls/tls12/tls_handshake_state.cpp
@@ -156,7 +156,7 @@ void Handshake_State::server_kex(Server_Key_Exchange* server_kex)
    note_message(*m_server_kex);
    }
 
-void Handshake_State::cert_req(Certificate_Req* cert_req)
+void Handshake_State::cert_req(Certificate_Request_12* cert_req)
    {
    m_cert_req.reset(cert_req);
    note_message(*m_cert_req);
diff --git a/src/lib/tls/tls12/tls_handshake_state.h b/src/lib/tls/tls12/tls_handshake_state.h
index a249940f7..1b9ec35ed 100644
--- a/src/lib/tls/tls12/tls_handshake_state.h
+++ b/src/lib/tls/tls12/tls_handshake_state.h
@@ -38,7 +38,7 @@ class Server_Hello_12;
 class Certificate_12;
 class Certificate_Status;
 class Server_Key_Exchange;
-class Certificate_Req;
+class Certificate_Request_12;
 class Server_Hello_Done;
 class Client_Key_Exchange;
 class Certificate_Verify_12;
@@ -117,7 +117,7 @@ class Handshake_State
       void server_hello(Server_Hello_12* server_hello);
       void server_cert_status(Certificate_Status* server_cert_status);
       void server_kex(Server_Key_Exchange* server_kex);
-      void cert_req(Certificate_Req* cert_req);
+      void cert_req(Certificate_Request_12* cert_req);
       void server_hello_done(Server_Hello_Done* server_hello_done);
       void client_kex(Client_Key_Exchange* client_kex);
 
@@ -144,7 +144,7 @@ class Handshake_State
       const Server_Key_Exchange* server_kex() const
          { return m_server_kex.get(); }
 
-      const Certificate_Req* cert_req() const
+      const Certificate_Request_12* cert_req() const
          { return m_cert_req.get(); }
 
       const Server_Hello_Done* server_hello_done() const
@@ -207,7 +207,7 @@ class Handshake_State
       std::unique_ptr<Certificate_12> m_server_certs;
       std::unique_ptr<Certificate_Status> m_server_cert_status;
       std::unique_ptr<Server_Key_Exchange> m_server_kex;
-      std::unique_ptr<Certificate_Req> m_cert_req;
+      std::unique_ptr<Certificate_Request_12> m_cert_req;
       std::unique_ptr<Server_Hello_Done> m_server_hello_done;
       std::unique_ptr<Certificate_12> m_client_certs;
       std::unique_ptr<Client_Key_Exchange> m_client_kex;
diff --git a/src/lib/tls/tls12/tls_server_impl_12.cpp b/src/lib/tls/tls12/tls_server_impl_12.cpp
index ce307e707..4b170d78a 100644
--- a/src/lib/tls/tls12/tls_server_impl_12.cpp
+++ b/src/lib/tls/tls12/tls_server_impl_12.cpp
@@ -916,7 +916,7 @@ void Server_Impl_12::session_create(Server_Handshake_State& pending_state,
    if(request_cert && pending_state.ciphersuite().signature_used())
       {
       pending_state.cert_req(
-         new Certificate_Req(pending_state.handshake_io(),
+         new Certificate_Request_12(pending_state.handshake_io(),
                              pending_state.hash(),
                              policy(),
                              client_auth_CAs));
diff --git a/src/lib/tls/tls13/msg_certificate_13.cpp b/src/lib/tls/tls13/msg_certificate_13.cpp
index 55f7359f5..35b822af2 100644
--- a/src/lib/tls/tls13/msg_certificate_13.cpp
+++ b/src/lib/tls/tls13/msg_certificate_13.cpp
@@ -49,6 +49,12 @@ void Certificate_13::validate_extensions(const std::set<Handshake_Extension_Type
       }
    }
 
+const X509_Certificate& Certificate_13::leaf() const
+   {
+   BOTAN_STATE_CHECK(!empty());
+   return m_entries.front().certificate;
+   }
+
 void Certificate_13::verify(Callbacks& callbacks,
                             const Policy& policy,
                             Credentials_Manager& creds,
@@ -96,6 +102,17 @@ void Certificate_13::verify(Callbacks& callbacks,
    callbacks.tls_verify_cert_chain(certs, ocsp_responses, trusted_CAs, usage, hostname, policy);
    }
 
+Certificate_13::Certificate_13(const std::vector<X509_Certificate>& certs,
+                               const Connection_Side side,
+                               const std::vector<uint8_t> &request_context)
+   : m_request_context(request_context)
+   , m_side(side)
+   {
+   // TODO: proper extensions
+   for (const auto& c : certs)
+      { m_entries.emplace_back(Certificate_Entry{c, Extensions()}); }
+   }
+
 /**
 * Deserialize a Certificate message
 */
@@ -161,7 +178,7 @@ Certificate_13::Certificate_13(const std::vector<uint8_t>& buf,
       //    message as well.
       if(entry.extensions.contains_implemented_extensions_other_than({
             TLSEXT_CERT_STATUS_REQUEST,
-            // SIGNED_CERTIFICATE_TIMESTAMP
+            // TLSEXT_SIGNED_CERTIFICATE_TIMESTAMP
          }))
          {
          throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "Certificate Entry contained an extension that is not allowed");
@@ -202,8 +219,20 @@ Certificate_13::Certificate_13(const std::vector<uint8_t>& buf,
 */
 std::vector<uint8_t> Certificate_13::serialize() const
    {
-   // Needed only for server implementation or client authentication
-   throw Not_Implemented("NYI");
+   std::vector<uint8_t> buf;
+
+   append_tls_length_value(buf, m_request_context, 1);
+
+   std::vector<uint8_t> entries;
+   for(const auto& entry : m_entries)
+      {
+      append_tls_length_value(entries, entry.certificate.BER_encode(), 3);
+      append_tls_length_value(entries, entry.extensions.serialize(m_side), 2);
+      }
+
+   append_tls_length_value(buf, entries, 3);
+
+   return buf;
    }
 
 }
diff --git a/src/lib/tls/tls13/msg_certificate_req_13.cpp b/src/lib/tls/tls13/msg_certificate_req_13.cpp
new file mode 100644
index 000000000..be783381b
--- /dev/null
+++ b/src/lib/tls/tls13/msg_certificate_req_13.cpp
@@ -0,0 +1,92 @@
+/*
+* (C) 2022 Jack Lloyd
+* (C) 2022 Hannes Rantzsch, René Meusel - neXenio GmbH
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/tls_messages.h>
+#include <botan/internal/tls_reader.h>
+#include <botan/tls_exceptn.h>
+
+namespace Botan::TLS
+{
+
+Handshake_Type Certificate_Request_13::type() const
+   {
+   return Botan::TLS::Handshake_Type::CERTIFICATE_REQUEST;
+   }
+
+Certificate_Request_13::Certificate_Request_13(const std::vector<uint8_t>& buf, const Connection_Side side)
+   {
+   TLS_Data_Reader reader("Certificate_Request_13", buf);
+
+   // RFC 8446 4.3.2
+   //    A server which is authenticating with a certificate MAY optionally
+   //    request a certificate from the client.
+   if(side != Connection_Side::SERVER)
+      {
+      throw TLS_Exception(Alert::UNEXPECTED_MESSAGE, "Received a Certificate_Request message from a client");
+      }
+
+   m_context = reader.get_tls_length_value(1);
+   m_extensions.deserialize(reader, side, type());
+
+   // RFC 8446 4.3.2
+   //    The "signature_algorithms" extension MUST be specified, and other
+   //    extensions may optionally be included if defined for this message.
+   //    Clients MUST ignore unrecognized extensions.
+
+   if(!m_extensions.has<Signature_Algorithms>())
+      {
+      throw TLS_Exception(Alert::MISSING_EXTENSION, "Certificate_Request message did not provide a signature_algorithms extension");
+      }
+
+   // RFC 8446 4.2.
+   //    The table below indicates the messages where a given extension may
+   //    appear [...].  If an implementation receives an extension which it
+   //    recognizes and which is not specified for the message in which it
+   //    appears, it MUST abort the handshake with an "illegal_parameter" alert.
+   //
+   // For Certificate Request said table states:
+   //    "status_request", "signature_algorithms", "signed_certificate_timestamp",
+   //     "certificate_authorities", "oid_filters", "signature_algorithms_cert",
+   std::set<Handshake_Extension_Type> allowed_extensions =
+      {
+      TLSEXT_CERT_STATUS_REQUEST,
+      TLSEXT_SIGNATURE_ALGORITHMS,
+      // TLSEXT_SIGNED_CERTIFICATE_TIMESTAMP,  // NYI
+      TLSEXT_CERTIFICATE_AUTHORITIES,
+      // TLSEXT_OID_FILTERS,                   // NYI
+      TLSEXT_SIGNATURE_ALGORITHMS_CERT,
+      };
+
+   if(m_extensions.contains_implemented_extensions_other_than(allowed_extensions))
+      {
+      throw TLS_Exception(Alert::ILLEGAL_PARAMETER,
+                          "Certificate Request contained an extension that is not allowed");
+      }
+   }
+
+std::vector<X509_DN> Certificate_Request_13::acceptable_CAs() const
+   {
+   if(m_extensions.has<Certificate_Authorities>())
+      return m_extensions.get<Certificate_Authorities>()->distinguished_names();
+   return {};
+   }
+
+const std::vector<Signature_Scheme>& Certificate_Request_13::signature_schemes() const
+   {
+   // RFC 8446 4.3.2
+   //    The "signature_algorithms" extension MUST be specified
+   BOTAN_ASSERT_NOMSG(m_extensions.has<Signature_Algorithms>());
+
+   return m_extensions.get<Signature_Algorithms>()->supported_schemes();
+   }
+
+std::vector<uint8_t> Certificate_Request_13::serialize() const
+   {
+   throw Botan::Not_Implemented("Certificate_Request_13::serialize");
+   }
+
+}  // namespace Botan::TLS
diff --git a/src/lib/tls/tls13/tls_channel_impl_13.cpp b/src/lib/tls/tls13/tls_channel_impl_13.cpp
index 1baa32a63..452d18213 100644
--- a/src/lib/tls/tls13/tls_channel_impl_13.cpp
+++ b/src/lib/tls/tls13/tls_channel_impl_13.cpp
@@ -202,16 +202,28 @@ size_t Channel_Impl_13::received_data(const uint8_t input[], size_t input_size)
       }
    }
 
-void Channel_Impl_13::send_handshake_message(const Handshake_Message_13_Ref message)
-   {
-   std::visit([&](const auto msg) { callbacks().tls_inspect_handshake_msg(msg.get()); }, message);
 
-   auto msg = m_handshake_layer.prepare_message(message, m_transcript_hash);
+Channel_Impl_13::AggregatedMessages::AggregatedMessages(Channel_Impl_13& channel,
+                                                        Handshake_Layer& handshake_layer,
+                                                        Transcript_Hash_State& transcript_hash)
+   : m_channel(channel)
+   , m_handshake_layer(handshake_layer)
+   , m_transcript_hash(transcript_hash)
+   {}
 
-   if(expects_downgrade() && std::holds_alternative<std::reference_wrapper<Client_Hello_13>>(message))
-      { preserve_client_hello(msg); }
+Channel_Impl_13::AggregatedMessages&
+Channel_Impl_13::AggregatedMessages::add(const Handshake_Message_13_Ref message)
+   {
+   std::visit([&](const auto msg) { m_channel.callbacks().tls_inspect_handshake_msg(msg.get()); }, message);
+   m_message_buffer += m_handshake_layer.prepare_message(message, m_transcript_hash);
+   return *this;
+   }
 
-   send_record(Record_Type::HANDSHAKE, msg);
+std::vector<uint8_t> Channel_Impl_13::AggregatedMessages::send()
+   {
+   BOTAN_STATE_CHECK(!m_message_buffer.empty());
+   m_channel.send_record(Record_Type::HANDSHAKE, m_message_buffer);
+   return std::exchange(m_message_buffer, {});
    }
 
 void Channel_Impl_13::send_post_handshake_message(const Post_Handshake_Message_13 message)
diff --git a/src/lib/tls/tls13/tls_channel_impl_13.h b/src/lib/tls/tls13/tls_channel_impl_13.h
index b060beaee..eddeb4d05 100644
--- a/src/lib/tls/tls13/tls_channel_impl_13.h
+++ b/src/lib/tls/tls13/tls_channel_impl_13.h
@@ -22,6 +22,49 @@ namespace Botan::TLS {
 */
 class Channel_Impl_13 : public Channel_Impl
    {
+   protected:
+      /**
+       * Helper class to coalesce handshake messages into a single TLS record of type
+       * 'Handshake'. This is used entirely internally in the Channel, Client and Server
+       * implementation.
+       */
+      class AggregatedMessages
+         {
+         public:
+            AggregatedMessages(Channel_Impl_13& channel,
+                               Handshake_Layer& handshake_layer,
+                               Transcript_Hash_State& transcript_hash);
+
+            AggregatedMessages(const AggregatedMessages&) = delete;
+            AggregatedMessages& operator=(const AggregatedMessages&) = delete;
+            AggregatedMessages(AggregatedMessages&&) = delete;
+            AggregatedMessages& operator=(AggregatedMessages&&) = delete;
+
+            ~AggregatedMessages() = default;
+
+            /**
+             * Adds a single handshake message to the send buffer. Note that this
+             * updates the handshake transcript hash regardless of sending the
+             * message.
+             */
+            AggregatedMessages& add(const Handshake_Message_13_Ref message);
+
+            /**
+             * Send the messages aggregated in the message buffer. The buffer
+             * is returned if the sender needs to also handle it somehow.
+             * Most notable use: book keeping for a potential protocol downgrade
+             * in the client implementation.
+             */
+            std::vector<uint8_t> send();
+
+         private:
+            std::vector<uint8_t> m_message_buffer;
+
+            Channel_Impl_13&       m_channel;
+            Handshake_Layer&       m_handshake_layer;
+            Transcript_Hash_State& m_transcript_hash;
+         };
+
    public:
       /**
       * Set up a new TLS 1.3 session
@@ -145,10 +188,20 @@ class Channel_Impl_13 : public Channel_Impl
        */
       void opportunistically_update_traffic_keys() { m_opportunistic_key_update = true; }
 
-      void send_handshake_message(const Handshake_Message_13_Ref message);
+      std::vector<uint8_t> send_handshake_message(const Handshake_Message_13_Ref message)
+         {
+         return aggregate_handshake_messages()
+                   .add(message)
+                   .send();
+         }
       void send_post_handshake_message(const Post_Handshake_Message_13 message);
       void send_dummy_change_cipher_spec();
 
+      AggregatedMessages aggregate_handshake_messages()
+         {
+         return AggregatedMessages(*this, m_handshake_layer, m_transcript_hash);
+         }
+
       Callbacks& callbacks() const { return m_callbacks; }
       Session_Manager& session_manager() { return m_session_manager; }
       Credentials_Manager& credentials_manager() { return m_credentials_manager; }
diff --git a/src/lib/tls/tls13/tls_client_impl_13.cpp b/src/lib/tls/tls13/tls_client_impl_13.cpp
index 3993d579d..b2af75a2c 100644
--- a/src/lib/tls/tls13/tls_client_impl_13.cpp
+++ b/src/lib/tls/tls13/tls_client_impl_13.cpp
@@ -38,13 +38,16 @@ Client_Impl_13::Client_Impl_13(Callbacks& callbacks,
       { expect_downgrade(info); }
 #endif
 
-   send_handshake_message(m_handshake_state.sent(Client_Hello_13(
+   auto msg = send_handshake_message(m_handshake_state.sending(Client_Hello_13(
                              policy,
                              callbacks,
                              rng,
                              m_info.hostname(),
                              next_protocols)));
 
+   if(expects_downgrade())
+      { preserve_client_hello(msg); }
+
    // RFC 8446 Appendix D.4
    //    If not offering early data, the client sends a dummy change_cipher_spec
    //    record [...] immediately before its second flight. This may either be before
@@ -361,8 +364,29 @@ void Client_Impl_13::handle(const Encrypted_Extensions& encrypted_extensions_msg
       }
    }
 
+void Client_Impl_13::handle(const Certificate_Request_13& certificate_request_msg)
+   {
+   // 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())
+      {
+      throw TLS_Exception(Alert::DECODE_ERROR, "Certificate_Request context must be empty in the main handshake");
+      }
+
+   m_transitions.set_expected_next(CERTIFICATE);
+   }
+
 void Client_Impl_13::handle(const Certificate_13& certificate_msg)
    {
+   // RFC 8446 4.4.2
+   //    certificate_request_context:  [...] In the case of server authentication,
+   //    this field SHALL be zero length.
+   if(!certificate_msg.request_context().empty())
+      {
+      throw TLS_Exception(Alert::DECODE_ERROR, "Received a server certificate message with non-empty request context");
+      }
+
    certificate_msg.validate_extensions(m_handshake_state.client_hello().extensions().extension_types());
    certificate_msg.verify(callbacks(),
                           policy(),
@@ -393,7 +417,7 @@ void Client_Impl_13::handle(const Certificate_Verify_13& certificate_verify_msg)
       }
 
    bool sig_valid = certificate_verify_msg.verify(
-                       m_handshake_state.certificate().cert_chain().front().certificate,
+                       m_handshake_state.server_certificate().leaf(),
                        callbacks(),
                        m_transcript_hash.previous());
 
@@ -403,6 +427,72 @@ void Client_Impl_13::handle(const Certificate_Verify_13& certificate_verify_msg)
    m_transitions.set_expected_next(FINISHED);
    }
 
+void Client_Impl_13::send_client_authentication(Channel_Impl_13::AggregatedMessages& flight)
+   {
+   BOTAN_ASSERT_NOMSG(m_handshake_state.has_certificate_request());
+   const auto& cert_request = m_handshake_state.certificate_request();
+
+   // From all the schemes the server advertised, we need to filter for those
+   // that we can actually support and that are suitable for this protocol version.
+   std::vector<std::string> peer_allowed_signature_algos;
+   for(const Signature_Scheme& scheme : cert_request.signature_schemes())
+      {
+      if(scheme.is_available() && scheme.is_compatible_with(Protocol_Version::TLS_V13))
+         peer_allowed_signature_algos.push_back(scheme.algorithm_name());
+      }
+
+   if(peer_allowed_signature_algos.empty())
+      {
+      throw TLS_Exception(Alert::HANDSHAKE_FAILURE, "Failed to negotiate a common signature algorithm for client authentication");
+      }
+
+   // RFC 4.4.2.1
+   //    A server MAY request that a client present an OCSP response with its
+   //    certificate by sending an empty "status_request" extension in its
+   //    CertificateRequest message.
+   //
+   // TODO: Implement OCSP stapling for client certificates.
+
+   std::vector<X509_Certificate> client_certs = credentials_manager().find_cert_chain(
+         peer_allowed_signature_algos,
+         cert_request.acceptable_CAs(),
+         "tls-client",
+         m_info.hostname());
+
+   // RFC 4.4.2
+   //    certificate_request_context:  If this message is in response to a
+   //       CertificateRequest, the value of certificate_request_context in
+   //       that message.
+   flight.add(m_handshake_state.sending(
+      Certificate_13(std::move(client_certs), CLIENT, cert_request.context())));
+
+   // RFC 4.4.2
+   //    If the server requests client authentication but no suitable certificate
+   //    is available, the client MUST send a Certificate message containing no
+   //    certificates.
+   //
+   // In that case, no Certificate Verify message will be sent.
+   if(!m_handshake_state.client_certificate().cert_chain().empty())
+      {
+      Private_Key* private_key = credentials_manager().private_key_for(
+            m_handshake_state.client_certificate().leaf(),
+            "tls-client",
+            m_info.hostname());
+
+      BOTAN_ASSERT_NONNULL(private_key);
+
+      flight.add(m_handshake_state.sending(Certificate_Verify_13(
+         cert_request.signature_schemes(),
+         Connection_Side::CLIENT,
+         *private_key,
+         policy(),
+         m_transcript_hash.current(),
+         callbacks(),
+         rng()
+      )));
+      }
+   }
+
 void Client_Impl_13::handle(const Finished_13& finished_msg)
    {
    // RFC 8446 4.4.4
@@ -413,13 +503,26 @@ void Client_Impl_13::handle(const Finished_13& finished_msg)
                            m_transcript_hash.previous()))
       { throw TLS_Exception(Alert::DECRYPT_ERROR, "Finished message didn't verify"); }
 
+   // save the current transcript hash as client auth might update the hash multiple times
+   const auto th_server_finished = m_transcript_hash.current();
+
+   auto flight = aggregate_handshake_messages();
+
+   // RFC 8446 4.4.2
+   //    The client MUST send a Certificate message if and only if the server
+   //    has requested client authentication via a CertificateRequest message.
+   if(m_handshake_state.has_certificate_request())
+      { send_client_authentication(flight); }
+
    // send client finished handshake message (still using handshake traffic secrets)
-   send_handshake_message(m_handshake_state.sent(Finished_13(m_cipher_state.get(),
-                          m_transcript_hash.current())));
+   flight.add(m_handshake_state.sending(Finished_13(m_cipher_state.get(),
+                                                 m_transcript_hash.current())));
+
+   flight.send();
 
    // derives the application traffic secrets and _replaces_ the handshake traffic secrets
    // Note: this MUST happen AFTER the client finished message was sent!
-   m_cipher_state->advance_with_server_finished(m_transcript_hash.previous());
+   m_cipher_state->advance_with_server_finished(th_server_finished);
    m_cipher_state->advance_with_client_finished(m_transcript_hash.current());
 
    // TODO: save session and invoke tls_session_established callback
diff --git a/src/lib/tls/tls13/tls_client_impl_13.h b/src/lib/tls/tls13/tls_client_impl_13.h
index 03b15cb3c..b1e5adeb0 100644
--- a/src/lib/tls/tls13/tls_client_impl_13.h
+++ b/src/lib/tls/tls13/tls_client_impl_13.h
@@ -75,12 +75,15 @@ class Client_Impl_13 : public Channel_Impl_13
       void handle(const Server_Hello_13& server_hello_msg);
       void handle(const Hello_Retry_Request& hrr_msg);
       void handle(const Encrypted_Extensions& encrypted_extensions_msg);
+      void handle(const Certificate_Request_13& certificate_request_msg);
       void handle(const Certificate_13& certificate_msg);
       void handle(const Certificate_Verify_13& certificate_verify_msg);
       void handle(const Finished_13& finished_msg);
       void handle(const New_Session_Ticket_13& new_session_ticket);
       void handle(const Key_Update& key_update);
 
+      void send_client_authentication(Channel_Impl_13::AggregatedMessages& flight);
+
    private:
       const Server_Information m_info;
 
diff --git a/src/lib/tls/tls13/tls_handshake_layer_13.cpp b/src/lib/tls/tls13/tls_handshake_layer_13.cpp
index 946d0fc4a..f27d59bca 100644
--- a/src/lib/tls/tls13/tls_handshake_layer_13.cpp
+++ b/src/lib/tls/tls13/tls_handshake_layer_13.cpp
@@ -37,7 +37,7 @@ Handshake_Type handshake_type_from_byte(uint8_t type)
          // case END_OF_EARLY_DATA:  // NYI: needs PSK/resumption support -- won't be offered in Client Hello for now
          case ENCRYPTED_EXTENSIONS:
          case CERTIFICATE:
-         // case CERTIFICATE_REQUEST:  // NYI: client auth -- server might still request, resulting in handshake failure
+         case CERTIFICATE_REQUEST:
          case CERTIFICATE_VERIFY:
          case FINISHED:
             return Handshake_Type(type);
@@ -93,8 +93,8 @@ std::optional<Msg_Type> parse_message(TLS::TLS_Data_Reader& reader, const Policy
             return Encrypted_Extensions(msg);
          case CERTIFICATE:
             return Certificate_13(msg, policy, peer_side);
-         // case CERTIFICATE_REQUEST:
-         //    return Certificate_Req_13(msg);
+         case CERTIFICATE_REQUEST:
+            return Certificate_Request_13(msg, peer_side);
          case CERTIFICATE_VERIFY:
             return Certificate_Verify_13(msg, peer_side);
          case FINISHED:
diff --git a/src/lib/tls/tls13/tls_handshake_state_13.cpp b/src/lib/tls/tls13/tls_handshake_state_13.cpp
index 8c086fd4f..25b60b0e4 100644
--- a/src/lib/tls/tls13/tls_handshake_state_13.cpp
+++ b/src/lib/tls/tls13/tls_handshake_state_13.cpp
@@ -40,16 +40,29 @@ Encrypted_Extensions& Handshake_State_13_Base::store(Encrypted_Extensions encryp
     return m_encrypted_extensions.value();
 }
 
-Certificate_13& Handshake_State_13_Base::store(Certificate_13 certificate, const bool)
+Certificate_Request_13& Handshake_State_13_Base::store(Certificate_Request_13 certificate_request, const bool)
 {
-    m_server_certs = std::move(certificate);
-    return m_server_certs.value();
+   m_certificate_request = std::move(certificate_request);
+   return m_certificate_request.value();
 }
 
-Certificate_Verify_13& Handshake_State_13_Base::store(Certificate_Verify_13 certificate_verify, const bool)
+
+Certificate_13& Handshake_State_13_Base::store(Certificate_13 certificate, const bool from_peer)
+{
+   auto& target = ((m_side == CLIENT) == from_peer)
+          ? m_server_certificate
+          : m_client_certificate;
+   target = std::move(certificate);
+   return target.value();
+}
+
+Certificate_Verify_13& Handshake_State_13_Base::store(Certificate_Verify_13 certificate_verify, const bool from_peer)
 {
-    m_server_verify = std::move(certificate_verify);
-    return m_server_verify.value();
+   auto& target = ((m_side == CLIENT) == from_peer)
+          ? m_server_certificate_verify
+          : m_client_certificate_verify;
+   target = std::move(certificate_verify);
+   return target.value();
 }
 
 Finished_13& Handshake_State_13_Base::store(Finished_13 finished, const bool from_peer)
diff --git a/src/lib/tls/tls13/tls_handshake_state_13.h b/src/lib/tls/tls13/tls_handshake_state_13.h
index 6abafb08d..0ccb9d82c 100644
--- a/src/lib/tls/tls13/tls_handshake_state_13.h
+++ b/src/lib/tls/tls13/tls_handshake_state_13.h
@@ -26,32 +26,37 @@ class BOTAN_TEST_API Handshake_State_13_Base
    public:
       bool has_client_hello() const { return m_client_hello.has_value(); }
       bool has_hello_retry_request() const { return m_hello_retry_request.has_value(); }
+      bool has_certificate_request() const { return m_certificate_request.has_value(); }
       bool has_server_finished() const { return m_server_finished.has_value(); }
       bool has_client_finished() const { return m_client_finished.has_value(); }
 
       bool handshake_finished() const { return has_server_finished() && has_client_finished(); }
 
       // Client_Hello_13 cannot be const because it might need modification due to a Hello_Retry_Request
-      Client_Hello_13&             client_hello() { return get(m_client_hello); }
-      const Server_Hello_13&       server_hello() const { return get(m_server_hello); }
-      const Hello_Retry_Request&   hello_retry_request() const { return get(m_hello_retry_request); }
-      const Encrypted_Extensions&  encrypted_extensions() const { return get(m_encrypted_extensions); }
-      const Certificate_13&        certificate() const { return get(m_server_certs); }
-      const Certificate_Verify_13& certificate_verify() const { return get(m_server_verify); }
-      const Finished_13&           client_finished() const { return get(m_client_finished); }
-      const Finished_13&           server_finished() const { return get(m_server_finished); }
+      Client_Hello_13&              client_hello() { return get(m_client_hello); }
+      const Server_Hello_13&        server_hello() const { return get(m_server_hello); }
+      const Hello_Retry_Request&    hello_retry_request() const { return get(m_hello_retry_request); }
+      const Encrypted_Extensions&   encrypted_extensions() const { return get(m_encrypted_extensions); }
+      const Certificate_Request_13& certificate_request() const { return get(m_certificate_request); }
+      const Certificate_13&         server_certificate() const { return get(m_server_certificate); }
+      const Certificate_13&         client_certificate() const { return get(m_client_certificate); }
+      const Certificate_Verify_13&  server_certificate_verify() const { return get(m_server_certificate_verify); }
+      const Certificate_Verify_13&  client_certificate_verify() const { return get(m_client_certificate_verify); }
+      const Finished_13&            server_finished() const { return get(m_server_finished); }
+      const Finished_13&            client_finished() const { return get(m_client_finished); }
 
    protected:
       Handshake_State_13_Base(Connection_Side whoami) : m_side(whoami) {}
 
-      Client_Hello_13&       store(Client_Hello_13 client_hello, const bool from_peer);
-      Server_Hello_13&       store(Server_Hello_13 server_hello, const bool from_peer);
-      Server_Hello_12&       store(Server_Hello_12 server_hello, const bool from_peer);
-      Hello_Retry_Request&   store(Hello_Retry_Request hello_retry_request, const bool from_peer);
-      Encrypted_Extensions&  store(Encrypted_Extensions encrypted_extensions, const bool from_peer);
-      Certificate_13&        store(Certificate_13 certificate, const bool from_peer);
-      Certificate_Verify_13& store(Certificate_Verify_13 certificate_verify, const bool from_peer);
-      Finished_13&           store(Finished_13 finished, const bool from_peer);
+      Client_Hello_13&        store(Client_Hello_13 client_hello, const bool from_peer);
+      Server_Hello_13&        store(Server_Hello_13 server_hello, const bool from_peer);
+      Server_Hello_12&        store(Server_Hello_12 server_hello, const bool from_peer);
+      Hello_Retry_Request&    store(Hello_Retry_Request hello_retry_request, const bool from_peer);
+      Encrypted_Extensions&   store(Encrypted_Extensions encrypted_extensions, const bool from_peer);
+      Certificate_Request_13& store(Certificate_Request_13 certificate_request, const bool from_peer);
+      Certificate_13&         store(Certificate_13 certificate, const bool from_peer);
+      Certificate_Verify_13&  store(Certificate_Verify_13 certificate_verify, const bool from_peer);
+      Finished_13&            store(Finished_13 finished, const bool from_peer);
 
    private:
       template<typename MessageT>
@@ -77,8 +82,11 @@ class BOTAN_TEST_API Handshake_State_13_Base
       std::optional<Server_Hello_12> m_server_hello_12;
       std::optional<Hello_Retry_Request> m_hello_retry_request;
       std::optional<Encrypted_Extensions> m_encrypted_extensions;
-      std::optional<Certificate_13> m_server_certs;
-      std::optional<Certificate_Verify_13> m_server_verify;
+      std::optional<Certificate_Request_13> m_certificate_request;
+      std::optional<Certificate_13> m_server_certificate;
+      std::optional<Certificate_13> m_client_certificate;
+      std::optional<Certificate_Verify_13> m_server_certificate_verify;
+      std::optional<Certificate_Verify_13> m_client_certificate_verify;
       std::optional<Finished_13> m_server_finished;
       std::optional<Finished_13> m_client_finished;
    };
@@ -101,7 +109,7 @@ class BOTAN_TEST_API Handshake_State_13 : public Internal::Handshake_State_13_Ba
    public:
       Handshake_State_13() : Handshake_State_13_Base(whoami) {}
 
-      decltype(auto) sent(Outbound_Message_T message)
+      decltype(auto) sending(Outbound_Message_T message)
          {
          return std::visit([&](auto msg) -> Handshake_Message_13_Ref
             {
diff --git a/src/lib/tls/tls_extensions.cpp b/src/lib/tls/tls_extensions.cpp
index 23a065553..b224e4800 100644
--- a/src/lib/tls/tls_extensions.cpp
+++ b/src/lib/tls/tls_extensions.cpp
@@ -12,6 +12,7 @@
 #include <botan/internal/tls_reader.h>
 #include <botan/tls_exceptn.h>
 #include <botan/tls_policy.h>
+#include <botan/ber_dec.h>
 
 #include <iterator>
 
@@ -73,6 +74,9 @@ std::unique_ptr<Extension> make_extension(TLS_Data_Reader& reader,
       case TLSEXT_PSK_KEY_EXCHANGE_MODES:
          return std::make_unique<PSK_Key_Exchange_Modes>(reader, size);
 
+      case TLSEXT_CERTIFICATE_AUTHORITIES:
+         return std::make_unique<Certificate_Authorities>(reader, size);
+
       case TLSEXT_SIGNATURE_ALGORITHMS_CERT:
          return std::make_unique<Signature_Algorithms_Cert>(reader, size);
 
@@ -797,6 +801,33 @@ PSK_Key_Exchange_Modes::PSK_Key_Exchange_Modes(TLS_Data_Reader& reader, uint16_t
       }
    }
 
+
+std::vector<uint8_t> Certificate_Authorities::serialize(Connection_Side) const
+   { throw Botan::Not_Implemented("serializing Certificate_Authorities is NYI"); }
+
+Certificate_Authorities::Certificate_Authorities(TLS_Data_Reader& reader, uint16_t extension_size)
+   {
+   if (extension_size < 2)
+      {
+      throw Decoding_Error("Empty certificate_authorities extension is illegal");
+      }
+
+   const uint16_t purported_size = reader.get_uint16_t();
+
+   if(reader.remaining_bytes() != purported_size)
+      throw Decoding_Error("Inconsistent length in certificate_authorities extension");
+
+   while(reader.has_remaining())
+      {
+      std::vector<uint8_t> name_bits = reader.get_tls_length_value(2);
+
+      BER_Decoder decoder(name_bits.data(), name_bits.size());
+      m_distinguished_names.emplace_back();
+      decoder.decode(m_distinguished_names.back());
+      }
+   }
+
+
 Signature_Algorithms_Cert::Signature_Algorithms_Cert(const std::vector<Signature_Scheme>& schemes)
       : m_siganture_algorithms(schemes)
    {
diff --git a/src/lib/tls/tls_extensions.h b/src/lib/tls/tls_extensions.h
index 57c5a8bb8..a79931769 100644
--- a/src/lib/tls/tls_extensions.h
+++ b/src/lib/tls/tls_extensions.h
@@ -56,6 +56,8 @@ enum Handshake_Extension_Type {
    TLSEXT_USE_SRTP                  = 14,
    TLSEXT_ALPN                      = 16,
 
+   // TLSEXT_SIGNED_CERTIFICATE_TIMESTAMP = 18,  // NYI
+
    TLSEXT_ENCRYPT_THEN_MAC          = 22,
    TLSEXT_EXTENDED_MASTER_SECRET    = 23,
 
@@ -68,6 +70,8 @@ enum Handshake_Extension_Type {
    TLSEXT_COOKIE                    = 44,
 
    TLSEXT_PSK_KEY_EXCHANGE_MODES    = 45,
+   TLSEXT_CERTIFICATE_AUTHORITIES   = 47,
+   // TLSEXT_OID_FILTERS               = 48,  // NYI
 
    TLSEXT_SIGNATURE_ALGORITHMS_CERT = 50,
    TLSEXT_KEY_SHARE                 = 51,
@@ -558,6 +562,30 @@ class BOTAN_UNSTABLE_API PSK_Key_Exchange_Modes final : public Extension
    };
 
 
+/**
+ * Certificate Authorities Extension from RFC 8446 4.2.4
+ */
+class BOTAN_UNSTABLE_API Certificate_Authorities final : public Extension
+   {
+   public:
+      static Handshake_Extension_Type static_type()
+         { return TLSEXT_CERTIFICATE_AUTHORITIES; }
+
+      Handshake_Extension_Type type() const override { return static_type(); }
+
+      std::vector<uint8_t> serialize(Connection_Side whoami) const override;
+
+      bool empty() const override { return m_distinguished_names.empty(); }
+
+      const std::vector<X509_DN>& distinguished_names() const
+         { return m_distinguished_names; }
+
+      explicit Certificate_Authorities(TLS_Data_Reader& reader, uint16_t extension_size);
+
+   private:
+      std::vector<X509_DN> m_distinguished_names;
+   };
+
 /**
 * Signature_Algorithms_Cert from RFC 8446
 */
diff --git a/src/lib/tls/tls_messages.h b/src/lib/tls/tls_messages.h
index 1acb3e1c9..f23e955bd 100644
--- a/src/lib/tls/tls_messages.h
+++ b/src/lib/tls/tls_messages.h
@@ -495,6 +495,19 @@ class BOTAN_UNSTABLE_API Certificate_13 final : public Handshake_Message
 
       size_t count() const { return m_entries.size(); }
       bool empty() const { return m_entries.empty(); }
+      const X509_Certificate& leaf() const;
+      const std::vector<uint8_t>& request_context() const { return m_request_context; }
+
+      /**
+       * Create a Certificate message
+       *
+       * @param certs            the certificate (chain) to be used for authentication
+       * @param request_context  the context provided in the Certificate Request
+       *                         message when performing post-handshake client auth
+       */
+      Certificate_13(const std::vector<X509_Certificate>& certs,
+                     const Connection_Side side,
+                     const std::vector<uint8_t> &request_context = {});
 
       /**
       * Deserialize a Certificate message
@@ -510,7 +523,7 @@ class BOTAN_UNSTABLE_API Certificate_13 final : public Handshake_Message
       * Validate a Certificate message regarding what extensions are expected based on
       * previous handshake messages.
       *
-      * @param requested_extensions Extensions of Client_Hello or Certificate_Req messages
+      * @param requested_extensions Extensions of Client_Hello or Certificate_Request messages
       */
       void validate_extensions(const std::set<Handshake_Extension_Type>& requested_extensions) const;
 
@@ -528,11 +541,6 @@ class BOTAN_UNSTABLE_API Certificate_13 final : public Handshake_Message
       std::vector<uint8_t> serialize() const override;
 
    private:
-      // RFC 8446 4.4.2
-      //    [...] (in the case of server authentication),
-      //    this field SHALL be zero length.
-      //
-      // TODO: implement when adding support for client certificates
       std::vector<uint8_t>           m_request_context;
       std::vector<Certificate_Entry> m_entries;
       Connection_Side                m_side;
@@ -569,10 +577,9 @@ class BOTAN_UNSTABLE_API Certificate_Status final : public Handshake_Message
    };
 
 /**
-* Certificate Request Message
-* TODO: this is 1.2 only
+* Certificate Request Message (TLS 1.2)
 */
-class BOTAN_UNSTABLE_API Certificate_Req final : public Handshake_Message
+class BOTAN_UNSTABLE_API Certificate_Request_12 final : public Handshake_Message
    {
    public:
       Handshake_Type type() const override;
@@ -583,12 +590,12 @@ class BOTAN_UNSTABLE_API Certificate_Req final : public Handshake_Message
 
       const std::vector<Signature_Scheme>& signature_schemes() const;
 
-      Certificate_Req(Handshake_IO& io,
+      Certificate_Request_12(Handshake_IO& io,
                       Handshake_Hash& hash,
                       const Policy& policy,
                       const std::vector<X509_DN>& allowed_cas);
 
-      explicit Certificate_Req(const std::vector<uint8_t>& buf);
+      explicit Certificate_Request_12(const std::vector<uint8_t>& buf);
 
       std::vector<uint8_t> serialize() const override;
 
@@ -598,6 +605,29 @@ class BOTAN_UNSTABLE_API Certificate_Req final : public Handshake_Message
       std::vector<Signature_Scheme> m_schemes;
    };
 
+#if defined(BOTAN_HAS_TLS_13)
+
+class BOTAN_UNSTABLE_API Certificate_Request_13 final : public Handshake_Message
+   {
+   public:
+      Handshake_Type type() const override;
+
+      Certificate_Request_13(const std::vector<uint8_t>& buf, const Connection_Side side);
+
+      std::vector<X509_DN> acceptable_CAs() const;
+      const std::vector<Signature_Scheme>& signature_schemes() const;
+
+      std::vector<uint8_t> serialize() const override;
+
+      const std::vector<uint8_t> context() const { return m_context; }
+
+   private:
+      std::vector<uint8_t> m_context;
+      Extensions m_extensions;
+   };
+
+#endif
+
 class BOTAN_UNSTABLE_API Certificate_Verify : public Handshake_Message
    {
    public:
@@ -605,13 +635,8 @@ class BOTAN_UNSTABLE_API Certificate_Verify : public Handshake_Message
 
       Signature_Scheme signature_scheme() const { return m_scheme; }
 
-      Certificate_Verify(Handshake_IO& io,
-                         Handshake_State& state,
-                         const Policy& policy,
-                         RandomNumberGenerator& rng,
-                         const Private_Key* key);
-
       Certificate_Verify(const std::vector<uint8_t>& buf);
+      Certificate_Verify() = default;
 
       std::vector<uint8_t> serialize() const override;
 
@@ -628,6 +653,12 @@ class BOTAN_UNSTABLE_API Certificate_Verify_12 final : public Certificate_Verify
    public:
       using Certificate_Verify::Certificate_Verify;
 
+      Certificate_Verify_12(Handshake_IO& io,
+                            Handshake_State& state,
+                            const Policy& policy,
+                            RandomNumberGenerator& rng,
+                            const Private_Key* key);
+
       /**
       * Check the signature on a certificate verify message
       * @param cert the purported certificate
@@ -655,6 +686,15 @@ class BOTAN_UNSTABLE_API Certificate_Verify_13 final : public Certificate_Verify
       Certificate_Verify_13(const std::vector<uint8_t>& buf,
                             const Connection_Side side);
 
+      Certificate_Verify_13(
+            const std::vector<Signature_Scheme>& peer_allowed_schemes,
+            Connection_Side whoami,
+            const Private_Key& key,
+            const Policy& policy,
+            const Transcript_Hash& hash,
+            Callbacks& callbacks,
+            RandomNumberGenerator& rng);
+
       bool verify(const X509_Certificate& cert,
                   Callbacks& callbacks,
                   const Transcript_Hash& transcript_hash) const;
@@ -892,7 +932,7 @@ using Handshake_Message_13 = std::variant<
                              // End_Of_Early_Data,
                              Encrypted_Extensions,
                              Certificate_13,
-                             // Certificate_Req_13,
+                             Certificate_Request_13,
                              Certificate_Verify_13,
                              Finished_13>;
 using Handshake_Message_13_Ref = as_wrapped_references_t<Handshake_Message_13>;
@@ -907,18 +947,16 @@ using Server_Handshake_13_Message = std::variant<
                                     Hello_Retry_Request,
                                     Encrypted_Extensions,
                                     Certificate_13,
+                                    Certificate_Request_13,
                                     Certificate_Verify_13,
                                     Finished_13>;
-                                    // Post-Handshake Messages
-                                    // New_Session_Ticket_13,
-                                    // Key_Update>;
 using Server_Handshake_13_Message_Ref = as_wrapped_references_t<Server_Handshake_13_Message>;
 
 using Client_Handshake_13_Message = std::variant<
                                     Client_Hello_13,
+                                    Certificate_13,
+                                    Certificate_Verify_13,
                                     Finished_13>;
-                                    // Post-Handshake Messages
-                                    // Key_Update>;
 using Client_Handshake_13_Message_Ref = as_wrapped_references_t<Client_Handshake_13_Message>;
 
 #endif // BOTAN_HAS_TLS_13


Test patch:
diff --git a/src/tests/data/tls_13_rfc8448/transcripts.vec b/src/tests/data/tls_13_rfc8448/transcripts.vec
index da4d4bb68..b2337431a 100644
--- a/src/tests/data/tls_13_rfc8448/transcripts.vec
+++ b/src/tests/data/tls_13_rfc8448/transcripts.vec
@@ -25,6 +25,18 @@ ClientFinished = 1703030035d74f1923c662fd34137c6f502f3dd2b93d951d1b3bc97e42afe23
 Client_CloseNotify = 17030300132ea6cdf749196023e2b3a49491695536426047
 Server_CloseNotify = 1703030013519fc5075cb0884349759ff9ef6f011bb4c6f2
 
+[Client_Authentication_Handshake]
+RNG_Pool = 6a472236328b83af40386d3a3e1f1ce624fa4ed89ab865a4ff0f4144ce3ae233c040b2bb8f3addd20fd4058c547003a3c6f9c1cd915d5e535c87d8d191aaf071
+CurrentTimestamp = 1651826546000
+ClientHello_1 = 16030100c0010000bc03036a472236328b83af40386d3a3e1f1ce624fa4ed89ab865a4ff0f4144ce3ae2330000061301130313020100008d0000000b0009000006736572766572ff01000100000a00140012001d00170018001901000101010201030104003300260024001d0020089cc2671f738d9a671e5b2e464981d05b76e361aa22aea91f1d49ca10a7a362002b0003020304000d0020001e040305030603020308040805080604010501060102010402050206020202002d00020101001c00024001
+ServerHello = 160303005a0200005603033b50fdf1c3d572e40e68953e7fff4e2758459c59afa0582c0ea032874255fe6e00130100002e00330024001d00206c2e50e865919a6b5a12dfaf918f92b442567b0f89bc54478c6921366658f062002b00020304
+ServerHandshakeMessages = 17030302166d0a7ac079b32a94aa68c4e2893e8bd0d3c185f549c236fbbce3d647f08f3c94a2bf424d8708883605ad8955f97718b0213dead13dfb23ebb8381da582756612bcb5a5d40847719fbe9f179bfae656f3ecfd59a4c0d35132ce418a7e46f6b6a60622f8a6c06b28d83360163563be9c37f97eb902326924a72b3ed8c8381277d1581cab9c3715ac2401398467ad7ebfab3d0c3419e750104f7d62c5027901f2e4cd4ca5b8071eb03d3c732d83215066dfc4d291d4c1ff3b8d7e4298f677d4d51dea1168d8f16cb27ba40266313a1fedf9e23cc77f765450f9e96f05d08f3da245b14d4946f07ec81eed6d56f26bd574f0b7f7c7047037c16fce3b23754e662fad73e2b7213f6af296769c99a1d38e6232e0ec8dc4f84d6aa6f7de3887be0057862f9018e0ab396705aa4090ab5f2dff6325a557e7320d4effd46bb4f997d163207cce6665294aa4465541e3fe37ee7350659ea550d6dcb6af3c518852c7a14c3cc15bc32b3273bdf1751da184203135b117d300204fb12d58ca9ac34b68eca27030832f7a4b46d2a55757f63fe8f6e85ac47469e6198da88a64586bf23c69590de822263be75fd836847240c48f8c145cd6bd698962e7edc234ebe59231351eef8d7652cf3b08ab3af6e5ec74c58a8da34b39f9b0d6c4279a9a1f82071729e7059dd7f7b95b9433c4684ce1891a6d33432d52eddb0b8cee9181d403eccc12991f1ad4aa62c36049713a7bb135fdda6661a05a93f8c16f
+MessageToSign = 20202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020544c5320312e332c20636c69656e742043657274696669636174655665726966790044026ef3331bc864da1c08ee32e196d93cc2c5d487e47e957d980e7c3d801b9a
+MessageSignature = 186b2223b503a759c35dba0e9721b4b579138d5f0f5e6ec7feaaf27f3ad7f386c2c7bd7cb2be52fbf5ed8393f406ee79369692ec7ac695651d858219e672a8eb7b2a677b640b46ab630edc5f3f2f8272b9c0d906f81f84ddc5b8c7bcf955c78a3cf99e5016f73e04eb7dfcb28833f13e8f75ec2ff3581e2f098ad4157fd6d6ad
+ClientFinished = 1703030280b46a63934e673841abaf267403bc677f6b6d2a1e2f12bb5f62683bfe36a82673f06d6287ddd609bcf2f5fd3225923d24af3c76682c180ee571a17ca4bfbe2f510dc9a0e1fca5cff2cee87d11cb531a6ef90bf5309a6b63bbbc0b88ea45103a4304091543859fa11ec032ed873444cd5185ead5f6a76420f0f0286acef802c8e4788c23275f1b06da600f4a7decd0bc59d7bef10e649ae32690397fc3d4ed6f30f801d8cd569b71ad4fa05ea7cf2ac2dfa150d220505d4011b34d09d53853eba61a101e4f8dca47d8171a884b19259a3dd48c5ac141983edc77814d25e7f66bbbdb9096839266e06561828ecfb27eafd4e9e81a0b96e3bfa42dae5ad80359b9a6661402c3a2104177030106dbd8f65bb6a0159d512eb13af22a259f313bd58c2e21fe053d57f2a962b0a4ea682c96f70b79b560136192823b27be6a2fb7b1c751ccc0e3303615541485b7b307b423332c11efa80b72f9b80a53e53f7bb38a3af4c59f8008bad0544e5614e688ff57bccd6935f81f447f420c1c1bf4058818e90bf5dc716ccae42524856df8250bcdbd7af65f82dd53061d024f6d2ff5c11e3792a9a70e0ee2a3c20a1b968ac391f8f92831135d25242ada2fe241c2653ec996339dfa12dfae7a3373df88b07ca27aef6dc266a25f13f75c76039c1f46fd7a53ae6399c999f4b2aee18e480d6d12bfae226bbdc92a6ad50b4d3bac7abc3b3651eb5be56f33bf41127b3ca886dc714a50d1490357bd40d9fd6be42209a4ddb9ebb2987e29f120f05814614d2c79320015b461fe7324447670a1af5f65caed15b474ab7faa495016adf808e53b94ef54afbb0e0a3a2732ab597f7d5923c77386aa5124731f8cc73e703b341c175a454939a77ab64313c15cf3fe03c4f33842564976
+Client_CloseNotify = 1703030013e4ad7d44c29245339d355962c779b89ef44c58
+Server_CloseNotify = 17030300131decc5d6e64bba8a6f21b4fd077497da2a90cb
+
 [Middlebox_Compatibility_Mode]
 RNG_Pool = 4e640a3f2c2738f09c9418bd78edccd7559d0531199276d4d92a0e9ee9d77d09a80c165581a8e0d06c0018d54d3a06dd32cfd4051eb026fad3fd0ba99269e6efdea00b45695dc781f19d34a62c1afd31ab4369af1e855a3bbb258d8442cde6d7
 CurrentTimestamp = 1651826546000
diff --git a/src/tests/test_tls_handshake_state_13.cpp b/src/tests/test_tls_handshake_state_13.cpp
index 23149911e..fc1c275a9 100644
--- a/src/tests/test_tls_handshake_state_13.cpp
+++ b/src/tests/test_tls_handshake_state_13.cpp
@@ -56,7 +56,7 @@ std::vector<Test::Result> finished_message_handling()
 
          Finished_13 client_finished(client_finished_message);
 
-         auto client_fin = state.sent(std::move(client_finished));
+         auto client_fin = state.sending(std::move(client_finished));
          result.require("client can send client finished",
                         std::holds_alternative<std::reference_wrapper<Finished_13>>(client_fin));
          result.test_throws("not stored as server Finished", [&]
@@ -84,7 +84,7 @@ std::vector<Test::Result> handshake_message_filtering()
          {
          Client_Handshake_State_13 state;
 
-         auto filtered = state.sent(Client_Hello_13(client_hello_message));
+         auto filtered = state.sending(Client_Hello_13(client_hello_message));
          result.confirm("client can send client hello",
                         std::holds_alternative<std::reference_wrapper<Client_Hello_13>>(filtered));
 
diff --git a/src/tests/test_tls_rfc8448.cpp b/src/tests/test_tls_rfc8448.cpp
index 2792e99d3..6d91a7198 100644
--- a/src/tests/test_tls_rfc8448.cpp
+++ b/src/tests/test_tls_rfc8448.cpp
@@ -44,6 +44,7 @@
 
    #include <botan/internal/loadstor.h>
 
+   #include <botan/pk_algs.h>
    #include <botan/pkcs8.h>
    #include <botan/data_src.h>
 #endif
@@ -74,6 +75,23 @@ Botan::X509_Certificate server_certificate()
    return Botan::X509_Certificate(in);
    }
 
+std::unique_ptr<Botan::Private_Key> client_private_key()
+   {
+   // RFC 8448 does not actually provide that key. Hence we generate one on the
+   // fly as a stand-in. Instead of actually using it, the signatures generated
+   // by this private key must be hard-coded in `Callbacks::sign_message()`; see
+   // `MockSignature_Fn` for more details.
+   return create_private_key("RSA", Test::rng(), "1024");
+   }
+
+Botan::X509_Certificate client_certificate()
+   {
+   // self-signed certificate with an RSA1024 public key valid until:
+   //   Jul 30 01:23:59 2026 GMT
+   Botan::DataSource_Memory in(Test::read_data_file("tls_13_rfc8448/client_certificate.pem"));
+   return Botan::X509_Certificate(in);
+   }
+
 /**
 * Simple version of the Padding extension (RFC 7685) to reproduce the
 * 2nd Client_Hello in RFC8448 Section 5 (HelloRetryRequest)
@@ -112,19 +130,25 @@ std::chrono::system_clock::time_point from_milliseconds_since_epoch(uint64_t mse
           std::chrono::milliseconds(additional_millis);
    }
 
+using Modify_Exts_Fn = std::function<void(Botan::TLS::Extensions&, Botan::TLS::Connection_Side)>;
+using MockSignature_Fn = std::function<std::vector<uint8_t>(const std::vector<uint8_t>&,const std::string&,Signature_Format)>;
+
 /**
  * Subclass of the Botan::TLS::Callbacks instrumenting all available callbacks.
  * The invocation counts can be checked in the integration tests to make sure
  * all expected callbacks are hit. Furthermore collects the received application
  * data and sent record bytes for further inspection by the test cases.
  */
-using Modify_Exts_Fn = std::function<void(Botan::TLS::Extensions&, Botan::TLS::Connection_Side)>;
 class Test_TLS_13_Callbacks : public Botan::TLS::Callbacks
    {
    public:
-      Test_TLS_13_Callbacks(Modify_Exts_Fn modify_exts_cb, uint64_t timestamp) :
+      Test_TLS_13_Callbacks(
+         Modify_Exts_Fn modify_exts_cb,
+         MockSignature_Fn mock_signature_fn,
+         uint64_t timestamp) :
          session_activated_called(false),
          m_modify_exts(std::move(modify_exts_cb)),
+         m_mock_signature(std::move(mock_signature_fn)),
          m_timestamp(from_milliseconds_since_epoch(timestamp))
          {}
 
@@ -197,9 +221,12 @@ class Test_TLS_13_Callbacks : public Botan::TLS::Callbacks
          const std::vector<uint8_t>& msg) override
          {
          count_callback_invocation("tls_sign_message");
-         return Callbacks::tls_sign_message(key, rng, emsa, format, msg);
-         }
 
+         auto mock_signature = m_mock_signature(msg, emsa, format);
+         return (mock_signature.empty())
+            ? Callbacks::tls_sign_message(key, rng, emsa, format, msg)
+            : mock_signature;
+         }
 
       bool tls_verify_message(
          const Public_Key& key,
@@ -317,11 +344,40 @@ class Test_TLS_13_Callbacks : public Botan::TLS::Callbacks
       std::vector<uint8_t>                  receive_buffer;
       uint64_t                              received_seq_no;
       Modify_Exts_Fn                        m_modify_exts;
+      MockSignature_Fn     m_mock_signature;
       std::chrono::system_clock::time_point m_timestamp;
 
       mutable std::map<std::string, unsigned int> m_callback_invocations;
    };
 
+class Test_Client_Credentials : public Botan::Credentials_Manager
+   {
+   public:
+      Test_Client_Credentials() : m_bogus_key(client_private_key()) {}
+
+      std::vector<Botan::X509_Certificate> cert_chain(
+         const std::vector<std::string>& cert_key_types,
+         const std::string& type,
+         const std::string& context) override
+         {
+         BOTAN_UNUSED(cert_key_types, type, context);
+         return { client_certificate() };
+         }
+
+      Botan::Private_Key* private_key_for(const Botan::X509_Certificate& cert,
+                                          const std::string& type,
+                                          const std::string& context) override
+         {
+         BOTAN_UNUSED(cert, type, context);
+         // Note that this key is just a stand-in and not the actual private
+         // key for the client certificate. RFC 8448 does not reveal this key.
+         return m_bogus_key.get();
+         }
+
+   private:
+      std::unique_ptr<Botan::Private_Key> m_bogus_key;
+   };
+
 class RFC8448_Text_Policy : public Botan::TLS::Text_Policy
    {
    public:
@@ -367,8 +423,9 @@ class TLS_Context
       TLS_Context(std::unique_ptr<Botan::RandomNumberGenerator> rng_in,
                   RFC8448_Text_Policy policy,
                   Modify_Exts_Fn modify_exts_cb,
+                  MockSignature_Fn mock_signature_cb,
                   uint64_t timestamp)
-         : m_callbacks(std::move(modify_exts_cb), timestamp)
+         : m_callbacks(std::move(modify_exts_cb), std::move(mock_signature_cb), timestamp)
          , m_rng(std::move(rng_in))
          , m_session_mgr(*m_rng)
          , m_policy(std::move(policy))
@@ -428,8 +485,8 @@ class TLS_Context
       virtual void send(const std::vector<uint8_t>& data) = 0;
 
    protected:
-      Test_TLS_13_Callbacks      m_callbacks;
-      Botan::Credentials_Manager m_creds;
+      Test_TLS_13_Callbacks   m_callbacks;
+      Test_Client_Credentials m_creds;
 
       std::unique_ptr<Botan::RandomNumberGenerator> m_rng;
       Botan::TLS::Session_Manager_In_Memory         m_session_mgr;
@@ -441,9 +498,10 @@ class Client_Context : public TLS_Context
    public:
       Client_Context(std::unique_ptr<Botan::RandomNumberGenerator> rng_in,
                      RFC8448_Text_Policy policy,
+                     uint64_t timestamp,
                      Modify_Exts_Fn modify_exts_cb,
-                     uint64_t timestamp)
-         : TLS_Context(std::move(rng_in), std::move(policy), std::move(modify_exts_cb), timestamp)
+                     MockSignature_Fn mock_signature_cb = [](auto,auto,auto) { return std::vector<uint8_t>(); })
+         : TLS_Context(std::move(rng_in), std::move(policy), std::move(modify_exts_cb), std::move(mock_signature_cb), timestamp)
          , client(m_callbacks, m_session_mgr, m_creds, m_policy, *m_rng,
                   Botan::TLS::Server_Information("server"),
                   Botan::TLS::Protocol_Version::TLS_V13)
@@ -536,7 +594,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test
    {
    public:
       Test_TLS_RFC8448()
-         : Text_Based_Test("tls_13_rfc8448/transcripts.vec", "RNG_Pool,CurrentTimestamp,ClientHello_1,ServerHello,ServerHandshakeMessages,ClientFinished,Client_CloseNotify,Server_CloseNotify", "HelloRetryRequest,ClientHello_2,NewSessionTicket,Client_AppData,Client_AppData_Record,Server_AppData,Server_AppData_Record") {}
+         : Text_Based_Test("tls_13_rfc8448/transcripts.vec", "RNG_Pool,CurrentTimestamp,ClientHello_1,ServerHello,ServerHandshakeMessages,ClientFinished,Client_CloseNotify,Server_CloseNotify", "HelloRetryRequest,ClientHello_2,NewSessionTicket,Client_AppData,Client_AppData_Record,Server_AppData,Server_AppData_Record,MessageToSign,MessageSignature") {}
 
    Test::Result run_one_test(const std::string& header,
                              const VarMap& vars) override
@@ -545,6 +603,8 @@ class Test_TLS_RFC8448 final : public Text_Based_Test
          return Test::Result("Simple 1-RTT (Client side)", simple_1_rtt_client_hello(vars));
       else if(header == "HelloRetryRequest_Handshake")
          return Test::Result("Handshake involving Hello Retry Request (Client side)", hello_retry_request(vars));
+      else if(header == "Client_Authentication_Handshake")
+         return Test::Result("Client Authentication (Client side)", client_authentication(vars));
       else if(header == "Middlebox_Compatibility_Mode")
          return Test::Result("Middlebox Compatibility Mode (Client side)", middlebox_compatibility(vars));
       else
@@ -578,7 +638,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test
          return {
             Botan_Tests::CHECK("Client Hello", [&](Test::Result& result)
                {
-               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_1rtt"), add_extensions_and_sort, vars.get_req_u64("CurrentTimestamp"));
+               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_1rtt"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort);
 
                result.confirm("client not closed", !ctx->client.is_closed());
                ctx->check_callback_invocations(result, "client hello prepared", { "tls_emit_data", "tls_inspect_handshake_msg_client_hello", "tls_modify_extensions" });
@@ -707,7 +767,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test
          return {
             Botan_Tests::CHECK("Client Hello", [&](Test::Result& result)
                {
-               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_hrr"), add_extensions_and_sort, vars.get_req_u64("CurrentTimestamp"));
+               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_hrr"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort);
                result.confirm("client not closed", !ctx->client.is_closed());
 
                ctx->check_callback_invocations(result, "client hello prepared", { "tls_emit_data", "tls_inspect_handshake_msg_client_hello", "tls_modify_extensions" });
@@ -773,6 +833,112 @@ class Test_TLS_RFC8448 final : public Text_Based_Test
             };
          }
 
+      static std::vector<Test::Result> client_authentication(const VarMap& vars)
+         {
+         auto rng = std::make_unique<Botan_Tests::Fixed_Output_RNG>("");
+         rng->add_entropy(std::vector<uint8_t>(32).data(), 32);  // used by session mgr for session key
+
+         // 32 - for client hello random
+         // 32 - for eph. x25519 key pair
+         add_entropy(*rng, vars.get_req_bin("RNG_Pool"));
+
+         auto add_extensions_and_sort = [&](Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side side)
+            {
+            add_psk_exchange_modes(exts);
+            add_renegotiation_extension(exts);
+            sort_extensions(exts, side);
+            };
+
+         auto sign_certificate_verify = [&](const std::vector<uint8_t>& msg,
+                                            const std::string& emsa,
+                                            Signature_Format format)
+            {
+            BOTAN_UNUSED(emsa, format);
+
+            if(msg != vars.get_req_bin("MessageToSign"))
+               {
+               throw Test_Error("Unexpected message to be signed: " + Botan::hex_encode(msg));
+               }
+
+            if(emsa != "PSSR(SHA-256,MGF1,32)" || format != Signature_Format::IEEE_1363)
+               {
+               throw Test_Error("TLS implementation selected unexpected signature format ("
+                                + std::to_string(format)
+                                + ") or EMSA: "
+                                + emsa);
+               }
+
+            return vars.get_req_bin("MessageSignature");
+            };
+
+         std::unique_ptr<Client_Context> ctx;
+
+         return {
+            Botan_Tests::CHECK("Client Hello", [&](Test::Result& result)
+               {
+               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_1rtt"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort, sign_certificate_verify);
+
+               ctx->check_callback_invocations(result, "initial callbacks", {
+                  "tls_emit_data",
+                  "tls_inspect_handshake_msg_client_hello",
+                  "tls_modify_extensions",
+                  });
+
+               result.test_eq("Client Hello", ctx->pull_send_buffer(), vars.get_req_bin("ClientHello_1"));
+               }),
+
+            Botan_Tests::CHECK("Server Hello", [&](auto& result)
+               {
+               ctx->client.received_data(vars.get_req_bin("ServerHello"));
+
+               ctx->check_callback_invocations(result, "callbacks after server hello", {
+                  "tls_examine_extensions",
+                  "tls_inspect_handshake_msg_server_hello",
+                  });
+               }),
+
+            Botan_Tests::CHECK("other handshake messages and client auth", [&](Test::Result& result)
+               {
+               ctx->client.received_data(vars.get_req_bin("ServerHandshakeMessages"));
+
+               ctx->check_callback_invocations(result, "signing callbacks invoked", {
+                  "tls_sign_message",
+                  "tls_emit_data",
+                  "tls_examine_extensions",
+                  "tls_inspect_handshake_msg_certificate",
+                  "tls_inspect_handshake_msg_certificate_request",
+                  "tls_inspect_handshake_msg_certificate_verify",
+                  "tls_inspect_handshake_msg_encrypted_extensions",
+                  "tls_inspect_handshake_msg_finished",
+                  "tls_session_activated",
+                  "tls_verify_cert_chain",
+                  "tls_verify_message",
+                  });
+
+               // ClientFinished contains the entire coalesced client authentication flight
+               // Messages: Certificate, CertificateVerify, Finished
+               result.test_eq("Client Auth and Finished", ctx->pull_send_buffer(), vars.get_req_bin("ClientFinished"));
+               }),
+
+            Botan_Tests::CHECK("Close Connection", [&](Test::Result& result)
+               {
+               ctx->client.close();
+               result.test_eq("Client close_notify", ctx->pull_send_buffer(), vars.get_req_bin("Client_CloseNotify"));
+
+               ctx->check_callback_invocations(result, "after sending close notify", {
+                  "tls_emit_data",
+                  });
+
+               ctx->client.received_data(vars.get_req_bin("Server_CloseNotify"));
+               result.confirm("connection closed", ctx->client.is_closed());
+
+               ctx->check_callback_invocations(result, "after receiving close notify", {
+                  "tls_alert",
+                  });
+               }),
+         };
+      }
+
       static std::vector<Test::Result> middlebox_compatibility(const VarMap& vars)
          {
          auto rng = std::make_unique<Botan_Tests::Fixed_Output_RNG>("");
@@ -795,7 +961,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test
          return {
             Botan_Tests::CHECK("Client Hello", [&](Test::Result& result)
                {
-               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_compat"), add_extensions_and_sort, vars.get_req_u64("CurrentTimestamp"));
+               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_compat"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort);
 
                result.test_eq("Client Hello", ctx->pull_send_buffer(), vars.get_req_bin("ClientHello_1"));
                }),