Repository: randombit/botan
Pull request #3053 (2022-09-28 12:58:06)

[TLS 1.3] Basic Server Implementation
## Pull Request Dependencies

* ~~First review/merge https://github.com/randombit/botan/pull/3062 and rebase.~~ That'll make this much easier digestable, I hope.

## Description

This provides a basic TLS 1.3 server implementation with the most common functionality. Relevant BoGo tests pass already, but many more are still disabled. The ordinary handshake (with or without Hello Retry Request) and subsequent application data exchange works. Also a protocol version downgrade to TLS 1.2 works fine.

For the sake of reviewability, I am planning to stabilize the current feature set and aim for review/merge to master. Further functionality will come. See below.

### New Callback: `bool tls_peer_closed_connection()`

This is called when we received a "close_notify" from the peer. In contrast to older revisions, TLS 1.3 does not mandate an immediate in-kind response in that case. Applications can override this callback to get notified that no more data is to be expected from the peer. Though they could legally continue sending data and finish with `Channel::close()` at their own discretion.

Returning `true` from this callback will make TLS 1.3 behave just like 1.2 and reply with a "close_notify" immediately. This is the default behaviour.

## Missing Functionality

* [ ] PSK Mode
* [ ] Resumption
* [ ] Creation and sending of Session Tickets
* [ ] Sending or receiving of Early Data
* [x] Client Authentication (#3081)
* [x] OCSP Stapling (#3112)
* [x] ALPN (#3117 )
* probably more...

lgtm-com[bot]: This pull request **introduces 1 alert** when merging 682ef9a7fb73390dfaaa494d966788aa16e760d7 into 26b593ce58c52713c53b4c57efdbccdd9125c5ac - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-45b7c8240600d1906e1630204880e88741ce7f76)

**new alerts:**

* 1 for Unused local variable
lgtm-com[bot]: This pull request **introduces 1 alert** when merging ea33d55689cab28927013d324c1d1f51b58ba5e0 into 76089a3e2452bf860fe3ecec86cf7406c53ed6f7 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-d27650b310594fb408cca427a6a41e2e58489b66)

**new alerts:**

* 1 for Unused local variable
lgtm-com[bot]: This pull request **introduces 1 alert** when merging 1057dcce52b0634a0088c78096ec3c5a439cfdf2 into 76089a3e2452bf860fe3ecec86cf7406c53ed6f7 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-27b60c54f69ecfb992b6f58a8d979c8651e2fd0f)

**new alerts:**

* 1 for Unused local variable
reneme: Milestone: This can now successfully handshake and exchange messages with `openssl`:

#### Terminal 1

```
./botan tls_server src/tests/data/tls_13_rfc8448/server_certificate.pem src/tests/data/tls_13_rfc8448/server_key.pem --port=8443 --policy=src/tests/data/tls-policy/rfc8448_1rtt.txt
```

#### Terminal 2

```
openssl s_client localhost:8443
```

Next steps: Support for Hello Retry Request and integration with basic BoGo tests. For the latter, I hope we can find a few server tests that don't require resumption support.
lgtm-com[bot]: This pull request **introduces 1 alert** when merging a65fbeff4038e553e612ef3098331cca71e3b29f into 76089a3e2452bf860fe3ecec86cf7406c53ed6f7 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-aa8464b90203480ea4b975978ab94b83c64c9c9a)

**new alerts:**

* 1 for Unused local variable
lgtm-com[bot]: This pull request **introduces 2 alerts** when merging 0cb64091389ce829a27fbe35a61c2da500625f41 into 76089a3e2452bf860fe3ecec86cf7406c53ed6f7 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-845ad2879a12b19e0cfec182d06fc806acba4a19)

**new alerts:**

* 1 for Futile conditional
* 1 for Unused local variable
lgtm-com[bot]: This pull request **introduces 1 alert** when merging f1d26346e6d2cc4eb05698aa9596308a8282b422 into 76089a3e2452bf860fe3ecec86cf7406c53ed6f7 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-1e2420699fb31a3643181ddde13b01f23dddc0ab)

**new alerts:**

