Repository: randombit/botan
Pull request #3622 (2023-07-11 14:43:50)

[TLS 1.3] Extend CredentialsManager for upcoming TLS 1.3 PSK
This attempts to step away from the generic `Credentials_Manager::psk()` method that is parameterized with `type` and `context`. Instead, the class now has dedicated getter-methods for the specific keys it manages.

| Before | After |
| --- | --- |
| type:"tls-server", context:"session-ticket" | `Credentials_Manager::session_ticket_key()` |
| type:"tls-server", context:"dtls-cookie-secret" | `Credentials_Manager::dtls_cookie_secret()` |
| type"tls-server"/"tls-client", context:"my.host.name" | `Credentials_Manager::find_preshared_keys()` |

The TLS 1.2 implementation keeps using the `::psk()` method as before -- so existing applications that override it will continue to work as before. We think, dedicated methods for specific artefacts is much more on-par with the other TLS dependency classes (e.g. `Callbacks` and `Policy`). Additionally, its easier to understand the API like that.

New applications can make use of the added default implementation of `::psk()` that orchestrates the new dedicated methods as shown in the table above. With Botan 4.0 the `::psk()` method should probably disappear entirely. (And `Credentials_Manager` should perhaps move into the `TLS` namespace.)

The new methods `find_preshared_keys()` and `choose_preshared_key()` take into account that one can offer multiple PSKs in a TLS 1.3 Client Hello. The server then gets to choose which PSK it wants to negotiate with.

Given that TLS 1.2 pre-negotiates a singular PSK identity prior to actually requesting the actual PSK-value, it can benefit from the same `find_preshared_keys()` (through the legacy `psk()`). If exactly one PSK identity is requested, it should be guaranteed that at most one PSK is returned by `find_preshared_keys()`.

The actual TLS 1.3 PSK implementation will come in a follow-up.


Patch:
diff --git a/doc/deprecated.rst b/doc/deprecated.rst
index e86e8f8ec..c9ddf161f 100644
--- a/doc/deprecated.rst
+++ b/doc/deprecated.rst
@@ -36,6 +36,11 @@ in a future major release:
 
 - All ciphersuites using static RSA key exchange
 
