Repository: randombit/botan
Pull request #3081 (2022-11-30 12:56:41)

[TLS 1.3] Server-Side Client Authentication
## Pull Request Dependencies

* #3062
* #3053

## Description

Adds support for client authentication to the TLS 1.3 server implementation. Respective RFC 8448 test is passing, BoGo tests are passing.

## Messages `Certificate`, `Certificate_Request` and `CertificateVerify`

The refactoring in this patch aims at making construction of those messages more self-contained. Instead of handing in the detailed information those messages need for their own construction, we hand in the higher-level objects that contain the necessary information. E.g. instead of passing the correct private key to `CertificateVerify`, it now gets a reference to the `Credentials_Manager` enabling its constructor to gather the correct private key pointer internally. This moves low-level logic and orchestration out of the Client/Server implementation classes.

For `Certificate_Request_13` this even moves the decision whether or not to even request certificate authentication from the client into a factory method (see `Certificate_Request_13::maybe_create()`). I'm myself not sure yet, whether I actually like that pattern. Though, it takes complexity out of `tls_server_impl_13.cpp` which I consider a good thing.

`Certificate::setup_entries()` might seem to be overly simple, but it was created as a pre-text to [the OCSP stapling pull request](https://github.com/randombit/botan/pull/3112) where it will be filled with more logic.

## FIX: Unprotected Alerts

During the development of this functionality, BoGo tests replied with a plaintext TLS alert when we (the server) already exclusively expected encrypted records. Turns out, that is not okay and needed a fix (`TLS::Cipher_State::must_expect_unprotected_alert_traffic()`).

Scenario: After receiving a Client Hello we (the server) respond with our first flight. Having just sent the server hello (with enough cryptographic information to encrypt further traffic) we expected the client to respond with protected records only. However, if the client fails to decode the Server Hello and must respond with an Alert, it is forced to send this alert in the clear, as it didn't understand our key negotiation info. We (as the server) need to expect that and allow unprotected Alerts, despite already having derived valid key material.

It appears to me, that the described situation is the only scenario where unprotected records need to be expected by either the client or the server despite cryptographic material already being available. Unfortunately, that was not explicitly pointed out in the RFC it seems.

codecov-commenter: # [Codecov](https://codecov.io/gh/randombit/botan/pull/3081?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) Report
Base: **91.80**% // Head: **91.83**% // Increases project coverage by **`+0.02%`** :tada:
> Coverage data is based on head [(`8ff80d0`)](https://codecov.io/gh/randombit/botan/pull/3081?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) compared to base [(`b202720`)](https://codecov.io/gh/randombit/botan/commit/b202720802ebcb2ea23301c535da9ffe5367291b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd).
> Patch coverage: 91.25% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3081      +/-   ##
==========================================
+ Coverage   91.80%   91.83%   +0.02%     
==========================================
  Files         599      600       +1     
  Lines       72086    72984     +898     
  Branches     6601     6684      +83     
==========================================
+ Hits        66181    67023     +842     
- Misses       5905     5961      +56     
```


| [Impacted Files](https://codecov.io/gh/randombit/botan/pull/3081?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) | Coverage Δ | |
|---|---|---|
| [src/lib/tls/tls\_session.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzX3Nlc3Npb24uY3Bw) | `89.43% <0.00%> (-7.07%)` | :arrow_down: |
| [src/lib/tls/tls\_client.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzX2NsaWVudC5jcHA=) | `86.56% <42.85%> (-5.10%)` | :arrow_down: |
| [src/bogo\_shim/bogo\_shim.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2JvZ29fc2hpbS9ib2dvX3NoaW0uY3Bw) | `87.88% <66.66%> (-0.11%)` | :arrow_down: |
| [src/lib/tls/tls\_server.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzX3NlcnZlci5jcHA=) | `70.96% <74.28%> (+4.30%)` | :arrow_up: |
| [src/cli/tls\_client.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2NsaS90bHNfY2xpZW50LmNwcA==) | `75.00% <75.00%> (+1.01%)` | :arrow_up: |
| [src/lib/tls/tls13/msg\_certificate\_req\_13.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzMTMvbXNnX2NlcnRpZmljYXRlX3JlcV8xMy5jcHA=) | `83.33% <82.60%> (+3.33%)` | :arrow_up: |
| [src/lib/tls/tls13/tls\_extensions\_key\_share.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzMTMvdGxzX2V4dGVuc2lvbnNfa2V5X3NoYXJlLmNwcA==) | `88.16% <83.60%> (+1.11%)` | :arrow_up: |
| [src/lib/tls/tls13/tls\_server\_impl\_13.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzMTMvdGxzX3NlcnZlcl9pbXBsXzEzLmNwcA==) | `85.71% <85.71%> (ø)` | |
| [src/lib/tls/msg\_cert\_verify.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvbXNnX2NlcnRfdmVyaWZ5LmNwcA==) | `94.84% <87.50%> (+0.28%)` | :arrow_up: |
| [src/lib/tls/tls13/msg\_encrypted\_extensions.cpp](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzMTMvbXNnX2VuY3J5cHRlZF9leHRlbnNpb25zLmNwcA==) | `93.10% <88.23%> (-6.90%)` | :arrow_down: |
| ... and [50 more](https://codecov.io/gh/randombit/botan/pull/3081/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/randombit/botan/pull/3081?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd).


Patch:
diff --git a/src/bogo_shim/bogo_shim.cpp b/src/bogo_shim/bogo_shim.cpp
index 6e9d7dd76..8affb62d0 100644
--- a/src/bogo_shim/bogo_shim.cpp
+++ b/src/bogo_shim/bogo_shim.cpp
@@ -131,6 +131,7 @@ std::string map_to_bogo_error(const std::string& e)
          { "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 agree on any 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:" },
diff --git a/src/lib/tls/msg_cert_verify.cpp b/src/lib/tls/msg_cert_verify.cpp
index dd00be4dd..b808beb2e 100644
--- a/src/lib/tls/msg_cert_verify.cpp
+++ b/src/lib/tls/msg_cert_verify.cpp
@@ -17,6 +17,7 @@
 #include <botan/pk_keys.h>
 #include <botan/tls_algos.h>
 #include <botan/tls_extensions.h>
+#include <botan/credentials_manager.h>
 
 namespace Botan::TLS {
 
@@ -150,21 +151,32 @@ Signature_Scheme choose_signature_scheme(
 * Create a new Certificate Verify message for TLS 1.3
 */
 Certificate_Verify_13::Certificate_Verify_13(
+      const Certificate_13& certificate_msg,
       const std::vector<Signature_Scheme>& peer_allowed_schemes,
+      const std::string& hostname,
+      const Transcript_Hash& hash,
       Connection_Side whoami,
-      const Private_Key& key,
+      Credentials_Manager& creds_mgr,
       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(!certificate_msg.empty());
+
+   const auto* private_key = creds_mgr.private_key_for(
+                                       certificate_msg.leaf(),
+                                       m_side == Connection_Side::CLIENT ? "tls-client" : "tls-server",
+                                       hostname);
+   if(!private_key)
+      { throw TLS_Exception(Alert::INTERNAL_ERROR, "Application did not provide a private key for its certificate"); }
+
+   m_scheme = choose_signature_scheme(*private_key, policy.allowed_signature_schemes(), peer_allowed_schemes);
    BOTAN_ASSERT_NOMSG(m_scheme.is_available());
    BOTAN_ASSERT_NOMSG(m_scheme.is_compatible_with(Protocol_Version::TLS_V13));
 
    m_signature =
-      callbacks.tls_sign_message(key,
+      callbacks.tls_sign_message(*private_key,
                                  rng,
                                  m_scheme.padding_string(),
                                  m_scheme.format().value(),
diff --git a/src/lib/tls/msg_client_hello.cpp b/src/lib/tls/msg_client_hello.cpp
index 802d0f489..3979b83f8 100644
--- a/src/lib/tls/msg_client_hello.cpp
+++ b/src/lib/tls/msg_client_hello.cpp
@@ -975,80 +975,6 @@ void Client_Hello_13::calculate_psk_binders(Transcript_Hash_State ths)
    psk->calculate_binders(ths);
    }
 
-std::vector<X509_Certificate> Client_Hello_13::find_certificate_chain(Credentials_Manager& creds) const
-   {
-   const auto& exts = extensions();
-
-   // RFC 8446 4.2.3
-   //    Clients which desire the server to authenticate itself via a
-   //    certificate MUST send the "signature_algorithms" extension.
-   //    [Otherwise] the server MUST abort the handshake with a
-   //    "missing_extension" alert.
-   if(!exts.has<Signature_Algorithms>())
-      {
-      throw TLS_Exception(Alert::MISSING_EXTENSION,
-                          "Missing certificate signature preferences, server cannot authenticate");
-      }
-
-   // RFC 8446 4.2.3
-   //    If no "signature_algorithms_cert" extension is present, then the
-   //    "signature_algorithms" extension also applies to signatures
-   //    appearing in certificates.
-   const auto& sig_algo_preference = (exts.has<Signature_Algorithms_Cert>())
-      ? exts.get<Signature_Algorithms_Cert>()->supported_schemes()
-      : exts.get<Signature_Algorithms>()->supported_schemes();
-
-   const std::string hostname = sni_hostname();
-
-   std::vector<Signature_Scheme> compatible_signature_schemes;
-   std::copy_if(sig_algo_preference.begin(), sig_algo_preference.end(),
-                std::back_inserter(compatible_signature_schemes),
-                [](const auto& scheme)
-                   {
-                   return scheme.is_available() &&
-                          scheme.is_compatible_with(Protocol_Version::TLS_V13);
-                   });
-
-   if(compatible_signature_schemes.empty())
-      {
-      throw TLS_Exception(Alert::HANDSHAKE_FAILURE,
-                          "Failed to agree on a signature algorithm");
-      }
-
-   // RFC 8446 4.2.3
-   //    Each SignatureScheme value lists a single signature algorithm that
-   //    the client is willing to verify.  The values are indicated in
-   //    descending order of preference.
-   for (const auto& scheme : compatible_signature_schemes)
-      {
-      // TODO: To fully support "signature_algorithm_cert", this would need to
-      //       receive two algorithm names. One for the signature algorithm used
-      //       to sign the certificate and one for the public key contained in
-      //       the certificate. Both must be supported to comply with the
-      //       client's asymmetric key requirements.
-      // Note: This requires a change in the public API of CredentialsManager.
-      //
-      // see: https://github.com/randombit/botan/issues/2714#issuecomment-1057175631
-      // see: RFC 8446 4.4.2.2
-      auto chain = creds.cert_chain_single_type(scheme.algorithm_name(), "tls-server", hostname);
-      if(!chain.empty())
-         { return chain; }
-      }
-
-   // RFC 8446 4.4.2.2
-   //    If the server cannot produce a certificate chain that is signed only
-   //    via the indicated supported algorithms, then it SHOULD continue the
-   //    handshake by sending the client a certificate chain of its choice
-   //    that may include algorithms that are not known to be supported by the
-   //    client.
-   //
-   // We rely on the application to hand out a valid (but maybe incompatible)
-   // server certificate chain. Otherwise, we have to abort.
-   throw TLS_Exception(Alert::INTERNAL_ERROR,
-                       "Application did not a provide a server certificate chain");
-   }
-
-
 std::optional<Protocol_Version> Client_Hello_13::highest_supported_version(const Policy& policy) const
    {
    // RFC 8446 4.2.1
diff --git a/src/lib/tls/tls13/msg_certificate_13.cpp b/src/lib/tls/tls13/msg_certificate_13.cpp
index b2e6e3060..313c662b4 100644
--- a/src/lib/tls/tls13/msg_certificate_13.cpp
+++ b/src/lib/tls/tls13/msg_certificate_13.cpp
@@ -28,13 +28,42 @@ bool certificate_allows_signing(const X509_Certificate& cert)
    {
    const auto constraints = cert.constraints();
    if(constraints == NO_CONSTRAINTS)
-      return true;
+      { return true; }
 
    return constraints & DIGITAL_SIGNATURE || constraints & NON_REPUDIATION;
    }
 
+std::vector<std::string>
+filter_signature_schemes(const std::vector<Signature_Scheme>& peer_scheme_preference)
+   {
+   std::vector<std::string> compatible_schemes;
+   for(const auto& scheme : peer_scheme_preference)
+      {
+      if(scheme.is_available() &&
+         scheme.is_compatible_with(Protocol_Version::TLS_V13))
+         {
+         compatible_schemes.push_back(scheme.algorithm_name());
+         }
+      }
+
+   if(compatible_schemes.empty())
+      {
+      throw TLS_Exception(Alert::HANDSHAKE_FAILURE, "Failed to agree on any signature algorithm");
+      }
+
+   return compatible_schemes;
+   }
+
 }
 
+std::vector<X509_Certificate> Certificate_13::cert_chain() const
+   {
+   std::vector<X509_Certificate> result;
+   std::transform(m_entries.cbegin(), m_entries.cend(), std::back_inserter(result),
+                  [](const auto& cert_entry) { return cert_entry.certificate; });
+   return result;
+   }
+
 void Certificate_13::validate_extensions(const std::set<Handshake_Extension_Type>& requested_extensions, Callbacks& cb) const
    {
    // RFC 8446 4.4.2
@@ -63,14 +92,6 @@ void Certificate_13::verify(Callbacks& callbacks,
                             const std::string& hostname,
                             bool use_ocsp) const
    {
-   // RFC 8446 4.4.2.4
-   //    If the server supplies an empty Certificate message, the client
-   //    MUST abort the handshake with a "decode_error" alert.
-   if(m_entries.empty())
-      { throw TLS_Exception(Alert::DECODE_ERROR, "Client: No certificates sent by server"); }
-
-   auto trusted_CAs = creds.trusted_certificate_authorities("tls-client", hostname);
-
    std::vector<X509_Certificate> certs;
    std::vector<std::optional<OCSP::Response>> ocsp_responses;
    for(const auto& entry : m_entries)
@@ -97,28 +118,86 @@ void Certificate_13::verify(Callbacks& callbacks,
    if(!certificate_allows_signing(server_cert))
       {
       throw TLS_Exception(Alert::BAD_CERTIFICATE,
-         "Certificate usage constraints do not allow signing");
+                          "Certificate usage constraints do not allow signing");
       }
 
+   const auto trusted_CAs = creds.trusted_certificate_authorities(
+                                     m_side == Connection_Side::CLIENT ? "tls-client" : "tls-server",
+                                     hostname);
+
    const auto usage = (m_side == CLIENT) ? Usage_Type::TLS_CLIENT_AUTH : Usage_Type::TLS_SERVER_AUTH;
    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,
-                               Callbacks& callbacks)
-   : m_request_context(request_context)
-   , m_side(side)
+void Certificate_13::setup_entries(std::vector<X509_Certificate> cert_chain,
+                                   Callbacks& callbacks)
    {
-   for (const auto& c : certs)
+   for(size_t i = 0; i < cert_chain.size(); ++i)
       {
       auto exts = Extensions();
-      callbacks.tls_modify_extensions(exts, side, type());
-      m_entries.emplace_back(Certificate_Entry{c, std::move(exts)});
+      // This will call the modification callback multiple times. Once for
+      // each certificate in the `cert_chain`. Users that want to add an
+      // extension to a specific Certificate Entry might have a hard time
+      // to distinguish them.
+      // TODO: Callbacks::tls_modify_extensions() might need even more
+      //       context depending on the message whose extensions should be
+      //       manipulatable.
+      callbacks.tls_modify_extensions(exts, m_side, type());
+      auto& entry = m_entries.emplace_back();
+      entry.certificate = cert_chain[i];
       }
    }
 
+/**
+ * Create a Client Certificate message
+ */
+Certificate_13::Certificate_13(const Certificate_Request_13& cert_request,
+                               const std::string& hostname,
+                               Credentials_Manager& credentials_manager,
+                               Callbacks& callbacks) :
+   m_request_context(cert_request.context()),
+   m_side(Connection_Side::CLIENT)
+   {
+   // TODO: implement "signature_algorithms_cert"
+   //       -> see c'tor for server certificates
+   setup_entries(credentials_manager.find_cert_chain(
+                    filter_signature_schemes(cert_request.signature_schemes()),
+                    cert_request.acceptable_CAs(),
+                    "tls-client",
+                    hostname),
+                 callbacks);
+   }
+
+/**
+ * Create a Server Certificate message
+ */
+Certificate_13::Certificate_13(const Client_Hello_13& client_hello,
+                               Credentials_Manager& credentials_manager,
+                               Callbacks& callbacks) :
+   // RFC 8446 4.4.2:
+   //    [In the case of server authentication], this field
+   //    SHALL be zero length
+   m_request_context(),
+   m_side(Connection_Side::SERVER)
+   {
+   // TODO: implement "signature_algorithms_cert"
+   BOTAN_ASSERT_NOMSG(client_hello.extensions().has<Signature_Algorithms>());
+
+   // TODO: To fully support "signature_algorithm_cert", this would need to
+   //       receive two algorithm names. One for the signature algorithm used
+   //       to sign the certificate and one for the public key contained in
+   //       the certificate. Both must be supported to comply with the
+   //       client's asymmetric key requirements.
+   // Note: This requires a change in the public API of CredentialsManager.
+   //
+   // see: https://github.com/randombit/botan/issues/2714#issuecomment-1057175631
+   // see: RFC 8446 4.4.2.2
+   setup_entries(credentials_manager.find_cert_chain(
+                    filter_signature_schemes(client_hello.signature_schemes()),
+                    {}, "tls-server", client_hello.sni_hostname()),
+                 callbacks);
+   }
+
 /**
 * Deserialize a Certificate message
 */
@@ -200,9 +279,12 @@ Certificate_13::Certificate_13(const std::vector<uint8_t>& buf,
    //    authentication request.
    if(m_entries.empty())
       {
+      // RFC 8446 4.4.2.4
+      //    If the server supplies an empty Certificate message, the client MUST
+      //    abort the handshake with a "decode_error" alert.
       if(m_side == SERVER)
          {
-         throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "No certificates sent by server");
+         throw TLS_Exception(Alert::DECODE_ERROR, "No certificates sent by server");
          }
       }
    else
diff --git a/src/lib/tls/tls13/msg_certificate_req_13.cpp b/src/lib/tls/tls13/msg_certificate_req_13.cpp
index fc694f37e..c72979126 100644
--- a/src/lib/tls/tls13/msg_certificate_req_13.cpp
+++ b/src/lib/tls/tls13/msg_certificate_req_13.cpp
@@ -8,6 +8,8 @@
 #include <botan/tls_messages.h>
 #include <botan/internal/tls_reader.h>
 #include <botan/tls_exceptn.h>
+#include <botan/tls_callbacks.h>
+#include <botan/credentials_manager.h>
 
 namespace Botan::TLS
 {
@@ -68,6 +70,68 @@ Certificate_Request_13::Certificate_Request_13(const std::vector<uint8_t>& buf,
       }
    }
 
+Certificate_Request_13::Certificate_Request_13(std::vector<Signature_Scheme> signature_schemes,
+                                               std::vector<X509_DN> acceptable_CAs,
+                                               Callbacks&)
+   {
+   // RFC 8446 4.3.2
+   //    The certificate_request_context [here: m_context] MUST be unique within
+   //    the scope of this connection (thus preventing replay of client
+   //    CertificateVerify messages).  This field SHALL be zero length unless
+   //    used for the post-handshake authentication exchanges described in
+   //    Section 4.6.2.
+   //
+   // TODO: Post-Handshake auth must fill m_context in an unpredictable way
+
+   // RFC 8446 4.3.2
+   //    [Supported signature algorithms are] expressed by sending the
+   //    "signature_algorithms" and optionally "signature_algorithms_cert"
+   //    extensions. [A list of certificate authorities which the server would
+   //    accept] is expressed by sending the "certificate_authorities" extension.
+   //
+   //    The "signature_algorithms" extension MUST be specified, and other
+   //    extensions may optionally be included if defined for this message.
+   //
+   // TODO: fully support 'signature_algorithms_cert'
+   m_extensions.add(std::make_unique<Signature_Algorithms>(std::move(signature_schemes)));
+
+   if(!acceptable_CAs.empty())
+      {
+      m_extensions.add(std::make_unique<Certificate_Authorities>(std::move(acceptable_CAs)));
+      }
+
+   // TODO: Support cert_status_request for OCSP stapling
+
+   // TODO: give the application a chance to modifying extensions
+   //       (after GH #2988 is merged)
+   // callbacks.tls_modify_extensions(m_extensions, Connection_Side::SERVER);
+   }
+
+std::optional<Certificate_Request_13>
+Certificate_Request_13::maybe_create(const Client_Hello_13& client_hello,
+                                     Credentials_Manager& cred_mgr,
+                                     Callbacks& callbacks,
+                                     const Policy& policy)
+   {
+   const auto trusted_CAs = cred_mgr.trusted_certificate_authorities("tls-server", client_hello.sni_hostname());
+
+   std::vector<X509_DN> client_auth_CAs;
+   for(const auto store : trusted_CAs)
+      {
+      const auto subjects = store->all_subjects();
+      client_auth_CAs.insert(client_auth_CAs.end(), subjects.begin(), subjects.end());
+      }
+
+   if(client_auth_CAs.empty() && !policy.request_client_certificate_authentication())
+      {
+      return std::nullopt;
+      }
+
+   return Certificate_Request_13(policy.acceptable_signature_schemes(),
+                                 std::move(client_auth_CAs),
+                                 callbacks);
+   }
+
 std::vector<X509_DN> Certificate_Request_13::acceptable_CAs() const
    {
    if(m_extensions.has<Certificate_Authorities>())
@@ -86,7 +150,10 @@ const std::vector<Signature_Scheme>& Certificate_Request_13::signature_schemes()
 
 std::vector<uint8_t> Certificate_Request_13::serialize() const
    {
-   throw Not_Implemented("Certificate_Request_13::serialize");
+   std::vector<uint8_t> buf;
+   append_tls_length_value(buf, m_context, 1);
+   buf += m_extensions.serialize(Connection_Side::SERVER);
+   return buf;
    }
 
 }  // namespace Botan::TLS
diff --git a/src/lib/tls/tls13/tls_cipher_state.cpp b/src/lib/tls/tls13/tls_cipher_state.cpp
index 86541afbd..733c773ed 100644
--- a/src/lib/tls/tls13/tls_cipher_state.cpp
+++ b/src/lib/tls/tls13/tls_cipher_state.cpp
@@ -31,7 +31,7 @@
  *                                     |
  *                                     *
  *                             STATE EARLY TRAFFIC
- * This state is reached by constructing Cipher_State using init_with_psk().
+ * This state is reached by calling advance_with_client_hello().
  * In this state the early data traffic secrets are available. TODO: implement early data.
  * The state can then be further advanced using advance_with_server_hello().
  *                                     *
@@ -279,6 +279,31 @@ size_t Cipher_State::minimum_decryption_input_length() const
    return m_decrypt->minimum_final_size();
    }
 
+bool Cipher_State::must_expect_unprotected_alert_traffic() const
+   {
+   // Client side:
+   //   After successfully receiving a Server Hello we expect servers to send
+   //   alerts as protected records only, just like they start protecting their
+   //   handshake data at this point.
+   if(m_connection_side == CLIENT && m_state == State::EarlyTraffic)
+      { return true; }
+
+   // Server side:
+   //   Servers must expect clients to send unprotected alerts during the hand-
+   //   shake. In particular, in the response to the server's first protected
+   //   flight. We don't expect the client to send alerts protected under the
+   //   early traffic secret.
+   //
+   // TODO: when implementing PSK and/or early data for the server, we might
+   //       need to reconsider this decision.
+   if(m_connection_side == SERVER &&
+         (m_state == State::HandshakeTraffic ||
+          m_state == State::ServerApplicationTraffic))
+      { return true; }
+
+   return false;
+   }
+
 bool Cipher_State::can_encrypt_application_traffic() const
    {
    // TODO: when implementing early traffic (0-RTT) this will likely need
diff --git a/src/lib/tls/tls13/tls_cipher_state.h b/src/lib/tls/tls13/tls_cipher_state.h
index 58857467d..1b84e48a8 100644
--- a/src/lib/tls/tls13/tls_cipher_state.h
+++ b/src/lib/tls/tls13/tls_cipher_state.h
@@ -199,6 +199,11 @@ class BOTAN_TEST_API Cipher_State
                 !m_exporter_master_secret.empty();
          }
 
+      /**
+       * Indicates whether unprotected Alert records are to be expected
+       */
+      bool must_expect_unprotected_alert_traffic() const;
+
       /**
        * Indicates whether the appropriate secrets to encrypt application traffic are available
        */
diff --git a/src/lib/tls/tls13/tls_client_impl_13.cpp b/src/lib/tls/tls13/tls_client_impl_13.cpp
index 170a1e355..5a3dbbdab 100644
--- a/src/lib/tls/tls13/tls_client_impl_13.cpp
+++ b/src/lib/tls/tls13/tls_client_impl_13.cpp
@@ -455,6 +455,9 @@ void Client_Impl_13::handle(const Certificate_13& certificate_msg)
       throw TLS_Exception(Alert::DECODE_ERROR, "Received a server certificate message with non-empty request context");
       }
 
+   // RFC 8446 4.4.2
+   //    Extensions in the Certificate message from the server MUST correspond
+   //    to ones from the ClientHello message.
    certificate_msg.validate_extensions(m_handshake_state.client_hello().extensions().extension_types(), callbacks());
    certificate_msg.verify(callbacks(),
                           policy(),
@@ -500,39 +503,17 @@ void Client_Impl_13::send_client_authentication(Channel_Impl_13::AggregatedMessa
    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(), callbacks())));
+                 Certificate_13(
+                    cert_request,
+                    m_info.hostname(),
+                    credentials_manager(),
+                    callbacks())
+              ));
 
    // RFC 4.4.2
    //    If the server requests client authentication but no suitable certificate
@@ -540,21 +521,16 @@ void Client_Impl_13::send_client_authentication(Channel_Impl_13::AggregatedMessa
    //    certificates.
    //
    // In that case, no Certificate Verify message will be sent.
-   if(!m_handshake_state.client_certificate().cert_chain().empty())
+   if(!m_handshake_state.client_certificate().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(
+         m_handshake_state.client_certificate(),
          cert_request.signature_schemes(),
+         m_info.hostname(),
+         m_transcript_hash.current(),
          Connection_Side::CLIENT,
-         *private_key,
+         credentials_manager(),
          policy(),
-         m_transcript_hash.current(),
          callbacks(),
          rng()
       )));
@@ -647,20 +623,9 @@ void TLS::Client_Impl_13::handle(const Key_Update& key_update)
 
 std::vector<X509_Certificate> Client_Impl_13::peer_cert_chain() const
    {
-   std::vector<X509_Certificate> result;
-
-   if(m_handshake_state.has_server_certificate_chain())
-      {
-      const auto& cert_chain = m_handshake_state.server_certificate().cert_chain();
-      std::transform(cert_chain.cbegin(), cert_chain.cend(), std::back_inserter(result),
-                     [](const auto& cert_entry) { return cert_entry.certificate; });
-      }
-   else if(m_resumed_session.has_value())
-      {
-      result = m_resumed_session->peer_certs();
-      }
-
-   return result;
+   return (m_handshake_state.has_server_certificate_chain())
+      ? m_handshake_state.server_certificate().cert_chain()
+      : m_resumed_session->peer_certs();
    }
 
 bool Client_Impl_13::prepend_ccs()
diff --git a/src/lib/tls/tls13/tls_handshake_state_13.h b/src/lib/tls/tls13/tls_handshake_state_13.h
index f442b4601..5a67927f7 100644
--- a/src/lib/tls/tls13/tls_handshake_state_13.h
+++ b/src/lib/tls/tls13/tls_handshake_state_13.h
@@ -27,6 +27,7 @@ class BOTAN_TEST_API Handshake_State_13_Base
    public:
       bool has_client_hello() const { return m_client_hello.has_value(); }
       bool has_server_certificate_chain() const { return m_server_certificate.has_value(); }
+      bool has_client_certificate_chain() const { return m_client_certificate.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(); }
diff --git a/src/lib/tls/tls13/tls_record_layer_13.cpp b/src/lib/tls/tls13/tls_record_layer_13.cpp
index da443377e..3eafcd95f 100644
--- a/src/lib/tls/tls13/tls_record_layer_13.cpp
+++ b/src/lib/tls/tls13/tls_record_layer_13.cpp
@@ -311,7 +311,8 @@ Record_Layer::ReadResult<Record> Record_Layer::next_record(Cipher_State* cipher_
    //    An implementation may receive an unencrypted [CCS] at any time
    if(cipher_state != nullptr
          && plaintext_header.type != APPLICATION_DATA
-         && plaintext_header.type != CHANGE_CIPHER_SPEC)
+         && plaintext_header.type != CHANGE_CIPHER_SPEC
+         && (!cipher_state->must_expect_unprotected_alert_traffic() || plaintext_header.type != ALERT))
       {
       throw TLS_Exception(Alert::UNEXPECTED_MESSAGE,
             "unprotected record received where protected traffic was expected");
diff --git a/src/lib/tls/tls13/tls_server_impl_13.cpp b/src/lib/tls/tls13/tls_server_impl_13.cpp
index ea88a7e40..cbcd2c792 100644
--- a/src/lib/tls/tls13/tls_server_impl_13.cpp
+++ b/src/lib/tls/tls13/tls_server_impl_13.cpp
@@ -39,9 +39,11 @@ std::string Server_Impl_13::application_protocol() const
 
 std::vector<X509_Certificate> Server_Impl_13::peer_cert_chain() const
    {
-   return {}; // TODO: implement!
-              //       Should return the client authentication certificate chain
-              //       once client authentication for the server side is ready.
+   return (m_handshake_state.has_client_certificate_chain())
+      ? m_handshake_state.client_certificate().cert_chain()
+      // TODO: after implementing session resumption, this may return the
+      //       client's certificate chain stored in the previous session.
+      : std::vector<X509_Certificate>{};
    }
 
 void Server_Impl_13::process_handshake_msg(Handshake_Message_13 message)
@@ -116,31 +118,56 @@ void Server_Impl_13::handle_reply_to_client_hello(const Server_Hello_13& server_
    m_cipher_state = Cipher_State::init_with_server_hello(m_side, std::move(shared_secret), cipher.value(),
                     m_transcript_hash.current());
 
-   // TODO: OCSP stapling: Invoke Callbacks::tls_provide_cert_status() to obtain an OCSP response
-   auto server_cert_chain = client_hello.find_certificate_chain(credentials_manager());
-   BOTAN_ASSERT_NOMSG(!server_cert_chain.empty());
-
-   auto private_key = credentials_manager().private_key_for(server_cert_chain.front(), "tls-server",
-                      client_hello.sni_hostname());
-   BOTAN_ASSERT_NONNULL(private_key);
-
    // TODO: ALPN - Invoke Callbacks::tls_server_choose_app_protocol() with
    //       suggestions sent by the client. This might happen in the Encrypted
    //       Extensions constructor. Also implement Channel::application_protocol().
 
-   aggregate_handshake_messages()
-   .add(m_handshake_state.sending(Encrypted_Extensions(client_hello, policy(), callbacks())))
-   .add(m_handshake_state.sending(Certificate_13(server_cert_chain, Connection_Side::SERVER, {}, callbacks())))
-   .add(m_handshake_state.sending(Certificate_Verify_13(client_hello.signature_schemes(), Connection_Side::SERVER,
-                                  *private_key, policy(), m_transcript_hash.current(), callbacks(), rng())))
-   .add(m_handshake_state.sending(Finished_13(m_cipher_state.get(), m_transcript_hash.current())))
-   .send();
+   auto flight = aggregate_handshake_messages();
+   flight
+      .add(m_handshake_state.sending(Encrypted_Extensions(client_hello, policy(), callbacks())));
 
-   m_cipher_state->advance_with_server_finished(m_transcript_hash.current());
+   // RFC 8446 4.3.2
+   //    A server which is authenticating with a certificate MAY optionally
+   //    request a certificate from the client. This message, if sent, MUST
+   //    follow EncryptedExtensions.
+   //
+   // Note: When implementing PSK, this message must not be sent
+   if(auto certificate_request = Certificate_Request_13::maybe_create(client_hello,
+                                                                      credentials_manager(),
+                                                                      callbacks(),
+                                                                      policy()))
+      {
+      flight.add(m_handshake_state.sending(std::move(certificate_request.value())));
+
+      // 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 the server requests client authentication but no
+      //    suitable certificate is available, the client MUST send a Certificate
+      //    message containing no certificates [...].
+      m_transitions.set_expected_next(CERTIFICATE);
+      }
+   else
+      {
+      m_transitions.set_expected_next(FINISHED);
+      }
 
-   // TODO: For Client Authentication this should expect appropriate client handshake messages
-   //       once we support/implement it.
-   m_transitions.set_expected_next(FINISHED);
+   flight
+      .add(m_handshake_state.sending(Certificate_13(client_hello, credentials_manager(), callbacks())))
+      .add(m_handshake_state.sending(Certificate_Verify_13(
+                                        m_handshake_state.server_certificate(),
+                                        client_hello.signature_schemes(),
+                                        client_hello.sni_hostname(),
+                                        m_transcript_hash.current(),
+                                        Connection_Side::SERVER,
+                                        credentials_manager(),
+                                        policy(),
+                                        callbacks(),
+                                        rng())))
+      .add(m_handshake_state.sending(Finished_13(m_cipher_state.get(), m_transcript_hash.current())))
+      .send();
+
+   m_cipher_state->advance_with_server_finished(m_transcript_hash.current());
    }
 
 void Server_Impl_13::handle_reply_to_client_hello(const Hello_Retry_Request& hello_retry_request)
@@ -263,14 +290,92 @@ void Server_Impl_13::handle(const Client_Hello_13& client_hello)
 
 void Server_Impl_13::handle(const Certificate_13& certificate_msg)
    {
-   BOTAN_UNUSED(certificate_msg);
-   throw Not_Implemented("Client Auth is currently not supported by the server");
+   // RFC 8446 4.3.2
+   //    certificate_request_context:  [...] This field SHALL be zero length
+   //    unless used for the post-handshake authentication exchanges [...].
+   if(!handshake_finished() && !certificate_msg.request_context().empty())
+      {
+      throw TLS_Exception(Alert::DECODE_ERROR, "Received a client certificate message with non-empty request context");
+      }
+
+   // RFC 8446 4.4.2
+   //    Extensions in the Certificate message from the client MUST correspond
+   //    to extensions in the CertificateRequest message from the server.
+   certificate_msg.validate_extensions(m_handshake_state.certificate_request().extensions().extension_types(), callbacks());
+
+   // RFC 8446 4.4.2.4
+   //   If the client does not send any certificates (i.e., it sends an empty
+   //   Certificate message), the server MAY at its discretion either continue
+   //   the handshake without client authentication or abort the handshake with
+   //   a "certificate_required" alert.
+   if(certificate_msg.empty())
+      {
+      if(policy().require_client_certificate_authentication())
+         {
+         throw TLS_Exception(Alert::CERTIFICATE_REQUIRED, "Policy requires client send a certificate, but it did not");
+         }
+
+      // RFC 8446 4.4.2
+      //    A Finished message MUST be sent regardless of whether the
+      //    Certificate message is empty.
+      m_transitions.set_expected_next(FINISHED);
+      }
+   else
+      {
+      // RFC 8446 4.4.2.4
+      //    [...], if some aspect of the certificate chain was unacceptable
+      //    (e.g., it was not signed by a known, trusted CA), the server MAY at
+      //    its discretion either continue the handshake (considering the client
+      //    unauthenticated) or abort the handshake.
+      //
+      // TODO: We could make this dependent on Policy::require_client_auth().
+      //       Though, apps may also override Callbacks::tls_verify_cert_chain()
+      //       and 'ignore' validation issues to a certain extent.
+      certificate_msg.verify(callbacks(),
+                             policy(),
+                             credentials_manager(),
+                             m_handshake_state.client_hello().sni_hostname(),
+                             m_handshake_state.client_hello().extensions().has<Certificate_Status_Request>());
+
+      // RFC 8446 4.4.3
+      //    Clients MUST send this message whenever authenticating via a
+      //    certificate (i.e., when the Certificate message
+      //    is non-empty). When sent, this message MUST appear immediately after
+      //    the Certificate message [...].
+      m_transitions.set_expected_next(CERTIFICATE_VERIFY);
+      }
    }
 
 void Server_Impl_13::handle(const Certificate_Verify_13& certificate_verify_msg)
    {
-   BOTAN_UNUSED(certificate_verify_msg);
-   throw Not_Implemented("Client Auth is currently not supported by the server");
+   // RFC 8446 4.4.3
+   //    If sent by a client, the signature algorithm used in the signature
+   //    MUST be one of those present in the supported_signature_algorithms
+   //    field of the "signature_algorithms" extension in the
+   //    CertificateRequest message.
+   const auto offered = m_handshake_state.certificate_request().signature_schemes();
+   if(!value_exists(offered, certificate_verify_msg.signature_scheme()))
+      {
+      throw TLS_Exception(Alert::ILLEGAL_PARAMETER,
+                          "We did not offer the usage of " +
+                          certificate_verify_msg.signature_scheme().to_string() +
+                          " as a signature scheme");
+      }
+
+   BOTAN_ASSERT_NOMSG(m_handshake_state.has_client_certificate_chain() &&
+                      !m_handshake_state.client_certificate().empty());
+   bool sig_valid = certificate_verify_msg.verify(
+                       m_handshake_state.client_certificate().leaf(),
+                       callbacks(),
+                       m_transcript_hash.previous());
+
+   // RFC 8446 4.4.3
+   //   If the verification fails, the receiver MUST terminate the handshake
+   //   with a "decrypt_error" alert.
+   if(!sig_valid)
+      { throw TLS_Exception(Alert::DECRYPT_ERROR, "Client certificate verification failed"); }
+
+   m_transitions.set_expected_next(FINISHED);
    }
 
 void Server_Impl_13::handle(const Finished_13& finished_msg)
diff --git a/src/lib/tls/tls_extensions.cpp b/src/lib/tls/tls_extensions.cpp
index 852a85913..b0b7696cc 100644
--- a/src/lib/tls/tls_extensions.cpp
+++ b/src/lib/tls/tls_extensions.cpp
@@ -877,6 +877,9 @@ Certificate_Authorities::Certificate_Authorities(TLS_Data_Reader& reader, uint16
       }
    }
 
+Certificate_Authorities::Certificate_Authorities(std::vector<X509_DN> acceptable_DNs)
+   : m_distinguished_names(std::move(acceptable_DNs)) {}
+
 
 std::vector<uint8_t> EarlyDataIndication::serialize(Connection_Side) const
    {
diff --git a/src/lib/tls/tls_extensions.h b/src/lib/tls/tls_extensions.h
index 33d7031cb..fb734d879 100644
--- a/src/lib/tls/tls_extensions.h
+++ b/src/lib/tls/tls_extensions.h
@@ -25,6 +25,7 @@
 #include <vector>
 #include <string>
 #include <set>
+#include <memory>
 
 namespace Botan {
 
@@ -624,7 +625,8 @@ class BOTAN_UNSTABLE_API Certificate_Authorities final : public Extension
       const std::vector<X509_DN>& distinguished_names() const
          { return m_distinguished_names; }
 
-      explicit Certificate_Authorities(TLS_Data_Reader& reader, uint16_t extension_size);
+      Certificate_Authorities(TLS_Data_Reader& reader, uint16_t extension_size);
+      explicit Certificate_Authorities(std::vector<X509_DN> acceptable_DNs);
 
    private:
       std::vector<X509_DN> m_distinguished_names;
diff --git a/src/lib/tls/tls_messages.h b/src/lib/tls/tls_messages.h
index 072067ea0..5c412ca6d 100644
--- a/src/lib/tls/tls_messages.h
+++ b/src/lib/tls/tls_messages.h
@@ -230,18 +230,6 @@ class BOTAN_UNSTABLE_API Client_Hello_13 final : public Client_Hello
                  Callbacks& cb,
                  RandomNumberGenerator& rng);
 
-      /**
-       * Select an available certificate chain that is compatible with the
-       * signature algorithm and hostname requirements advertised by the
-       * client.
-       *
-       * This method throws an exception if @p cerds cannot produce a usable
-       * server certificate chain.
-       *
-       * @returns a server certificate chain
-       */
-      std::vector<X509_Certificate> find_certificate_chain(Credentials_Manager& creds) const;
-
       /**
        * Select the highest protocol version from the list of versions
        * supported by the client. If no such version can be determind this
@@ -529,6 +517,8 @@ class BOTAN_UNSTABLE_API Certificate_12 final : public Handshake_Message
       std::vector<X509_Certificate> m_certs;
    };
 
+class Certificate_Request_13;
+
 /**
 * Certificate Message of TLS 1.3
 */
@@ -546,7 +536,7 @@ class BOTAN_UNSTABLE_API Certificate_13 final : public Handshake_Message
 
    public:
       Handshake_Type type() const override { return CERTIFICATE; }
-      const std::vector<Certificate_Entry>& cert_chain() const { return m_entries; }
+      std::vector<X509_Certificate> cert_chain() const;
 
       size_t count() const { return m_entries.size(); }
       bool empty() const { return m_entries.empty(); }
@@ -554,17 +544,21 @@ class BOTAN_UNSTABLE_API Certificate_13 final : public Handshake_Message
       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 side             the connection side which constructs this message
-       * @param request_context  the context provided in the Certificate Request
-       *                         message when performing post-handshake client auth
-       * @param callbacks        the "callbacks" handle to use (for tls_modify_extensions)
+       * Create a Client Certificate message
+       * ... in response to a Certificate Request message.
+       */
+      Certificate_13(const Certificate_Request_13& cert_request,
+                     const std::string& hostname,
+                     Credentials_Manager& credentials_manager,
+                     Callbacks& callbacks);
+
+      /**
+       * Create a Server Certificate message
+       * ... in response to a Client Hello indicating the need to authenticate
+       *     with a server certificate.
        */
-      Certificate_13(const std::vector<X509_Certificate>& certs,
-                     const Connection_Side side,
-                     const std::vector<uint8_t>& request_context,
+      Certificate_13(const Client_Hello_13& client_hello,
+                     Credentials_Manager& credentials_manager,
                      Callbacks& callbacks);
 
       /**
@@ -599,6 +593,10 @@ class BOTAN_UNSTABLE_API Certificate_13 final : public Handshake_Message
 
       std::vector<uint8_t> serialize() const override;
 
+   private:
+      void setup_entries(std::vector<X509_Certificate> cert_chain,
+                         Callbacks& callbacks);
+
    private:
       std::vector<uint8_t>           m_request_context;
       std::vector<Certificate_Entry> m_entries;
@@ -673,6 +671,13 @@ class BOTAN_UNSTABLE_API Certificate_Request_13 final : public Handshake_Message
 
       Certificate_Request_13(const std::vector<uint8_t>& buf, const Connection_Side side);
 
+      //! Creates a Certificate_Request message if it is required by the configuration
+      //! @return std::nullopt if configuration does not require client authentication
+      static std::optional<Certificate_Request_13> maybe_create(const Client_Hello_13& sni_hostname,
+                                                                Credentials_Manager& cred_mgr,
+                                                                Callbacks& callbacks,
+                                                                const Policy& policy);
+
       std::vector<X509_DN> acceptable_CAs() const;
       const std::vector<Signature_Scheme>& signature_schemes() const;
       const Extensions& extensions() const { return m_extensions; }
@@ -681,6 +686,11 @@ class BOTAN_UNSTABLE_API Certificate_Request_13 final : public Handshake_Message
 
       const std::vector<uint8_t> context() const { return m_context; }
 
+   private:
+      Certificate_Request_13(std::vector<Signature_Scheme> signature_schemes,
+                             std::vector<X509_DN> acceptable_CAs,
+                             Callbacks& callbacks);
+
    private:
       std::vector<uint8_t> m_context;
       Extensions m_extensions;
@@ -747,11 +757,13 @@ class BOTAN_UNSTABLE_API Certificate_Verify_13 final : public Certificate_Verify
                             const Connection_Side side);
 
       Certificate_Verify_13(
+            const Certificate_13& certificate_message,
             const std::vector<Signature_Scheme>& peer_allowed_schemes,
+            const std::string& hostname,
+            const Transcript_Hash& hash,
             Connection_Side whoami,
-            const Private_Key& key,
+            Credentials_Manager& creds_mgr,
             const Policy& policy,
-            const Transcript_Hash& hash,
             Callbacks& callbacks,
             RandomNumberGenerator& rng);
 


Test patch:
diff --git a/src/tests/data/tls-policy/rfc8448_client_auth_server.txt b/src/tests/data/tls-policy/rfc8448_client_auth_server.txt
new file mode 100644
index 000000000..75bd4b2ff
--- /dev/null
+++ b/src/tests/data/tls-policy/rfc8448_client_auth_server.txt
@@ -0,0 +1,28 @@
+allow_tls10 = false
+allow_tls11 = false
+allow_tls12 = false
+allow_tls13 = true
+allow_dtls10 = false
+allow_dtls12 = false
+ciphers = AES-128/GCM ChaCha20Poly1305 AES-256/GCM
+macs = AEAD
+signature_hashes = SHA-512 SHA-384 SHA-256
+signature_methods = ECDSA RSA
+key_exchange_groups = x25519 secp256r1 secp384r1 secp521r1 ffdhe/ietf/2048 ffdhe/ietf/3072 ffdhe/ietf/4096 ffdhe/ietf/6144 ffdhe/ietf/8192
+key_exchange_groups_to_offer = x25519
+allow_insecure_renegotiation = false
+include_time_in_hello_random = false
+allow_server_initiated_renegotiation = false
+hide_unknown_users = false
+server_uses_own_ciphersuite_preferences = true
+negotiate_encrypt_then_mac = true
+session_ticket_lifetime = 86400
+minimum_dh_group_size = 2048
+minimum_ecdh_group_size = 255
+minimum_rsa_bits = 1024
+minimum_signature_strength = 110
+record_size_limit = 16385
+tls_13_middlebox_compatibility_mode = false
+hash_hello_random = false
+support_cert_status_message = false
+require_client_certificate_authentication = true
diff --git a/src/tests/data/tls_13_rfc8448/server_certificate_client_auth.pem b/src/tests/data/tls_13_rfc8448/server_certificate_client_auth.pem
new file mode 100644
index 000000000..3643fd2fd
--- /dev/null
+++ b/src/tests/data/tls_13_rfc8448/server_certificate_client_auth.pem
@@ -0,0 +1,8 @@
+-----BEGIN CERTIFICATE-----
+MIIBLjCB1aADAgECAgEHMAoGCCqGSM49BAMCMBMxETAPBgNVBAMTCGVjZHNhMjU2MB4XDTE
+2MDczMDAxMjQwMFoXDTI2MDczMDAxMjQwMFowEzERMA8GA1UEAxMIZWNkc2EyNTYwWTATBg
+cqhkjOPQIBBggqhkjOPQMBBwNCAAQI1TAWFXX0z+fxVO40SBgAhgAeiEMaee5i7m4vg+84u
+mHp+zfzTgB6ffTS9bVtHwTs5F1iH0aEBvXDoVFYlI3QoxowGDAJBgNVHRMEAjAAMAsGA1Ud
+DwQEAwIHgDAKBggqhkjOPQQDAgNIADBFAiEA3zD9RQf17dIsGm/4bbR5ymk/7so7cbP571V
+rKTfAWU0CIGLipHJQ0yD+qDx+LctbdqUOAgDAmtvRP+6UblE+AR0R
+-----END CERTIFICATE-----
diff --git a/src/tests/data/tls_13_rfc8448/transcripts.vec b/src/tests/data/tls_13_rfc8448/transcripts.vec
index 0d339b2be..8b1e2d301 100644
--- a/src/tests/data/tls_13_rfc8448/transcripts.vec
+++ b/src/tests/data/tls_13_rfc8448/transcripts.vec
@@ -66,11 +66,19 @@ Record_Server_CloseNotify = 1703030013519fc5075cb0884349759ff9ef6f011bb4c6f2
 
 [Client_Authentication_Handshake]
 Client_RNG_Pool = 6a472236328b83af40386d3a3e1f1ce624fa4ed89ab865a4ff0f4144ce3ae233c040b2bb8f3addd20fd4058c547003a3c6f9c1cd915d5e535c87d8d191aaf071
-Server_RNG_Pool = 3b50fdf1c3d572e40e68953e7fff4e2758459c59afa0582c0ea032874255fe6e
+Server_RNG_Pool = 3b50fdf1c3d572e40e68953e7fff4e2758459c59afa0582c0ea032874255fe6e7382a5ad1cdd2056ae18cc708bd007d98130dbe2cd4d9ead9b96952becbb0888
 CurrentTimestamp = 1651826546000
 Record_ClientHello_1 = 16030100c0010000bc03036a472236328b83af40386d3a3e1f1ce624fa4ed89ab865a4ff0f4144ce3ae2330000061301130313020100008d0000000b0009000006736572766572ff01000100000a00140012001d00170018001901000101010201030104003300260024001d0020089cc2671f738d9a671e5b2e464981d05b76e361aa22aea91f1d49ca10a7a362002b0003020304000d0020001e040305030603020308040805080604010501060102010402050206020202002d00020101001c00024001
+Server_MessageToSign = 20202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020544C5320312E332C2073657276657220436572746966696361746556657269667900859EAF14BD760766C74A398668367BC3A32687CE5A598B4383726AB82A911E26
+Server_MessageSignature = 3045022100d7a4d34bd54f55fee1a89625678c3dd5e5f60dac73ec940c5c7b9304a02084a90220289f595ed488b9ac689a3d192b1a8bb38f34af7874c059c9806a1f38269353e8
 Record_ServerHello = 160303005a0200005603033b50fdf1c3d572e40e68953e7fff4e2758459c59afa0582c0ea032874255fe6e00130100002e00330024001d00206c2e50e865919a6b5a12dfaf918f92b442567b0f89bc54478c6921366658f062002b00020304
+Message_ServerHello = 0200005603033b50fdf1c3d572e40e68953e7fff4e2758459c59afa0582c0ea032874255fe6e00130100002e00330024001d00206c2e50e865919a6b5a12dfaf918f92b442567b0f89bc54478c6921366658f062002b00020304
 Record_ServerHandshakeMessages = 17030302166d0a7ac079b32a94aa68c4e2893e8bd0d3c185f549c236fbbce3d647f08f3c94a2bf424d8708883605ad8955f97718b0213dead13dfb23ebb8381da582756612bcb5a5d40847719fbe9f179bfae656f3ecfd59a4c0d35132ce418a7e46f6b6a60622f8a6c06b28d83360163563be9c37f97eb902326924a72b3ed8c8381277d1581cab9c3715ac2401398467ad7ebfab3d0c3419e750104f7d62c5027901f2e4cd4ca5b8071eb03d3c732d83215066dfc4d291d4c1ff3b8d7e4298f677d4d51dea1168d8f16cb27ba40266313a1fedf9e23cc77f765450f9e96f05d08f3da245b14d4946f07ec81eed6d56f26bd574f0b7f7c7047037c16fce3b23754e662fad73e2b7213f6af296769c99a1d38e6232e0ec8dc4f84d6aa6f7de3887be0057862f9018e0ab396705aa4090ab5f2dff6325a557e7320d4effd46bb4f997d163207cce6665294aa4465541e3fe37ee7350659ea550d6dcb6af3c518852c7a14c3cc15bc32b3273bdf1751da184203135b117d300204fb12d58ca9ac34b68eca27030832f7a4b46d2a55757f63fe8f6e85ac47469e6198da88a64586bf23c69590de822263be75fd836847240c48f8c145cd6bd698962e7edc234ebe59231351eef8d7652cf3b08ab3af6e5ec74c58a8da34b39f9b0d6c4279a9a1f82071729e7059dd7f7b95b9433c4684ce1891a6d33432d52eddb0b8cee9181d403eccc12991f1ad4aa62c36049713a7bb135fdda6661a05a93f8c16f
+Message_EncryptedExtensions = 080000240022000a00140012001d00170018001901000101010201030104001c0002400100000000
+Message_CertificateRequest = 0d000027000024000d0020001e040305030603020308040805080604010501060102010402050206020202
+Message_Server_Certificate = 0b00013b000001370001323082012e3081d5a003020102020107300a06082a8648ce3d04030230133111300f060355040313086563647361323536301e170d3136303733303031323430305a170d3236303733303031323430305a30133111300f0603550403130865636473613235363059301306072a8648ce3d020106082a8648ce3d0301070342000408d530161575f4cfe7f154ee3448180086001e88431a79ee62ee6e2f83ef38ba61e9fb37f34e007a7df4d2f5b56d1f04ece45d621f468406f5c3a15158948dd0a31a301830090603551d1304023000300b0603551d0f040403020780300a06082a8648ce3d0403020348003045022100df30fd4507f5edd22c1a6ff86db479ca693feeca3b71b3f9ef556b2937c0594d022062e2a47250d320fea83c7e2dcb5b76a50e0200c09adbd13fee946e513e011d110000
+Message_Server_CertificateVerify = 0f00004b040300473045022100d7a4d34bd54f55fee1a89625678c3dd5e5f60dac73ec940c5c7b9304a02084a90220289f595ed488b9ac689a3d192b1a8bb38f34af7874c059c9806a1f38269353e8
+Message_Server_Finished = 1400002093b70cdf4781985b96345caac701b4e750d3042df1a689d8faca812251113c11
 Client_MessageToSign = 20202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020544c5320312e332c20636c69656e742043657274696669636174655665726966790044026ef3331bc864da1c08ee32e196d93cc2c5d487e47e957d980e7c3d801b9a
 Client_MessageSignature = 186b2223b503a759c35dba0e9721b4b579138d5f0f5e6ec7feaaf27f3ad7f386c2c7bd7cb2be52fbf5ed8393f406ee79369692ec7ac695651d858219e672a8eb7b2a677b640b46ab630edc5f3f2f8272b9c0d906f81f84ddc5b8c7bcf955c78a3cf99e5016f73e04eb7dfcb28833f13e8f75ec2ff3581e2f098ad4157fd6d6ad
 Record_ClientFinished = 1703030280b46a63934e673841abaf267403bc677f6b6d2a1e2f12bb5f62683bfe36a82673f06d6287ddd609bcf2f5fd3225923d24af3c76682c180ee571a17ca4bfbe2f510dc9a0e1fca5cff2cee87d11cb531a6ef90bf5309a6b63bbbc0b88ea45103a4304091543859fa11ec032ed873444cd5185ead5f6a76420f0f0286acef802c8e4788c23275f1b06da600f4a7decd0bc59d7bef10e649ae32690397fc3d4ed6f30f801d8cd569b71ad4fa05ea7cf2ac2dfa150d220505d4011b34d09d53853eba61a101e4f8dca47d8171a884b19259a3dd48c5ac141983edc77814d25e7f66bbbdb9096839266e06561828ecfb27eafd4e9e81a0b96e3bfa42dae5ad80359b9a6661402c3a2104177030106dbd8f65bb6a0159d512eb13af22a259f313bd58c2e21fe053d57f2a962b0a4ea682c96f70b79b560136192823b27be6a2fb7b1c751ccc0e3303615541485b7b307b423332c11efa80b72f9b80a53e53f7bb38a3af4c59f8008bad0544e5614e688ff57bccd6935f81f447f420c1c1bf4058818e90bf5dc716ccae42524856df8250bcdbd7af65f82dd53061d024f6d2ff5c11e3792a9a70e0ee2a3c20a1b968ac391f8f92831135d25242ada2fe241c2653ec996339dfa12dfae7a3373df88b07ca27aef6dc266a25f13f75c76039c1f46fd7a53ae6399c999f4b2aee18e480d6d12bfae226bbdc92a6ad50b4d3bac7abc3b3651eb5be56f33bf41127b3ca886dc714a50d1490357bd40d9fd6be42209a4ddb9ebb2987e29f120f05814614d2c79320015b461fe7324447670a1af5f65caed15b474ab7faa495016adf808e53b94ef54afbb0e0a3a2732ab597f7d5923c77386aa5124731f8cc73e703b341c175a454939a77ab64313c15cf3fe03c4f33842564976
diff --git a/src/tests/test_tls_cipher_state.cpp b/src/tests/test_tls_cipher_state.cpp
index e87840b38..d45e564ac 100644
--- a/src/tests/test_tls_cipher_state.cpp
+++ b/src/tests/test_tls_cipher_state.cpp
@@ -309,13 +309,23 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
 
    return flatten(
       {
-      CHECK_both("ciphersuite compatibility", [&](Cipher_State* cs, Connection_Side, Test::Result& result)
+      CHECK_both("ciphersuite compatibility", [&](Cipher_State* cs, Connection_Side side, Test::Result& result)
          {
          result.confirm("self-compatibility", cs->is_compatible_with(cipher));
          result.confirm("fully defined state is not compatible to other suites",
                         !cs->is_compatible_with(Ciphersuite::from_name("CHACHA20_POLY1305_SHA256").value()) &&
                         !cs->is_compatible_with(Ciphersuite::from_name("AES_128_CCM_SHA256").value()) &&
                         !cs->is_compatible_with(Ciphersuite::from_name("PSK_WITH_AES_128_GCM_SHA256").value()));
+         if(side == Connection_Side::CLIENT)
+            {
+            result.confirm("Clients don't expect unprotected alerts after server hello",
+                           !cs->must_expect_unprotected_alert_traffic());
+            }
+         else
+            {
+            result.confirm("Servers must expect unprotected alerts in response to their server hello",
+                           cs->must_expect_unprotected_alert_traffic());
+            }
          }),
 
       CHECK_both("handshake traffic without PSK", [&](Cipher_State* cs, Connection_Side side, Test::Result& result)
@@ -350,11 +360,15 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
             {
             result.confirm("can read application data", cs->can_decrypt_application_traffic());
             result.confirm("can not yet write application data", !cs->can_encrypt_application_traffic());
+            result.confirm("Clients don't expect unprotected alerts after server hello",
+                           !cs->must_expect_unprotected_alert_traffic());
             }
          else
             {
             result.confirm("can not yet read application data", !cs->can_decrypt_application_traffic());
             result.confirm("can write application data", cs->can_encrypt_application_traffic());
+            result.confirm("Servers must expect unprotected alerts in response to their first flight",
+                           cs->must_expect_unprotected_alert_traffic());
             }
 
          // generate the MAC for the client Finished message
@@ -382,6 +396,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
 
          result.confirm("can write application data", cs->can_encrypt_application_traffic());
          result.confirm("can read application data", cs->can_decrypt_application_traffic());
+         result.confirm("doesn't need to expect unprotected alerts", !cs->must_expect_unprotected_alert_traffic());
          result.confirm("can export key material", cs->can_export_keys());
          result.test_eq("key export produces expected result", cs->export_key(export_label, export_context, 16), expected_key_export);
 
@@ -595,12 +610,23 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
                         !cs->is_compatible_with(Ciphersuite::from_name("AES_256_GCM_SHA384").value()));
          }),
 
-      CHECK_both("calculate the early traffic secrets", [&] (Cipher_State* cs, Connection_Side, Test::Result& result)
+      CHECK_both("calculate the early traffic secrets", [&] (Cipher_State* cs, Connection_Side side, Test::Result& result)
          {
          cs->advance_with_client_hello(th_client_hello);
          result.require("early key export is possible", cs->can_export_keys());
          result.test_eq("early key export produces expected result", cs->export_key(early_export_label, early_export_context, 16), early_expected_key_export);
 
+         if(side == Connection_Side::CLIENT)
+            {
+            result.confirm("Clients must expect servers to respond with an unprotected alert",
+                           cs->must_expect_unprotected_alert_traffic());
+            }
+         else
+            {
+            result.confirm("Servers do not expect clients to send alerts protected with the early data secret",
+                           !cs->must_expect_unprotected_alert_traffic());
+            }
+
          // TODO: Once 0-RTT traffic is implemented this will likely allow handling of
          //       application traffic in this state.
          result.confirm("can not yet write application data", !cs->can_encrypt_application_traffic());
@@ -623,11 +649,15 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
             {
             result.confirm("expecting the correct MAC for server finished", cs->verify_peer_finished_mac(th_pre_server_finished,
                            expected_server_mac));
+            result.confirm("Clients don't expect unprotected alerts after server hello",
+                           !cs->must_expect_unprotected_alert_traffic());
             }
          else
             {
             result.test_eq("expecting the correct MAC for server finished", cs->finished_mac(th_pre_server_finished),
                            expected_server_mac);
+            result.confirm("Servers must expect unprotected alerts in response to their server hello",
+                           cs->must_expect_unprotected_alert_traffic());
             }
 
          result.confirm("cannot read application data", !cs->can_decrypt_application_traffic());
@@ -644,11 +674,15 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
             {
             result.confirm("can read application data", cs->can_decrypt_application_traffic());
             result.confirm("cannot write application data", !cs->can_encrypt_application_traffic());
+            result.confirm("Clients don't expect unprotected alerts after server hello",
+                           !cs->must_expect_unprotected_alert_traffic());
             }
          else
             {
             result.confirm("cannot read application data", !cs->can_decrypt_application_traffic());
             result.confirm("can write application data", cs->can_encrypt_application_traffic());
+            result.confirm("Servers must expect unprotected alerts in response to their first flight",
+                           cs->must_expect_unprotected_alert_traffic());
             }
 
          // generate the MAC for the client Finished message
@@ -679,7 +713,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
 
          result.confirm("can read application data", cs->can_decrypt_application_traffic());
          result.confirm("can write application data", cs->can_encrypt_application_traffic());
-
+         result.confirm("doesn't need to expect unprotected alerts", !cs->must_expect_unprotected_alert_traffic());
          result.confirm("can export key material", cs->can_export_keys());
          result.test_eq("key export produces expected result", cs->export_key(export_label, export_context, 16), expected_key_export);
 
diff --git a/src/tests/test_tls_record_layer_13.cpp b/src/tests/test_tls_record_layer_13.cpp
index 9a2427fd3..81c4ac227 100644
--- a/src/tests/test_tls_record_layer_13.cpp
+++ b/src/tests/test_tls_record_layer_13.cpp
@@ -610,6 +610,45 @@ read_encrypted_records()
             });
          }),
 
+      Botan_Tests::CHECK("unprotected Alert message might be legal", [&](Test::Result& result)
+         {
+         const auto alert = Botan::hex_decode("15030300020232"); // decode error
+         const auto hsmsg = Botan::hex_decode( // factored 'certificate_request' message
+            "160303002a0d000027000024000d0020001e040305030603"
+            "020308040805080604010501060102010402050206020202");
+
+         result.test_no_throw("Server allows unprotected alerts after its first flight", [&]
+            {
+            auto cs = rfc8448_rtt1_handshake_traffic(TLS::Connection_Side::SERVER);
+            auto rl = parse_records(alert);
+            rl.next_record(cs.get());
+            });
+
+         result.test_throws<Botan::TLS::TLS_Exception>("Unprotected handshake messages are not allowed for servers",
+               "unprotected record received where protected traffic was expected", [&]
+            {
+            auto cs = rfc8448_rtt1_handshake_traffic(TLS::Connection_Side::SERVER);
+            auto rl = parse_records(hsmsg);
+            rl.next_record(cs.get());
+            });
+
+         result.test_throws<Botan::TLS::TLS_Exception>("Clients don't allow unprotected alerts after Server Hello",
+               "unprotected record received where protected traffic was expected", [&]
+            {
+            auto cs = rfc8448_rtt1_handshake_traffic(TLS::Connection_Side::CLIENT);
+            auto rl = parse_records(alert);
+            rl.next_record(cs.get());
+            });
+
+         result.test_throws<Botan::TLS::TLS_Exception>("Unprotected handshake messages are not allowed for clients",
+               "unprotected record received where protected traffic was expected", [&]
+            {
+            auto cs = rfc8448_rtt1_handshake_traffic(TLS::Connection_Side::CLIENT);
+            auto rl = parse_records(hsmsg);
+            rl.next_record(cs.get());
+            });
+         }),
+
       Botan_Tests::CHECK("unprotected traffic is illegal when encrypted traffic is expected", [&](Test::Result& result)
          {
          result.test_throws("unprotected record is unacceptable",  [&]
diff --git a/src/tests/test_tls_rfc8448.cpp b/src/tests/test_tls_rfc8448.cpp
index fc0ec2437..d116b7add 100644
--- a/src/tests/test_tls_rfc8448.cpp
+++ b/src/tests/test_tls_rfc8448.cpp
@@ -17,7 +17,8 @@
    defined(BOTAN_HAS_AES) && \
    defined(BOTAN_HAS_CURVE_25519) && \
    defined(BOTAN_HAS_SHA2_32) && \
-   defined(BOTAN_HAS_SHA2_64)
+   defined(BOTAN_HAS_SHA2_64) && \
+   defined(BOTAN_HAS_ECDSA)
    #define BOTAN_CAN_RUN_TEST_TLS_RFC8448
 #endif
 
@@ -25,10 +26,10 @@
    #include "test_rng.h"
    #include "test_tls_utils.h"
 
-   #include <botan/auto_rng.h>  // TODO: replace me, otherwise we depend on auto_rng module
    #include <botan/credentials_manager.h>
    #include <botan/rsa.h>
    #include <botan/hash.h>
+   #include <botan/ecdsa.h>
    #include <botan/tls_alert.h>
    #include <botan/tls_callbacks.h>
    #include <botan/tls_client.h>
@@ -62,10 +63,21 @@ void add_entropy(Botan_Tests::Fixed_Output_RNG& rng, const std::vector<uint8_t>&
    rng.add_entropy(bin.data(), bin.size());
    }
 
-std::unique_ptr<Botan::Private_Key> server_private_key()
+Botan::Private_Key* server_private_key()
    {
-   Botan::DataSource_Memory in(Test::read_data_file("tls_13_rfc8448/server_key.pem"));
-   return Botan::PKCS8::load_key(in);
+   static Botan::DataSource_Memory in(Test::read_data_file("tls_13_rfc8448/server_key.pem"));
+   static auto key = Botan::PKCS8::load_key(in);
+   return key.get();
+   }
+
+Botan::Private_Key* bogus_alternative_server_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.
+   static auto key = Botan::ECDSA_PrivateKey(Test::rng(), Botan::EC_Group("secp256r1"));
+   return &key;
    }
 
 Botan::X509_Certificate server_certificate()
@@ -76,13 +88,25 @@ Botan::X509_Certificate server_certificate()
    return Botan::X509_Certificate(in);
    }
 
-std::unique_ptr<Botan::Private_Key> client_private_key()
+Botan::X509_Certificate alternative_server_certificate()
+   {
+   // self-signed certificate with a P-256 public key valid until:
+   //   Jul 30 01:24:00 2026 GMT
+   //
+   // This certificate is presented by the server in the "Client Authentication"
+   // test case. Why the certificate differs in that case remains unclear.
+   Botan::DataSource_Memory in(Test::read_data_file("tls_13_rfc8448/server_certificate_client_auth.pem"));
+   return Botan::X509_Certificate(in);
+   }
+
+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");
+   static auto key = create_private_key("RSA", Test::rng(), "1024");
+   return key.get();
    }
 
 Botan::X509_Certificate client_certificate()
@@ -248,14 +272,34 @@ class Test_TLS_13_Callbacks : public Botan::TLS::Callbacks
          BOTAN_UNUSED(key, rng);
          count_callback_invocation("tls_sign_message");
 
-         if(format != Signature_Format::Standard)
+         if(key.algo_name() == "RSA")
             {
-            throw Test_Error("TLS implementation selected unexpected signature format");
+            if(format != Signature_Format::Standard)
+               {
+               throw Test_Error("TLS implementation selected unexpected signature format for RSA");
+               }
+
+            if(emsa != "PSSR(SHA-256,MGF1,32)")
+               {
+               throw Test_Error("TLS implementation selected unexpected padding for RSA: " + emsa);
+               }
             }
+         else if(key.algo_name() == "ECDSA")
+            {
+            if(format != Signature_Format::DerSequence)
+               {
+               throw Test_Error("TLS implementation selected unexpected signature format for ECDSA");
+               }
 
-         if(emsa != "PSSR(SHA-256,MGF1,32)")
+            if(emsa != "EMSA1(SHA-256)")
+               {
+               throw Test_Error("TLS implementation selected unexpected padding for ECDSA: " + emsa);
+               }
+            }
+         else
             {
-            throw Test_Error("TLS implementation selected unexpected padding " + emsa);
+            throw Test_Error("TLS implementation trying to sign with unexpected algorithm ("
+                              + key.algo_name() + ")");
             }
 
          for(const auto& mock : m_mock_signatures)
@@ -411,9 +455,8 @@ class Test_TLS_13_Callbacks : public Botan::TLS::Callbacks
 class Test_Credentials : public Botan::Credentials_Manager
    {
    public:
-      Test_Credentials()
-         : m_bogus_client_key(client_private_key())
-         , m_server_key(server_private_key()) {}
+      explicit Test_Credentials(bool use_alternative_server_certificate)
+         : m_alternative_server_certificate(use_alternative_server_certificate) {}
 
       std::vector<Botan::X509_Certificate> cert_chain(
          const std::vector<std::string>& cert_key_types,
@@ -425,7 +468,9 @@ class Test_Credentials : public Botan::Credentials_Manager
             {
             (type == "tls-client")
             ? client_certificate()
-            : server_certificate()
+            : ((m_alternative_server_certificate)
+               ? alternative_server_certificate()
+               : server_certificate())
             };
          }
 
@@ -436,15 +481,14 @@ class Test_Credentials : public Botan::Credentials_Manager
          BOTAN_UNUSED(cert, context);
 
          return (type == "tls-client")
-         // 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.
-            ? m_bogus_client_key.get()
-            : m_server_key.get();
+            ? client_private_key()
+            : ((m_alternative_server_certificate)
+               ? bogus_alternative_server_private_key()
+               : server_private_key());
          }
 
    private:
-      std::unique_ptr<Botan::Private_Key> m_bogus_client_key;
-      std::unique_ptr<Botan::Private_Key> m_server_key;
+      bool m_alternative_server_certificate;
    };
 
 class RFC8448_Text_Policy : public Botan::TLS::Text_Policy
@@ -600,8 +644,10 @@ class TLS_Context
                   Modify_Exts_Fn modify_exts_cb,
                   std::vector<MockSignature> mock_signatures,
                   uint64_t timestamp,
-                  std::optional<std::vector<uint8_t>> session)
+                  std::optional<std::vector<uint8_t>> session,
+                  bool use_alternative_server_certificate)
          : m_callbacks(std::move(modify_exts_cb), std::move(mock_signatures), timestamp)
+         , m_creds(use_alternative_server_certificate)
          , m_rng(std::move(rng_in))
          , m_policy(std::move(policy))
          {
@@ -695,7 +741,7 @@ class Client_Context : public TLS_Context
                      Modify_Exts_Fn modify_exts_cb,
                      std::optional<std::vector<uint8_t>> session = std::nullopt,
                      std::vector<MockSignature> mock_signatures = {})
-         : TLS_Context(std::move(rng_in), std::move(policy), std::move(modify_exts_cb), std::move(mock_signatures), timestamp, std::move(session))
+         : TLS_Context(std::move(rng_in), std::move(policy), std::move(modify_exts_cb), std::move(mock_signatures), timestamp, std::move(session), false)
          , client(m_callbacks, m_session_mgr, m_creds, m_policy, *m_rng,
                   Botan::TLS::Server_Information("server"),
                   Botan::TLS::Protocol_Version::TLS_V13)
@@ -716,12 +762,14 @@ class Server_Context : public TLS_Context
                      RFC8448_Text_Policy policy,
                      uint64_t timestamp,
                      Modify_Exts_Fn modify_exts_cb,
-                     std::vector<MockSignature> mock_signatures)
+                     std::vector<MockSignature> mock_signatures,
+                     bool use_alternative_server_certificate = false)
          : TLS_Context(std::move(rng), std::move(policy),
             std::move(modify_exts_cb),
             std::move(mock_signatures),
             timestamp,
-            std::nullopt /* session not yet needed */)
+            std::nullopt /* session not yet needed */,
+            use_alternative_server_certificate)
          , server(m_callbacks, m_session_mgr, m_creds, m_policy, *m_rng, false /* DTLS NYI */) {}
 
       void send(const std::vector<uint8_t>& data) override
@@ -852,6 +900,9 @@ std::vector<MockSignature> make_mock_signatures(const VarMap& vars)
  * tls_13_rfc8448/server_key.pem
  *   The server certificate and its associated private key.
  *
+ * tls_13_rfc8448/server_certificate_client_auth.pem
+ *   The server certificate used in the Client Authentication test case.
+ *
  * tls-policy/rfc8448_*.txt
  *   Each RFC 8448 section test required a slightly adapted Botan TLS policy
  *   to enable/disable certain features under test.
@@ -887,6 +938,7 @@ class Test_TLS_RFC8448 : public Text_Based_Test
                            // optional data fields
                            "Message_ServerHello,"
                            "Message_EncryptedExtensions,"
+                           "Message_CertificateRequest,"
                            "Message_Server_Certificate,"
                            "Message_Server_CertificateVerify,"
                            "Message_Server_Finished,"
@@ -1611,8 +1663,108 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448
             };
          }
 
-      // TODO: once client authentication is added this test trace should pass, too
-      std::vector<Test::Result> client_authentication(const VarMap& /*vars*/) override { return {}; }
+      std::vector<Test::Result> client_authentication(const VarMap& vars) override
+         {
+         auto rng = std::make_unique<Botan_Tests::Fixed_Output_RNG>("");
+
+         // 32 - for server hello random
+         // 32 - for KeyShare (eph. x25519 pair)
+         add_entropy(*rng, vars.get_req_bin("Server_RNG_Pool"));
+
+         std::unique_ptr<Server_Context> ctx;
+
+         return
+            {
+            Botan_Tests::CHECK("Receive Client Hello", [&](Test::Result& result)
+               {
+               ctx = std::make_unique<Server_Context>(std::move(rng), read_tls_policy("rfc8448_client_auth_server"), vars.get_req_u64("CurrentTimestamp"), sort_server_extensions, make_mock_signatures(vars), true /* use alternative certificate */);
+               result.confirm("server not closed", !ctx->server.is_closed());
+
+               ctx->server.received_data(vars.get_req_bin("Record_ClientHello_1"));
+
+               ctx->check_callback_invocations(result, "client hello received", {
+                  "tls_emit_data",
+                  "tls_examine_extensions_client_hello",
+                  "tls_modify_extensions_server_hello",
+                  "tls_modify_extensions_encrypted_extensions",
+                  "tls_modify_extensions_certificate",
+                  "tls_sign_message",
+                  "tls_inspect_handshake_msg_client_hello",
+                  "tls_inspect_handshake_msg_server_hello",
+                  "tls_inspect_handshake_msg_encrypted_extensions",
+                  "tls_inspect_handshake_msg_certificate_request",
+                  "tls_inspect_handshake_msg_certificate",
+                  "tls_inspect_handshake_msg_certificate_verify",
+                  "tls_inspect_handshake_msg_finished"
+                  });
+               }),
+
+            Botan_Tests::CHECK("Verify server's generated handshake messages", [&](Test::Result& result)
+               {
+               const auto& msgs = ctx->observed_handshake_messages();
+
+               result.test_eq("Server Hello", msgs.at("server_hello")[0], strip_message_header(vars.get_opt_bin("Message_ServerHello")));
+               result.test_eq("Encrypted Extensions", msgs.at("encrypted_extensions")[0], strip_message_header(vars.get_opt_bin("Message_EncryptedExtensions")));
+               result.test_eq("Certificate Request", msgs.at("certificate_request")[0], strip_message_header(vars.get_opt_bin("Message_CertificateRequest")));
+               result.test_eq("Certificate", msgs.at("certificate")[0], strip_message_header(vars.get_opt_bin("Message_Server_Certificate")));
+               result.test_eq("CertificateVerify", msgs.at("certificate_verify")[0], strip_message_header(vars.get_opt_bin("Message_Server_CertificateVerify")));
+               result.test_eq("Finished", msgs.at("finished")[0], strip_message_header(vars.get_opt_bin("Message_Server_Finished")));
+
+               result.test_eq("Server's entire first flight", ctx->pull_send_buffer(), concat(vars.get_req_bin("Record_ServerHello"),
+                                                                                              vars.get_req_bin("Record_ServerHandshakeMessages")));
+
+               result.confirm("Not yet aware of client's cert chain", ctx->server.peer_cert_chain().empty());
+               result.confirm("Server could now send application data", ctx->server.is_active());
+               }),
+
+            Botan_Tests::CHECK("Receive Client's second flight", [&](Test::Result& result)
+               {
+               // This encrypted message contains the following messages:
+               // * client's Certificate message
+               // * client's Certificate_Verify message
+               // * client's Finished message
+               ctx->server.received_data(vars.get_req_bin("Record_ClientFinished"));
+
+               ctx->check_callback_invocations(result, "client finished received", {
+                  "tls_inspect_handshake_msg_certificate",
+                  "tls_inspect_handshake_msg_certificate_verify",
+                  "tls_inspect_handshake_msg_finished",
+                  "tls_examine_extensions_certificate",
+                  "tls_verify_cert_chain",
+                  "tls_verify_message",
+                  "tls_session_activated"
+                  });
+
+               const auto cert_chain = ctx->server.peer_cert_chain();
+               result.confirm("Received client's cert chain", !cert_chain.empty() && cert_chain.front() == client_certificate());
+
+               result.confirm("TLS handshake finished", ctx->server.is_active());
+               }),
+
+            Botan_Tests::CHECK("Receive Client close_notify", [&](Test::Result& result)
+               {
+               ctx->server.received_data(vars.get_req_bin("Record_Client_CloseNotify"));
+
+               ctx->check_callback_invocations(result, "client finished received", {
+                  "tls_alert",
+                  "tls_peer_closed_connection"
+                  });
+
+               result.confirm("connection is not yet closed", !ctx->server.is_closed());
+               result.confirm("connection is still active", ctx->server.is_active());
+               }),
+
+            Botan_Tests::CHECK("Expect Server close_notify", [&](Test::Result& result)
+               {
+               ctx->server.close();
+
+               result.confirm("connection is now inactive", !ctx->server.is_active());
+               result.confirm("connection is now closed", ctx->server.is_closed());
+               result.test_eq("Server's close notify", ctx->pull_send_buffer(), vars.get_req_bin("Record_Server_CloseNotify"));
+               }),
+
+            };
+         }
 
       std::vector<Test::Result> middlebox_compatibility(const VarMap& vars) override
          {