* 1 for Unused local variable
lgtm-com[bot]: This pull request **introduces 1 alert** when merging e099626e1e9e4f96c9108068440264e2cefc5fbd into 76089a3e2452bf860fe3ecec86cf7406c53ed6f7 - [view on LGTM.com](https://lgtm.com/projects/g/randombit/botan/rev/pr-dc161752d3e2f7d5c7fe24af19507929ac3e464a)

**new alerts:**

* 1 for Unused local variable
reneme: I'll clean up the commit history tomorrow and would consider this ready for review/merge now.
codecov-commenter: # [Codecov](https://codecov.io/gh/randombit/botan/pull/3053?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) Report
Base: **92.56**% // Head: **92.60**% // Increases project coverage by **`+0.03%`** :tada:
> Coverage data is based on head [(`3a0cc0f`)](https://codecov.io/gh/randombit/botan/pull/3053?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) compared to base [(`4090574`)](https://codecov.io/gh/randombit/botan/commit/4090574dea14aa0b3ed31189d4b3a68020088a36?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd).
> Patch coverage: 90.93% of modified lines in pull request are covered.

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


```diff
@@            Coverage Diff             @@
##           master    #3053      +/-   ##
==========================================
+ Coverage   92.56%   92.60%   +0.03%     
==========================================
  Files         602      603       +1     
  Lines       70650    71365     +715     
  Branches     6662     6735      +73     
==========================================
+ Hits        65398    66086     +688     
- Misses       5222     5249      +27     
  Partials       30       30              
```


| [Impacted Files](https://codecov.io/gh/randombit/botan/pull/3053?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/3053/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.00% <0.00%> (-6.70%)` | :arrow_down: |
| [src/lib/tls/tls\_client.cpp](https://codecov.io/gh/randombit/botan/pull/3053/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzX2NsaWVudC5jcHA=) | `85.93% <42.85%> (-5.30%)` | :arrow_down: |
| [src/bogo\_shim/bogo\_shim.cpp](https://codecov.io/gh/randombit/botan/pull/3053/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2JvZ29fc2hpbS9ib2dvX3NoaW0uY3Bw) | `88.73% <66.66%> (-0.12%)` | :arrow_down: |
| [src/lib/tls/tls\_server.cpp](https://codecov.io/gh/randombit/botan/pull/3053/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.00% <73.52%> (+4.09%)` | :arrow_up: |
| [src/cli/tls\_client.cpp](https://codecov.io/gh/randombit/botan/pull/3053/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.41% <75.00%> (+1.00%)` | :arrow_up: |
| [src/lib/tls/tls13/tls\_server\_impl\_13.cpp](https://codecov.io/gh/randombit/botan/pull/3053/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzMTMvdGxzX3NlcnZlcl9pbXBsXzEzLmNwcA==) | `82.45% <82.45%> (ø)` | |
| [src/lib/tls/tls13/msg\_encrypted\_extensions.cpp](https://codecov.io/gh/randombit/botan/pull/3053/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: |
| [src/tests/test\_tls\_stream\_integration.cpp](https://codecov.io/gh/randombit/botan/pull/3053/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL3Rlc3RzL3Rlc3RfdGxzX3N0cmVhbV9pbnRlZ3JhdGlvbi5jcHA=) | `92.54% <89.02%> (-4.41%)` | :arrow_down: |
| [src/lib/tls/msg\_server\_hello.cpp](https://codecov.io/gh/randombit/botan/pull/3053/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvbXNnX3NlcnZlcl9oZWxsby5jcHA=) | `92.88% <90.90%> (-0.48%)` | :arrow_down: |
| [src/lib/tls/tls13/tls\_extensions\_key\_share.cpp](https://codecov.io/gh/randombit/botan/pull/3053/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL2xpYi90bHMvdGxzMTMvdGxzX2V4dGVuc2lvbnNfa2V5X3NoYXJlLmNwcA==) | `90.43% <91.07%> (+2.93%)` | :arrow_up: |
| ... and [49 more](https://codecov.io/gh/randombit/botan/pull/3053/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/3053?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).

randombit: I'm deferring a review on this until #3062 lands
reneme: > I'm deferring a review on this until #3062 lands

Rebased to master. This has no more underlying PR dependencies.
randombit: I will review this asap
reneme: I left this open despite @lieser approving since you had started a review as well, @randombit.

In case you want to continue reviewing this after your christmas break, please don't hesitate to point that out.

Otherwise, I'd like to merge this soonish and integrate the remaining (approved) PRs. This would particularly simplify development in [the resumption PR](https://github.com/randombit/botan/pull/3140) as it will contain some cross-cutting concerns.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c5454b9cc..2bf23b498 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -104,7 +104,7 @@ jobs:
           - target: amalgamation
             compiler: clang
 
-    runs-on: macos-12
+    runs-on: macos-latest
 
     steps:
       - uses: actions/checkout@v3
@@ -139,7 +139,7 @@ jobs:
             host_os: ubuntu-22.04
           - target: emscripten
             compiler: emcc
-            host_os: macos-12
+            host_os: macos-latest
           - target: minimized
             compiler: gcc
             host_os: ubuntu-22.04
@@ -224,7 +224,7 @@ jobs:
             host_os: ubuntu-22.04
           - target: cross-ios-arm64
             compiler: clang
-            host_os: macos-12
+            host_os: macos-latest
 
     runs-on: ${{ matrix.host_os }}
 
diff --git a/src/bogo_shim/bogo_shim.cpp b/src/bogo_shim/bogo_shim.cpp
index f599cc4c3..6e9d7dd76 100644
--- a/src/bogo_shim/bogo_shim.cpp
+++ b/src/bogo_shim/bogo_shim.cpp
@@ -14,6 +14,7 @@
 #include <botan/tls_server.h>
 #include <botan/tls_exceptn.h>
 #include <botan/tls_algos.h>
+#include <botan/tls_messages.h>
 #include <botan/data_src.h>
 #include <botan/pkcs8.h>
 #include <botan/internal/loadstor.h>
@@ -106,6 +107,7 @@ std::string map_to_bogo_error(const std::string& e)
          { "Client cert verify failed", ":BAD_SIGNATURE:" },
          { "Client certificate does not support signing", ":KEY_USAGE_BIT_INCORRECT:" },
          { "Client did not offer NULL compression", ":INVALID_COMPRESSION_LIST:" },
+         { "Client Hello must either contain both key_share and supported_groups extensions or neither", ":MISSING_KEY_SHARE:" },
          { "Client offered DTLS version with major version 0xFF",  ":UNSUPPORTED_PROTOCOL:" },
          { "Client offered SSLv3 which is not supported", ":UNSUPPORTED_PROTOCOL:" },
          { "Client offered TLS version with major version under 3", ":UNSUPPORTED_PROTOCOL:" },
@@ -120,7 +122,9 @@ std::string map_to_bogo_error(const std::string& e)
          { "Client version TLS v1.1 is unacceptable by policy", ":UNSUPPORTED_PROTOCOL:" },
          { "No shared TLS version based on supported versions extension", ":UNSUPPORTED_PROTOCOL:" },
          { "Client: No certificates sent by server", ":DECODE_ERROR:" },
+         { "Non-PSK Client Hello did not contain supported_groups and signature_algorithms extensions", ":NO_SHARED_GROUP:" },
          { "No certificates sent by server", ":PEER_DID_NOT_RETURN_A_CERTIFICATE:" },
+         { "Not enough data to read another KeyShareEntry", ":DECODE_ERROR:" },
          { "Counterparty sent inconsistent key and sig types", ":WRONG_SIGNATURE_TYPE:" },
          { "Downgrade attack detected", ":TLS13_DOWNGRADE:" },
          { "Empty ALPN protocol not allowed", ":PARSE_TLSEXT:" },
@@ -163,10 +167,11 @@ std::string map_to_bogo_error(const std::string& e)
          { "Received a record that exceeds maximum size", ":ENCRYPTED_LENGTH_TOO_LONG:" },
          { "Received an encrypted record that exceeds maximum size", ":ENCRYPTED_LENGTH_TOO_LONG:" },
          { "received an illegal handshake message", ":UNEXPECTED_MESSAGE:" },
-         { "Received an unexpected legacy Client Hello", ":UNSUPPORTED_PROTOCOL:" },
+         { "Received a legacy Client Hello", ":UNSUPPORTED_PROTOCOL:" },
          { "Received an unexpected legacy Server Hello", ":UNSUPPORTED_PROTOCOL:" },
          { "Received application data after connection closure", ":APPLICATION_DATA_ON_SHUTDOWN:" },
          { "Received handshake data after connection closure", ":NO_RENEGOTIATION:" },
+         { "Received multiple key share entries for the same group", ":DUPLICATE_KEY_SHARE:" },
          { "Received unexpected record version in initial record", ":WRONG_VERSION_NUMBER:" },
          { "Received unexpected record version", ":WRONG_VERSION_NUMBER:" },
          { "Rejecting ALPN request with alert", ":NO_APPLICATION_PROTOCOL:" },
@@ -714,6 +719,7 @@ std::unique_ptr<Shim_Arguments> parse_options(char* argv[])
       "verify-fail",
       "verify-peer",
       //"verify-peer-if-no-obc",
+      "wait-for-debugger",
       "write-different-record-sizes",
    };
 
@@ -951,7 +957,22 @@ class Shim_Policy final : public Botan::TLS::Policy
 
       bool use_ecc_point_compression() const override { return false; } // BoGo expects this
 
-      //Botan::TLS::Group_Params choose_key_exchange_group(const std::vector<Botan::TLS::Group_Params>& peer_groups) const override;
+      Botan::TLS::Group_Params choose_key_exchange_group(const std::vector<Botan::TLS::Group_Params>& supported_by_peer,
+                                                         const std::vector<Botan::TLS::Group_Params>& offered_by_peer) const override
+         {
+         BOTAN_UNUSED(offered_by_peer);
+
+         // always insist on our most preferred group regardless of the peer's
+         // pre-offers (BoGo expects it like that)
+         const auto our_groups = key_exchange_groups();
+         for(auto g : our_groups)
+            {
+            if(Botan::value_exists(supported_by_peer, g))
+               return g;
+            }
+
+         return Botan::TLS::Group_Params::NONE;
+         }
 
       bool require_client_certificate_authentication() const override
          {
@@ -1168,22 +1189,9 @@ std::vector<uint16_t> Shim_Policy::ciphersuite_list(Botan::TLS::Protocol_Version
       for(auto i = ciphersuites.rbegin(); i != ciphersuites.rend(); ++i)
          {
          const auto suite = *i;
-         const bool is_tls13_suite =
-            suite.kex_method() == Botan::TLS::Kex_Algo::UNDEFINED &&
-            suite.auth_method() == Botan::TLS::Auth_Method::UNDEFINED;
-
-         const bool is_client = !m_args.flag_set("server");
-
-         // client should only offer suites appropriate to version
-         if(is_client && (version == Botan::TLS::Protocol_Version::TLS_V13) != is_tls13_suite)
-            continue;
-
-         // tls 1.3 server is nyi
-         if (!is_client && is_tls13_suite)
-            continue;
 
          // Can we use it?
-         if(suite.valid() == false)
+         if(suite.valid() == false || !suite.usable_in_version(version))
             continue;
 
          if(cipher_limit != "")
@@ -1324,6 +1332,7 @@ class Shim_Callbacks final : public Botan::TLS::Callbacks
          m_empty_records(0),
          m_sessions_established(0),
          m_got_close(false),
+         m_hello_retry_request(false),
          m_clock_skew(0)
          {}
 
@@ -1633,6 +1642,16 @@ class Shim_Callbacks final : public Botan::TLS::Callbacks
             m_channel->close();
             }
 
+         if(m_args.flag_set("expect-hrr") && !m_hello_retry_request)
+            {
+            throw Shim_Exception("Expected Hello Retry Request but didn't see one");
+            }
+
+         if(m_args.flag_set("expect-no-hrr") && m_hello_retry_request)
+            {
+            throw Shim_Exception("Hello Retry Request seen but didn't expect one");
+            }
+
          if(m_args.flag_set("key-update"))
             {
             shim_log("Updating traffic keys without asking for reciprocation");
@@ -1650,6 +1669,14 @@ class Shim_Callbacks final : public Botan::TLS::Callbacks
          return g_now + m_clock_skew;
          }
 
+      void tls_inspect_handshake_msg(const Botan::TLS::Handshake_Message& msg) override
+         {
+         if(msg.type() == Botan::TLS::Handshake_Type::HELLO_RETRY_REQUEST)
+            {
+            m_hello_retry_request = true;
+            }
+         }
+
    private:
       Botan::TLS::Channel* m_channel;
       const Shim_Arguments& m_args;
@@ -1660,6 +1687,7 @@ class Shim_Callbacks final : public Botan::TLS::Callbacks
       size_t m_empty_records;
       size_t m_sessions_established;
       bool m_got_close;
+      bool m_hello_retry_request;
       std::chrono::seconds m_clock_skew;
    };
 
@@ -1686,6 +1714,11 @@ int main(int /*argc*/, char* argv[])
       Botan::TLS::Session_Manager_In_Memory session_manager(rng, 1024);
       Shim_Credentials creds(*args);
 
+      if(args->flag_set("wait-for-debugger"))
+         {
+         sleep(20);
+         }
+
       for(size_t i = 0; i != resume_count+1; ++i)
          {
 
diff --git a/src/bogo_shim/config.json b/src/bogo_shim/config.json
index 46a52c5f3..798473b7b 100644
--- a/src/bogo_shim/config.json
+++ b/src/bogo_shim/config.json
@@ -14,7 +14,8 @@
         "HelloRetryRequest-DuplicateCookie-TLS13": "expects 'illegal parameter' but we want to stick with 'decode error'",
         "EncryptedExtensionsWithKeyShare-TLS13": "expects 'unsupported extension' but RFC requires 'illegal parameter'",
         "ClientSkipCertificateVerify-TLS13": "would require ambiguous error mapping",
-        "Resume-Client-Mismatch-TLS13-TLS12-TLS": "server requests a downgrade to TLS 1.2, echoing the random session ID during a TLS 1.3 resumption. => error mapping conflict"
+        "Resume-Client-Mismatch-TLS13-TLS12-TLS": "server requests a downgrade to TLS 1.2, echoing the random session ID during a TLS 1.3 resumption. => error mapping conflict",
+        "ServerAuth-NoFallback-TLS13": "would require ambiguous error mapping"
     },
 
     "DisabledTests": {
@@ -36,10 +37,13 @@
         "AllExtensions-*": "Not all extensions are implemented",
 
         "EchoTLS13CompatibilitySessionID": "Succeeds, but we prepend a TLS 1.3 CCS and BoGo doesn't like that",
+        "VersionTolerance-TLS13": "We are not tolerating 0x0400 as Client Hello legacy_version",
 
+        "Server-JDK11-*": "We don't implement JDK-specific workarounds",
         "Client-RejectJDK11DowngradeRandom": "We don't implement this workaround",
         "ExportTrafficSecrets-*": "Exporting traffic secrets is not implemented",
         "TooManyChangeCipherSpec-Client-TLS13": "Limits on the number of CCS are not implemented",
+        "TooManyChangeCipherSpec-Server-TLS13": "Limits on the number of CCS are not implemented",
         "TooManyKeyUpdates": "Limits on the number of KeyUpdates are not implemented",
 
         "TLS12SessionID-TLS13": "We don't offer TLS 1.3 when a TLS 1.2 session was found",
@@ -48,52 +52,91 @@
         "Resume-Client-Mismatch-TLS12-TLS13-TLS": "We don't offer TLS 1.3 when a TLS 1.2 session was found",
 
         "KeyUpdate-FromServer": "No TLS 1.3 server, yet",
-        "FragmentedClientVersion": "No TLS 1.3 server, yet",
         "DelegatedCredentials*": "No TLS 1.3 server, yet",
-        "IgnoreClientVersionOrder": "No TLS 1.3 server, yet",
         "Resume-Server-OmitPSKsOnSecondClientHello": "No TLS 1.3 server, yet",
         "PartialClientFinishedWithSecondClientHello": "No TLS 1.3 server, yet",
-        "Server-JDK11*": "No TLS 1.3 server, yet",
 
-        "CertCompression*-TLS13": "No TLS 1.3 server, yet",
-        "DuplicateKeyShares-TLS13": "No TLS 1.3 server, yet",
-        "ECDSACurveMismatch-Sign-TLS13": "No TLS 1.3 server, yet",
-        "MinimumVersion-Server*-TLS13*": "No TLS 1.3 server, yet",
         "Resume-Server*TLS13*": "No TLS 1.3 server, yet",
         "Resume-Server-*": "No TLS 1.3 server, yet",
-        "Server-Sign-*-TLS13": "No TLS 1.3 server, yet",
-        "Server-Verify-*-TLS13": "No TLS 1.3 server, yet",
-        "Server-VerifyDefault-*-TLS13": "No TLS 1.3 server, yet",
-
-        "TLS-TLS13-*-server": "No TLS 1.3 server, yet",
-        "TLS13-Server-*": "No TLS 1.3 server, yet",
-        "TLS13-*-Server*": "No TLS 1.3 server, yet",
-        "*-Server-TLS13": "No TLS 1.3 server, yet",
-        "*-TLS13-Server": "No TLS 1.3 server, yet",
-        "RSAKeyUsage-Server-*-TLS13": "No TLS 1.3 server, yet",
 
         "ExportKeyingMaterial-Server-HalfRTT-TLS13": "No TLS 1.3 server, yet",
-        "ExtraClientEncryptedExtension-TLS-TLS13": "No TLS 1.3 server, yet",
-        "ExtraCompressionMethods-TLS13": "No TLS 1.3 server, yet",
-        "LooseInitialRecordVersion-TLS13": "No TLS 1.3 server, yet",
-        "NoSupportedCurves-TLS13": "No TLS 1.3 server, yet",
         "RequireAnyClientCertificate-TLS13": "No TLS 1.3 server, yet",
         "SecondClientHelloMissingKeyShare-TLS13": "No TLS 1.3 server, yet",
         "SecondClientHelloWrongCurve-TLS13": "No TLS 1.3 server, yet",
         "SendExtensionOnClientCertificate-TLS13": "No TLS 1.3 server, yet",
-        "ServerAuth-NoFallback-TLS13": "No TLS 1.3 server, yet",
-        "ServerSkipCertificateVerify-TLS13": "No TLS 1.3 server, yet",
         "SkipClientCertificate-TLS13": "No TLS 1.3 server, yet",
-        "TLS13-NoTicket-NoMint": "No TLS 1.3 server, yet",
-        "TrailingKeyShareData-TLS13": "No TLS 1.3 server, yet",
-        "UnexpectedClientEncryptedExtensions-TLS-TLS13": "No TLS 1.3 server, yet",
-        "UnknownCipher-TLS13": "No TLS 1.3 server, yet",
-        "VersionTolerance-TLS13": "No TLS 1.3 server, yet",
         "TLS13-TicketAgeSkew-*": "No TLS 1.3 server, yet",
 
+        "HttpGET": "TLS 1.3 server does not detect HTTP",
+        "HttpPOST": "TLS 1.3 server does not detect HTTP",
+        "HttpPUT": "TLS 1.3 server does not detect HTTP",
+        "HttpHEAD": "TLS 1.3 server does not detect HTTP",
+        "HttpCONNECT": "TLS 1.3 server does not detect HTTP",
+
+        "KeyUpdate-ToServer": "TLS 1.3 server key update NYI",
+
+        "ExtraClientEncryptedExtension-TLS-TLS13": "TLS 1.3 server UNIMPLEMENTED",
+        "TLS13-1RTT-Server-TLS-Sync": "TLS 1.3 server UNIMPLEMENTED",
+        "TLS13-1RTT-Server-TLS-Sync-ImplicitHandshake": "TLS 1.3 server UNIMPLEMENTED",
+        "TLS13-1RTT-Server-TLS-Sync-SplitHandshakeRecords": "TLS 1.3 server UNIMPLEMENTED",
+        "TLS13-1RTT-Server-TLS-Sync-PackHandshake": "TLS 1.3 server UNIMPLEMENTED",
+        "TLS13-1RTT-Server-TLS-Async": "TLS 1.3 server UNIMPLEMENTED",
+        "TLS13-1RTT-Server-TLS-Async-ImplicitHandshake": "TLS 1.3 server UNIMPLEMENTED",
+        "TLS13-1RTT-Server-TLS-Async-SplitHandshakeRecords": "TLS 1.3 server UNIMPLEMENTED",
+        "TLS13-1RTT-Server-TLS-Async-PackHandshake": "TLS 1.3 server UNIMPLEMENTED",
+        "CertCompressionNoCommonAlgs-TLS13": "TLS 1.3 server UNIMPLEMENTED",
+
+        "ALPNServer-Decline-TLS-TLS13": "TLS 1.3 server session resumption NYI",
+        "CertificateVerificationSucceed-Server-TLS13-*": "TLS 1.3 server session resumption NYI",
+        "CurveID-Resume-Server-TLS13": "TLS 1.3 server session resumption NYI",
+        "ExtraPSKIdentity-TLS13": "TLS 1.3 server session resumption NYI",
+        "NoClientCertificate-Server-TLS13": "TLS 1.3 server session resumption NYI",
+        "NoClientCertificateRequested-Server-TLS13": "TLS 1.3 server session resumption NYI",
+        "Server-Verify-*-TLS13": "TLS 1.3 server session resumption NYI",
+        "Server-VerifyDefault-*-TLS13": "TLS 1.3 server session resumption NYI",
+        "ServerNameExtensionServer-TLS-TLS13": "TLS 1.3 server session resumption NYI",
+        "TLS-TLS13-*-server": "TLS 1.3 server session resumption NYI",
+        "TLS13-SendNoKEMModesWithPSK-Server": "TLS 1.3 server session resumption NYI",
+        "TrailingDataWithFinished-Resume-Server-TLS13": "TLS 1.3 server session resumption NYI",
+        "TLS13-ExpectNoSessionTicketOnBadKEMode-Server": "TLS 1.3 server session resumption NYI",
+        "TLS13-SendUnknownModeSessionTicket-Server": "TLS 1.3 server session resumption NYI",
+        "TLS13-SendBadKEModeSessionTicket-Server": "TLS 1.3 server session resumption NYI",
+        "TLS13-HelloRetryRequest-Server-*": "TLS 1.3 server session resumption NYI",
+
+        "ALPNServer-TLS-TLS13": "TLS 1.3 server ALPN NYI",
+        "ALPNServer-Async-TLS-TLS13": "TLS 1.3 server ALPN NYI",
+        "ALPNServer-Reject-TLS-TLS13": "TLS 1.3 server ALPN NYI",
+
+        "OCSPStapling-Server-TLS13-*": "TLS 1.3 server OCSP NYI",
+        "ServerOCSPCallback-Decline-TLS13-*": "TLS 1.3 server OCSP NYI",
+        "ServerOCSPCallback-Fail-TLS13-*": "TLS 1.3 server OCSP NYI",
+        "ServerOCSPCallback-SetInCallback-TLS13-*": "TLS 1.3 server OCSP NYI",
+
         "*EarlyData*": "No TLS 1.3 Early Data, yet",
         "TLS13-1RTT-Client-*": "No TLS 1.3 Early Data, yet",
 
+        "Server-InvalidSignature-*": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "ClientAuth-Enforced-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertificateVerificationFail-Server-TLS13-*": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "WrongMessageType-TLS13-ClientCertificate-TLS": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "WrongMessageType-TLS13-ClientCertificateVerify-TLS": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "TrailingMessageData-TLS13-ClientCertificateVerify-TLS": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "TrailingMessageData-TLS13-ClientCertificate-TLS": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "ServerSkipCertificateVerify-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "ECDSAKeyUsage-Server-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "GarbageCertificate-Server-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionShrinksClient-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionExpandsClient-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionBadAlgIDClient-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionTooSmallClient-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionTooLargeClient-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionShrinks-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionRandom-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionPriority-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "RSAKeyUsage-Server-WantSignature-GotEncipherment-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "CertCompressionExpands-TLS13": "No TLS 1.3 (server-side) Client Authentication, yet",
+        "Verify-ClientAuth-SignatureType-TLS13": "No TLS 1.3 (server-side) Client Authentication, 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",
 
@@ -107,7 +150,6 @@
         "SupportedVersionSelection-TLS12": "We just ignore the version extension in this case",
 
         "Downgrade-*-Client-Ignore": "Not possible to ignore downgrade indicator",
-        "Downgrade-TLS12-Server": "Not a downgrade when we don't support v1.3",
 
         "Agree-Digest-SHA1": "No SHA-1 in TLS 1.2",
         "ServerAuth-SHA1-Fallback-*": "No SHA-1 in TLS 1.2",
diff --git a/src/cli/tls_client.cpp b/src/cli/tls_client.cpp
index d2449fdaa..7ec2843d1 100644
--- a/src/cli/tls_client.cpp
+++ b/src/cli/tls_client.cpp
@@ -37,7 +37,7 @@ class TLS_Client final : public Command, public Botan::TLS::Callbacks
          : Command("tls_client host --port=443 --print-certs --policy=default "
                    "--skip-system-cert-store --trusted-cas= --tls-version=default "
                    "--session-db= --session-db-pass= --next-protocols= --type=tcp "
-                   "--client-cert= --client-cert-key=")
+                   "--client-cert= --client-cert-key= --debug")
          {
          init_sockets();
          }
@@ -180,6 +180,11 @@ class TLS_Client final : public Command, public Botan::TLS::Callbacks
                   continue;
                   }
 
+               if(flag_set("debug"))
+                  {
+                  output() << ">> " << Botan::hex_encode(buf, got) << "\n";
+                  }
+
                client.received_data(buf, got);
                }
 
@@ -315,6 +320,11 @@ class TLS_Client final : public Command, public Botan::TLS::Callbacks
             }
          }
 
+      void tls_session_activated() override
+         {
+         output() << "Handshake complete\n";
+         }
+
       bool tls_session_established(const Botan::TLS::Session& session) override
          {
          output() << "Handshake complete, " << session.version().to_string()
@@ -359,6 +369,11 @@ class TLS_Client final : public Command, public Botan::TLS::Callbacks
          {
          size_t offset = 0;
 
+         if(flag_set("debug"))
+            {
+            output() << "<< " << Botan::hex_encode(buf, length) << "\n";
+            }
+
          while(length)
             {
             ssize_t sent = ::send(m_sockfd, buf + offset, length, MSG_NOSIGNAL);
diff --git a/src/cli/tls_http_server.cpp b/src/cli/tls_http_server.cpp
index 9f9c0f94b..7cbf3376a 100644
--- a/src/cli/tls_http_server.cpp
+++ b/src/cli/tls_http_server.cpp
@@ -246,7 +246,7 @@ class TLS_Asio_HTTP_Session final : public std::enable_shared_from_this<TLS_Asio
 
          m_s2c.clear();
 
-         if(m_s2c_pending.empty() && m_tls.is_closed())
+         if(m_s2c_pending.empty() && m_tls.is_closed_for_writing())
             {
             m_client_socket.close();
             }
@@ -292,7 +292,7 @@ class TLS_Asio_HTTP_Session final : public std::enable_shared_from_this<TLS_Asio
                {
                const std::string http_summary = summarize_request(request);
 
-               const std::string report = m_session_summary + m_chello_summary + http_summary;
+               const std::string report = m_connection_summary + m_session_summary + m_chello_summary + http_summary;
 
                response << "HTTP/1.0 200 OK\r\n";
                response << "Server: " << Botan::version_string() << "\r\n";
@@ -340,12 +340,19 @@ class TLS_Asio_HTTP_Session final : public std::enable_shared_from_this<TLS_Asio
             }
          }
 
-      bool tls_session_established(const Botan::TLS::Session& session) override
+      void tls_session_activated() override
          {
          std::ostringstream strm;
 
          strm << "TLS negotiation with " << Botan::version_string() << " test server\n\n";
 
+         m_connection_summary = strm.str();
+         }
+
+      bool tls_session_established(const Botan::TLS::Session& session) override
+         {
+         std::ostringstream strm;
+
          strm << "Version: " << session.version().to_string() << "\n";
          strm << "Ciphersuite: " << session.ciphersuite().to_string() << "\n";
          if(session.session_id().empty() == false)
@@ -413,6 +420,7 @@ class TLS_Asio_HTTP_Session final : public std::enable_shared_from_this<TLS_Asio
       std::unique_ptr<Botan::RandomNumberGenerator> m_rng;
       Botan::TLS::Server m_tls;
       std::string m_chello_summary;
+      std::string m_connection_summary;
       std::string m_session_summary;
       std::unique_ptr<HTTP_Parser> m_http_parser;
 
diff --git a/src/cli/tls_proxy.cpp b/src/cli/tls_proxy.cpp
index c1d1136cb..af7c72671 100644
--- a/src/cli/tls_proxy.cpp
+++ b/src/cli/tls_proxy.cpp
@@ -313,10 +313,8 @@ class tls_proxy_session final : public std::enable_shared_from_this<tls_proxy_se
                            boost::asio::placeholders::bytes_transferred)));
          }
 
-      bool tls_session_established(const Botan::TLS::Session& session) override
+      void tls_session_activated() override
          {
-         m_hostname = session.server_info().hostname();
-
          auto onConnect = [this](boost::system::error_code ec, tcp::resolver::iterator /*endpoint*/)
             {
             if(ec)
@@ -328,6 +326,12 @@ class tls_proxy_session final : public std::enable_shared_from_this<tls_proxy_se
             proxy_write_to_server(nullptr, 0);
             };
          async_connect(m_server_socket, m_server_endpoints, onConnect);
+         }
+
+      bool tls_session_established(const Botan::TLS::Session& session) override
+         {
+         m_hostname = session.server_info().hostname();
+
          return true;
          }
 
diff --git a/src/fuzzer/tls_server.cpp b/src/fuzzer/tls_server.cpp
index a31bde83c..202db822d 100644
--- a/src/fuzzer/tls_server.cpp
+++ b/src/fuzzer/tls_server.cpp
@@ -112,6 +112,10 @@ class Fuzzer_TLS_Server_Creds : public Botan::Credentials_Manager
 class Fuzzer_TLS_Policy : public Botan::TLS::Policy
    {
    public:
+      // TODO: Enable this once the TLS 1.3 server implementation is ready.
+      //       Maybe even build individual fuzz targets for different versions.
+      bool allow_tls13() const override { return false; }
+
       std::vector<uint16_t> ciphersuite_list(Botan::TLS::Protocol_Version) const override
          {
          std::vector<uint16_t> ciphersuites;
diff --git a/src/lib/math/bigint/big_io.cpp b/src/lib/math/bigint/big_io.cpp
index 7314e5c02..ae2855fcc 100644
--- a/src/lib/math/bigint/big_io.cpp
+++ b/src/lib/math/bigint/big_io.cpp
@@ -22,15 +22,22 @@ std::ostream& operator<<(std::ostream& stream, const BigInt& n)
 
    const size_t base = (stream_flags & std::ios::hex) ? 16 : 10;
 
-   std::string enc;
-
-   if(base == 10)
-      enc = n.to_dec_string();
+   if(n.is_zero())
+      stream.write("0", 1);
    else
-      enc = n.to_hex_string();
+      {
+      if(n.is_negative())
+         stream.write("-", 1);
+
+      std::string enc;
 
-   stream.write(enc.data(), enc.size());
+      if(base == 10)
+         enc = n.to_dec_string();
+      else
+         enc = n.to_hex_string();
 
+      stream.write(enc.data(), enc.size());
+      }
    if(!stream.good())
       throw Stream_IO_Error("BigInt output operator has failed");
    return stream;
diff --git a/src/lib/math/bigint/bigint.h b/src/lib/math/bigint/bigint.h
index 84ca2b46e..cd7195e73 100644
--- a/src/lib/math/bigint/bigint.h
+++ b/src/lib/math/bigint/bigint.h
@@ -500,22 +500,11 @@ class BOTAN_PUBLIC_API(2,0) BigInt final
      /**
      * Convert this value to a decimal string.
      * Warning: decimal conversions are relatively slow
-     *
-     * If the integer is zero then "0" is returned.
-     * If the integer is negative then "-" is prefixed.
      */
      std::string to_dec_string() const;
 
      /**
      * Convert this value to a hexadecimal string.
-     *
-     * If the integer is negative then "-" is prefixed.
-     * Then a prefix of "0x" is added.
-     * Follows is a sequence of hexadecimal characters in uppercase.
-     *
-     * The number of hexadecimal characters is always an even number,
-     * with a zero prefix being included if necessary.
-     * For example encoding the integer "5" results in "0x05"
      */
      std::string to_hex_string() const;
 
diff --git a/src/lib/tls/asio/asio_stream.h b/src/lib/tls/asio/asio_stream.h
index abd99119f..4a5589999 100644
--- a/src/lib/tls/asio/asio_stream.h
+++ b/src/lib/tls/asio/asio_stream.h
@@ -723,7 +723,7 @@ class Stream
                                 m_context.m_policy,
                                 m_context.m_rng,
                                 m_context.m_server_info,
-                                Protocol_Version::latest_tls_version()));
+                                m_context.m_policy.latest_supported_version(false /* no DTLS */)));
                   }
                else
                   {
diff --git a/src/lib/tls/msg_cert_verify.cpp b/src/lib/tls/msg_cert_verify.cpp
index e4ec7ce1b..dd00be4dd 100644
--- a/src/lib/tls/msg_cert_verify.cpp
+++ b/src/lib/tls/msg_cert_verify.cpp
@@ -161,8 +161,7 @@ Certificate_Verify_13::Certificate_Verify_13(
    {
    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
+   BOTAN_ASSERT_NOMSG(m_scheme.is_compatible_with(Protocol_Version::TLS_V13));
 
    m_signature =
       callbacks.tls_sign_message(key,
diff --git a/src/lib/tls/msg_client_hello.cpp b/src/lib/tls/msg_client_hello.cpp
index 05362904d..802d0f489 100644
--- a/src/lib/tls/msg_client_hello.cpp
+++ b/src/lib/tls/msg_client_hello.cpp
@@ -15,6 +15,7 @@
 #include <botan/tls_callbacks.h>
 #include <botan/rng.h>
 #include <botan/hash.h>
+#include <botan/credentials_manager.h>
 #include <botan/tls_version.h>
 
 #include <botan/internal/stl_util.h>
@@ -29,6 +30,7 @@
 #endif
 
 #include <chrono>
+#include <iterator>
 
 namespace Botan::TLS {
 
@@ -561,6 +563,8 @@ Client_Hello_12::Client_Hello_12(Handshake_IO& io,
 Client_Hello_13::Client_Hello_13(std::unique_ptr<Client_Hello_Internal> data)
    : Client_Hello(std::move(data))
    {
+   const auto& exts = m_data->extensions;
+
    // RFC 8446 4.1.2
    //    TLS 1.3 ClientHellos are identified as having a legacy_version of
    //    0x0303 and a "supported_versions" extension present with 0x0304 as the
@@ -568,12 +572,12 @@ Client_Hello_13::Client_Hello_13(std::unique_ptr<Client_Hello_Internal> data)
    //
    // Note that we already checked for "supported_versions" before entering this
    // c'tor in `Client_Hello_13::parse()`. This is just to be doubly sure.
-   BOTAN_ASSERT_NOMSG(m_data->extensions.has<Supported_Versions>());
+   BOTAN_ASSERT_NOMSG(exts.has<Supported_Versions>());
 
    // RFC 8446 4.2.1
    //    Servers MAY abort the handshake upon receiving a ClientHello with
    //    legacy_version 0x0304 or later.
-   if(m_data->legacy_version != Protocol_Version::TLS_V12)
+   if(m_data->legacy_version.is_tls_13_or_later())
       {
       throw TLS_Exception(Alert::DECODE_ERROR,
                           "TLS 1.3 Client Hello has invalid legacy_version");
@@ -587,8 +591,98 @@ Client_Hello_13::Client_Hello_13(std::unique_ptr<Client_Hello_Internal> data)
    if(m_data->comp_methods.size() != 1 || m_data->comp_methods.front() != 0)
       {
       throw TLS_Exception(Alert::ILLEGAL_PARAMETER,
-                          "Client sent TLS 1.3 Client Hello with compression "
-                          "method != null");
+                          "Client did not offer NULL compression");
+      }
+
+   // RFC 8446 4.2.9
+   //    A client MUST provide a "psk_key_exchange_modes" extension if it
+   //    offers a "pre_shared_key" extension. If clients offer "pre_shared_key"
+   //    without a "psk_key_exchange_modes" extension, servers MUST abort
+   //    the handshake.
+   if(exts.has<PSK>())
+      {
+      if(!exts.has<PSK_Key_Exchange_Modes>())
+         {
+         throw TLS_Exception(Alert::MISSING_EXTENSION,
+                             "Client Hello offered a PSK without a psk_key_exchange_modes extension");
+         }
+      }
+
+   // RFC 8446 9.2
+   //    [A TLS 1.3 ClientHello] message MUST meet the following requirements:
+   //
+   //     -  If not containing a "pre_shared_key" extension, it MUST contain
+   //        both a "signature_algorithms" extension and a "supported_groups"
+   //        extension.
+   //
+   //     -  If containing a "supported_groups" extension, it MUST also contain
+   //        a "key_share" extension, and vice versa.  An empty
+   //        KeyShare.client_shares vector is permitted.
+   //
+   //    Servers receiving a ClientHello which does not conform to these
+   //    requirements MUST abort the handshake with a "missing_extension"
+   //    alert.
+   if(!exts.has<PSK>())
+      {
+      if(!exts.has<Supported_Groups>() || !exts.has<Signature_Algorithms>())
+         {
+         throw TLS_Exception(Alert::MISSING_EXTENSION,
+                             "Non-PSK Client Hello did not contain supported_groups and signature_algorithms extensions");
+         }
+
+      }
+   if(exts.has<Supported_Groups>() != exts.has<Key_Share>())
+      {
+      throw TLS_Exception(Alert::MISSING_EXTENSION,
+                          "Client Hello must either contain both key_share and supported_groups extensions or neither");
+      }
+
+   if(exts.has<Key_Share>())
+      {
+      const auto supported_ext = exts.get<Supported_Groups>();
+      BOTAN_ASSERT_NONNULL(supported_ext);
+      const auto supports = supported_ext->groups();
+      const auto offers = exts.get<Key_Share>()->offered_groups();
+
+      // RFC 8446 4.2.8
+      //    Each KeyShareEntry value MUST correspond to a group offered in the
+      //    "supported_groups" extension and MUST appear in the same order.
+      //    [...]
+      //    Clients MUST NOT offer any KeyShareEntry values for groups not
+      //    listed in the client's "supported_groups" extension.
+      //
+      // Note: We can assume that both `offers` and `supports` are unique lists
+      //       as this is ensured in the parsing code of the extensions.
+      auto found_in_supported_groups = [&supports,support_offset = -1](auto group) mutable
+         {
+         const auto i = std::find(supports.begin(), supports.end(), group);
+         if(i == supports.end())
+            {
+            return false;
+            }
+
+         const auto found_at = std::distance(supports.begin(), i);
+         if(found_at <= support_offset)
+            {
+            return false;  // The order that groups appear in "key_share" and
+                           // "supported_groups" must be the same
+            }
+
+         support_offset = static_cast<decltype(support_offset)>(found_at);
+         return true;
+         };
+
+      for(const auto offered : offers)
+         {
+         // RFC 8446 4.2.8
+         //    Servers MAY check for violations of these rules and abort the
+         //    handshake with an "illegal_parameter" alert if one is violated.
+         if(!found_in_supported_groups(offered))
+            {
+            throw TLS_Exception(Alert::ILLEGAL_PARAMETER,
+                                "Offered key exchange groups do not align with claimed supported groups");
+            }
+         }
       }
 
    // TODO: Reject oid_filters extension if found (which is the only known extension that
@@ -762,6 +856,106 @@ void Client_Hello_13::retry(const Hello_Retry_Request& hrr,
       }
    }
 
+void Client_Hello_13::validate_updates(const Client_Hello_13& new_ch)
+   {
+   // RFC 8446 4.1.2
+   //    The client will also send a ClientHello when the server has responded
+   //    to its ClientHello with a HelloRetryRequest. In that case, the client
+   //    MUST send the same ClientHello without modification, except as follows:
+
+   if(m_data->session_id != new_ch.m_data->session_id ||
+      m_data->random != new_ch.m_data->random ||
+      m_data->suites != new_ch.m_data->suites ||
+      m_data->comp_methods != new_ch.m_data->comp_methods)
+      {
+      throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "Client Hello core values changed after Hello Retry Request");
+      }
+
+   const auto oldexts = extension_types();
+   const auto newexts = new_ch.extension_types();
+
+   // Check that extension omissions are justified
+   for(const auto oldext : oldexts)
+      {
+      if(!newexts.contains(oldext))
+         {
+         const auto ext = extensions().get(oldext);
+
+         // We don't make any assumptions about unimplemented extensions.
+         if(!ext->is_implemented())
+            continue;
+
+         // RFC 8446 4.1.2
+         //    Removing the "early_data" extension (Section 4.2.10) if one was
+         //    present.  Early data is not permitted after a HelloRetryRequest.
+         if(oldext == EarlyDataIndication::static_type())
+            continue;
+
+         // RFC 8446 4.1.2
+         //    Optionally adding, removing, or changing the length of the
+         //    "padding" extension.
+         //
+         // TODO: implement the Padding extension
+         // if(oldext == Padding::static_type())
+         //    continue;
+
+         throw TLS_Exception(Alert::MISSING_EXTENSION, "Extension removed in updated Client Hello");
+         }
+      }
+
+   // Check that extension additions are justified
+   for(const auto newext : newexts)
+      {
+      if(!oldexts.contains(newext))
+         {
+         const auto ext = new_ch.extensions().get(newext);
+
+         // We don't make any assumptions about unimplemented extensions.
+         if(!ext->is_implemented())
+            continue;
+
+         // RFC 8446 4.1.2
+         //    Including a "cookie" extension if one was provided in the
+         //    HelloRetryRequest.
+         if(newext == Cookie::static_type())
+            continue;
+
+         // RFC 8446 4.1.2
+         //    Optionally adding, removing, or changing the length of the
+         //    "padding" extension.
+         //
+         // TODO: implement the Padding extension
+         // if(newext == Padding::static_type())
+         //    continue;
+
+         throw TLS_Exception(Alert::UNSUPPORTED_EXTENSION, "Added an extension in updated Client Hello");
+         }
+      }
+
+   // RFC 8446 4.1.2
+   //    Removing the "early_data" extension (Section 4.2.10) if one was
+   //    present.  Early data is not permitted after a HelloRetryRequest.
+   if(new_ch.extensions().has<EarlyDataIndication>())
+      {
+      throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "Updated Client Hello indicates early data");
+      }
+
+   // TODO: Contents of extensions are not checked for update compatibility, see:
+   //
+   // RFC 8446 4.1.2
+   //    If a "key_share" extension was supplied in the HelloRetryRequest,
+   //    replacing the list of shares with a list containing a single
+   //    KeyShareEntry from the indicated group.
+   //
+   //    Updating the "pre_shared_key" extension if present by recomputing
+   //    the "obfuscated_ticket_age" and binder values and (optionally)
+   //    removing any PSKs which are incompatible with the server's
+   //    indicated cipher suite.
+   //
+   //    Optionally adding, removing, or changing the length of the
+   //    "padding" extension.
+   }
+
 void Client_Hello_13::calculate_psk_binders(Transcript_Hash_State ths)
    {
    auto psk = m_data->extensions.get<PSK>();
@@ -781,6 +975,109 @@ 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
+   //    The "supported_versions" extension is used by the client to indicate
+   //    which versions of TLS it supports and by the server to indicate which
+   //    version it is using. The extension contains a list of supported
+   //    versions in preference order, with the most preferred version first.
+   const auto supvers = m_data->extensions.get<Supported_Versions>();
+   BOTAN_ASSERT_NONNULL(supvers);
+
+   std::optional<Protocol_Version> result;
+
+   for(const auto& v : supvers->versions())
+      {
+      // RFC 8446 4.2.1
+      //    Servers MUST only select a version of TLS present in that extension
+      //    and MUST ignore any unknown versions that are present in that
+      //    extension.
+      if(!v.known_version() || !policy.acceptable_protocol_version(v))
+         { continue; }
+
+      result = (result.has_value())
+         ? std::optional(std::max(result.value(), v))
+         : std::optional(v);
+      }
+
+   return result;
+   }
+
 #endif // BOTAN_HAS_TLS_13
 
 }
diff --git a/src/lib/tls/msg_server_hello.cpp b/src/lib/tls/msg_server_hello.cpp
index a3b51c438..0a2a98887 100644
--- a/src/lib/tls/msg_server_hello.cpp
+++ b/src/lib/tls/msg_server_hello.cpp
@@ -19,6 +19,7 @@
 #include <botan/internal/tls_session_key.h>
 #include <botan/internal/tls_handshake_io.h>
 #include <botan/internal/tls_handshake_hash.h>
+#include <botan/internal/stl_util.h>
 
 #include <array>
 
@@ -30,7 +31,7 @@ const uint64_t DOWNGRADE_TLS11 = 0x444F574E47524400;
 const uint64_t DOWNGRADE_TLS12 = 0x444F574E47524401;
 
 // SHA-256("HelloRetryRequest")
-const std::array<uint8_t, 32> HELLO_RETRY_REQUEST_MARKER =
+const std::vector<uint8_t> HELLO_RETRY_REQUEST_MARKER =
    {
    0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11, 0xBE, 0x1D, 0x8C, 0x02,
    0x1E, 0x65, 0xB8, 0x91, 0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E,
@@ -50,6 +51,23 @@ make_server_hello_random(RandomNumberGenerator& rng,
    {
    BOTAN_UNUSED(offered_version);
    auto random = make_hello_random(rng, cb, policy);
+
+   // RFC 8446 4.1.3
+   //    TLS 1.3 has a downgrade protection mechanism embedded in the server's
+   //    random value. TLS 1.3 servers which negotiate TLS 1.2 or below in
+   //    response to a ClientHello MUST set the last 8 bytes of their Random
+   //    value specially in their ServerHello.
+   //
+   //    If negotiating TLS 1.2, TLS 1.3 servers MUST set the last 8 bytes of
+   //    their Random value to the bytes: [DOWNGRADE_TLS12]
+   if(offered_version.is_pre_tls_13() && policy.allow_tls13())
+      {
+      constexpr size_t downgrade_signal_length = sizeof(DOWNGRADE_TLS12);
+      BOTAN_ASSERT_NOMSG(random.size() >= downgrade_signal_length);
+      auto lastbytes = random.data() + random.size() - downgrade_signal_length;
+      store_be(DOWNGRADE_TLS12, lastbytes);
+      }
+
    return random;
    }
 
@@ -106,10 +124,12 @@ class Server_Hello_Internal
                             std::vector<uint8_t> sid,
                             std::vector<uint8_t> r,
                             const uint16_t cs,
-                            const uint8_t cm)
+                            const uint8_t cm,
+                            bool is_hrr = false)
          : legacy_version(lv)
          , session_id(std::move(sid))
          , random(std::move(r))
+         , is_hello_retry_request(is_hrr)
          , ciphersuite(cs)
          , comp_method(cm) {}
 
@@ -477,6 +497,41 @@ std::vector<uint8_t> Server_Hello_Done::serialize() const
 
 Server_Hello_13::Server_Hello_Tag Server_Hello_13::as_server_hello;
 Server_Hello_13::Hello_Retry_Request_Tag Server_Hello_13::as_hello_retry_request;
+Server_Hello_13::Hello_Retry_Request_Creation_Tag Server_Hello_13::as_new_hello_retry_request;
+
+std::variant<Hello_Retry_Request, Server_Hello_13> Server_Hello_13::create(const Client_Hello_13& ch,
+      bool hello_retry_request_allowed,
+      RandomNumberGenerator& rng, const Policy& policy, Callbacks& cb)
+   {
+   const auto& exts = ch.extensions();
+   const auto& supported_by_client = exts.get<Supported_Groups>()->groups();
+   const auto& offered_by_client = exts.get<Key_Share>()->offered_groups();
+   const auto selected_group = policy.choose_key_exchange_group(supported_by_client, offered_by_client);
+
+   // RFC 8446 4.1.1
+   //    If there is no overlap between the received "supported_groups" and the
+   //    groups supported by the server, then the server MUST abort the
+   //    handshake with a "handshake_failure" or an "insufficient_security" alert.
+   if(selected_group == Named_Group::NONE)
+      {
+      throw TLS_Exception(Alert::HANDSHAKE_FAILURE, "Client did not offer any acceptable group");
+      }
+
+   if(!value_exists(offered_by_client, selected_group))
+      {
+      // RFC 8446 4.1.4
+      //    If a client receives a second HelloRetryRequest in the same
+      //    connection (i.e., where the ClientHello was itself in response to a
+      //    HelloRetryRequest), it MUST abort the handshake with an
+      //    "unexpected_message" alert.
+      BOTAN_STATE_CHECK(hello_retry_request_allowed);
+      return Hello_Retry_Request(ch, selected_group, policy, cb);
+      }
+   else
+      {
+      return Server_Hello_13(ch, selected_group, rng, cb, policy);
+      }
+   }
 
 std::variant<Hello_Retry_Request, Server_Hello_13, Server_Hello_12>
 Server_Hello_13::parse(const std::vector<uint8_t>& buf)
@@ -638,6 +693,66 @@ Server_Hello_13::Server_Hello_13(std::unique_ptr<Server_Hello_Internal> data, Se
       }
    }
 
+Server_Hello_13::Server_Hello_13(std::unique_ptr<Server_Hello_Internal> data, Hello_Retry_Request_Creation_Tag)
+   : Server_Hello(std::move(data)) {}
+
+namespace {
+
+uint16_t choose_ciphersuite(const Client_Hello_13& ch, const Policy& policy)
+   {
+   auto pref_list = ch.ciphersuites();
+   // TODO: DTLS might need to make this version dynamic
+   auto other_list = policy.ciphersuite_list(Protocol_Version::TLS_V13);
+
+   if(policy.server_uses_own_ciphersuite_preferences())
+      {
+      std::swap(pref_list, other_list);
+      }
+
+   for(auto suite_id : pref_list)
+      {
+      if(value_exists(other_list, suite_id))
+         { return suite_id; }
+      }
+
+   // RFC 8446 4.1.1
+   //     If the server is unable to negotiate a supported set of parameters
+   //     [...], it MUST abort the handshake with either a "handshake_failure"
+   //     or "insufficient_security" fatal alert [...].
+   throw TLS_Exception(Alert::HANDSHAKE_FAILURE,
+                       "Can't agree on a ciphersuite with client");
+   }
+}
+
+Server_Hello_13::Server_Hello_13(const Client_Hello_13& ch,
+                                 std::optional<Named_Group> key_exchange_group,
+                                 RandomNumberGenerator& rng,
+                                 Callbacks& cb,
+                                 const Policy& policy)
+   : Server_Hello(std::make_unique<Server_Hello_Internal>(
+                     Protocol_Version::TLS_V12,
+                     ch.session_id(),
+                     make_server_hello_random(rng, Protocol_Version::TLS_V13, cb, policy),
+                     choose_ciphersuite(ch, policy),
+                     uint8_t(0) /* compression method */
+                  ))
+   {
+   // RFC 8446 4.2.1
+   //    A server which negotiates TLS 1.3 MUST respond by sending a
+   //    "supported_versions" extension containing the selected version
+   //    value (0x0304). It MUST set the ServerHello.legacy_version field to
+   //     0x0303 (TLS 1.2).
+   //
+   // Note that the legacy version (TLS 1.2) is set in this constructor's
+   // initializer list, accordingly.
+   m_data->extensions.add(new Supported_Versions(Protocol_Version::TLS_V13));
+
+   if(key_exchange_group.has_value())
+      { m_data->extensions.add(new Key_Share(key_exchange_group.value(), cb, rng)); }
+
+   cb.tls_modify_extensions(m_data->extensions, SERVER, type());
+   }
+
 std::optional<Protocol_Version> Server_Hello_13::random_signals_downgrade() const
    {
    const uint64_t last8 = load_be<uint64_t>(m_data->random.data(), 3);
@@ -661,6 +776,44 @@ Protocol_Version Server_Hello_13::selected_version() const
 Hello_Retry_Request::Hello_Retry_Request(std::unique_ptr<Server_Hello_Internal> data)
    : Server_Hello_13(std::move(data), Server_Hello_13::as_hello_retry_request) {}
 
+Hello_Retry_Request::Hello_Retry_Request(const Client_Hello_13& ch, Named_Group selected_group, const Policy& policy, Callbacks& cb)
+   : Server_Hello_13(std::make_unique<Server_Hello_Internal>(
+                        Protocol_Version::TLS_V12 /* legacy_version */,
+                        ch.session_id(),
+                        HELLO_RETRY_REQUEST_MARKER,
+                        choose_ciphersuite(ch, policy),
+                        uint8_t(0) /* compression method */,
+                        true /* is Hello Retry Request */
+                     ), as_new_hello_retry_request)
+   {
+   // RFC 8446 4.1.4
+   //     As with the ServerHello, a HelloRetryRequest MUST NOT contain any
+   //     extensions that were not first offered by the client in its
+   //     ClientHello, with the exception of optionally the "cookie" [...]
+   //     extension.
+   BOTAN_STATE_CHECK(ch.extensions().has<Supported_Groups>());
+   BOTAN_STATE_CHECK(ch.extensions().has<Key_Share>());
+
+   BOTAN_STATE_CHECK(!value_exists(ch.extensions().get<Key_Share>()->offered_groups(), selected_group));
+
+   // RFC 8446 4.1.4
+   //    The server's extensions MUST contain "supported_versions".
+   //
+   // RFC 8446 4.2.1
+   //    A server which negotiates TLS 1.3 MUST respond by sending a
+   //    "supported_versions" extension containing the selected version
+   //    value (0x0304). It MUST set the ServerHello.legacy_version field to
+   //    0x0303 (TLS 1.2).
+   //
+   // Note that the legacy version (TLS 1.2) is set in this constructor's
+   // initializer list, accordingly.
+   m_data->extensions.add(new Supported_Versions(Protocol_Version::TLS_V13));
+
+   m_data->extensions.add(new Key_Share(selected_group));
+
+   cb.tls_modify_extensions(m_data->extensions, SERVER, type());
+   }
+
 #endif // BOTAN_HAS_TLS_13
 
 }
diff --git a/src/lib/tls/tls12/tls_server_impl_12.cpp b/src/lib/tls/tls12/tls_server_impl_12.cpp
index 3e0abf397..a3e2e3bd6 100644
--- a/src/lib/tls/tls12/tls_server_impl_12.cpp
+++ b/src/lib/tls/tls12/tls_server_impl_12.cpp
@@ -275,6 +275,18 @@ Server_Impl_12::Server_Impl_12(Callbacks& callbacks,
    {
    }
 
+Server_Impl_12::Server_Impl_12(const Channel_Impl::Downgrade_Information& downgrade_info) :
+   Channel_Impl_12(downgrade_info.callbacks,
+                   downgrade_info.session_manager,
+                   downgrade_info.rng,
+                   downgrade_info.policy,
+                   true /* is_server*/,
+                   false /* TLS 1.3 does not support DTLS yet */,
+                   downgrade_info.io_buffer_size),
+   m_creds(downgrade_info.creds)
+   {
+   }
+
 std::unique_ptr<Handshake_State> Server_Impl_12::new_handshake_state(std::unique_ptr<Handshake_IO> io)
    {
    std::unique_ptr<Handshake_State> state(new Server_Handshake_State(std::move(io), callbacks()));
diff --git a/src/lib/tls/tls12/tls_server_impl_12.h b/src/lib/tls/tls12/tls_server_impl_12.h
index 5f491eff9..5b4158d24 100644
--- a/src/lib/tls/tls12/tls_server_impl_12.h
+++ b/src/lib/tls/tls12/tls_server_impl_12.h
@@ -57,6 +57,9 @@ class Server_Impl_12 : public Channel_Impl_12
                               RandomNumberGenerator& rng,
                               bool is_datagram = false,
                               size_t reserved_io_buffer_size = TLS::Channel::IO_BUF_DEFAULT_SIZE);
+
+      explicit Server_Impl_12(const Channel_Impl::Downgrade_Information& downgrade_info);
+
    private:
       /**
       * Return the protocol notification set by the client (using the
diff --git a/src/lib/tls/tls13/info.txt b/src/lib/tls/tls13/info.txt
index 69c0541ac..148d8954a 100644
--- a/src/lib/tls/tls13/info.txt
+++ b/src/lib/tls/tls13/info.txt
@@ -16,6 +16,7 @@ tls_client_impl_13.h
 tls_handshake_layer_13.h
 tls_handshake_state_13.h
 tls_record_layer_13.h
+tls_server_impl_13.h
 tls_transcript_hash_13.h
 </header:internal>
 
diff --git a/src/lib/tls/tls13/msg_encrypted_extensions.cpp b/src/lib/tls/tls13/msg_encrypted_extensions.cpp
index 6c856e5de..09d451de4 100644
--- a/src/lib/tls/tls13/msg_encrypted_extensions.cpp
+++ b/src/lib/tls/tls13/msg_encrypted_extensions.cpp
@@ -8,10 +8,63 @@
 
 #include <botan/tls_messages.h>
 #include <botan/tls_exceptn.h>
+#include <botan/tls_callbacks.h>
 #include <botan/internal/tls_reader.h>
 
 namespace Botan::TLS {
 
+Encrypted_Extensions::Encrypted_Extensions(const Client_Hello_13& client_hello, const Policy& policy, Callbacks& cb)
+   {
+   const auto& exts = client_hello.extensions();
+
+   // RFC 8446 4.2.7
+   //    As of TLS 1.3, servers are permitted to send the "supported_groups"
+   //    extension to the client.  Clients [...] MAY use the information
+   //    learned from a successfully completed handshake to change what groups
+   //    they use in their "key_share" extension in subsequent connections.
+   if(exts.has<Supported_Groups>())
+      {
+      m_extensions.add(new Supported_Groups(policy.key_exchange_groups()));
+      }
+
+   const auto record_size_limit = policy.record_size_limit();
+   const auto max_record_size = MAX_PLAINTEXT_SIZE + 1 /* encrypted content type byte */;
+   if(exts.has<Record_Size_Limit>())
+      {
+      // RFC 8449 4
+      //    Endpoints SHOULD advertise the "record_size_limit" extension, even
+      //    if they have no need to limit the size of records. [...]  For
+      //    servers, this allows clients to know that their limit will be
+      //    respected.
+      m_extensions.add(new Record_Size_Limit(record_size_limit.value_or(max_record_size)));
+      }
+   else if(record_size_limit.has_value() && record_size_limit.value() < max_record_size)
+      {
+      // RFC 8449 4
+      //    Endpoints SHOULD advertise the "record_size_limit" extension, even if
+      //    they have no need to limit the size of records. For clients, this
+      //    allows servers to advertise a limit at their discretion.
+      throw TLS_Exception(Alert::MISSING_EXTENSION,
+                          "Server cannot enforce record size limit without the client supporting it");
+      }
+
+   // RFC 6066 3
+   //    A server that receives a client hello containing the "server_name"
+   //    extension [...] SHALL include an extension of type "server_name" in the
+   //    (extended) server hello. The "extension_data" field of this extension
+   //    SHALL be empty.
+   if(exts.has<Server_Name_Indicator>())
+      {
+      m_extensions.add(new Server_Name_Indicator(""));
+      }
+
+   // TODO: Implement handling for (at least)
+   //       * SRTP
+   //       * ALPN
+
+   cb.tls_modify_extensions(m_extensions, SERVER, type());
+   }
+
 Encrypted_Extensions::Encrypted_Extensions(const std::vector<uint8_t>& buf)
    {
    TLS_Data_Reader reader("encrypted extensions reader", buf);
@@ -58,4 +111,9 @@ Encrypted_Extensions::Encrypted_Extensions(const std::vector<uint8_t>& buf)
 
    }
 
+std::vector<uint8_t> Encrypted_Extensions::serialize() const
+   {
+   return m_extensions.serialize(Connection_Side::SERVER);
+   }
+
 }
diff --git a/src/lib/tls/tls13/tls_channel_impl_13.h b/src/lib/tls/tls13/tls_channel_impl_13.h
index 12eeb282e..76e08b8c7 100644
--- a/src/lib/tls/tls13/tls_channel_impl_13.h
+++ b/src/lib/tls/tls13/tls_channel_impl_13.h
@@ -14,6 +14,7 @@
 #include <botan/internal/tls_record_layer_13.h>
 #include <botan/internal/tls_handshake_layer_13.h>
 #include <botan/internal/tls_transcript_hash_13.h>
+#include <botan/internal/stl_util.h>
 
 namespace Botan::TLS {
 
@@ -192,12 +193,20 @@ class Channel_Impl_13 : public Channel_Impl
        */
       void opportunistically_update_traffic_keys() { m_opportunistic_key_update = true; }
 
-      std::vector<uint8_t> send_handshake_message(const Handshake_Message_13_Ref message)
+      template<typename... MsgTs>
+      std::vector<uint8_t> send_handshake_message(const std::variant<MsgTs...>& message)
          {
          return aggregate_handshake_messages()
-                   .add(message)
+                   .add(generalize_to<Handshake_Message_13_Ref>(message))
                    .send();
          }
+
+      template<typename MsgT>
+      std::vector<uint8_t> send_handshake_message(std::reference_wrapper<MsgT> message)
+         {
+         return send_handshake_message(generalize_to<Handshake_Message_13_Ref>(message));
+         }
+
       void send_post_handshake_message(const Post_Handshake_Message_13 message);
       void send_dummy_change_cipher_spec();
 
@@ -229,7 +238,9 @@ class Channel_Impl_13 : public Channel_Impl
       std::unique_ptr<Cipher_State> m_cipher_state;
 
       /**
-       * Indicate that this (Client_Impl_13) instance has to expect a downgrade to TLS 1.2.
+       * Indicate that we have to expect a downgrade to TLS 1.2. In which case the current
+       * implementation (i.e. Client_Impl_13 or Server_Impl_13) will need to be replaced
+       * by their respective counter parts.
        *
        * This will prepare an internal structure where any information required to downgrade
        * can be preserved.
diff --git a/src/lib/tls/tls13/tls_client_impl_13.cpp b/src/lib/tls/tls13/tls_client_impl_13.cpp
index 3bd1c61ed..170a1e355 100644
--- a/src/lib/tls/tls13/tls_client_impl_13.cpp
+++ b/src/lib/tls/tls13/tls_client_impl_13.cpp
@@ -376,7 +376,7 @@ void Client_Impl_13::handle(const Hello_Retry_Request& hrr)
 
    callbacks().tls_examine_extensions(hrr.extensions(), SERVER, Handshake_Type::HELLO_RETRY_REQUEST);
 
-   send_handshake_message(ch);
+   send_handshake_message(std::reference_wrapper(ch));
 
    // RFC 8446 4.1.4
    //    If a client receives a second HelloRetryRequest in the same connection [...],
diff --git a/src/lib/tls/tls13/tls_extensions_key_share.cpp b/src/lib/tls/tls13/tls_extensions_key_share.cpp
index 1469a70b1..9990e13c8 100644
--- a/src/lib/tls/tls13/tls_extensions_key_share.cpp
+++ b/src/lib/tls/tls13/tls_extensions_key_share.cpp
@@ -8,6 +8,9 @@
 * Botan is released under the Simplified BSD License (see license.txt)
 */
 
+#include <functional>
+#include <iterator>
+
 #include <botan/tls_extensions.h>
 #include <botan/internal/tls_reader.h>
 #include <botan/tls_exceptn.h>
@@ -192,11 +195,16 @@ class Key_Share_Entry
       std::unique_ptr<Private_Key> m_private_key;
    };
 
+class Key_Share_ClientHello;
+
 class Key_Share_ServerHello
    {
    public:
       Key_Share_ServerHello(TLS_Data_Reader& reader, uint16_t)
          : m_server_share(reader) {}
+      Key_Share_ServerHello(Named_Group group, Callbacks& cb, RandomNumberGenerator& rng)
+         : m_server_share(group, cb, rng) {}
+
       ~Key_Share_ServerHello() = default;
 
       Key_Share_ServerHello(const Key_Share_ServerHello&) = delete;
@@ -220,6 +228,19 @@ class Key_Share_ServerHello
          return m_server_share;
          }
 
+      secure_vector<uint8_t> exchange(const Key_Share_ClientHello& client_shares, const Policy& policy, Callbacks& cb,
+                                      RandomNumberGenerator& rng) const;
+
+      std::vector<Named_Group> offered_groups() const
+         {
+         return {selected_group()};
+         }
+
+      Named_Group selected_group() const
+         {
+         return m_server_share.group();
+         }
+
       void erase()
          {
          m_server_share.erase();
@@ -234,12 +255,47 @@ class Key_Share_ClientHello
    public:
       Key_Share_ClientHello(TLS_Data_Reader& reader, uint16_t /* extension_size */)
          {
+         // This construction is a crutch to make working with the incoming
+         // TLS_Data_Reader bearable. Currently, this reader spans the entire
+         // Client_Hello message. Hence, if offset or length fields are skewed
+         // or maliciously fabricated, it is possible to read further than the
+         // bounds of the current extension.
+         // Note that this aplies to many locations in the code base.
+         //
+         // TODO: Overhaul the TLS_Data_Reader to allow for cheap "sub-readers"
+         //       that enforce read bounds of sub-structures while parsing.
          const auto client_key_share_length = reader.get_uint16_t();
          const auto read_bytes_so_far_begin = reader.read_so_far();
+         auto remaining = [&]
+            {
+            const auto read_so_far = reader.read_so_far() - read_bytes_so_far_begin;
+            BOTAN_STATE_CHECK(read_so_far <= client_key_share_length);
+            return client_key_share_length - read_so_far;
+            };
 
-         while(reader.has_remaining() && ((reader.read_so_far() - read_bytes_so_far_begin) < client_key_share_length))
+         while(reader.has_remaining() && remaining() > 0)
             {
-            m_client_shares.emplace_back(reader);
+            if(remaining() < 4)
+               {
+               throw TLS_Exception(Alert::DECODE_ERROR, "Not enough data to read another KeyShareEntry");
+               }
+
+            Key_Share_Entry new_entry(reader);
+
+            // RFC 8446 4.2.8
+            //    Clients MUST NOT offer multiple KeyShareEntry values for the same
+            //    group. [...]
+            //    Servers MAY check for violations of these rules and abort the
+            //    handshake with an "illegal_parameter" alert if one is violated.
+            if(std::find_if(m_client_shares.begin(), m_client_shares.end(),
+                            [&](const auto& entry) { return entry.group() == new_entry.group(); } )
+               != m_client_shares.end())
+               {
+               throw TLS_Exception(Alert::ILLEGAL_PARAMETER,
+                                   "Received multiple key share entries for the same group");
+               }
+
+            m_client_shares.emplace_back(std::move(new_entry));
             }
 
          if((reader.read_so_far() - read_bytes_so_far_begin) != client_key_share_length)
@@ -297,6 +353,36 @@ class Key_Share_ClientHello
          m_client_shares.emplace_back(to_offer, cb, rng);
          }
 
+      std::vector<Named_Group> offered_groups() const
+         {
+         std::vector<Named_Group> offered_groups;
+         offered_groups.reserve(m_client_shares.size());
+         for(const auto& share : m_client_shares)
+            offered_groups.push_back(share.group());
+         return offered_groups;
+         }
+
+      Named_Group selected_group() const
+         {
+         throw Invalid_Argument("Client Hello Key Share does not select a group");
+         }
+
+      std::optional<std::reference_wrapper<const Key_Share_Entry>>
+            find_matching_keyshare(const Key_Share_Entry& server_share) const
+         {
+         auto match = std::find_if(m_client_shares.cbegin(), m_client_shares.cend(), [&](const auto& offered)
+            {
+            return offered.group() == server_share.group();
+            });
+
+         if(match == m_client_shares.end())
+            {
+            return std::nullopt;
+            }
+
+         return *match;
+         }
+
       std::vector<uint8_t> serialize() const
          {
          std::vector<uint8_t> shares;
@@ -326,10 +412,7 @@ class Key_Share_ClientHello
          const auto& server_selected = server_share.get_singleton_entry();
 
          // find the client offer that matches the server offer
-         auto match = std::find_if(m_client_shares.cbegin(), m_client_shares.cend(), [&server_selected](const auto& offered)
-            {
-            return offered.group() == server_selected.group();
-            });
+         auto match = find_matching_keyshare(server_selected);
 
          // RFC 8446 4.2.8:
          //   [The KeyShareEntry in the ServerHello] MUST be in the same group
@@ -337,13 +420,12 @@ class Key_Share_ClientHello
          //   has selected for the negotiated key exchange.  Servers MUST NOT
          //   send a KeyShareEntry for any group not indicated in the client's
          //   "supported_groups" extension [...]
-         if(!value_exists(policy.key_exchange_groups(), server_selected.group()) ||
-               match == m_client_shares.cend())
+         if(!value_exists(policy.key_exchange_groups(), server_selected.group()) || !match.has_value())
             {
             throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "Server selected an unexpected key exchange group.");
             }
 
-         return match->exchange(server_selected, policy, cb, rng);
+         return match->get().exchange(server_selected, policy, cb, rng);
          }
 
       void erase()
@@ -356,6 +438,20 @@ class Key_Share_ClientHello
       std::vector<Key_Share_Entry> m_client_shares;
    };
 
+
+secure_vector<uint8_t> Key_Share_ServerHello::exchange(const Key_Share_ClientHello& client_shares, const Policy& policy,
+      Callbacks& cb,
+      RandomNumberGenerator& rng) const
+   {
+   const auto client_share = client_shares.find_matching_keyshare(m_server_share);
+
+   // The server hello was created based on the client hello's key share set.
+   BOTAN_ASSERT_NOMSG(client_share.has_value());
+
+   return m_server_share.exchange(client_share->get(), policy, cb, rng);
+   }
+
+
 class Key_Share_HelloRetryRequest
    {
    public:
@@ -372,6 +468,8 @@ class Key_Share_HelloRetryRequest
 
          m_selected_group = static_cast<Named_Group>(reader.get_uint16_t());
          }
+      Key_Share_HelloRetryRequest(Named_Group selected_group) :
+         m_selected_group(selected_group) {}
 
       ~Key_Share_HelloRetryRequest() = default;
 
@@ -387,11 +485,16 @@ class Key_Share_HelloRetryRequest
                   get_byte<1>(static_cast<uint16_t>(m_selected_group)) };
          }
 
-      Named_Group get_selected_group() const
+      Named_Group selected_group() const
          {
          return m_selected_group;
          }
 
+      std::vector<Named_Group> offered_groups() const
+         {
+         throw Invalid_Argument("Hello Retry Request never offers any key exchange groups");
+         }
+
       bool empty() const
          {
          return m_selected_group == Group_Params::NONE;
@@ -442,6 +545,14 @@ Key_Share::Key_Share(TLS_Data_Reader& reader,
 Key_Share::Key_Share(const Policy& policy, Callbacks& cb, RandomNumberGenerator& rng) :
    m_impl(std::make_unique<Key_Share_Impl>(Key_Share_ClientHello(policy, cb, rng))) {}
 
+// ServerHello
+Key_Share::Key_Share(Named_Group group, Callbacks& cb, RandomNumberGenerator& rng) :
+   m_impl(std::make_unique<Key_Share_Impl>(Key_Share_ServerHello(group, cb, rng))) {}
+
+// HelloRetryRequest
+Key_Share::Key_Share(Named_Group selected_group) :
+   m_impl(std::make_unique<Key_Share_Impl>(Key_Share_HelloRetryRequest(selected_group))) {}
+
 Key_Share::~Key_Share() = default;
 
 std::vector<uint8_t> Key_Share::serialize(Connection_Side /*whoami*/) const
@@ -467,7 +578,7 @@ secure_vector<uint8_t> Key_Share::exchange(const Key_Share& peer_keyshare,
          },
       [&](const Key_Share_ServerHello& sh, const Key_Share_ClientHello& ch)
          {
-         return ch.exchange(sh, policy, cb, rng);
+         return sh.exchange(ch, policy, cb, rng);
          },
       [](const auto&, const auto&) -> secure_vector<uint8_t>
          {
@@ -476,6 +587,21 @@ secure_vector<uint8_t> Key_Share::exchange(const Key_Share& peer_keyshare,
       }, m_impl->key_share, peer_keyshare.m_impl->key_share);
    }
 
+std::vector<Named_Group> Key_Share::offered_groups() const
+   {
+   return std::visit([](const auto& keyshare)
+         { return keyshare.offered_groups(); },
+         m_impl->key_share);
+   }
+
+
+Named_Group Key_Share::selected_group() const
+   {
+   return std::visit([](const auto& keyshare)
+         { return keyshare.selected_group(); },
+         m_impl->key_share);
+   }
+
 void Key_Share::retry_offer(const Key_Share& retry_request_keyshare,
                             const std::vector<Named_Group>& supported_groups,
                             Callbacks& cb,
@@ -485,7 +611,7 @@ void Key_Share::retry_offer(const Key_Share& retry_request_keyshare,
       {
       [&](Key_Share_ClientHello& ch, const Key_Share_HelloRetryRequest& hrr)
          {
-         auto selected = hrr.get_selected_group();
+         auto selected = hrr.selected_group();
          // RFC 8446 4.2.8
          //    [T]he selected_group field [MUST correspond] to a group which was provided in
          //    the "supported_groups" extension in the original ClientHello
diff --git a/src/lib/tls/tls13/tls_handshake_state_13.cpp b/src/lib/tls/tls13/tls_handshake_state_13.cpp
index 40114ec35..37fea16ad 100644
--- a/src/lib/tls/tls13/tls_handshake_state_13.cpp
+++ b/src/lib/tls/tls13/tls_handshake_state_13.cpp
@@ -11,73 +11,80 @@
 namespace Botan::TLS::Internal {
 
 Client_Hello_13& Handshake_State_13_Base::store(Client_Hello_13 client_hello, const bool)
-{
-    m_client_hello = std::move(client_hello);
-    return m_client_hello.value();
-}
+   {
+   if(m_client_hello)
+      {
+      // Make sure that the updated Client Hello is compatible to the initial one.
+      BOTAN_STATE_CHECK(has_hello_retry_request());
+      m_client_hello->validate_updates(client_hello);
+      }
+
+   m_client_hello = std::move(client_hello);
+   return m_client_hello.value();
+   }
 
 Client_Hello_12& Handshake_State_13_Base::store(Client_Hello_12 client_hello, const bool)
-{
-    m_client_hello_12 = std::move(client_hello);
-    return m_client_hello_12.value();
-}
+   {
+   m_client_hello_12 = std::move(client_hello);
+   return m_client_hello_12.value();
+   }
 
 Server_Hello_13& Handshake_State_13_Base::store(Server_Hello_13 server_hello, const bool)
-{
-    m_server_hello = std::move(server_hello);
-    return m_server_hello.value();
-}
+   {
+   m_server_hello = std::move(server_hello);
+   return m_server_hello.value();
+   }
 
 Server_Hello_12& Handshake_State_13_Base::store(Server_Hello_12 server_hello, const bool)
-{
-    m_server_hello_12 = std::move(server_hello);
-    return m_server_hello_12.value();
-}
+   {
+   m_server_hello_12 = std::move(server_hello);
+   return m_server_hello_12.value();
+   }
 
 Hello_Retry_Request& Handshake_State_13_Base::store(Hello_Retry_Request hello_retry_request, const bool)
-{
-    m_hello_retry_request = std::move(hello_retry_request);
-    return m_hello_retry_request.value();
-}
+   {
+   m_hello_retry_request = std::move(hello_retry_request);
+   return m_hello_retry_request.value();
+   }
 
 Encrypted_Extensions& Handshake_State_13_Base::store(Encrypted_Extensions encrypted_extensions, const bool)
-{
-    m_encrypted_extensions = std::move(encrypted_extensions);
-    return m_encrypted_extensions.value();
-}
+   {
+   m_encrypted_extensions = std::move(encrypted_extensions);
+   return m_encrypted_extensions.value();
+   }
 
 Certificate_Request_13& Handshake_State_13_Base::store(Certificate_Request_13 certificate_request, const bool)
-{
+   {
    m_certificate_request = std::move(certificate_request);
    return m_certificate_request.value();
-}
+   }
 
 
 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;
+                  ? 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)
-{
+   {
    auto& target = ((m_side == CLIENT) == from_peer)
-          ? m_server_certificate_verify
-          : m_client_certificate_verify;
+                  ? 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)
-{
+   {
    auto& target = ((m_side == CLIENT) == from_peer)
-          ? m_server_finished
-          : m_client_finished;
+                  ? m_server_finished
+                  : m_client_finished;
    target = std::move(finished);
    return target.value();
-}
+   }
 
 }
diff --git a/src/lib/tls/tls13/tls_handshake_state_13.h b/src/lib/tls/tls13/tls_handshake_state_13.h
index 1ecbb8f39..f442b4601 100644
--- a/src/lib/tls/tls13/tls_handshake_state_13.h
+++ b/src/lib/tls/tls13/tls_handshake_state_13.h
@@ -17,6 +17,7 @@
 #include <botan/tls_magic.h>
 #include <botan/tls_messages.h>
 #include <botan/tls_exceptn.h>
+#include <botan/internal/stl_util.h>
 
 namespace Botan::TLS {
 
@@ -112,11 +113,24 @@ class BOTAN_TEST_API Handshake_State_13 : public Internal::Handshake_State_13_Ba
    public:
       Handshake_State_13() : Handshake_State_13_Base(whoami) {}
 
-      decltype(auto) sending(Outbound_Message_T message)
+      template<typename MsgT>
+      std::reference_wrapper<MsgT> sending(MsgT msg)
          {
-         return std::visit([&](auto msg) -> Handshake_Message_13_Ref
+         static_assert(std::is_constructible_v<Outbound_Message_T, MsgT>,
+                       "Cannot send handshake message of type MsgT");
+
+         return std::reference_wrapper<decltype(msg)>(store(std::move(msg), false));
+         }
+
+      template<typename... MsgTs>
+      decltype(auto) sending(std::variant<MsgTs...> message)
+         {
+         static_assert(is_generalizable_to<Outbound_Message_T>(message),
+                       "Cannot send handshake message of types MsgTs...");
+
+         return std::visit([&](auto msg) -> as_wrapped_references_t<std::variant<MsgTs...>>
             {
-            return std::reference_wrapper<decltype(msg)>(store(std::move(msg), false));
+            return sending(std::move(msg));
             }, std::move(message));
          }
 
diff --git a/src/lib/tls/tls13/tls_server_impl_13.cpp b/src/lib/tls/tls13/tls_server_impl_13.cpp
new file mode 100644
index 000000000..ea88a7e40
--- /dev/null
+++ b/src/lib/tls/tls13/tls_server_impl_13.cpp
@@ -0,0 +1,294 @@
+/*
+* TLS Server - implementation for TLS 1.3
+* (C) 2022 Jack Lloyd
+*     2022 René Meusel - Rohde & Schwarz Cybersecurity
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/internal/tls_server_impl_13.h>
+#include <botan/internal/tls_cipher_state.h>
+#include <botan/internal/loadstor.h>
+#include <botan/internal/stl_util.h>
+#include <botan/credentials_manager.h>
+#include <botan/rng.h>
+
+namespace Botan::TLS {
+
+Server_Impl_13::Server_Impl_13(Callbacks& callbacks,
+                               Session_Manager& session_manager,
+                               Credentials_Manager& credentials_manager,
+                               const Policy& policy,
+                               RandomNumberGenerator& rng)
+   : Channel_Impl_13(callbacks, session_manager, credentials_manager,
+                     rng, policy, true /* is_server */)
+   {
+#if defined(BOTAN_HAS_TLS_12)
+   if(policy.allow_tls12())
+      { expect_downgrade({}); }
+#endif
+
+   m_transitions.set_expected_next(CLIENT_HELLO);
+   }
+
+std::string Server_Impl_13::application_protocol() const
+   {
+   // ALPN is NYI for TLS 1.3 server
+   return "";
+   }
+
+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.
+   }
+
+void Server_Impl_13::process_handshake_msg(Handshake_Message_13 message)
+   {
+   std::visit([&](auto msg)
+      {
+      // first verify that the message was expected by the state machine...
+      m_transitions.confirm_transition_to(msg.get().type());
+
+      // ... then allow the library user to abort on their discretion
+      callbacks().tls_inspect_handshake_msg(msg.get());
+
+      // ... finally handle the message
+      handle(msg.get());
+      }, m_handshake_state.received(std::move(message)));
+   }
+
+void Server_Impl_13::process_post_handshake_msg(Post_Handshake_Message_13 /*msg*/)
+   {
+   throw Not_Implemented("NYI: process_post_handshake_msg");
+   }
+
+void Server_Impl_13::process_dummy_change_cipher_spec()
+   {
+   // RFC 8446 5.
+   //    If an implementation detects a change_cipher_spec record received before
+   //    the first ClientHello message or after the peer's Finished message, it MUST be
+   //    treated as an unexpected record type [("unexpected_message" alert)].
+   if(!m_handshake_state.has_client_hello() || m_handshake_state.has_client_finished())
+      {
+      throw TLS_Exception(Alert::UNEXPECTED_MESSAGE, "Received an unexpected dummy Change Cipher Spec");
+      }
+
+   // RFC 8446 5.
+   //    An implementation may receive an unencrypted record of type change_cipher_spec [...]
+   //    at any time after the first ClientHello message has been sent or received
+   //    and before the peer's Finished message has been received [...]
+   //    and MUST simply drop it without further processing.
+   //
+   // ... no further processing.
+   }
+
+bool Server_Impl_13::handshake_finished() const
+   {
+   return m_handshake_state.handshake_finished();
+   }
+
+
+void Server_Impl_13::downgrade()
+   {
+   BOTAN_ASSERT_NOMSG(expects_downgrade());
+
+   request_downgrade();
+
+   // After this, no further messages are expected here because this instance
+   // will be replaced by a Server_Impl_12.
+   m_transitions.set_expected_next({});
+   }
+
+void Server_Impl_13::handle_reply_to_client_hello(const Server_Hello_13& server_hello)
+   {
+   const auto& client_hello = m_handshake_state.client_hello();
+   const auto& exts = client_hello.extensions();
+
+   const auto cipher = Ciphersuite::by_id(server_hello.ciphersuite());
+   m_transcript_hash.set_algorithm(cipher->prf_algo());
+
+   const auto my_keyshare = server_hello.extensions().get<Key_Share>();
+   auto shared_secret = my_keyshare->exchange(*exts.get<Key_Share>(), policy(), callbacks(), rng());
+   my_keyshare->erase();
+
+   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();
+
+   m_cipher_state->advance_with_server_finished(m_transcript_hash.current());
+
+   // TODO: For Client Authentication this should expect appropriate client handshake messages
+   //       once we support/implement it.
+   m_transitions.set_expected_next(FINISHED);
+   }
+
+void Server_Impl_13::handle_reply_to_client_hello(const Hello_Retry_Request& hello_retry_request)
+   {
+   auto cipher = Ciphersuite::by_id(hello_retry_request.ciphersuite());
+   BOTAN_ASSERT_NOMSG(cipher.has_value());  // should work, since we chose that suite
+
+   m_transcript_hash = Transcript_Hash_State::recreate_after_hello_retry_request(cipher->prf_algo(), m_transcript_hash);
+
+   m_transitions.set_expected_next(CLIENT_HELLO);
+   }
+
+void Server_Impl_13::handle(const Client_Hello_12& ch)
+   {
+   // The detailed handling of the TLS 1.2 compliant Client Hello is left to
+   // the TLS 1.2 server implementation.
+   BOTAN_UNUSED(ch);
+
+   // After we sent a Hello Retry Request we must not accept a downgrade.
+   if(m_handshake_state.has_hello_retry_request())
+      {
+      throw TLS_Exception(Alert::UNEXPECTED_MESSAGE,
+                          "Received a TLS 1.2 Client Hello after Hello Retry Request");
+      }
+
+   // RFC 8446 Appendix D.2
+   //    If the "supported_versions" extension is absent and the server only
+   //    supports versions greater than ClientHello.legacy_version, the server
+   //    MUST abort the handshake with a "protocol_version" alert.
+   //
+   // If we're not expecting a downgrade, we only support TLS 1.3.
+   if(!expects_downgrade())
+      {
+      throw TLS_Exception(Alert::PROTOCOL_VERSION, "Received a legacy Client Hello");
+      }
+
+   downgrade();
+   }
+
+void Server_Impl_13::handle(const Client_Hello_13& client_hello)
+   {
+   const auto& exts = client_hello.extensions();
+
+   const bool is_initial_client_hello = !m_handshake_state.has_hello_retry_request();
+
+   if(is_initial_client_hello)
+      {
+      const auto preferred_version = client_hello.highest_supported_version(policy());
+      if(!preferred_version)
+         {
+         throw TLS_Exception(Alert::PROTOCOL_VERSION, "No shared TLS version");
+         }
+
+      // RFC 8446 4.2.2
+      //   Clients MUST NOT use cookies in their initial ClientHello in subsequent
+      //   connections.
+      if(exts.has<Cookie>())
+         {
+         throw TLS_Exception(Alert::ILLEGAL_PARAMETER,
+                           "Received a Cookie in the initial client hello");
+         }
+      }
+
+   // TODO: Implement support for PSK. For now, we ignore any such extensions
+   //       and always revert to a standard key exchange.
+   if(!exts.has<Supported_Groups>())
+      {
+      throw Not_Implemented("PSK-only handshake NYI");
+      }
+
+   // RFC 8446 9.2
+   //    If containing a "supported_groups" extension, [Client Hello] MUST
+   //    also contain a "key_share" extension, and vice versa.
+   //
+   // This was validated before in the Client_Hello_13 constructor.
+   BOTAN_ASSERT_NOMSG(exts.has<Key_Share>());
+
+   if(!is_initial_client_hello)
+      {
+      const auto& hrr_exts = m_handshake_state.hello_retry_request().extensions();
+      const auto offered_groups = exts.get<Key_Share>()->offered_groups();
+      const auto selected_group = hrr_exts.get<Key_Share>()->selected_group();
+      if(offered_groups.size() != 1 || offered_groups.at(0) != selected_group)
+         {
+         throw TLS_Exception(Alert::ILLEGAL_PARAMETER,
+                             "Client did not comply with the requested key exchange group");
+         }
+      }
+
+   callbacks().tls_examine_extensions(exts, CLIENT, client_hello.type());
+   const auto sh_or_hrr = m_handshake_state.sending(Server_Hello_13::create(
+      client_hello, is_initial_client_hello, rng(), policy(), callbacks()));
+   send_handshake_message(sh_or_hrr);
+
+   // RFC 8446 Appendix D.4  (Middlebox Compatibility Mode)
+   //    The server sends a dummy change_cipher_spec record immediately after
+   //    its first handshake message. This may either be after a ServerHello or
+   //    a HelloRetryRequest.
+   //
+   //    This "compatibility mode" is partially negotiated: the client can opt
+   //    to provide a session ID or not, and the server has to echo it. Either
+   //    side can send change_cipher_spec at any time during the handshake, as
+   //    they must be ignored by the peer, but if the client sends a non-empty
+   //    session ID, the server MUST send the change_cipher_spec as described
+   //    [above].
+   //
+   // Technically, the usage of compatibility mode is fully up to the client
+   // sending a non-empty session ID. Nevertheless, when the policy requests
+   // it we send a CCS regardless. Note that this is perfectly legal and also
+   // satisfies some BoGo tests that expect this behaviour.
+   if(is_initial_client_hello &&
+         (policy().tls_13_middlebox_compatibility_mode() ||
+          !client_hello.session_id().empty()))
+      {
+      send_dummy_change_cipher_spec();
+      }
+
+   std::visit([this](auto msg) { handle_reply_to_client_hello(msg); }, sh_or_hrr);
+   }
+
+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");
+   }
+
+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");
+   }
+
+void Server_Impl_13::handle(const Finished_13& finished_msg)
+   {
+   // RFC 8446 4.4.4
+   //    Recipients of Finished messages MUST verify that the contents are
+   //    correct and if incorrect MUST terminate the connection with a
+   //    "decrypt_error" alert.
+   if(!finished_msg.verify(m_cipher_state.get(),
+                           m_transcript_hash.previous()))
+      { throw TLS_Exception(Alert::DECRYPT_ERROR, "Finished message didn't verify"); }
+
+   m_cipher_state->advance_with_client_finished(m_transcript_hash.current());
+
+   // no more handshake messages expected
+   m_transitions.set_expected_next({});
+
+   callbacks().tls_session_activated();
+   }
+
+}  // namespace Botan::TLS
diff --git a/src/lib/tls/tls13/tls_server_impl_13.h b/src/lib/tls/tls13/tls_server_impl_13.h
new file mode 100644
index 000000000..7db5e166f
--- /dev/null
+++ b/src/lib/tls/tls13/tls_server_impl_13.h
@@ -0,0 +1,65 @@
+/*
+* TLS Server - implementation for TLS 1.3
+* (C) 2022 Jack Lloyd
+*     2022 René Meusel - Rohde & Schwarz Cybersecurity
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_TLS_SERVER_IMPL_13_H_
+#define BOTAN_TLS_SERVER_IMPL_13_H_
+
+#include <botan/internal/tls_channel_impl_13.h>
+#include <botan/internal/tls_handshake_state_13.h>
+#include <botan/internal/tls_handshake_transitions.h>
+
+namespace Botan::TLS {
+
+/**
+* SSL/TLS Server 1.3 implementation
+*/
+class Server_Impl_13 : public Channel_Impl_13
+   {
+   public:
+      explicit Server_Impl_13(Callbacks& callbacks,
+                              Session_Manager& session_manager,
+                              Credentials_Manager& credentials_manager,
+                              const Policy& policy,
+                              RandomNumberGenerator& rng);
+
+      /**
+       * @return network protocol as notified by the TLS client
+       */
+      std::string application_protocol() const override;
+
+      /**
+      * @return certificate chain of the peer (may be empty)
+      */
+      std::vector<X509_Certificate> peer_cert_chain() const override;
+
+   private:
+      void process_handshake_msg(Handshake_Message_13 msg) override;
+      void process_post_handshake_msg(Post_Handshake_Message_13 msg) override;
+      void process_dummy_change_cipher_spec() override;
+
+      void handle(const Client_Hello_12& client_hello_msg);
+      void handle(const Client_Hello_13& client_hello_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_reply_to_client_hello(const Server_Hello_13& server_hello);
+      void handle_reply_to_client_hello(const Hello_Retry_Request& hello_retry_request);
+
+      bool handshake_finished() const override;
+
+      void downgrade();
+
+   private:
+      Server_Handshake_State_13 m_handshake_state;
+      Handshake_Transitions m_transitions;
+   };
+
+}
+
+#endif
diff --git a/src/lib/tls/tls_client.cpp b/src/lib/tls/tls_client.cpp
index 2b8735ee8..711ab23ea 100644
--- a/src/lib/tls/tls_client.cpp
+++ b/src/lib/tls/tls_client.cpp
@@ -37,6 +37,9 @@ Client::Client(Callbacks& callbacks,
                size_t io_buf_sz)
    {
    Protocol_Version effective_version = offer_version;
+   BOTAN_ARG_CHECK(policy.acceptable_protocol_version(offer_version),
+                   "Policy does not allow to offer requested protocol version");
+
 #if defined(BOTAN_HAS_TLS_13)
    // downgrade to TLS 1.2 directly if we have a legacy session to resume
    if(effective_version == Protocol_Version::TLS_V13)
diff --git a/src/lib/tls/tls_extensions.cpp b/src/lib/tls/tls_extensions.cpp
index 6b8493c60..852a85913 100644
--- a/src/lib/tls/tls_extensions.cpp
+++ b/src/lib/tls/tls_extensions.cpp
@@ -10,6 +10,7 @@
 
 #include <botan/tls_extensions.h>
 #include <botan/internal/tls_reader.h>
+#include <botan/internal/stl_util.h>
 #include <botan/tls_exceptn.h>
 #include <botan/tls_policy.h>
 #include <botan/ber_dec.h>
@@ -446,8 +447,10 @@ Supported_Groups::Supported_Groups(TLS_Data_Reader& reader,
 
    for(size_t i = 0; i != elems; ++i)
       {
-      const uint16_t id = reader.get_uint16_t();
-      m_groups.push_back(static_cast<Group_Params>(id));
+      const auto group = static_cast<Group_Params>(reader.get_uint16_t());
+      // Note: RFC 8446 does not explicitly enforce that groups must be unique.
+      if(!value_exists(m_groups, group))
+         m_groups.push_back(group);
       }
    }
 
diff --git a/src/lib/tls/tls_extensions.h b/src/lib/tls/tls_extensions.h
index f9757d5ce..33d7031cb 100644
--- a/src/lib/tls/tls_extensions.h
+++ b/src/lib/tls/tls_extensions.h
@@ -702,17 +702,33 @@ class BOTAN_UNSTABLE_API Key_Share final : public Extension
        */
       void retry_offer(const Key_Share& retry_request_keyshare, const std::vector<Named_Group>& supported_groups, Callbacks& cb, RandomNumberGenerator& rng);
 
+      /**
+       * @return key exchange groups the peer offered key share entries for
+       */
+      std::vector<Named_Group> offered_groups() const;
+
+      /**
+       * @return key exchange group that was selected by a Hello Retry Request
+       */
+      Named_Group selected_group() const;
+
       /**
        * Delete all private keys that might be contained in Key_Share_Entries in this extension.
        */
       void erase();
 
-      explicit Key_Share(TLS_Data_Reader& reader,
-                         uint16_t extension_size,
-                         Handshake_Type message_type);
+      Key_Share(TLS_Data_Reader& reader,
+                uint16_t extension_size,
+                Handshake_Type message_type);
+
+      // constructor used for ClientHello msg
+      Key_Share(const Policy& policy, Callbacks& cb, RandomNumberGenerator& rng);
+
+      // constructor used for ServerHello msg
+      Key_Share(Named_Group group, Callbacks& cb, RandomNumberGenerator& rng);
 
-      // constuctor used for ClientHello msg
-      explicit Key_Share(const Policy& policy, Callbacks& cb, RandomNumberGenerator& rng);
+      // constructor used for HelloRetryRequest msg
+      explicit Key_Share(Named_Group selected_group);
 
       // destructor implemented in .cpp to hide Key_Share_Impl
       ~Key_Share();
diff --git a/src/lib/tls/tls_messages.h b/src/lib/tls/tls_messages.h
index 83efa1f75..072067ea0 100644
--- a/src/lib/tls/tls_messages.h
+++ b/src/lib/tls/tls_messages.h
@@ -230,6 +230,32 @@ 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
+       * returns std::nullopt.
+       */
+      std::optional<Protocol_Version> highest_supported_version(const Policy& policy) const;
+
+      /**
+       * This validates that a Client Hello received after sending a Hello
+       * Retry Request was updated in accordance with RFC 8446 4.1.2. If issues
+       * are found, this method throws accordingly.
+       */
+      void validate_updates(const Client_Hello_13& new_ch);
+
    private:
       Client_Hello_13(std::unique_ptr<Client_Hello_Internal> data);
 
@@ -381,12 +407,26 @@ class BOTAN_UNSTABLE_API Server_Hello_13 : public Server_Hello
    protected:
       static struct Server_Hello_Tag {} as_server_hello;
       static struct Hello_Retry_Request_Tag {} as_hello_retry_request;
+      static struct Hello_Retry_Request_Creation_Tag {} as_new_hello_retry_request;
 
+      // These constructors are meant for instantiating Server Hellos
+      // after parsing a peer's message. They perform basic validation
+      // and are therefore not suitable for constructing a message to
+      // be sent to a client.
       explicit Server_Hello_13(std::unique_ptr<Server_Hello_Internal> data, Server_Hello_Tag tag = as_server_hello);
       explicit Server_Hello_13(std::unique_ptr<Server_Hello_Internal> data, Hello_Retry_Request_Tag tag);
       void basic_validation() const;
 
+      // Instantiate a Server Hello as response to a client's Client Hello
+      // (called from Server_Hello_13::create())
+      Server_Hello_13(const Client_Hello_13& ch, std::optional<Named_Group> key_exchange_group, RandomNumberGenerator& rng, Callbacks& cb, const Policy& policy);
+
+      explicit Server_Hello_13(std::unique_ptr<Server_Hello_Internal> data, Hello_Retry_Request_Creation_Tag tag);
+
    public:
+      static std::variant<Hello_Retry_Request, Server_Hello_13>
+      create(const Client_Hello_13& ch, bool hello_retry_request_allowed, RandomNumberGenerator& rng, const Policy& policy, Callbacks& cb);
+
       static std::variant<Hello_Retry_Request, Server_Hello_13, Server_Hello_12>
       parse(const std::vector<uint8_t>& buf);
 
@@ -404,8 +444,9 @@ class BOTAN_UNSTABLE_API Server_Hello_13 : public Server_Hello
 class BOTAN_UNSTABLE_API Hello_Retry_Request final : public Server_Hello_13
    {
    protected:
-      friend class Server_Hello_13;  // to allow construction by Server_Hello_13::parse()
+      friend class Server_Hello_13;  // to allow construction by Server_Hello_13::parse() and ::create()
       explicit Hello_Retry_Request(std::unique_ptr<Server_Hello_Internal> data);
+      Hello_Retry_Request(const Client_Hello_13& ch, Named_Group selected_group, const Policy& policy, Callbacks& cb);
 
    public:
       Handshake_Type type() const override { return HELLO_RETRY_REQUEST; }
@@ -418,12 +459,13 @@ class BOTAN_UNSTABLE_API Encrypted_Extensions final : public Handshake_Message
    {
    public:
       explicit Encrypted_Extensions(const std::vector<uint8_t>& buf);
+      Encrypted_Extensions(const Client_Hello_13& client_hello, const Policy& policy, Callbacks& cb);
 
       Handshake_Type type() const override { return Handshake_Type::ENCRYPTED_EXTENSIONS; }
 
       const Extensions& extensions() const { return m_extensions; }
 
-      std::vector<uint8_t> serialize() const override { return {}; }
+      std::vector<uint8_t> serialize() const override;
 
    private:
       Extensions m_extensions;
diff --git a/src/lib/tls/tls_server.cpp b/src/lib/tls/tls_server.cpp
index 67a0e8c82..4f6ffc37b 100644
--- a/src/lib/tls/tls_server.cpp
+++ b/src/lib/tls/tls_server.cpp
@@ -15,6 +15,11 @@
 #include <botan/internal/tls_server_impl_12.h>
 #include <botan/tls_magic.h>
 
+#include <botan/internal/tls_server_impl_12.h>
+#if defined(BOTAN_HAS_TLS_13)
+  #include <botan/internal/tls_server_impl_13.h>
+#endif
+
 namespace Botan::TLS {
 
 /*
@@ -26,17 +31,45 @@ Server::Server(Callbacks& callbacks,
                const Policy& policy,
                RandomNumberGenerator& rng,
                bool is_datagram,
-               size_t io_buf_sz) :
-   m_impl(std::make_unique<Server_Impl_12>(callbacks, session_manager, creds, policy,
-                                           rng, is_datagram,io_buf_sz))
+               size_t io_buf_sz)
    {
+   const auto max_version = policy.latest_supported_version(is_datagram);
+
+   if(!max_version.is_pre_tls_13())
+      {
+#if defined(BOTAN_HAS_TLS_13)
+      m_impl = std::make_unique<Server_Impl_13>(
+         callbacks, session_manager, creds, policy, rng);
+
+      if(m_impl->expects_downgrade())
+         { m_impl->set_io_buffer_size(io_buf_sz); }
+#else
+      throw Not_Implemented("TLS 1.3 server is not available in this build");
+#endif
+      }
+   else
+      {
+      m_impl = std::make_unique<Server_Impl_12>(
+         callbacks, session_manager, creds, policy, rng, is_datagram, io_buf_sz);
+      }
    }
 
 Server::~Server() = default;
 
 size_t Server::received_data(const uint8_t buf[], size_t buf_size)
    {
-   return m_impl->received_data(buf, buf_size);
+   auto read = m_impl->received_data(buf, buf_size);
+
+   if(m_impl->is_downgrading())
+      {
+      auto info = m_impl->extract_downgrade_info();
+      m_impl = std::make_unique<Server_Impl_12>(*info);
+
+      // replay peer data received so far
+      read = m_impl->received_data(info->peer_transcript.data(), info->peer_transcript.size());
+      }
+
+   return read;
    }
 
 bool Server::is_active() const
diff --git a/src/lib/tls/tls_session.cpp b/src/lib/tls/tls_session.cpp
index fb99fb737..3896498c6 100644
--- a/src/lib/tls/tls_session.cpp
+++ b/src/lib/tls/tls_session.cpp
@@ -15,6 +15,9 @@
 #include <botan/mac.h>
 #include <botan/rng.h>
 
+#include <botan/tls_messages.h>
+#include <botan/tls_callbacks.h>
+
 namespace Botan::TLS {
 
 Session::Session() :
@@ -64,6 +67,8 @@ Session::Session(const std::vector<uint8_t>& session_identifier,
                    "Instantiated a TLS 1.2 session object with a TLS version newer than 1.2");
    }
 
+#if defined(BOTAN_HAS_TLS_13)
+
 Session::Session(const std::vector<uint8_t>& session_ticket,
                  const secure_vector<uint8_t>& session_psk,
                  const std::optional<uint32_t>& max_early_data_bytes,
@@ -111,6 +116,35 @@ Session::Session(const std::vector<uint8_t>& session_ticket,
                    "Instantiated a TLS 1.3 session object with a TLS version older than 1.3");
    }
 
+Session::Session(secure_vector<uint8_t>&& session_psk,
+                 const std::optional<uint32_t>& max_early_data_bytes,
+                 std::chrono::seconds lifetime_hint,
+                 const std::vector<X509_Certificate>& peer_certs,
+                 const Client_Hello_13& client_hello,
+                 const Server_Hello_13& server_hello,
+                 Callbacks& callbacks,
+                 RandomNumberGenerator& rng) :
+   m_start_time(callbacks.tls_current_timestamp()),
+   m_master_secret(std::move(session_psk)),
+   m_version(server_hello.selected_version()),
+   m_ciphersuite(server_hello.ciphersuite()),
+   m_connection_side(Connection_Side::SERVER),
+   m_extended_master_secret(true),
+   m_encrypt_then_mac(true),
+   m_peer_certs(peer_certs),
+   m_server_info(client_hello.sni_hostname()),
+   m_early_data_allowed(max_early_data_bytes.has_value()),
+   m_max_early_data_bytes(max_early_data_bytes.value_or(0)),
+   m_ticket_age_add(load_be<uint32_t>(rng.random_vec(4).data(), 0))
+   {
+   const auto lifetime_ms = std::chrono::duration_cast<std::chrono::milliseconds>(lifetime_hint).count();
+   BOTAN_ARG_CHECK(lifetime_ms <= std::numeric_limits<uint32_t>::max(),
+                   "lifetime is too long");
+   m_lifetime_hint = static_cast<uint32_t>(lifetime_ms);
+   }
+
+#endif
+
 Session::Session(const std::string& pem)
    {
    secure_vector<uint8_t> der = PEM_Code::decode_check_label(pem, "TLS SESSION");
diff --git a/src/lib/tls/tls_session.h b/src/lib/tls/tls_session.h
index 8702e7db2..b4b1a717d 100644
--- a/src/lib/tls/tls_session.h
+++ b/src/lib/tls/tls_session.h
@@ -21,6 +21,10 @@ namespace Botan {
 
 namespace TLS {
 
+class Client_Hello_13;
+class Server_Hello_13;
+class Callbacks;
+
 /**
 * Class representing a TLS session state
 */
@@ -49,6 +53,8 @@ class BOTAN_PUBLIC_API(2,0) Session final
               uint16_t srtp_profile,
               std::chrono::system_clock::time_point current_timestamp);
 
+#if defined(BOTAN_HAS_TLS_13)
+
       /**
       * New TLS 1.3 session (sets session start time)
       */
@@ -64,6 +70,21 @@ class BOTAN_PUBLIC_API(2,0) Session final
               const Server_Information& server_info,
               std::chrono::system_clock::time_point current_timestamp);
 
+      /**
+       * Create a new TLS 1.3 session object from server data structures
+       * after a successful handshake with a TLS 1.3 client
+       */
+      Session(secure_vector<uint8_t>&& session_psk,
+              const std::optional<uint32_t>& max_early_data_bytes,
+              std::chrono::seconds lifetime_hint,
+              const std::vector<X509_Certificate>& peer_certs,
+              const Client_Hello_13& client_hello,
+              const Server_Hello_13& server_hello,
+              Callbacks& callbacks,
+              RandomNumberGenerator& rng);
+
+#endif
+
       /**
       * Load a session from DER representation (created by DER_encode)
       * @param ber DER representation buffer
diff --git a/src/lib/tls/tls_version.cpp b/src/lib/tls/tls_version.cpp
index 884437e19..7e313ffe0 100644
--- a/src/lib/tls/tls_version.cpp
+++ b/src/lib/tls/tls_version.cpp
@@ -41,6 +41,12 @@ bool Protocol_Version::is_pre_tls_13() const
           ( is_datagram_protocol() && *this <= Protocol_Version::DTLS_V12);
    }
 
+bool Protocol_Version::is_tls_13_or_later() const
+   {
+   return (!is_datagram_protocol() && *this >= Protocol_Version::TLS_V13) ||
+          ( is_datagram_protocol() && *this >= Protocol_Version::DTLS_V13);
+   }
+
 
 bool Protocol_Version::operator>(const Protocol_Version& other) const
    {
diff --git a/src/lib/tls/tls_version.h b/src/lib/tls/tls_version.h
index 5ab4a31cf..5ef6ed576 100644
--- a/src/lib/tls/tls_version.h
+++ b/src/lib/tls/tls_version.h
@@ -27,7 +27,8 @@ class BOTAN_PUBLIC_API(2,0) Protocol_Version final
          TLS_V11            = 0x0302,  // not supported by Botan
          TLS_V12            = 0x0303,
          TLS_V13            = 0x0304,
-         DTLS_V12           = 0xFEFD
+         DTLS_V12           = 0xFEFD,
+         DTLS_V13           = 0xFEFC,  // not supported by Botan
       };
 
       /**
@@ -107,6 +108,11 @@ class BOTAN_PUBLIC_API(2,0) Protocol_Version final
        */
       bool is_pre_tls_13() const;
 
+      /**
+       * @return true if this version indicates a (D)TLS newer than 1.3
+       */
+      bool is_tls_13_or_later() const;
+
       /**
       * @return if this version is equal to other
       */
diff --git a/src/lib/x509/certstor_system_macos/certstor_macos.cpp b/src/lib/x509/certstor_system_macos/certstor_macos.cpp
index 8c62c45ed..0dae4822c 100644
--- a/src/lib/x509/certstor_system_macos/certstor_macos.cpp
+++ b/src/lib/x509/certstor_system_macos/certstor_macos.cpp
@@ -266,16 +266,10 @@ class Certificate_Store_MacOS_Impl
          m_system_chain(nullptr),
          m_keychains(nullptr)
          {
-BOTAN_DIAGNOSTIC_PUSH
-BOTAN_DIAGNOSTIC_IGNORE_DEPRECATED
-         // macOS 12.0 deprecates 'Custom keychain management', though the API still works.
-         // Ideas for a replacement can be found in the discussion of GH #3122:
-         //   https://github.com/randombit/botan/pull/3122
          check_success(SecKeychainOpen(system_roots, &m_system_roots.get()),
                        "open system root certificates");
          check_success(SecKeychainOpen(system_keychain, &m_system_chain.get()),
                        "open system keychain");
-BOTAN_DIAGNOSTIC_POP
          check_notnull(m_system_roots, "open system root certificate chain");
          check_notnull(m_system_chain, "open system certificate chain");
 


Test patch:
diff --git a/src/scripts/test_cli.py b/src/scripts/test_cli.py
index 141a91bef..a0734b9e5 100755
--- a/src/scripts/test_cli.py
+++ b/src/scripts/test_cli.py
@@ -875,7 +875,7 @@ def cli_tls_socket_tests(tmp_dir):
 
     test_cli("sign_cert", "%s %s %s --output=%s" % (ca_cert, priv_key, crt_req, server_cert))
 
-    tls_server = subprocess.Popen([CLI_PATH, 'tls_server', '--max-clients=1',
+    tls_server = subprocess.Popen([CLI_PATH, 'tls_server', '--max-clients=2',
                                    '--port=%d' % (server_port), server_cert, priv_key],
                                   stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
@@ -883,39 +883,41 @@ def cli_tls_socket_tests(tmp_dir):
 
     time.sleep(wait_time)
 
-    tls_client = subprocess.Popen([CLI_PATH, 'tls_client', 'localhost',
-                                   '--port=%d' % (server_port), '--trusted-cas=%s' % (ca_cert),
-                                   '--tls-version=1.2'], # TODO: test TLS 1.3 once it becomes available
-                                  stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    for tls_version in ['1.2', '1.3']:
+        tls_client = subprocess.Popen([CLI_PATH, 'tls_client', 'localhost',
+                                       '--port=%d' % (server_port), '--trusted-cas=%s' % (ca_cert),
+                                       '--tls-version=%s' % (tls_version)],
+                                      stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
-    time.sleep(wait_time)
+        time.sleep(wait_time)
 
-    try:
-        tls_client.stdin.write(client_msg)
-        tls_client.stdin.flush()
-    except BrokenPipeError:
-        pass # On handshake failure, the stdin pipe of is already closed here
-             # and error reporting below would not be reached.
+        try:
+            tls_client.stdin.write(client_msg)
+            tls_client.stdin.flush()
+        except BrokenPipeError:
+            pass # On handshake failure, the stdin pipe of is already closed here
+                # and error reporting below would not be reached.
 
-    time.sleep(wait_time)
+        time.sleep(wait_time)
 
-    (stdout, stderr) = tls_client.communicate()
+        (stdout, stderr) = tls_client.communicate()
 
-    if stderr:
-        logging.error("Got unexpected stderr output %s", stderr)
+        if stderr:
+            logging.error("Got unexpected stderr output %s", stderr)
 
-    if b'Handshake complete' not in stdout:
-        logging.error('Failed to complete handshake: %s', stdout)
+        if b'Handshake complete' not in stdout:
+            logging.error('Failed to complete handshake: %s', stdout)
 
-    if client_msg not in stdout:
-        logging.error("Missing client message from stdout %s", stdout)
+        if client_msg not in stdout:
+            logging.error("Missing client message from stdout %s", stdout)
 
-    (srv_stdout, srv_stderr) = tls_server.communicate()
+    (srv_stdout, srv_stderr) = tls_server.communicate(None, 5)
     if srv_stderr:
         logging.error("server said (stdout): %s", srv_stdout)
         logging.error("server said (stderr): %s", srv_stderr)
 
 def cli_tls_http_server_tests(tmp_dir):
+    # pylint: disable=too-many-locals
     if not run_socket_tests() or not check_for_command("tls_http_server"):
         return
 
@@ -937,31 +939,36 @@ def cli_tls_http_server_tests(tmp_dir):
 
     test_cli("sign_cert", "%s %s %s --output=%s" % (ca_cert, priv_key, crt_req, server_cert))
 
-    tls_server = subprocess.Popen([CLI_PATH, 'tls_http_server', '--max-clients=2',
+    tls_server = subprocess.Popen([CLI_PATH, 'tls_http_server', '--max-clients=4',
                                    '--port=%d' % (server_port), server_cert, priv_key],
                                   stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
+
     wait_time = 1.0
     time.sleep(wait_time)
 
-    context = ssl.create_default_context(cafile=ca_cert)
-    conn = HTTPSConnection('localhost', port=server_port, context=context)
-    conn.request("GET", "/")
-    resp = conn.getresponse()
+    for tls_version in [ssl.TLSVersion.TLSv1_2, ssl.TLSVersion.TLSv1_3]:
+        context = ssl.create_default_context(cafile=ca_cert)
+        context.minimum_version = tls_version
+        context.maximum_version = tls_version
 
-    if resp.status != 200:
-        logging.error('Unexpected response status %d', resp.status)
+        conn = HTTPSConnection('localhost', port=server_port, context=context)
+        conn.request("GET", "/")
+        resp = conn.getresponse()
+
+        if resp.status != 200:
+            logging.error('Unexpected response status %d', resp.status)
 
-    body = str(resp.read())
+        body = str(resp.read())
 
-    if body.find('TLS negotiation with Botan 3.') < 0:
-        logging.error('Unexpected response body')
+        if body.find('TLS negotiation with Botan 3.') < 0:
+            logging.error('Unexpected response body %s', body)
 
-    conn.request("POST", "/logout")
-    resp = conn.getresponse()
+        conn.request("POST", "/logout")
+        resp = conn.getresponse()
 
-    if resp.status != 405:
-        logging.error('Unexpected response status %d', resp.status)
+        if resp.status != 405:
+            logging.error('Unexpected response status %d', resp.status)
 
     rc = tls_server.wait(5)
 
@@ -996,7 +1003,7 @@ def cli_tls_proxy_tests(tmp_dir):
     test_cli("sign_cert", "%s %s %s --output=%s" % (ca_cert, priv_key, crt_req, server_cert))
 
     tls_proxy = subprocess.Popen([CLI_PATH, 'tls_proxy', str(proxy_port), '127.0.0.1', str(server_port),
-                                  server_cert, priv_key, '--output=/tmp/proxy.err', '--max-clients=2'],
+                                  server_cert, priv_key, '--output=/tmp/proxy.err', '--max-clients=4'],
                                  stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
     wait_time = 1.0
@@ -1007,6 +1014,8 @@ def cli_tls_proxy_tests(tmp_dir):
 
     def run_http_server():
         class Handler(BaseHTTPRequestHandler):
+            def log_message(self, _fmt, *_args): # pylint: disable=arguments-differ
+                pass  # muzzle log output
 
             def do_GET(self): # pylint: disable=invalid-name
                 self.send_response(200)
@@ -1023,8 +1032,15 @@ def cli_tls_proxy_tests(tmp_dir):
     time.sleep(wait_time)
 
     context = ssl.create_default_context(cafile=ca_cert)
+    context.minimum_version = ssl.TLSVersion.TLSv1_3
+    context.maximum_version = ssl.TLSVersion.TLSv1_3
+
+    for i in range(4):
+        # Make sure that TLS protocol version downgrade works
+        if i > 2:
+            context.minimum_version = ssl.TLSVersion.TLSv1_2
+            context.maximum_version = ssl.TLSVersion.TLSv1_2
 
-    for _i in range(2):
         conn = HTTPSConnection('localhost', port=proxy_port, context=context)
         conn.request("GET", "/")
         resp = conn.getresponse()
diff --git a/src/tests/data/tls-policy/rfc8448_compat.txt b/src/tests/data/tls-policy/rfc8448_compat_client.txt
similarity index 100%
rename from src/tests/data/tls-policy/rfc8448_compat.txt
rename to src/tests/data/tls-policy/rfc8448_compat_client.txt
diff --git a/src/tests/data/tls-policy/rfc8448_compat_server.txt b/src/tests/data/tls-policy/rfc8448_compat_server.txt
new file mode 100644
index 000000000..bfb128fe1
--- /dev/null
+++ b/src/tests/data/tls-policy/rfc8448_compat_server.txt
@@ -0,0 +1,33 @@
+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 1.3 compatibility mode is partially negotiated by the client. The server
+# has no influence on whether compatibility mode is used or not. Hence, we
+# disable compatibility mode for this server integration test to make sure that
+# this configuration has no negative impact on the server's behaviour.
+tls_13_middlebox_compatibility_mode = false
+
+hash_hello_random = false
+support_cert_status_message = false
diff --git a/src/tests/data/tls-policy/rfc8448_hrr.txt b/src/tests/data/tls-policy/rfc8448_hrr_client.txt
similarity index 100%
rename from src/tests/data/tls-policy/rfc8448_hrr.txt
rename to src/tests/data/tls-policy/rfc8448_hrr_client.txt
diff --git a/src/tests/data/tls-policy/rfc8448_hrr_server.txt b/src/tests/data/tls-policy/rfc8448_hrr_server.txt
new file mode 100644
index 000000000..7269a7c44
--- /dev/null
+++ b/src/tests/data/tls-policy/rfc8448_hrr_server.txt
@@ -0,0 +1,26 @@
+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 = secp256r1 secp384r1 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
diff --git a/src/tests/data/tls_13/client_hello.vec b/src/tests/data/tls_13/client_hello.vec
index 0960b0180..24d86c541 100644
--- a/src/tests/data/tls_13/client_hello.vec
+++ b/src/tests/data/tls_13/client_hello.vec
@@ -51,7 +51,7 @@ Protocol = 0304
 AdditionalData = 0000000A000D001C0023002B002D0033FF01
 Ciphersuite = 130113031302
 Message_Type = client_hello_13
-Exception = Client sent TLS 1.3 Client Hello with compression method != null
+Exception = Client did not offer NULL compression
 
 # TLS 1.3 client hello with two compression methods '00' and '01'
 Buffer = 0303cb34ecb1e78163ba1c38c6dacb196a6dffa21a8d9912ec18a2ef6283024dece700000613011303130202000100910000000b0009000006736572766572ff01000100000a00140012001d0017001800190100010101020103010400230000003300260024001d002099381de560e4bd43d23d8e435a7dbafeb3c06e51c13cae4d5413691e529aaf2c002b0003020304000d0020001e040305030603020308040805080604010501060102010402050206020202002d00020101001c00024001
@@ -59,7 +59,7 @@ Protocol = 0304
 AdditionalData = 0000000A000D001C0023002B002D0033FF01
 Ciphersuite = 130113031302
 Message_Type = client_hello_13
-Exception = Client sent TLS 1.3 Client Hello with compression method != null
+Exception = Client did not offer NULL compression
 
 # empty
 Buffer =
diff --git a/src/tests/test_bigint.cpp b/src/tests/test_bigint.cpp
index 05945745a..4e76524d0 100644
--- a/src/tests/test_bigint.cpp
+++ b/src/tests/test_bigint.cpp
@@ -200,41 +200,17 @@ class BigInt_Unit_Tests final : public Test
             result.test_eq("input '" + vec.first + "'", n, vec.second);
             }
 
-         auto check_bigint_formatting = [&](const Botan::BigInt& n,
-                                            const std::string& dec,
-                                            const std::string& hex,
-                                            const std::string& neg_dec,
-                                            const std::string& neg_hex)
-            {
-            std::ostringstream oss;
-            oss << n;
-            result.test_eq("output decimal", oss.str(), dec);
-
-            oss.str("");
-            oss << (-n);
-            result.test_eq("output negative decimal", oss.str(), neg_dec);
+         BigInt n = 33;
 
-            oss.str("");
-            oss << std::hex << n;
-            result.test_eq("output hex", oss.str(), hex);
+         std::ostringstream oss;
+         oss << n;
+         result.test_eq("output 33 dec", oss.str(), "33");
 
-            oss.str("");
-            oss << std::hex << (-n);
-            result.test_eq("output negative hex", oss.str(), neg_hex);
-            };
+         oss.str("");
+         oss << std::hex << n;
+         result.test_eq("output 33 hex", oss.str(), "0x21");
 
-         check_bigint_formatting(Botan::BigInt(33), "33", "0x21", "-33", "-0x21");
-         check_bigint_formatting(Botan::BigInt::from_s32(-33), "-33", "-0x21", "33", "0x21");
-         check_bigint_formatting(Botan::BigInt(255), "255", "0xFF", "-255", "-0xFF");
-         check_bigint_formatting(Botan::BigInt(0), "0", "0x00", "0", "0x00");
-         check_bigint_formatting(Botan::BigInt(5), "5", "0x05", "-5", "-0x05");
-
-         result.test_throws("octal output not supported", [&]()
-            {
-            Botan::BigInt n(5);
-            std::ostringstream oss;
-            oss << std::oct << n;
-            });
+         result.test_throws("octal output not supported", [&]() { oss << std::oct << n; });
 
          return result;
          }
diff --git a/src/tests/test_tls_cipher_state.cpp b/src/tests/test_tls_cipher_state.cpp
index db1eee91d..e87840b38 100644
--- a/src/tests/test_tls_cipher_state.cpp
+++ b/src/tests/test_tls_cipher_state.cpp
@@ -20,20 +20,49 @@ using Test = Botan_Tests::Test;
 using namespace Botan;
 using namespace Botan::TLS;
 
+std::vector<Test::Result> flatten(std::vector<std::vector<Test::Result>> result_lists)
+   {
+   std::vector<Test::Result> results;
+   for(auto& result_list : result_lists)
+      {
+      for(auto& result : result_list)
+         {
+         results.emplace_back(std::move(result));
+         }
+      }
+   return results;
+   }
+
+decltype(auto) make_CHECK_both(Cipher_State* cs_client, Cipher_State* cs_server)
+   {
+   using namespace std::placeholders;
+   return [=](std::string name, auto lambda) -> std::vector<Test::Result>
+      {
+      return
+         {
+         Botan_Tests::CHECK(std::string(name + " (client)").c_str(), std::bind(lambda, cs_client, Connection_Side::CLIENT, _1)),
+         Botan_Tests::CHECK(std::string(name + " (server)").c_str(), std::bind(lambda, cs_server, Connection_Side::SERVER, _1))
+         };
+      };
+   }
+
 class RFC8448_TestData
    {
 private:
    const std::string            name;
+   const Connection_Side        emitter;
    const std::vector<uint8_t>   record_header;
    const secure_vector<uint8_t> encrypted_fragment;
    const secure_vector<uint8_t> plaintext_fragment;
 
 public:
    RFC8448_TestData(std::string n,
+                    Connection_Side em,
                     std::vector<uint8_t> rh,
                     secure_vector<uint8_t> ef,
                     secure_vector<uint8_t> pf)
       : name(std::move(n))
+      , emitter(em)
       , record_header(std::move(rh))
       , encrypted_fragment(std::move(ef))
       , plaintext_fragment(std::move(pf)) {}
@@ -59,6 +88,18 @@ public:
 
       result.test_eq("plaintext for " + name, encrypted_fragment_copy, plaintext_fragment);
       }
+
+   void xxcrypt(Test::Result &result, Cipher_State* cs, Connection_Side side) const
+      {
+      if(emitter == side)
+         {
+         encrypt(result, cs);
+         }
+      else
+         {
+         decrypt(result, cs);
+         }
+      }
    };
 
 std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
@@ -102,6 +143,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
    const auto encrypted_extensions = RFC8448_TestData
       (
       "encrypted_extensions",
+      Connection_Side::SERVER,
       Botan::hex_decode("17 03 03 02 a2"),
       Botan::hex_decode_locked("d1 ff 33 4a 56 f5 bf"
                                "f6 59 4a 07 cc 87 b5 80 23 3f 50 0f 45 e4 89 e7 f3 3a f3 5e df"
@@ -176,6 +218,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
    const auto encrypted_client_finished_message = RFC8448_TestData
       (
       "encrypted_client_finished_message",
+      Connection_Side::CLIENT,
       Botan::hex_decode("17 03 03 00 35"),
       Botan::hex_decode_locked("75 ec 4d c2 38 cc e6"
          "0b 29 80 44 a7 1e 21 9c 56 cc 77 b0 51 7f e9 b9 3c 7a 4b fc 44"
@@ -190,6 +233,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
    const auto encrypted_new_session_ticket = RFC8448_TestData
       (
       "encrypted_new_session_ticket",
+      Connection_Side::SERVER,
       Botan::hex_decode("17 03 03 00 de"),
       Botan::hex_decode_locked(
          "3a 6b 8f 90 41 4a 97"
@@ -223,6 +267,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
    const auto encrypted_application_data_client = RFC8448_TestData
       (
       "encrypted_application_data_client",
+      Connection_Side::CLIENT,
       Botan::hex_decode("17 03 03 00 43"),
       Botan::hex_decode_locked("a2 3f 70 54 b6 2c 94"
                                "d0 af fa fe 82 28 ba 55 cb ef ac ea 42 f9 14 aa 66 bc ab 3f 2b"
@@ -239,6 +284,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
    const auto encrypted_application_data_server = RFC8448_TestData
       (
       "encrypted_application_data_server",
+      Connection_Side::SERVER,
       Botan::hex_decode("17 03 03 00 43"),
       Botan::hex_decode_locked("2e 93 7e 11 ef 4a c7"
          "40 e5 38 ad 36 00 5f c4 a4 69 32 fc 32 25 d0 5f 82 aa 1b 36 e3"
@@ -254,13 +300,16 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
    auto cipher = Ciphersuite::from_name("AES_128_GCM_SHA256").value();
 
    // initialize Cipher_State with client_hello...server_hello
-   auto my_shared_secret = shared_secret;
-   auto cs = Cipher_State::init_with_server_hello(Connection_Side::CLIENT, std::move(my_shared_secret), cipher,
+   auto cs_client = Cipher_State::init_with_server_hello(Connection_Side::CLIENT, secure_vector<uint8_t>(shared_secret), cipher,
+         th_server_hello);
+   auto cs_server = Cipher_State::init_with_server_hello(Connection_Side::SERVER, secure_vector<uint8_t>(shared_secret), cipher,
          th_server_hello);
 
-   return
+   auto CHECK_both = make_CHECK_both(cs_client.get(), cs_server.get());
+
+   return flatten(
       {
-      Botan_Tests::CHECK("ciphersuite compatibility", [&](Test::Result& result)
+      CHECK_both("ciphersuite compatibility", [&](Cipher_State* cs, Connection_Side, Test::Result& result)
          {
          result.confirm("self-compatibility", cs->is_compatible_with(cipher));
          result.confirm("fully defined state is not compatible to other suites",
@@ -269,19 +318,26 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
                         !cs->is_compatible_with(Ciphersuite::from_name("PSK_WITH_AES_128_GCM_SHA256").value()));
          }),
 
-      Botan_Tests::CHECK("handshake traffic without PSK (client side)", [&](Test::Result& result)
+      CHECK_both("handshake traffic without PSK", [&](Cipher_State* cs, Connection_Side side, Test::Result& result)
          {
          result.confirm("can not yet write application data", !cs->can_encrypt_application_traffic());
          result.confirm("can not yet export key material", !cs->can_export_keys());
 
          // decrypt encrypted extensions from server
-         encrypted_extensions.decrypt(result, cs.get());
+         encrypted_extensions.xxcrypt(result, cs, side);
 
          // validate the MAC we receive in server Finished message
          const auto expected_server_mac = Botan::hex_decode("9b 9b 14 1d 90 63 37 fb d2 cb dc e7 1d f4"
                                                             "de da 4a b4 2c 30 95 72 cb 7f ff ee 54 54 b7 8f 07 18");
-         result.confirm("expecting the correct MAC for server finished", cs->verify_peer_finished_mac(th_pre_server_finished,
-                        expected_server_mac));
+         if(side == Connection_Side::CLIENT)
+            {
+            result.confirm("expecting the correct MAC for server finished", cs->verify_peer_finished_mac(th_pre_server_finished,
+                           expected_server_mac));
+            }
+         else
+            {
+            result.test_eq("expecting the correct MAC for server finished", cs->finished_mac(th_pre_server_finished), expected_server_mac);
+            }
 
          // advance Cipher_State with client_hello...server_Finished
          // (allows receiving of application data, but does not yet allow such sending)
@@ -290,17 +346,32 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
                cs->advance_with_server_finished(th_server_finished);
                });
 
-         result.confirm("can read application data", cs->can_decrypt_application_traffic());
-         result.confirm("can not yet write application data", !cs->can_encrypt_application_traffic());
+         if(side == Connection_Side::CLIENT)
+            {
+            result.confirm("can read application data", cs->can_decrypt_application_traffic());
+            result.confirm("can not yet write application data", !cs->can_encrypt_application_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());
+            }
 
          // generate the MAC for the client Finished message
          const auto expected_client_mac = Botan::hex_decode("a8 ec 43 6d 67 76 34 ae 52 5a c1 fc eb e1 1a 03"
                                                             "9e c1 76 94 fa c6 e9 85 27 b6 42 f2 ed d5 ce 61");
-         result.test_eq("generating the correct MAC for client finished", cs->finished_mac(th_server_finished), expected_client_mac);
+         if(side == Connection_Side::CLIENT)
+            {
+            result.test_eq("generating the correct MAC for client finished", cs->finished_mac(th_server_finished), expected_client_mac);
+            }
+         else
+            {
+            result.confirm("verify the correct MAC for client finished", cs->verify_peer_finished_mac(th_server_finished, expected_client_mac));
+            }
 
          // encrypt client Finished message by client
          // (under the client handshake traffic secret)
-         encrypted_client_finished_message.encrypt(result, cs.get());
+         encrypted_client_finished_message.xxcrypt(result, cs, side);
 
          // advance Cipher_State with client_hello...client_Finished
          // (allows generation of resumption PSKs)
@@ -316,39 +387,43 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt1()
 
          // decrypt "new session ticket" post-handshake message from server
          // (encrypted under the application traffic secret)
-         encrypted_new_session_ticket.decrypt(result, cs.get());
+         encrypted_new_session_ticket.xxcrypt(result, cs, side);
 
          // encrypt application data by client
-         encrypted_application_data_client.encrypt(result, cs.get());
+         encrypted_application_data_client.xxcrypt(result, cs, side);
 
          // decrypt application data from server
          // (encrypted under the application traffic secret -- and a new sequence number)
-         encrypted_application_data_server.decrypt(result, cs.get());
+         encrypted_application_data_server.xxcrypt(result, cs, side);
 
          result.confirm("can export key material still", cs->can_export_keys());
          result.test_eq("key export result did not change", cs->export_key(export_label, export_context, 16), expected_key_export);
       }),
 
-      Botan_Tests::CHECK("PSK", [&](Test::Result &result) {
+      CHECK_both("PSK", [&](Cipher_State* cs, Connection_Side, Test::Result &result) {
          // derive PSK for resumption
          const auto psk = cs->psk({0x00, 0x00} /* ticket_nonce as defined in RFC 8448 */);
          result.test_eq("PSK matches", psk, expected_psk);
-
       }),
 
-      Botan_Tests::CHECK("key update", [&](Test::Result &result) {
+      CHECK_both("key update", [&](Cipher_State* cs, Connection_Side, Test::Result &result) {
          cs->update_read_keys();
          cs->update_write_keys();
 
          result.confirm("can encrypt application traffic", cs->can_encrypt_application_traffic());
       }),
 
-      Botan_Tests::CHECK("cleanup", [&](Test::Result &result) {
+      CHECK_both("cleanup", [&](Cipher_State* cs, Connection_Side, Test::Result &result) {
          // cleanup
          cs->clear_write_keys();
          result.confirm("can no longer write application data", !cs->can_encrypt_application_traffic());
+         result.confirm("can still read application data", cs->can_decrypt_application_traffic());
+
+         cs->clear_read_keys();
+         result.confirm("can no longer write application data", !cs->can_encrypt_application_traffic());
+         result.confirm("can no longer read application data", !cs->can_decrypt_application_traffic());
          })
-      };
+      });
    }
 
 std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
@@ -419,6 +494,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
    const auto encrypted_extensions = RFC8448_TestData
       (
       "encrypted_extensions",
+      Connection_Side::SERVER,
       Botan::hex_decode("17 03 03 00 61"),
       Botan::hex_decode_locked(
          "dc 48 23 7b 4b 87 9f"
@@ -440,6 +516,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
    const auto encrypted_client_finished_message = RFC8448_TestData
       (
       "encrypted_client_finished_message",
+      Connection_Side::CLIENT,
       Botan::hex_decode("17 03 03 00 35"),
       Botan::hex_decode_locked("00 f8 b4 67 d1 4c f2"
          "2a 4b 3f 0b 6a e0 d8 e6 cc 8d 08 e0 db 35 15 ef 5c 2b df 19 22"
@@ -454,6 +531,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
    const auto encrypted_application_data_client = RFC8448_TestData
       (
       "encrypted_application_data_client",
+      Connection_Side::CLIENT,
       Botan::hex_decode("17 03 03 00 43"),
       Botan::hex_decode_locked("b1 ce bc e2 42 aa 20"
          "1b e9 ae 5e 1c b2 a9 aa 4b 33 d4 e8 66 af 1e db 06 89 19 23 77"
@@ -470,6 +548,7 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
    const auto encrypted_application_data_server = RFC8448_TestData
       (
       "encrypted_application_data_server",
+      Connection_Side::SERVER,
       Botan::hex_decode("17 03 03 00 43"),
       Botan::hex_decode_locked("27 5e 9f 20 ac ff 57"
          "bc 00 06 57 d3 86 7d f0 39 cc cf 79 04 78 84 cf 75 77 17 46 f7"
@@ -484,20 +563,26 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
 
    auto cipher = Ciphersuite::from_name("AES_128_GCM_SHA256").value();
 
-   auto cs = Cipher_State::init_with_psk(Connection_Side::CLIENT,
+   auto cs_client = Cipher_State::init_with_psk(Connection_Side::CLIENT,
+                                         Cipher_State::PSK_Type::RESUMPTION,
+                                         secure_vector<uint8_t>(psk.begin(), psk.end()),
+                                         cipher);
+   auto cs_server = Cipher_State::init_with_psk(Connection_Side::SERVER,
                                          Cipher_State::PSK_Type::RESUMPTION,
                                          secure_vector<uint8_t>(psk.begin(), psk.end()),
                                          cipher);
 
-   return
+   auto CHECK_both = make_CHECK_both(cs_client.get(), cs_server.get());
+
+   return flatten(
       {
-      Botan_Tests::CHECK("calculating PSK binder (client side)", [&] (Test::Result& result)
+      CHECK_both("calculating PSK binder", [&] (Cipher_State* cs, Connection_Side, Test::Result& result)
          {
          const auto mac = cs->psk_binder_mac(th_client_hello_prefix);
          result.test_eq("PSK binder is as expected", mac, expected_psk_binder);
          }),
 
-      Botan_Tests::CHECK("ciphersuite compatibility", [&](Test::Result& result)
+      CHECK_both("ciphersuite compatibility", [&](Cipher_State* cs, Connection_Side, Test::Result& result)
          {
          result.confirm("self-compatibility", cs->is_compatible_with(cipher));
          result.confirm("partially defined state is compatible with suites using the same hash",
@@ -510,31 +595,40 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
                         !cs->is_compatible_with(Ciphersuite::from_name("AES_256_GCM_SHA384").value()));
          }),
 
-      Botan_Tests::CHECK("calculate the early traffic secrets", [&] (Test::Result& result)
+      CHECK_both("calculate the early traffic secrets", [&] (Cipher_State* cs, Connection_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);
 
-         // TODO: Once 0-RTT traffic is implemented this will likely allow encrypting
+         // 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());
+         result.confirm("can not yet read application data", !cs->can_decrypt_application_traffic());
          }),
 
-      Botan_Tests::CHECK("handshake traffic after PSK (client side)", [&](Test::Result& result)
+      CHECK_both("handshake traffic after PSK", [&](Cipher_State* cs, Connection_Side side, Test::Result& result)
          {
          cs->advance_with_server_hello(cipher, secure_vector<uint8_t>(shared_secret), th_server_hello);
 
          // decrypt encrypted extensions from server
-         encrypted_extensions.decrypt(result, cs.get());
+         encrypted_extensions.xxcrypt(result, cs, side);
 
          // TODO: Handling of early traffic is left out as 0-RTT is not implemented yet.
 
          // validate the MAC we receive in server Finished message
          const auto expected_server_mac = Botan::hex_decode("48 d3 e0 e1 b3 d9 07 c6 ac ff 14 5e 16 09 03 88"
                                                             "c7 7b 05 c0 50 b6 34 ab 1a 88 bb d0 dd 1a 34 b2");
-         result.confirm("expecting the correct MAC for server finished", cs->verify_peer_finished_mac(th_pre_server_finished,
-                        expected_server_mac));
+         if(side == Connection_Side::CLIENT)
+            {
+            result.confirm("expecting the correct MAC for server finished", cs->verify_peer_finished_mac(th_pre_server_finished,
+                           expected_server_mac));
+            }
+         else
+            {
+            result.test_eq("expecting the correct MAC for server finished", cs->finished_mac(th_pre_server_finished),
+                           expected_server_mac);
+            }
 
          result.confirm("cannot read application data", !cs->can_decrypt_application_traffic());
          result.confirm("cannot write application data", !cs->can_encrypt_application_traffic());
@@ -546,20 +640,35 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
                cs->advance_with_server_finished(th_server_finished);
                });
 
-         result.confirm("can read application data", cs->can_decrypt_application_traffic());
-         result.confirm("cannot write application data", !cs->can_encrypt_application_traffic());
+         if(side == Connection_Side::CLIENT)
+            {
+            result.confirm("can read application data", cs->can_decrypt_application_traffic());
+            result.confirm("cannot write application data", !cs->can_encrypt_application_traffic());
+            }
+         else
+            {
+            result.confirm("cannot read application data", !cs->can_decrypt_application_traffic());
+            result.confirm("can write application data", cs->can_encrypt_application_traffic());
+            }
 
          // generate the MAC for the client Finished message
          const auto expected_client_mac = Botan::hex_decode("72 30 a9 c9 52 c2 5c d6 13 8f c5 e6 62 83 08 c4"
                                                             "1c 53 35 dd 81 b9 f9 6b ce a5 0f d3 2b da 41 6d");
-         result.test_eq("generating the correct MAC for client finished", cs->finished_mac(th_end_of_early_data), expected_client_mac);
+         if(side == Connection_Side::CLIENT)
+            {
+            result.test_eq("generating the correct MAC for client finished", cs->finished_mac(th_end_of_early_data), expected_client_mac);
+            }
+         else
+            {
+            result.confirm("verify the correct MAC for client finished", cs->verify_peer_finished_mac(th_end_of_early_data, expected_client_mac));
+            }
 
          // encrypt client Finished message by client
          // (under the client handshake traffic secret)
-         encrypted_client_finished_message.encrypt(result, cs.get());
+         encrypted_client_finished_message.xxcrypt(result, cs, side);
          }),
 
-      Botan_Tests::CHECK("application traffic after PSK (client side)", [&](Test::Result& result)
+      CHECK_both("application traffic after PSK", [&](Cipher_State* cs, Connection_Side side, Test::Result& result)
          {
          // advance Cipher_State with client_hello...client_Finished
          // (allows generation of resumption PSKs)
@@ -575,13 +684,13 @@ std::vector<Test::Result> test_secret_derivation_rfc8448_rtt0()
          result.test_eq("key export produces expected result", cs->export_key(export_label, export_context, 16), expected_key_export);
 
          // encrypt application data by client
-         encrypted_application_data_client.encrypt(result, cs.get());
+         encrypted_application_data_client.xxcrypt(result, cs, side);
 
          // decrypt application data from server
          // (encrypted under the application traffic secret -- and a new sequence number)
-         encrypted_application_data_server.decrypt(result, cs.get());
+         encrypted_application_data_server.xxcrypt(result, cs, side);
          })
-      };
+      });
    }
 
 }  // namespace
diff --git a/src/tests/test_tls_handshake_state_13.cpp b/src/tests/test_tls_handshake_state_13.cpp
index 235001f97..381ddbb44 100644
--- a/src/tests/test_tls_handshake_state_13.cpp
+++ b/src/tests/test_tls_handshake_state_13.cpp
@@ -56,9 +56,8 @@ std::vector<Test::Result> finished_message_handling()
 
          Finished_13 client_finished(client_finished_message);
 
-         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));
+         [[maybe_unused]] // just making sure that the return type of .sending is correct
+         std::reference_wrapper<Botan::TLS::Finished_13> client_fin = state.sending(std::move(client_finished));
          result.test_throws("not stored as server Finished", [&]
             {
             state.server_finished();
@@ -86,10 +85,8 @@ std::vector<Test::Result> handshake_message_filtering()
 
          auto client_hello = std::get<Client_Hello_13>(Client_Hello_13::parse(client_hello_message));
 
-         auto filtered = state.sending(std::move(client_hello));
-         result.confirm("client can send client hello",
-                        std::holds_alternative<std::reference_wrapper<Client_Hello_13>>(filtered));
-
+         [[maybe_unused]] // just making sure that the return type of .sending is correct
+         std::reference_wrapper<Client_Hello_13> filtered = state.sending(std::move(client_hello));
          result.test_eq("correct client hello stored", state.client_hello().serialize(), client_hello_message);
 
          result.template test_throws<TLS_Exception>("client cannot receive client hello",
diff --git a/src/tests/test_tls_rfc8448.cpp b/src/tests/test_tls_rfc8448.cpp
index 8c36c2c7c..fc0ec2437 100644
--- a/src/tests/test_tls_rfc8448.cpp
+++ b/src/tests/test_tls_rfc8448.cpp
@@ -478,6 +478,26 @@ class RFC8448_Text_Policy : public Botan::TLS::Text_Policy
             Botan::TLS::Signature_Scheme::DSA_SHA1          // not actually supported
             };
          }
+
+      // Overriding the key exchange group selection to favour the server's key
+      // exchange group preference. This is required to enforce a Hello Retry Request
+      // when testing RFC 8448 5. from the server side.
+      Named_Group choose_key_exchange_group(const std::vector<Group_Params>& supported_by_peer,
+                                            const std::vector<Group_Params>& offered_by_peer) const override
+         {
+         BOTAN_UNUSED(offered_by_peer);
+
+         const auto supported_by_us = key_exchange_groups();
+         const auto selected_group = std::find_if(supported_by_us.begin(), supported_by_us.end(),
+                                                  [&](const auto group)
+                                                     {
+                                                     return value_exists(supported_by_peer, group);
+                                                     });
+
+         return selected_group != supported_by_us.end()
+            ? *selected_group
+            : Named_Group::NONE;
+         }
    };
 
 /**
@@ -751,7 +771,7 @@ void sort_client_extensions(Botan::TLS::Extensions& exts, Botan::TLS::Connection
  * Because of the nature of the RFC 8448 test data we need to produce bit-compatible
  * TLS messages. Hence we sort the generated TLS extensions exactly as expected.
  */
-[[maybe_unused]] void sort_server_extensions(Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side side)
+void sort_server_extensions(Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side side, Botan::TLS::Handshake_Type /*type*/)
    {
    if(side == Botan::TLS::Connection_Side::SERVER)
       {
@@ -759,6 +779,7 @@ void sort_client_extensions(Botan::TLS::Extensions& exts, Botan::TLS::Connection
          {
          Botan::TLS::Handshake_Extension_Type::TLSEXT_SUPPORTED_GROUPS,
          Botan::TLS::Handshake_Extension_Type::TLSEXT_KEY_SHARE,
+         Botan::TLS::Handshake_Extension_Type::TLSEXT_COOKIE,
          Botan::TLS::Handshake_Extension_Type::TLSEXT_SUPPORTED_VERSIONS,
          Botan::TLS::Handshake_Extension_Type::TLSEXT_SIGNATURE_ALGORITHMS,
          Botan::TLS::Handshake_Extension_Type::TLSEXT_RECORD_SIZE_LIMIT,
@@ -788,7 +809,7 @@ void add_early_data_indication(Botan::TLS::Extensions& exts)
    exts.add(new Botan::TLS::EarlyDataIndication());
    }
 
-[[maybe_unused]] std::vector<uint8_t> strip_message_header(const std::vector<uint8_t>& msg)
+std::vector<uint8_t> strip_message_header(const std::vector<uint8_t>& msg)
    {
    BOTAN_ASSERT_NOMSG(msg.size() >= 4);
    return {msg.begin() + 4, msg.end()};
@@ -1143,7 +1164,7 @@ class Test_TLS_RFC8448_Client : public Test_TLS_RFC8448
          return {
             Botan_Tests::CHECK("Client Hello", [&](Test::Result& result)
                {
-               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_hrr"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort);
+               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_hrr_client"), 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",
@@ -1337,7 +1358,7 @@ class Test_TLS_RFC8448_Client : public Test_TLS_RFC8448
          return {
             Botan_Tests::CHECK("Client Hello", [&](Test::Result& result)
                {
-               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_compat"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort);
+               ctx = std::make_unique<Client_Context>(std::move(rng), read_tls_policy("rfc8448_compat_client"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort);
 
                result.test_eq("Client Hello", ctx->pull_send_buffer(), vars.get_req_bin("Record_ClientHello_1"));
 
@@ -1401,11 +1422,286 @@ class Test_TLS_RFC8448_Server : public Test_TLS_RFC8448
    private:
       std::string side() const override { return "Server"; }
 
-      std::vector<Test::Result> simple_1_rtt(const VarMap& /*vars*/) override { return {}; }
+      std::vector<Test::Result> simple_1_rtt(const VarMap& vars) override
+         {
+         auto rng = std::make_unique<Botan_Tests::Fixed_Output_RNG>("");
+
+         // 32 - for server hello random
+         // 32 - for KeyShare (eph. x25519 key pair)  --  I guess?
+         //  4 - for ticket_age_add (in New Session Ticket)
+         add_entropy(*rng, vars.get_req_bin("Server_RNG_Pool"));
+
+         std::unique_ptr<Server_Context> ctx;
+
+         return {
+            Botan_Tests::CHECK("Send Client Hello", [&](Test::Result& result)
+               {
+               ctx = std::make_unique<Server_Context>(std::move(rng), read_tls_policy("rfc8448_1rtt"), vars.get_req_u64("CurrentTimestamp"), sort_server_extensions, make_mock_signatures(vars));
+               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",
+                  "tls_inspect_handshake_msg_certificate_verify",
+                  "tls_inspect_handshake_msg_finished"
+                  });
+               }),
+
+            Botan_Tests::CHECK("Verify generated messages in server's first flight", [&](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", 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("Server's entire first flight", ctx->pull_send_buffer(), concat(vars.get_req_bin("Record_ServerHello"),
+                                                                                              vars.get_req_bin("Record_ServerHandshakeMessages")));
+
+               result.confirm("Server can now send application data", ctx->server.is_active());
+               }),
+
+            Botan_Tests::CHECK("Send Client Finished", [&](Test::Result& result)
+               {
+               ctx->server.received_data(vars.get_req_bin("Record_ClientFinished"));
+
+               ctx->check_callback_invocations(result, "client finished received", {
+                  "tls_inspect_handshake_msg_finished",
+                  "tls_session_activated"
+                  });
+               }),
+
+            Botan_Tests::CHECK("Send Session Ticket", [&](Test::Result&)
+               {
+               // ctx->server.send_new_session_ticket(???);
+               })
+            };
+         }
+
+      // TODO: once session resumption is implemented this test trace should be added
       std::vector<Test::Result> resumed_handshake_with_0_rtt(const VarMap& /*vars*/) override { return {}; }
-      std::vector<Test::Result> hello_retry_request(const VarMap& /*vars*/) override { return {}; }
+
+      std::vector<Test::Result> hello_retry_request(const VarMap& vars) override
+         {
+         // Fallback RNG is required to for blinding in ECDH with P-256
+         auto& fallback_rng = Test::rng();
+         auto rng = std::make_unique<Botan_Tests::Fixed_Output_RNG>(fallback_rng);
+
+         // 32 - for server hello random
+         // 32 - for KeyShare (eph. P-256 key 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)
+               {
+               auto add_cookie_and_sort = [&](Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side side, Botan::TLS::Handshake_Type type)
+                  {
+                  if(type == Handshake_Type::HELLO_RETRY_REQUEST)
+                     {
+                     // This cookie needs to be mocked into the HRR since RFC 8448 contains it.
+                     exts.add(new Cookie(vars.get_opt_bin("HelloRetryRequest_Cookie")));
+                     }
+                  sort_server_extensions(exts, side, type);
+                  };
+
+               ctx = std::make_unique<Server_Context>(std::move(rng), read_tls_policy("rfc8448_hrr_server"), vars.get_req_u64("CurrentTimestamp"), add_cookie_and_sort, make_mock_signatures(vars));
+               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_hello_retry_request",
+                  "tls_inspect_handshake_msg_client_hello",
+                  "tls_inspect_handshake_msg_hello_retry_request"
+                  });
+               }),
+
+            Botan_Tests::CHECK("Verify generated Hello Retry Request message", [&](Test::Result& result)
+               {
+               result.test_eq("Server's Hello Retry Request record", ctx->pull_send_buffer(), vars.get_req_bin("Record_HelloRetryRequest"));
+               result.confirm("TLS handshake not yet finished", !ctx->server.is_active());
+               }),
+
+            Botan_Tests::CHECK("Receive updated Client Hello message", [&](Test::Result& result)
+               {
+               ctx->server.received_data(vars.get_req_bin("Record_ClientHello_2"));
+
+               ctx->check_callback_invocations(result, "updated 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_decode_group_param",
+                  "tls_inspect_handshake_msg_client_hello",
+                  "tls_inspect_handshake_msg_server_hello",
+                  "tls_inspect_handshake_msg_encrypted_extensions",
+                  "tls_inspect_handshake_msg_certificate",
+                  "tls_inspect_handshake_msg_certificate_verify",
+                  "tls_inspect_handshake_msg_finished"
+                  });
+               }),
+
+            Botan_Tests::CHECK("Verify generated messages in server's second flight", [&](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", 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 second flight", ctx->pull_send_buffer(), concat(vars.get_req_bin("Record_ServerHello"),
+                                                                                               vars.get_req_bin("Record_ServerHandshakeMessages")));
+               result.confirm("Server could now send application data", ctx->server.is_active());
+               }),
+
+            Botan_Tests::CHECK("Receive Client Finished", [&](Test::Result& result)
+               {
+               ctx->server.received_data(vars.get_req_bin("Record_ClientFinished"));
+
+               ctx->check_callback_invocations(result, "client finished received", {
+                  "tls_inspect_handshake_msg_finished",
+                  "tls_session_activated"
+                  });
+
+               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"));
+               }),
+
+            };
+         }
+
+      // 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> middlebox_compatibility(const VarMap& /*vars*/) override { return {}; }
+
+      std::vector<Test::Result> middlebox_compatibility(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_compat_server"), vars.get_req_u64("CurrentTimestamp"), sort_server_extensions, make_mock_signatures(vars));
+               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",
+                  "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", 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")));
+
+               // Those records contain the required Change Cipher Spec message the server must produce for compatibility mode compliance
+               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("Server could now send application data", ctx->server.is_active());
+               }),
+
+            Botan_Tests::CHECK("Receive Client Finished", [&](Test::Result& result)
+               {
+               ctx->server.received_data(vars.get_req_bin("Record_ClientFinished"));
+
+               ctx->check_callback_invocations(result, "client finished received", {
+                  "tls_inspect_handshake_msg_finished",
+                  "tls_session_activated"
+                  });
+
+               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"));
+               }),
+
+            };
+         }
    };
 
 BOTAN_REGISTER_TEST("tls", "tls_rfc8448_client", Test_TLS_RFC8448_Client);
diff --git a/src/tests/test_tls_stream_integration.cpp b/src/tests/test_tls_stream_integration.cpp
index de1617c88..5ace2ed32 100644
--- a/src/tests/test_tls_stream_integration.cpp
+++ b/src/tests/test_tls_stream_integration.cpp
@@ -862,6 +862,14 @@ std::vector<SystemConfiguration> get_configurations()
          Botan::TLS::Text_Policy("allow_tls12=true\nallow_tls13=false"),
          Botan::TLS::Text_Policy("allow_tls12=true\nallow_tls13=false")),
 #if defined(BOTAN_HAS_TLS_13)
+      SystemConfiguration(
+         "TLS 1.3 only",
+         Botan::TLS::Text_Policy("allow_tls12=false\nallow_tls13=true"),
+         Botan::TLS::Text_Policy("allow_tls12=false\nallow_tls13=true")),
+      SystemConfiguration(
+         "TLS 1.x server, TLS 1.2 client",
+         Botan::TLS::Text_Policy("allow_tls12=true\nallow_tls13=false"),
+         Botan::TLS::Text_Policy("allow_tls12=true\nallow_tls13=true")),
       SystemConfiguration(
          "TLS 1.2 server, TLS 1.x client",
          Botan::TLS::Text_Policy("allow_tls12=true\nallow_tls13=true"),
diff --git a/src/tests/unit_tls.cpp b/src/tests/unit_tls.cpp
index d5cf700f4..d764fe302 100644
--- a/src/tests/unit_tls.cpp
+++ b/src/tests/unit_tls.cpp
@@ -32,6 +32,16 @@
       #include <botan/tls_session_manager_sqlite.h>
    #endif
 
+namespace Botan::TLS {
+
+// TODO: remove this, once TLS 1.3 is fully implemented
+class Strict_Policy_Without_TLS13 : public Strict_Policy
+   {
+   bool allow_tls13() const override { return false; }
+   };
+
+}
+
 #endif
 
 namespace Botan_Tests {
@@ -608,9 +618,10 @@ class Test_Policy final : public Botan::TLS::Text_Policy
    {
    public:
       Test_Policy() : Text_Policy("") {}
-      bool acceptable_protocol_version(Botan::TLS::Protocol_Version /*version*/) const override
+      bool acceptable_protocol_version(Botan::TLS::Protocol_Version version) const override
          {
-         return true;
+         // TODO: handle TLS 1.3 server once the time is ripe.
+         return version.is_pre_tls_13();
          }
 
       size_t dtls_initial_timeout() const override
@@ -801,7 +812,7 @@ class TLS_Unit_Tests final : public Test
 
 #endif
 
-         Botan::TLS::Strict_Policy strict_policy;
+         Botan::TLS::Strict_Policy_Without_TLS13 strict_policy;
          test_with_policy("Strict policy", results, *client_ses, *server_ses, *creds,
             {Botan::TLS::Protocol_Version::TLS_V12}, strict_policy);