+- ``Credentials_Manager::psk()`` to provide various TLS-specific keys and
+  secrets, most notably "session-ticket", "dtls-cookie-secret" and the actual
+  TLS PSKs for given identities and hosts. Instead, use the dedicated methods in
+  ``Credentials_Manager`` and do not override the ``psk()`` method any longer.
+
 Deprecated Functionality
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
diff --git a/src/bogo_shim/bogo_shim.cpp b/src/bogo_shim/bogo_shim.cpp
index 3ee3b7aa5..b94524cfd 100644
--- a/src/bogo_shim/bogo_shim.cpp
+++ b/src/bogo_shim/bogo_shim.cpp
@@ -1228,10 +1228,18 @@ std::vector<uint16_t> Shim_Policy::ciphersuite_list(Botan::TLS::Protocol_Version
 class Shim_Credentials final : public Botan::Credentials_Manager {
    public:
       Shim_Credentials(const Shim_Arguments& args) : m_args(args) {
-         m_psk_identity = m_args.get_string_opt_or_else("psk-identity", "");
+         const auto psk_identity = m_args.get_string_opt_or_else("psk-identity", "");
+         const auto psk_str = m_args.get_string_opt_or_else("psk", "");
 
-         const std::string psk_str = m_args.get_string_opt_or_else("psk", "");
-         m_psk = Botan::SymmetricKey(reinterpret_cast<const uint8_t*>(psk_str.data()), psk_str.size());
+         if(!psk_identity.empty() || !psk_str.empty()) {
+            // If the shim received a -psk param but no -psk-identity param,
+            // we have to initialize the identity as "empty string".
+            m_psk_identity = psk_identity;
+         }
+
+         if(!psk_str.empty()) {
+            m_psk = Botan::SymmetricKey(reinterpret_cast<const uint8_t*>(psk_str.data()), psk_str.size());
+         }
 
          if(m_args.option_used("key-file") && m_args.option_used("cert-file")) {
             Botan::DataSource_Stream key_stream(m_args.get_string_opt("key-file"));
@@ -1267,28 +1275,51 @@ class Shim_Credentials final : public Botan::Credentials_Manager {
       std::string psk_identity(const std::string& /*type*/,
                                const std::string& /*context*/,
                                const std::string& /*identity_hint*/) override {
-         return m_psk_identity;
+         return m_psk_identity.value_or("");
       }
 
       std::string psk_identity_hint(const std::string& /*type*/, const std::string& /*context*/) override {
-         return m_psk_identity;
+         return m_psk_identity.value_or("");
       }
 
-      Botan::SymmetricKey psk(const std::string& type,
-                              const std::string& context,
-                              const std::string& identity) override {
-         if(type == "tls-server" && context == "session-ticket") {
-            return Botan::SymmetricKey("ABCDEF0123456789");
-         }
+      Botan::secure_vector<uint8_t> session_ticket_key() override {
+         return Botan::hex_decode_locked("ABCDEF0123456789");
+      }
+
+      Botan::secure_vector<uint8_t> dtls_cookie_secret() override {
+         return Botan::hex_decode_locked("F00FB00FD00F100F700F");
+      }
 
-         if(type == "tls-server" && context == "dtls-cookie-secret") {
-            return Botan::SymmetricKey("F00FB00FD00F100F700F");
+      std::vector<Botan::TLS::ExternalPSK> find_preshared_keys(
+         std::string_view host,
+         Botan::TLS::Connection_Side whoami,
+         const std::vector<std::string>& identities = {},
+         const std::optional<std::string>& prf = std::nullopt) override {
+         if(!m_psk_identity.has_value()) {
+            return Botan::Credentials_Manager::find_preshared_keys(host, whoami, identities, prf);
          }
 
-         if(identity != m_psk_identity) {
+         auto id_matches =
+            identities.empty() || std::find(identities.begin(), identities.end(), m_psk_identity) != identities.end();
+
+         if(!id_matches) {
             throw Shim_Exception("Unexpected PSK identity");
          }
-         return m_psk;
+
+         if(!m_psk.has_value()) {
+            throw Shim_Exception("PSK identified but not set");
+         }
+
+         std::vector<Botan::TLS::ExternalPSK> psks;
+
+         // Currently, BoGo tests PSK with TLS 1.2 only. In TLS 1.2 the PRF does not
+         // need to be specified for PSKs.
+         //
+         // TODO: Once BoGo has tests for TLS 1.3 with externally provided PSKs, this
+         //       will need to be handled somehow.
+         const std::string psk_prf = "SHA-256";
+         psks.emplace_back(m_psk_identity.value(), psk_prf, m_psk->bits_of());
+         return psks;
       }
 
       std::vector<Botan::X509_Certificate> cert_chain(
@@ -1320,8 +1351,8 @@ class Shim_Credentials final : public Botan::Credentials_Manager {
 
    private:
       const Shim_Arguments& m_args;
-      Botan::SymmetricKey m_psk;
-      std::string m_psk_identity;
+      std::optional<Botan::SymmetricKey> m_psk;
+      std::optional<std::string> m_psk_identity;
       std::shared_ptr<Botan::Private_Key> m_key;
       std::vector<Botan::X509_Certificate> m_cert_chain;
 };
diff --git a/src/cli/tls_client.cpp b/src/cli/tls_client.cpp
index 32294ea93..3cdee3499 100644
--- a/src/cli/tls_client.cpp
+++ b/src/cli/tls_client.cpp
@@ -215,10 +215,10 @@ class TLS_Client final : public Command {
          const auto client_crt_path = get_arg_maybe("client-cert");
          const auto client_key_path = get_arg_maybe("client-cert-key");
 
-         const auto psk = [this]() -> std::optional<Botan::SymmetricKey> {
+         auto psk = [this]() -> std::optional<Botan::secure_vector<uint8_t>> {
             auto psk_hex = get_arg_maybe("psk");
             if(psk_hex) {
-               return Botan::SymmetricKey(Botan::hex_decode_locked(psk_hex.value()));
+               return Botan::hex_decode_locked(psk_hex.value());
             } else {
                return {};
             }
@@ -226,7 +226,7 @@ class TLS_Client final : public Command {
          const std::optional<std::string> psk_identity = get_arg_maybe("psk-identity");
 
          auto creds = std::make_shared<Basic_Credentials_Manager>(
-            use_system_cert_store, trusted_CAs, client_crt_path, client_key_path, psk, psk_identity);
+            use_system_cert_store, trusted_CAs, client_crt_path, client_key_path, std::move(psk), psk_identity);
 
          Botan::TLS::Client client(callbacks,
                                    session_mgr,
diff --git a/src/cli/tls_helpers.h b/src/cli/tls_helpers.h
index 07d01cb91..afdacaad4 100644
--- a/src/cli/tls_helpers.h
+++ b/src/cli/tls_helpers.h
@@ -56,7 +56,7 @@ class Basic_Credentials_Manager : public Botan::Credentials_Manager {
                                 std::string ca_path,
                                 std::optional<std::string> client_crt = std::nullopt,
                                 std::optional<std::string> client_key = std::nullopt,
-                                std::optional<Botan::SymmetricKey> psk = std::nullopt,
+                                std::optional<Botan::secure_vector<uint8_t>> psk = std::nullopt,
                                 std::optional<std::string> psk_identity = std::nullopt) :
             m_psk(std::move(psk)), m_psk_identity(std::move(psk_identity)) {
          if(ca_path.empty() == false) {
@@ -81,7 +81,7 @@ class Basic_Credentials_Manager : public Botan::Credentials_Manager {
 
       Basic_Credentials_Manager(const std::string& server_crt,
                                 const std::string& server_key,
-                                std::optional<Botan::SymmetricKey> psk = std::nullopt,
+                                std::optional<Botan::secure_vector<uint8_t>> psk = std::nullopt,
                                 std::optional<std::string> psk_identity = std::nullopt) :
             m_psk(std::move(psk)), m_psk_identity(std::move(psk_identity)) {
          load_credentials(server_crt, server_key);
@@ -155,13 +155,25 @@ class Basic_Credentials_Manager : public Botan::Credentials_Manager {
                    : Botan::Credentials_Manager::psk_identity(type, context, identity_hint);
       }
 
-      Botan::SymmetricKey psk(const std::string& type,
-                              const std::string& context,
-                              const std::string& identity) override {
-         return (m_psk && m_psk_identity && identity == m_psk_identity.value() &&
-                 (type == "tls-client" || type == "tls-server"))
-                   ? m_psk.value()
-                   : Botan::Credentials_Manager::psk(type, context, identity);
+      std::vector<Botan::TLS::ExternalPSK> find_preshared_keys(
+         std::string_view host,
+         Botan::TLS::Connection_Side peer_type,
+         const std::vector<std::string>& ids = {},
+         const std::optional<std::string>& prf = std::nullopt) override {
+         if(!m_psk.has_value() || !m_psk_identity.has_value()) {
+            return Botan::Credentials_Manager::find_preshared_keys(host, peer_type, ids, prf);
+         }
+
+         std::vector<Botan::TLS::ExternalPSK> psks;
+
+         const bool prf_matches = !prf.has_value() || m_psk_prf == prf.value();
+         const bool id_matches = ids.empty() || std::find(ids.begin(), ids.end(), m_psk_identity.value()) != ids.end();
+
+         if(prf_matches && id_matches) {
+            psks.emplace_back(m_psk_identity.value(), m_psk_prf, m_psk.value());
+         }
+
+         return psks;
       }
 
    private:
@@ -172,8 +184,9 @@ class Basic_Credentials_Manager : public Botan::Credentials_Manager {
 
       std::vector<Certificate_Info> m_creds;
       std::vector<std::shared_ptr<Botan::Certificate_Store>> m_certstores;
-      std::optional<Botan::SymmetricKey> m_psk;
+      std::optional<Botan::secure_vector<uint8_t>> m_psk;
       std::optional<std::string> m_psk_identity;
+      std::string m_psk_prf;
 };
 
 class TLS_All_Policy final : public Botan::TLS::Policy {
diff --git a/src/cli/tls_server.cpp b/src/cli/tls_server.cpp
index 904ed3a17..467d3b023 100644
--- a/src/cli/tls_server.cpp
+++ b/src/cli/tls_server.cpp
@@ -127,10 +127,10 @@ class TLS_Server final : public Command {
          const size_t max_clients = get_arg_sz("max-clients");
          const std::string transport = get_arg("type");
          const std::string dump_traces_to = get_arg("dump-traces");
-         const auto psk = [this]() -> std::optional<Botan::SymmetricKey> {
+         auto psk = [this]() -> std::optional<Botan::secure_vector<uint8_t>> {
             auto psk_hex = get_arg_maybe("psk");
             if(psk_hex) {
-               return Botan::SymmetricKey(Botan::hex_decode_locked(psk_hex.value()));
+               return Botan::hex_decode_locked(psk_hex.value());
             } else {
                return {};
             }
@@ -149,7 +149,7 @@ class TLS_Server final : public Command {
          auto policy = load_tls_policy(get_arg("policy"));
          auto session_manager =
             std::make_shared<Botan::TLS::Session_Manager_In_Memory>(rng_as_shared());  // TODO sqlite3
-         auto creds = std::make_shared<Basic_Credentials_Manager>(server_crt, server_key, psk, psk_identity);
+         auto creds = std::make_shared<Basic_Credentials_Manager>(server_crt, server_key, std::move(psk), psk_identity);
          auto callbacks = std::make_shared<Callbacks>(*this);
 
          output() << "Listening for new connections on " << transport << " port " << port << std::endl;
diff --git a/src/fuzzer/tls_client.cpp b/src/fuzzer/tls_client.cpp
index fe9ff0fcd..11d13a890 100644
--- a/src/fuzzer/tls_client.cpp
+++ b/src/fuzzer/tls_client.cpp
@@ -6,6 +6,7 @@
 
 #include "fuzzers.h"
 
+#include <botan/hex.h>
 #include <botan/tls_client.h>
 #include <botan/tls_session_manager_noop.h>
 
@@ -15,8 +16,26 @@ class Fuzzer_TLS_Client_Creds : public Botan::Credentials_Manager {
 
       std::string psk_identity(const std::string&, const std::string&, const std::string&) override { return "psk_id"; }
 
-      Botan::SymmetricKey psk(const std::string&, const std::string&, const std::string&) override {
-         return Botan::SymmetricKey("AABBCCDDEEFF00112233445566778899");
+      Botan::secure_vector<uint8_t> session_ticket_key() override {
+         return Botan::hex_decode_locked("AABBCCDDEEFF00112233445566778899");
+      }
+
+      Botan::secure_vector<uint8_t> dtls_cookie_secret() override {
+         return Botan::hex_decode_locked("AABBCCDDEEFF00112233445566778899");
+      }
+
+      std::vector<Botan::TLS::ExternalPSK> find_preshared_keys(
+         std::string_view host,
+         Botan::TLS::Connection_Side whoami,
+         const std::vector<std::string>& identities = {},
+         const std::optional<std::string>& prf = std::nullopt) override {
+         if(!identities.empty() && std::find(identities.begin(), identities.end(), "psk_id") == identities.end()) {
+            return Botan::Credentials_Manager::find_preshared_keys(host, whoami, identities, prf);
+         }
+
+         std::vector<Botan::TLS::ExternalPSK> psks;
+         psks.emplace_back("psk_id", "SHA-256", Botan::hex_decode_locked("AABBCCDDEEFF00112233445566778899"));
+         return psks;
       }
 };
 
diff --git a/src/fuzzer/tls_server.cpp b/src/fuzzer/tls_server.cpp
index 9c3559f43..2cb0d1d65 100644
--- a/src/fuzzer/tls_server.cpp
+++ b/src/fuzzer/tls_server.cpp
@@ -7,6 +7,7 @@
 #include "fuzzers.h"
 
 #include <botan/data_src.h>
+#include <botan/hex.h>
 #include <botan/pkcs8.h>
 #include <botan/tls_server.h>
 #include <botan/tls_session_manager_noop.h>
@@ -103,12 +104,30 @@ class Fuzzer_TLS_Server_Creds : public Botan::Credentials_Manager {
          return nullptr;
       }
 
+      Botan::secure_vector<uint8_t> session_ticket_key() override {
+         return Botan::hex_decode_locked("AABBCCDDEEFF00112233445566778899");
+      }
+
+      Botan::secure_vector<uint8_t> dtls_cookie_secret() override {
+         return Botan::hex_decode_locked("AABBCCDDEEFF00112233445566778899");
+      }
+
       std::string psk_identity_hint(const std::string&, const std::string&) override { return "psk_hint"; }
 
       std::string psk_identity(const std::string&, const std::string&, const std::string&) override { return "psk_id"; }
 
-      Botan::SymmetricKey psk(const std::string&, const std::string&, const std::string&) override {
-         return Botan::SymmetricKey("AABBCCDDEEFF00112233445566778899");
+      std::vector<Botan::TLS::ExternalPSK> find_preshared_keys(
+         std::string_view host,
+         Botan::TLS::Connection_Side whoami,
+         const std::vector<std::string>& identities = {},
+         const std::optional<std::string>& prf = std::nullopt) override {
+         if(!identities.empty() && std::find(identities.begin(), identities.end(), "psk_id") == identities.end()) {
+            return Botan::Credentials_Manager::find_preshared_keys(host, whoami, identities, prf);
+         }
+
+         std::vector<Botan::TLS::ExternalPSK> psks;
+         psks.emplace_back("psk_id", "SHA-256", Botan::hex_decode_locked("AABBCCDDEEFF00112233445566778899"));
+         return psks;
       }
 
    private:
diff --git a/src/lib/tls/credentials_manager.cpp b/src/lib/tls/credentials_manager.cpp
index 714d10c20..2b3a91f11 100644
--- a/src/lib/tls/credentials_manager.cpp
+++ b/src/lib/tls/credentials_manager.cpp
@@ -8,6 +8,7 @@
 #include <botan/credentials_manager.h>
 
 #include <botan/pkix_types.h>
+#include <botan/internal/fmt.h>
 
 namespace Botan {
 
@@ -21,10 +22,60 @@ std::string Credentials_Manager::psk_identity(const std::string& /*unused*/,
    return "";
 }
 
-SymmetricKey Credentials_Manager::psk(const std::string& /*unused*/,
-                                      const std::string& /*unused*/,
+SymmetricKey Credentials_Manager::psk(const std::string& type,
+                                      const std::string& context,
                                       const std::string& identity) {
-   throw Internal_Error("No PSK set for identity " + identity);
+   auto side = [&] {
+      if(type == "tls-client") {
+         return TLS::Connection_Side::Client;
+      } else if(type == "tls-server") {
+         return TLS::Connection_Side::Server;
+      } else {
+         throw Internal_Error(fmt("No PSK set for type {}", type));
+      }
+   }();
+
+   // New applications should use the appropriate credentials methods. This is a
+   // retrofit of the behaviour before Botan 3.2.0 and will be removed in a
+   // future major release.
+   //
+   // TODO: deprecate `psk("...", "session-ticket" | "dtls-cookie-secret")`
+   if(side == TLS::Connection_Side::Server && context == "session-ticket") {
+      if(auto key = session_ticket_key(); !key.empty()) {
+         return SymmetricKey(std::move(key));
+      }
+   } else if(side == TLS::Connection_Side::Server && context == "dtls-cookie-secret") {
+      if(auto key = dtls_cookie_secret(); !key.empty()) {
+         return SymmetricKey(std::move(key));
+      }
+   } else /* context is a host name */ {
+      // Assuming that find_preshared_keys returns _exactly_ one or no keys when
+      // searching for a single specific identity.
+      if(auto psks = find_preshared_keys(context, side, {identity}); psks.size() == 1) {
+         return SymmetricKey(psks.front().extract_master_secret());
+      }
+   }
+
+   throw Internal_Error(fmt("No PSK set for identity {}", identity));
+}
+
+std::vector<TLS::ExternalPSK> Credentials_Manager::find_preshared_keys(std::string_view /* host */,
+                                                                       TLS::Connection_Side /* whoami */,
+                                                                       const std::vector<std::string>& /* identities */,
+                                                                       const std::optional<std::string>& /* prf */) {
+   return {};
+}
+
+std::optional<TLS::ExternalPSK> Credentials_Manager::choose_preshared_key(std::string_view host,
+                                                                          TLS::Connection_Side whoami,
+                                                                          const std::vector<std::string>& identities,
+                                                                          const std::optional<std::string>& prf) {
+   auto psks = find_preshared_keys(host, whoami, identities, prf);
+   if(psks.empty()) {
+      return std::nullopt;
+   } else {
+      return std::move(psks.front());
+   }
 }
 
 std::vector<X509_Certificate> Credentials_Manager::find_cert_chain(
@@ -57,6 +108,14 @@ std::shared_ptr<Private_Key> Credentials_Manager::private_key_for(const X509_Cer
    return std::shared_ptr<Private_Key>();
 }
 
+secure_vector<uint8_t> Credentials_Manager::session_ticket_key() {
+   return {};
+}
+
+secure_vector<uint8_t> Credentials_Manager::dtls_cookie_secret() {
+   return {};
+}
+
 std::vector<Certificate_Store*> Credentials_Manager::trusted_certificate_authorities(const std::string& /*unused*/,
                                                                                      const std::string& /*unused*/) {
    return std::vector<Certificate_Store*>();
diff --git a/src/lib/tls/credentials_manager.h b/src/lib/tls/credentials_manager.h
index d78b58283..75d772f25 100644
--- a/src/lib/tls/credentials_manager.h
+++ b/src/lib/tls/credentials_manager.h
@@ -13,6 +13,8 @@
 #include <botan/pk_keys.h>
 #include <botan/strong_type.h>
 #include <botan/symkey.h>
+#include <botan/tls_external_psk.h>
+#include <botan/tls_magic.h>
 #include <botan/x509cert.h>
 #include <string>
 
@@ -21,9 +23,6 @@ namespace Botan {
 class X509_DN;
 class BigInt;
 
-/// @brief holds a PSK identity as used in TLS 1.3
-using PresharedKeyID = Strong<std::vector<uint8_t>, struct PresharedKeyID_>;
-
 /**
 * Interface for a credentials manager.
 *
@@ -135,6 +134,26 @@ class BOTAN_PUBLIC_API(2, 0) Credentials_Manager {
                                                            const std::string& type,
                                                            const std::string& context);
 
+      /**
+       * Provides a secret value to encrypt session tickets for stateless
+       * session resumptions. The default implementation returns an empty
+       * key that effectively disables session tickets.
+       *
+       * @returns a secret value to be used to encrypt session tickets in
+       *          subclasses of Session_Manager_Stateless.
+       */
+      virtual secure_vector<uint8_t> session_ticket_key();
+
+      /**
+       * Provides a secret to authenticate DTLS hello cookies. The default
+       * implementation returns an empty key that effectively disables hello
+       * cookies. Applications that wish to use DTLS are strongly advised to
+       * implement this method.
+       *
+       * @returns a secret value to authenticate DTLS hello cookies
+       */
+      virtual secure_vector<uint8_t> dtls_cookie_secret();
+
       /**
       * @param type specifies the type of operation occurring
       * @param context specifies a context relative to type.
@@ -153,14 +172,86 @@ class BOTAN_PUBLIC_API(2, 0) Credentials_Manager {
                                        const std::string& identity_hint);
 
       /**
+      * Retrieves the PSK with the given @p identity or throws an exception.
+      * It's default implementation uses find_preshared_keys() with @p identity
+      * as the single allowed identity.
+      *
+      * This method is called by the TLS 1.2 implementation exclusively and will
+      * eventually be deprecated in favor of find_preshared_keys(). Going
+      * forward, new applications should implement find_preshared_keys() and
+      * rely on psk()'s default implementation.
+      *
+      * Also, the default implementation delegates @p context "session-ticket"
+      * and "dtls-cookie-secret" to the methods session_ticket_key() and
+      * dtls_cookie_secret() respectively. New applications should implement
+      * those methods and rely on the default implementation of psk().
+      *
       * @param type specifies the type of operation occurring
       * @param context specifies a context relative to type.
       * @param identity is a PSK identity previously returned by
                psk_identity for the same type and context.
       * @return the PSK used for identity, or throw an exception if no
-      * key exists
+      *         key exists
       */
       virtual SymmetricKey psk(const std::string& type, const std::string& context, const std::string& identity);
+
+      /**
+       * Filters all available PSKs with the given criterions. Note that omitted
+       * criterions (like an empty @p identities list or an unspecified @p PRF)
+       * must be interpreted as "no restriction".
+       *
+       * Note that this is used as the underlying API for the legacy psk()
+       * method currently still used in TLS 1.2. New applications should override
+       * find_preshared_keys() and leave psk() with the default implementation.
+       *
+       * In TLS 1.3 the @p identities might contain opaque session ticket data
+       * that is not necessarily a printable string, despite the utilized
+       * std::string type. Implementations must be prepared to ignore identities
+       * generated via the TLS 1.3 resumption mechanism.
+       *
+       * @param host        the host name for which a PSK is requested (may be empty)
+       * @param whoami      the type of the host (client or server) that is requesting
+       * @param identities  an optional filter for PSK identities to be returned
+       *                    (an empty list means: all identities are welcome)
+       * @param prf         an optional filter for the Pseudo Random Function the PRFs
+       *                    must be provisioned for
+       *
+       * @returns a list of PSKs that meet the defined criterions in preference order
+       */
+      virtual std::vector<TLS::ExternalPSK> find_preshared_keys(std::string_view host,
+                                                                TLS::Connection_Side whoami,
+                                                                const std::vector<std::string>& identities = {},
+                                                                const std::optional<std::string>& prf = std::nullopt);
+
+      /**
+       * Selects a single PSK identity from the given @p identities and returns
+       * its details (i.e. the secret value) for it to be used in the handshake.
+       *
+       * The default implementation relies on the filtering capabilities
+       * provided by find_preshared_keys() and simply selects the first PSK
+       * returned. If applications need finer grained control, they should
+       * override this method.
+       *
+       * In TLS 1.3 the @p identities might contain opaque session ticket data
+       * that is not necessarily a printable string, despite the utilized
+       * std::string type. Implementations must be prepared to ignore identities
+       * generated via the TLS 1.3 resumption mechanism.
+       *
+       * @param host        the host name for which a PSK is requested (may be empty)
+       * @param whoami      the type of the host (client or server) that is requesting
+       * @param identities  an optional filter for PSK identities to be returned
+       *                    (an empty list means: all identities are welcome)
+       * @param prf         an optional filter for the Pseudo Random Function the PRFs
+       *                    must be provisioned for
+       *
+       * @returns the PSK for the selected identity or std::nullopt if no PSK
+       *          meets the requirements
+       */
+      virtual std::optional<TLS::ExternalPSK> choose_preshared_key(
+         std::string_view host,
+         TLS::Connection_Side whoami,
+         const std::vector<std::string>& identities,
+         const std::optional<std::string>& prf = std::nullopt);
 };
 
 }  // namespace Botan
diff --git a/src/lib/tls/info.txt b/src/lib/tls/info.txt
index f555b992e..1123775c8 100644
--- a/src/lib/tls/info.txt
+++ b/src/lib/tls/info.txt
@@ -19,6 +19,7 @@ tls_ciphersuite.h
 tls_client.h
 tls_exceptn.h
 tls_extensions.h
+tls_external_psk.h
 tls_handshake_msg.h
 tls_magic.h
 tls_messages.h
diff --git a/src/lib/tls/tls13/tls_psk_identity_13.cpp b/src/lib/tls/tls13/tls_psk_identity_13.cpp
index 309a3bc31..d4cdd595b 100644
--- a/src/lib/tls/tls13/tls_psk_identity_13.cpp
+++ b/src/lib/tls/tls13/tls_psk_identity_13.cpp
@@ -8,6 +8,8 @@
 
 #include <botan/tls_psk_identity_13.h>
 
+#include <botan/internal/stl_util.h>
+
 namespace Botan::TLS {
 
 namespace {
@@ -28,8 +30,20 @@ PskIdentity::PskIdentity(Opaque_Session_Handle identity,
                          const uint32_t ticket_age_add) :
       PskIdentity(std::move(identity.get()), obfuscate_ticket_age(age.count(), ticket_age_add)) {}
 
+PskIdentity::PskIdentity(PresharedKeyID identity) :
+      m_identity(to_byte_vector(identity.get())),
+
+      // RFC 8446 4.2.11
+      //    For identities established externally, an obfuscated_ticket_age of
+      //    0 SHOULD be used, and servers MUST ignore the value.
+      m_obfuscated_age(0) {}
+
 std::chrono::milliseconds PskIdentity::age(const uint32_t ticket_age_add) const {
    return std::chrono::milliseconds(obfuscate_ticket_age(m_obfuscated_age, ticket_age_add));
 }
 
+std::string PskIdentity::identity_as_string() const {
+   return Botan::to_string(m_identity);
+}
+
 }  // namespace Botan::TLS
diff --git a/src/lib/tls/tls13/tls_psk_identity_13.h b/src/lib/tls/tls13/tls_psk_identity_13.h
index c6957d7bd..7154fe911 100644
--- a/src/lib/tls/tls13/tls_psk_identity_13.h
+++ b/src/lib/tls/tls13/tls_psk_identity_13.h
@@ -19,6 +19,9 @@
 
 namespace Botan::TLS {
 
+/// @brief holds a PSK identity as used in TLS 1.3
+using PresharedKeyID = Strong<std::string, struct PresharedKeyID_>;
+
 /**
  * Represents a TLS 1.3 PSK identity as found in the Preshared Key extension
  * with an opaque identity and an associated (obfuscated) ticket age. The latter
@@ -40,16 +43,12 @@ class BOTAN_PUBLIC_API(3, 1) PskIdentity {
       /**
        * Construct from an externally provided PSK in the client
        */
-      PskIdentity(PresharedKeyID identity) :
-            m_identity(std::move(identity.get())),
-
-            // RFC 8446 4.2.11
-            //    For identities established externally, an obfuscated_ticket_age of
-            //    0 SHOULD be used, and servers MUST ignore the value.
-            m_obfuscated_age(0) {}
+      PskIdentity(PresharedKeyID identity);
 
       const std::vector<uint8_t>& identity() const { return m_identity; }
 
+      std::string identity_as_string() const;
+
       /**
        * If this represents a PSK for session resumption, it returns the
        * session's age given the de-obfuscation parameter @p ticket_age_add. For
diff --git a/src/lib/tls/tls_external_psk.h b/src/lib/tls/tls_external_psk.h
new file mode 100644
index 000000000..d41dcc7d1
--- /dev/null
+++ b/src/lib/tls/tls_external_psk.h
@@ -0,0 +1,66 @@
+/*
+ * TLS 1.3 Preshared Key Container
+ * (C) 2023 Jack Lloyd
+ *     2023 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity
+ *
+ * Botan is released under the Simplified BSD License (see license.txt)
+ */
+
+#ifndef BOTAN_TLS_EXTERNAL_PSK_H_
+#define BOTAN_TLS_EXTERNAL_PSK_H_
+
+#include <botan/secmem.h>
+#include <botan/strong_type.h>
+
+#include <utility>
+#include <vector>
+
+namespace Botan::TLS {
+
+/**
+ * This is an externally provided PreSharedKey along with its identity, master
+ * secret and (in case of TLS 1.3) a pre-provisioned Pseudo Random Function.
+ */
+class ExternalPSK {
+   public:
+      ExternalPSK(const ExternalPSK&) = delete;
+      ExternalPSK& operator=(const ExternalPSK&) = delete;
+      ExternalPSK(ExternalPSK&&) = default;
+      ExternalPSK& operator=(ExternalPSK&&) = default;
+      ~ExternalPSK() = default;
+
+      ExternalPSK(std::string_view identity, std::string_view prf_algo, secure_vector<uint8_t> psk) :
+            m_identity(identity), m_prf_algo(prf_algo), m_master_secret(std::move(psk)) {}
+
+      /**
+       * Identity (e.g. username of the PSK owner) of the preshared key.
+       * Despite the std::string return type, this may or may not be a
+       * human-readable/printable string.
+       */
+      const std::string& identity() const { return m_identity; }
+
+      /**
+       * Returns the master secret by moving it out of this object. Do not call
+       * this method more than once.
+       */
+      secure_vector<uint8_t> extract_master_secret() {
+         BOTAN_STATE_CHECK(!m_master_secret.empty());
+         return std::exchange(m_master_secret, {});
+      }
+
+      /**
+       * External preshared keys in TLS 1.3 must be provisioned with a
+       * pseudo-random function (typically SHA-256 or the like). This is
+       * needed to calculate/verify the PSK binder values in the client hello.
+       */
+      const std::string& prf_algo() const { return m_prf_algo; }
+
+   private:
+      std::string m_identity;
+      std::string m_prf_algo;
+      secure_vector<uint8_t> m_master_secret;
+};
+
+}  // namespace Botan::TLS
+
+#endif
diff --git a/src/lib/utils/stl_util.h b/src/lib/utils/stl_util.h
index 9854552ed..ec2fefeaa 100644
--- a/src/lib/utils/stl_util.h
+++ b/src/lib/utils/stl_util.h
@@ -23,12 +23,13 @@
 
 namespace Botan {
 
-inline std::vector<uint8_t> to_byte_vector(std::string_view s) {
-   return std::vector<uint8_t>(s.cbegin(), s.cend());
+template <concepts::contiguous_container T = std::vector<uint8_t>>
+inline T to_byte_vector(std::string_view s) {
+   return T(s.cbegin(), s.cend());
 }
 
-inline std::string to_string(const secure_vector<uint8_t>& bytes) {
-   return std::string(bytes.cbegin(), bytes.cend());
+inline std::string to_string(std::span<const uint8_t> bytes) {
+   return std::string(bytes.begin(), bytes.end());
 }
 
 /**


Test patch:
diff --git a/src/tests/unit_tls.cpp b/src/tests/unit_tls.cpp
index 82788c377..ec9867879 100644
--- a/src/tests/unit_tls.cpp
+++ b/src/tests/unit_tls.cpp
@@ -130,26 +130,40 @@ class Credentials_Manager_Test final : public Botan::Credentials_Manager {
          return nullptr;
       }
 
-      Botan::SymmetricKey psk(const std::string& type,
-                              const std::string& context,
-                              const std::string& /*identity*/) override {
-         if(type == "tls-server" && context == "session-ticket") {
-            return Botan::SymmetricKey("AABBCCDDEEFF012345678012345678");
-         }
+      Botan::secure_vector<uint8_t> session_ticket_key() override {
+         return Botan::hex_decode_locked("AABBCCDDEEFF012345678012345678");
+      }
 
-         if(type == "tls-server" && context == "dtls-cookie-secret") {
-            return Botan::SymmetricKey("4AEA5EAD279CADEB537A594DA0E9DE3A");
-         }
+      Botan::secure_vector<uint8_t> dtls_cookie_secret() override {
+         return Botan::hex_decode_locked("4AEA5EAD279CADEB537A594DA0E9DE3A");
+      }
 
-         if(context == "server.example.com" && type == "tls-client") {
-            return Botan::SymmetricKey("20B602D1475F2DF888FCB60D2AE03AFD");
+      std::vector<Botan::TLS::ExternalPSK> find_preshared_keys(
+         std::string_view host,
+         Botan::TLS::Connection_Side whoami,
+         const std::vector<std::string>& identities = {},
+         const std::optional<std::string>& prf = std::nullopt) override {
+         if(identities.empty()) {
+            return find_preshared_keys(host, whoami, identities, prf);
          }
 
-         if(context == "server.example.com" && type == "tls-server") {
-            return Botan::SymmetricKey("20B602D1475F2DF888FCB60D2AE03AFD");
+         std::vector<Botan::TLS::ExternalPSK> psks;
+
+         if(host == "server.example.com") {
+            // PSKs for server and client are equal. For the sake of clarity,
+            // we're not simplifying this code further
+            if(whoami == Botan::TLS::Connection_Side::Client) {
+               psks.emplace_back(
+                  std::string{}, "SHA-256", Botan::hex_decode_locked("20B602D1475F2DF888FCB60D2AE03AFD"));
+            }
+
+            if(whoami == Botan::TLS::Connection_Side::Server) {
+               psks.emplace_back(
+                  std::string{}, "SHA-256", Botan::hex_decode_locked("20B602D1475F2DF888FCB60D2AE03AFD"));
+            }
          }
 
-         throw Test_Error("No PSK set for " + type + "/" + context);
+         return psks;
       }
 
       const std::vector<Botan::X509_DN>& get_acceptable_cas() const { return m_acceptable_cas; }