This is an automated email from the ASF dual-hosted git repository.

bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new db21b1b3d4 Make QUIC token secrets configurable (#13397)
db21b1b3d4 is described below

commit db21b1b3d4b3f6905bf3a9abfab7c7616b62cd04
Author: Brian Neradt <[email protected]>
AuthorDate: Mon Aug 3 21:46:43 2026 -0500

    Make QUIC token secrets configurable (#13397)
    
    QUIC address-validation and stateless-reset tokens use fixed
    compile-time secrets, allowing anyone with the source to reproduce
    valid token MACs.
    
    This replaces the fixed values with reloadable 32-byte keys and a
    random per-process fallback. Multiple file keys allow rotation without
    immediately invalidating address-validation tokens, and HMAC-SHA256
    protects all token types.
    
    This also rejects malformed tokens before parsing and adds coverage for
    file loading, key rotation, invalid files, and fallback-key stability.
---
 doc/admin-guide/files/records.yaml.en.rst          |  18 +-
 include/iocore/net/quic/QUICConfig.h               |  41 ++++
 include/iocore/net/quic/QUICTypes.h                |  13 +-
 src/iocore/net/CMakeLists.txt                      |   3 +
 src/iocore/net/quic/QUICConfig.cc                  | 143 +++++++++++++
 src/iocore/net/quic/QUICTypes.cc                   | 222 +++++++++++++++------
 .../net/unit_tests/test_QUICTokenKeyConfig.cc      | 138 +++++++++++++
 src/mgmt/config/FileManager.cc                     |   5 +
 src/records/RecordsConfig.cc                       |   2 +
 9 files changed, 516 insertions(+), 69 deletions(-)

diff --git a/doc/admin-guide/files/records.yaml.en.rst 
b/doc/admin-guide/files/records.yaml.en.rst
index 043ed594b6..16802493d5 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -5231,8 +5231,8 @@ removed in the future without prior notice.
 .. ts:cv:: CONFIG proxy.config.quic.instance_id INT 0
    :reloadable:
 
-   A static key used for calculating Stateless Reset Token. All instances in a
-   cluster need to share the same value.
+   An instance identifier mixed into Stateless Reset Tokens. All instances in a
+   cluster that share token keys need to use the same value.
 
 .. ts:cv:: CONFIG proxy.config.quic.connection_table.size INT 65521
 
@@ -5247,6 +5247,20 @@ removed in the future without prior notice.
 
    Enables Stateless Retry.
 
+.. ts:cv:: CONFIG proxy.config.quic.server.token_key.filename STRING NULL
+   :reloadable:
+
+   The file containing the secret keys used to generate QUIC address-validation
+   and stateless-reset tokens. Relative paths are resolved from the |TS|
+   configuration directory. The file must contain one or more raw 32-byte keys.
+   The first key generates new tokens, while all keys validate 
address-validation
+   tokens to support key rotation. For example, generate a key with
+   ``head -c32 /dev/urandom > quic_token.key``.
+
+   When this is not set, |TS| generates a random per-process key at startup. 
Set
+   the same key file on each server that must validate tokens generated by 
other
+   servers. Reload configuration after changing the key file.
+
 .. ts:cv:: CONFIG proxy.config.quic.client.vn_exercise_enabled INT 0
    :reloadable:
 
diff --git a/include/iocore/net/quic/QUICConfig.h 
b/include/iocore/net/quic/QUICConfig.h
index b0b1ea25b2..ee95c2773f 100644
--- a/include/iocore/net/quic/QUICConfig.h
+++ b/include/iocore/net/quic/QUICConfig.h
@@ -34,6 +34,47 @@
 #include "iocore/net/SSLTypes.h"
 #include "mgmt/config/ConfigContext.h"
 
+#include <array>
+#include <string>
+#include <vector>
+
+class QUICTokenKeyConfigParams : public ConfigInfo
+{
+public:
+  static constexpr size_t KEY_LENGTH = 32;
+  using Key                          = std::array<uint8_t, KEY_LENGTH>;
+
+  ~QUICTokenKeyConfigParams() override;
+
+  /** Load one or more raw token keys from @a path. */
+  bool load(const char *path, ConfigContext ctx = {});
+
+  /** Generate a random primary token key. */
+  bool generate(ConfigContext ctx = {});
+
+  const std::vector<Key> &keys() const;
+  const std::string      &filename() const;
+
+private:
+  std::vector<Key> m_keys;
+  std::string      m_filename;
+};
+
+class QUICTokenKeyConfig
+{
+public:
+  static void startup();
+  static bool reconfigure(ConfigContext ctx = {});
+
+  static QUICTokenKeyConfigParams *acquire();
+  static void                      release(QUICTokenKeyConfigParams *params);
+
+  using scoped_config = ConfigProcessor::scoped_config<QUICTokenKeyConfig, 
QUICTokenKeyConfigParams>;
+
+private:
+  static int _config_id;
+};
+
 class QUICConfigParams : public ConfigInfo
 {
 public:
diff --git a/include/iocore/net/quic/QUICTypes.h 
b/include/iocore/net/quic/QUICTypes.h
index 951226b054..f48b2c7c8b 100644
--- a/include/iocore/net/quic/QUICTypes.h
+++ b/include/iocore/net/quic/QUICTypes.h
@@ -321,13 +321,20 @@ private:
 class QUICAddressValidationToken
 {
 public:
+  static constexpr size_t MAC_LENGTH = 32;
+
   enum class Type : uint8_t {
     RESUMPTION,
     RETRY,
   };
 
-  // FIXME Check token length
-  QUICAddressValidationToken(const uint8_t *buf, size_t len) : _token_len(len) 
{ memcpy(this->_token, buf, len); }
+  QUICAddressValidationToken(const uint8_t *buf, size_t len)
+  {
+    if (buf != nullptr && len <= sizeof(_token)) {
+      memcpy(_token, buf, len);
+      _token_len = len;
+    }
+  }
   virtual ~QUICAddressValidationToken(){};
 
   static Type
@@ -354,7 +361,7 @@ protected:
 
   // The size should be smaller than maximum size of Retry packet
   uint8_t      _token[1200] = {0};
-  unsigned int _token_len;
+  unsigned int _token_len   = 0;
 };
 
 class QUICResumptionToken : public QUICAddressValidationToken
diff --git a/src/iocore/net/CMakeLists.txt b/src/iocore/net/CMakeLists.txt
index bd1abce1b6..b317e26b3e 100644
--- a/src/iocore/net/CMakeLists.txt
+++ b/src/iocore/net/CMakeLists.txt
@@ -156,6 +156,9 @@ if(BUILD_TESTING)
   if(SSLLIB_IS_AT_LEAST_OPENSSL3)
     target_sources(test_net PRIVATE unit_tests/test_SSLDHParams.cc)
   endif()
+  if(TS_USE_QUIC)
+    target_sources(test_net PRIVATE unit_tests/test_QUICTokenKeyConfig.cc)
+  endif()
   # Use link groups to solve circular dependency
   set(LINK_GROUP_LIBS
       ts::logging
diff --git a/src/iocore/net/quic/QUICConfig.cc 
b/src/iocore/net/quic/QUICConfig.cc
index 7d9fe28927..7e25ae669b 100644
--- a/src/iocore/net/quic/QUICConfig.cc
+++ b/src/iocore/net/quic/QUICConfig.cc
@@ -23,13 +23,19 @@
 
 #include "iocore/net/quic/QUICConfig.h"
 
+#include <openssl/crypto.h>
+#include <openssl/rand.h>
 #if TS_HAS_OPENSSL_QUIC
 #include <openssl/quic.h>
 #endif
 #include <openssl/ssl.h>
 
 #include "mgmt/config/ConfigContextDiags.h"
+#include "mgmt/config/ConfigRegistry.h"
 #include "records/RecHttp.h"
+#include "tscore/Layout.h"
+#include "tscore/MatcherUtils.h"
+#include "tscore/ink_memory.h"
 
 #include "../P_SSLConfig.h"
 #include "../P_TLSKeyLogger.h"
@@ -37,9 +43,145 @@
 #include "iocore/net/quic/QUICGlobals.h"
 #include "iocore/net/quic/QUICTransportParameters.h"
 
+int QUICTokenKeyConfig::_config_id           = 0;
 int QUICConfig::_config_id                   = 0;
 int QUICConfigParams::_connection_table_size = 65521;
 
+QUICTokenKeyConfigParams::~QUICTokenKeyConfigParams()
+{
+  if (!m_keys.empty()) {
+    OPENSSL_cleanse(m_keys.data(), m_keys.size() * sizeof(Key));
+  }
+}
+
+bool
+QUICTokenKeyConfigParams::load(const char *path, ConfigContext ctx)
+{
+  int            key_data_len = 0;
+  ats_scoped_str key_data{readIntoBuffer(path, __func__, &key_data_len)};
+
+  if (!key_data) {
+    CfgLoadFail(ctx, "Could not load QUIC token key from %s", path);
+    return false;
+  }
+
+  if (key_data_len < 0) {
+    CfgLoadFail(ctx, "QUIC token key file %s is too large", path);
+    return false;
+  }
+
+  size_t const key_data_size = static_cast<size_t>(key_data_len);
+
+  if (key_data_size < KEY_LENGTH || key_data_size % KEY_LENGTH != 0) {
+    CfgLoadFail(ctx, "QUIC token key file %s must contain one or more %zu-byte 
keys", path, KEY_LENGTH);
+    OPENSSL_cleanse(key_data.get(), key_data_size);
+    return false;
+  }
+
+  m_keys.resize(key_data_size / KEY_LENGTH);
+  memcpy(m_keys.data(), key_data.get(), key_data_size);
+  OPENSSL_cleanse(key_data.get(), key_data_size);
+  m_filename = path;
+  return true;
+}
+
+bool
+QUICTokenKeyConfigParams::generate(ConfigContext ctx)
+{
+  m_keys.resize(1);
+  if (RAND_bytes(m_keys.front().data(), 
static_cast<int>(m_keys.front().size())) != 1) {
+    CfgLoadFail(ctx, "Could not generate a random QUIC token key");
+    OPENSSL_cleanse(m_keys.data(), m_keys.size() * sizeof(Key));
+    m_keys.clear();
+    return false;
+  }
+  return true;
+}
+
+const std::vector<QUICTokenKeyConfigParams::Key> &
+QUICTokenKeyConfigParams::keys() const
+{
+  return m_keys;
+}
+
+const std::string &
+QUICTokenKeyConfigParams::filename() const
+{
+  return m_filename;
+}
+
+void
+QUICTokenKeyConfig::startup()
+{
+  
config::ConfigRegistry::Get_Instance().register_record_config("quic_token_key",
+                                                                
[](ConfigContext ctx) {
+                                                                  
CfgLoadLog(ctx, DL_Note, "QUIC token key loading ...");
+                                                                  if 
(QUICTokenKeyConfig::reconfigure(ctx)) {
+                                                                    
ctx.complete("QUIC token key reloaded");
+                                                                  } else {
+                                                                    
ctx.fail("Failed to reload QUIC token key");
+                                                                  }
+                                                                },
+                                                                
{"proxy.config.quic.server.token_key.filename"});
+
+  if (!reconfigure()) {
+    Fatal("Failed to initialize QUIC token key");
+  }
+}
+
+bool
+QUICTokenKeyConfig::reconfigure(ConfigContext ctx)
+{
+  std::string path;
+  if (auto rec_str = 
RecGetRecordStringAlloc("proxy.config.quic.server.token_key.filename"); rec_str 
&& !rec_str->empty()) {
+    path = Layout::relative_to(Layout::get()->sysconfdir, *rec_str);
+  }
+
+  if (path.empty()) {
+    bool already_random = false;
+    {
+      scoped_config current;
+      already_random = current && current->filename().empty();
+    }
+    if (already_random) {
+      return true;
+    }
+  }
+
+  auto *params = new QUICTokenKeyConfigParams;
+  if ((!path.empty() && !params->load(path.c_str(), ctx)) || (path.empty() && 
!params->generate(ctx))) {
+    delete params;
+    return false;
+  }
+
+  bool unchanged = false;
+  {
+    scoped_config current;
+    unchanged = current && current->filename() == params->filename() && 
current->keys() == params->keys();
+  }
+  if (unchanged) {
+    delete params;
+    return true;
+  }
+
+  _config_id = configProcessor.set(_config_id, params);
+  return true;
+}
+
+QUICTokenKeyConfigParams *
+QUICTokenKeyConfig::acquire()
+{
+  return static_cast<QUICTokenKeyConfigParams 
*>(configProcessor.get(_config_id));
+}
+
+void
+QUICTokenKeyConfig::release(QUICTokenKeyConfigParams *params)
+{
+  if (_config_id > 0) {
+    configProcessor.release(_config_id, params);
+  }
+}
+
 SSL_CTX *
 quic_new_ssl_ctx()
 {
@@ -471,6 +613,7 @@ QUICConfigParams::get_cc_algorithm() const
 void
 QUICConfig::startup()
 {
+  QUICTokenKeyConfig::startup();
   reconfigure();
 }
 
diff --git a/src/iocore/net/quic/QUICTypes.cc b/src/iocore/net/quic/QUICTypes.cc
index 419c586a54..9de55694fb 100644
--- a/src/iocore/net/quic/QUICTypes.cc
+++ b/src/iocore/net/quic/QUICTypes.cc
@@ -27,13 +27,82 @@
 #include <sstream>
 
 #include "iocore/net/quic/QUICTypes.h"
+#include "iocore/net/quic/QUICConfig.h"
 #include "iocore/net/quic/QUICIntUtil.h"
-#include "tscore/CryptoHash.h"
 #include <random>
+#include <openssl/crypto.h>
 #include <openssl/hmac.h>
 
 uint8_t QUICConnectionId::SCID_LEN = 0;
 
+namespace
+{
+bool
+token_hmac(const QUICTokenKeyConfigParams::Key &key, const uint8_t *data, 
size_t data_len,
+           uint8_t (&digest)[QUICAddressValidationToken::MAC_LENGTH])
+{
+  unsigned int digest_len = 0;
+  return HMAC(EVP_sha256(), key.data(), static_cast<int>(key.size()), data, 
data_len, digest, &digest_len) != nullptr &&
+         digest_len == sizeof(digest);
+}
+
+bool
+generate_token_hmac(const uint8_t *data, size_t data_len, uint8_t 
(&digest)[QUICAddressValidationToken::MAC_LENGTH])
+{
+  QUICTokenKeyConfig::scoped_config key_config;
+  return key_config && !key_config->keys().empty() && 
token_hmac(key_config->keys().front(), data, data_len, digest);
+}
+
+bool
+validate_token_hmac(const uint8_t *data, size_t data_len, const uint8_t 
*expected)
+{
+  QUICTokenKeyConfig::scoped_config key_config;
+  uint8_t                           
digest[QUICAddressValidationToken::MAC_LENGTH];
+  bool                              valid = false;
+
+  if (!key_config) {
+    return false;
+  }
+
+  for (auto const &key : key_config->keys()) {
+    if (token_hmac(key, data, data_len, digest)) {
+      valid |= CRYPTO_memcmp(digest, expected, sizeof(digest)) == 0;
+    }
+  }
+  OPENSSL_cleanse(digest, sizeof(digest));
+  return valid;
+}
+
+size_t
+retry_token_data(const IpEndpoint &src, QUICConnectionId original_dcid, 
QUICConnectionId scid, uint8_t *data, size_t data_size)
+{
+  ats_ip_nptop(src, reinterpret_cast<char *>(data), data_size);
+  size_t data_len = strlen(reinterpret_cast<char *>(data));
+  size_t cid_len  = 0;
+
+  data[data_len++] = original_dcid.length();
+  QUICTypeUtil::write_QUICConnectionId(original_dcid, data + data_len, 
&cid_len);
+  data_len         += cid_len;
+  data[data_len++]  = scid.length();
+  QUICTypeUtil::write_QUICConnectionId(scid, data + data_len, &cid_len);
+  return data_len + cid_len;
+}
+
+size_t
+resumption_token_data(const IpEndpoint &src, QUICConnectionId cid, ink_hrtime 
expire_time, uint8_t *data, size_t data_size)
+{
+  ats_ip_nptop(src, reinterpret_cast<char *>(data), data_size);
+  size_t data_len = strlen(reinterpret_cast<char *>(data));
+  size_t cid_len  = 0;
+  size_t ignored  = 0;
+
+  QUICTypeUtil::write_QUICConnectionId(cid, data + data_len, &cid_len);
+  data_len += cid_len;
+  QUICIntUtil::write_uint_as_nbytes(expire_time >> 30, 4, data + data_len, 
&ignored);
+  return data_len + 4;
+}
+} // namespace
+
 // TODO: move to somewhere in lib/ts/
 int
 to_hex_str(char *dst, size_t dst_len, const uint8_t *src, size_t src_len)
@@ -276,17 +345,18 @@ QUICTypeUtil::write_QUICMaxData(uint64_t max_data, 
uint8_t *buf, size_t *len)
 
 QUICStatelessResetToken::QUICStatelessResetToken(const QUICConnectionId 
&conn_id, uint32_t instance_id)
 {
-  uint64_t              data = conn_id ^ instance_id;
-  CryptoHash            _hash;
-  static constexpr char STATELESS_RESET_TOKEN_KEY[] = 
"stateless_token_reset_key";
-  CryptoContext         ctx;
-  ctx.update(STATELESS_RESET_TOKEN_KEY, strlen(STATELESS_RESET_TOKEN_KEY));
-  ctx.update(reinterpret_cast<void *>(&data), 8);
-  ctx.finalize(_hash);
+  uint8_t data[QUICConnectionId::MAX_LENGTH + sizeof(instance_id)];
+  uint8_t digest[QUICAddressValidationToken::MAC_LENGTH];
+  size_t  data_len = conn_id.length();
+  size_t  ignored  = 0;
 
-  size_t dummy;
-  QUICIntUtil::write_uint_as_nbytes(_hash.u64[0], 8, _token, &dummy);
-  QUICIntUtil::write_uint_as_nbytes(_hash.u64[1], 8, _token + 8, &dummy);
+  memcpy(data, static_cast<const uint8_t *>(conn_id), data_len);
+  QUICIntUtil::write_uint_as_nbytes(instance_id, sizeof(instance_id), data + 
data_len, &ignored);
+  data_len += sizeof(instance_id);
+
+  ink_release_assert(generate_token_hmac(data, data_len, digest));
+  memcpy(_token, digest, sizeof(_token));
+  OPENSSL_cleanse(digest, sizeof(digest));
 }
 
 uint64_t
@@ -312,29 +382,19 @@ QUICStatelessResetToken::hex() const
 
 QUICResumptionToken::QUICResumptionToken(const IpEndpoint &src, 
QUICConnectionId cid, ink_hrtime expire_time)
 {
-  // TODO: read cookie secret from file like SSLTicketKeyConfig
-  static constexpr char stateless_retry_token_secret[] = 
"stateless_cookie_secret";
-  size_t                dummy;
-
-  uint8_t data[1 + INET6_ADDRPORTSTRLEN + QUICConnectionId::MAX_LENGTH + 4] = 
{0};
-  size_t  data_len                                                          = 
0;
-  ats_ip_nptop(src, reinterpret_cast<char *>(data), sizeof(data));
-  data_len = strlen(reinterpret_cast<char *>(data));
-
-  size_t cid_len;
-  QUICTypeUtil::write_QUICConnectionId(cid, data + data_len, &cid_len);
-  data_len += cid_len;
-
-  QUICIntUtil::write_uint_as_nbytes(expire_time >> 30, 4, data + data_len, 
&dummy);
-  data_len += 4;
+  size_t  ignored                                                       = 0;
+  size_t  cid_len                                                       = 0;
+  uint8_t data[INET6_ADDRPORTSTRLEN + QUICConnectionId::MAX_LENGTH + 4] = {0};
+  size_t  data_len = resumption_token_data(src, cid, expire_time, data, 
sizeof(data));
 
   this->_token[0] = static_cast<uint8_t>(Type::RESUMPTION);
-  HMAC(EVP_sha1(), stateless_retry_token_secret, 
sizeof(stateless_retry_token_secret), data, data_len, this->_token + 1,
-       &this->_token_len);
-  ink_assert(this->_token_len == 20);
-  this->_token_len += 1;
+  uint8_t digest[MAC_LENGTH];
+  ink_release_assert(generate_token_hmac(data, data_len, digest));
+  memcpy(this->_token + 1, digest, sizeof(digest));
+  OPENSSL_cleanse(digest, sizeof(digest));
+  this->_token_len = 1 + MAC_LENGTH;
 
-  QUICIntUtil::write_uint_as_nbytes(expire_time >> 30, 4, this->_token + 
this->_token_len, &dummy);
+  QUICIntUtil::write_uint_as_nbytes(expire_time >> 30, 4, this->_token + 
this->_token_len, &ignored);
   this->_token_len += 4;
 
   QUICTypeUtil::write_QUICConnectionId(cid, this->_token + this->_token_len, 
&cid_len);
@@ -344,48 +404,49 @@ QUICResumptionToken::QUICResumptionToken(const IpEndpoint 
&src, QUICConnectionId
 bool
 QUICResumptionToken::is_valid(const IpEndpoint &src) const
 {
-  QUICResumptionToken x(src, this->cid(), this->expire_time() << 30);
-  return *this == x && this->expire_time() >= (ink_get_hrtime() >> 30);
+  if (this->_token_len < 1 + MAC_LENGTH + 4 || this->_token[0] != 
static_cast<uint8_t>(Type::RESUMPTION) ||
+      this->_token_len > 1 + MAC_LENGTH + 4 + QUICConnectionId::MAX_LENGTH) {
+    return false;
+  }
+
+  auto    token_cid                                                     = 
this->cid();
+  auto    token_expire_time                                             = 
this->expire_time();
+  uint8_t data[INET6_ADDRPORTSTRLEN + QUICConnectionId::MAX_LENGTH + 4] = {0};
+  size_t  data_len = resumption_token_data(src, token_cid, token_expire_time 
<< 30, data, sizeof(data));
+  return token_expire_time >= (ink_get_hrtime() >> 30) && 
validate_token_hmac(data, data_len, this->_token + 1);
 }
 
 const QUICConnectionId
 QUICResumptionToken::cid() const
 {
-  // Type uses 1 byte and output of EVP_sha1() should be 160 bits
-  return QUICTypeUtil::read_QUICConnectionId(this->_token + (1 + 20 + 4), 
this->_token_len - (1 + 20 + 4));
+  constexpr size_t prefix_len = 1 + MAC_LENGTH + 4;
+  if (this->_token_len < prefix_len || this->_token_len > prefix_len + 
QUICConnectionId::MAX_LENGTH) {
+    return QUICConnectionId::ZERO();
+  }
+  return QUICTypeUtil::read_QUICConnectionId(this->_token + prefix_len, 
this->_token_len - prefix_len);
 }
 
 ink_hrtime
 QUICResumptionToken::expire_time() const
 {
-  return QUICIntUtil::read_nbytes_as_uint(this->_token + (1 + 20), 4);
+  if (this->_token_len < 1 + MAC_LENGTH + 4) {
+    return 0;
+  }
+  return QUICIntUtil::read_nbytes_as_uint(this->_token + (1 + MAC_LENGTH), 4);
 }
 
 QUICRetryToken::QUICRetryToken(const IpEndpoint &src, QUICConnectionId 
original_dcid, QUICConnectionId scid)
 {
-  // TODO: read cookie secret from file like SSLTicketKeyConfig
-  static constexpr char stateless_retry_token_secret[] = 
"stateless_cookie_secret";
-
-  uint8_t data[1 + INET6_ADDRPORTSTRLEN + QUICConnectionId::MAX_LENGTH] = {0};
-  size_t  data_len                                                      = 0;
-  ats_ip_nptop(src, reinterpret_cast<char *>(data), sizeof(data));
-  data_len = strlen(reinterpret_cast<char *>(data));
-
-  size_t cid_len;
-  *(data + data_len)  = original_dcid.length();
-  data_len           += 1;
-  QUICTypeUtil::write_QUICConnectionId(original_dcid, data + data_len, 
&cid_len);
-  data_len           += cid_len;
-  *(data + data_len)  = scid.length();
-  data_len           += 1;
-  QUICTypeUtil::write_QUICConnectionId(scid, data + data_len, &cid_len);
-  data_len += cid_len;
+  uint8_t data[INET6_ADDRPORTSTRLEN + 2 + 2 * QUICConnectionId::MAX_LENGTH] = 
{0};
+  size_t  data_len = retry_token_data(src, original_dcid, scid, data, 
sizeof(data));
+  size_t  cid_len  = 0;
 
   this->_token[0] = static_cast<uint8_t>(Type::RETRY);
-  HMAC(EVP_sha1(), stateless_retry_token_secret, 
sizeof(stateless_retry_token_secret), data, data_len, this->_token + 1,
-       &this->_token_len);
-  ink_assert(this->_token_len == 20);
-  this->_token_len += 1;
+  uint8_t digest[MAC_LENGTH];
+  ink_release_assert(generate_token_hmac(data, data_len, digest));
+  memcpy(this->_token + 1, digest, sizeof(digest));
+  OPENSSL_cleanse(digest, sizeof(digest));
+  this->_token_len = 1 + MAC_LENGTH;
 
   *(this->_token + this->_token_len)  = original_dcid.length();
   this->_token_len                   += 1;
@@ -400,24 +461,57 @@ QUICRetryToken::QUICRetryToken(const IpEndpoint &src, 
QUICConnectionId original_
 bool
 QUICRetryToken::is_valid(const IpEndpoint &src) const
 {
-  return *this == QUICRetryToken(src, this->original_dcid(), this->scid());
+  constexpr size_t fixed_len = 1 + MAC_LENGTH + 2;
+  if (this->_token_len < fixed_len || this->_token[0] != 
static_cast<uint8_t>(Type::RETRY)) {
+    return false;
+  }
+
+  size_t original_dcid_len = this->_token[1 + MAC_LENGTH];
+  size_t scid_len_offset   = 1 + MAC_LENGTH + 1 + original_dcid_len;
+  if (original_dcid_len > QUICConnectionId::MAX_LENGTH || scid_len_offset >= 
this->_token_len) {
+    return false;
+  }
+
+  size_t scid_len = this->_token[scid_len_offset];
+  if (scid_len > QUICConnectionId::MAX_LENGTH || scid_len_offset + 1 + 
scid_len != this->_token_len) {
+    return false;
+  }
+
+  uint8_t data[INET6_ADDRPORTSTRLEN + 2 + 2 * QUICConnectionId::MAX_LENGTH] = 
{0};
+  size_t  data_len = retry_token_data(src, this->original_dcid(), 
this->scid(), data, sizeof(data));
+  return validate_token_hmac(data, data_len, this->_token + 1);
 }
 
 const QUICConnectionId
 QUICRetryToken::original_dcid() const
 {
-  // Type uses 1 byte and output of EVP_sha1() should be 160 bits
-  auto len   = *(this->_token + (1 + 20));
-  auto start = this->_token + (1 + 20 + 1);
+  if (this->_token_len < 1 + MAC_LENGTH + 2) {
+    return QUICConnectionId::ZERO();
+  }
+  auto len = this->_token[1 + MAC_LENGTH];
+  if (len > QUICConnectionId::MAX_LENGTH || 1 + MAC_LENGTH + 1 + len >= 
this->_token_len) {
+    return QUICConnectionId::ZERO();
+  }
+  auto start = this->_token + (1 + MAC_LENGTH + 1);
   return QUICTypeUtil::read_QUICConnectionId(start, len);
 }
 
 const QUICConnectionId
 QUICRetryToken::scid() const
 {
-  auto len   = *(this->_token + (1 + 20));
-  auto start = this->_token + (1 + 20 + 1 + len + 1);
-  len        = *(this->_token + (1 + 20 + 1 + len));
+  if (this->_token_len < 1 + MAC_LENGTH + 2) {
+    return QUICConnectionId::ZERO();
+  }
+  auto original_dcid_len = this->_token[1 + MAC_LENGTH];
+  auto scid_len_offset   = 1 + MAC_LENGTH + 1 + original_dcid_len;
+  if (original_dcid_len > QUICConnectionId::MAX_LENGTH || scid_len_offset >= 
this->_token_len) {
+    return QUICConnectionId::ZERO();
+  }
+  auto len = this->_token[scid_len_offset];
+  if (len > QUICConnectionId::MAX_LENGTH || scid_len_offset + 1 + len != 
this->_token_len) {
+    return QUICConnectionId::ZERO();
+  }
+  auto start = this->_token + scid_len_offset + 1;
   return QUICTypeUtil::read_QUICConnectionId(start, len);
 }
 
diff --git a/src/iocore/net/unit_tests/test_QUICTokenKeyConfig.cc 
b/src/iocore/net/unit_tests/test_QUICTokenKeyConfig.cc
new file mode 100644
index 0000000000..4e0a066c2a
--- /dev/null
+++ b/src/iocore/net/unit_tests/test_QUICTokenKeyConfig.cc
@@ -0,0 +1,138 @@
+/** @file
+
+  Tests for QUIC token key configuration.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include "iocore/net/quic/QUICConfig.h"
+#include "iocore/net/quic/QUICTypes.h"
+#include "records/RecCore.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <filesystem>
+#include <fstream>
+#include <string>
+#include <unistd.h>
+
+namespace
+{
+class TokenKeyFile
+{
+public:
+  explicit TokenKeyFile(const std::string &contents)
+    : _path(std::filesystem::temp_directory_path() / ("ats-quic-token-key-" + 
std::to_string(getpid())))
+  {
+    write(contents);
+  }
+
+  ~TokenKeyFile()
+  {
+    std::error_code ec;
+    std::filesystem::remove(_path, ec);
+  }
+
+  void
+  write(const std::string &contents) const
+  {
+    std::ofstream output(_path, std::ios::binary | std::ios::trunc);
+    REQUIRE(output.is_open());
+    output.write(contents.data(), contents.size());
+    REQUIRE(output.good());
+  }
+
+  std::string
+  path() const
+  {
+    return _path.string();
+  }
+
+private:
+  std::filesystem::path _path;
+};
+} // namespace
+
+TEST_CASE("QUIC tokens use reloadable key files", "[quic][security]")
+{
+  std::string const key_a(QUICTokenKeyConfigParams::KEY_LENGTH, 'A');
+  std::string const key_b(QUICTokenKeyConfigParams::KEY_LENGTH, 'B');
+  TokenKeyFile      key_file(key_a + key_b);
+
+  REQUIRE(RecSetRecordString("proxy.config.quic.server.token_key.filename", 
key_file.path().c_str(), REC_SOURCE_EXPLICIT) ==
+          REC_ERR_OKAY);
+  REQUIRE(QUICTokenKeyConfig::reconfigure());
+
+  IpEndpoint source;
+  IpEndpoint other_source;
+  REQUIRE(ats_ip_pton("192.0.2.1:443", &source.sa) == 0);
+  REQUIRE(ats_ip_pton("192.0.2.2:443", &other_source.sa) == 0);
+
+  uint8_t const    original_dcid_data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 
0x07, 0x08};
+  uint8_t const    scid_data[]          = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 
0x17, 0x18};
+  QUICConnectionId original_dcid(original_dcid_data, 
sizeof(original_dcid_data));
+  QUICConnectionId scid(scid_data, sizeof(scid_data));
+  ink_hrtime const expire_time = ink_get_hrtime() + HRTIME_SECONDS(60);
+
+  QUICRetryToken          retry_with_a(source, original_dcid, scid);
+  QUICResumptionToken     resumption_with_a(source, scid, expire_time);
+  QUICStatelessResetToken reset_with_a(scid, 1);
+  CHECK(retry_with_a.is_valid(source));
+  CHECK_FALSE(retry_with_a.is_valid(other_source));
+  CHECK(resumption_with_a.is_valid(source));
+  CHECK_FALSE(resumption_with_a.is_valid(other_source));
+
+  uint8_t const       malformed_data[] = 
{static_cast<uint8_t>(QUICAddressValidationToken::Type::RETRY)};
+  QUICRetryToken      malformed_retry(malformed_data, sizeof(malformed_data));
+  QUICResumptionToken malformed_resumption(malformed_data, 
sizeof(malformed_data));
+  CHECK_FALSE(malformed_retry.is_valid(source));
+  CHECK_FALSE(malformed_resumption.is_valid(source));
+
+  key_file.write(key_b + key_a);
+  REQUIRE(QUICTokenKeyConfig::reconfigure());
+
+  QUICRetryToken          retry_with_b(source, original_dcid, scid);
+  QUICResumptionToken     resumption_with_b(source, scid, expire_time);
+  QUICStatelessResetToken reset_with_b(scid, 1);
+  CHECK(retry_with_a.is_valid(source));
+  CHECK(resumption_with_a.is_valid(source));
+  CHECK(retry_with_a != retry_with_b);
+  CHECK(resumption_with_a != resumption_with_b);
+  CHECK(reset_with_a != reset_with_b);
+
+  key_file.write(key_b);
+  REQUIRE(QUICTokenKeyConfig::reconfigure());
+  CHECK_FALSE(retry_with_a.is_valid(source));
+  CHECK_FALSE(resumption_with_a.is_valid(source));
+  CHECK(retry_with_b.is_valid(source));
+  CHECK(resumption_with_b.is_valid(source));
+
+  key_file.write(std::string(QUICTokenKeyConfigParams::KEY_LENGTH - 1, 'C'));
+  CHECK_FALSE(QUICTokenKeyConfig::reconfigure());
+  CHECK(retry_with_b.is_valid(source));
+
+  REQUIRE(RecSetRecordString("proxy.config.quic.server.token_key.filename", 
"", REC_SOURCE_EXPLICIT) == REC_ERR_OKAY);
+  REQUIRE(QUICTokenKeyConfig::reconfigure());
+  CHECK_FALSE(retry_with_b.is_valid(source));
+
+  QUICRetryToken random_retry(source, original_dcid, scid);
+  REQUIRE(QUICTokenKeyConfig::reconfigure());
+  CHECK(random_retry.is_valid(source));
+  CHECK(random_retry == QUICRetryToken(source, original_dcid, scid));
+}
diff --git a/src/mgmt/config/FileManager.cc b/src/mgmt/config/FileManager.cc
index 6928b40cc5..192fd0b199 100644
--- a/src/mgmt/config/FileManager.cc
+++ b/src/mgmt/config/FileManager.cc
@@ -252,6 +252,11 @@ FileManager::rereadConfig()
     ret.note(r);
   }
 
+  if (auto const &r = 
fileChanged("proxy.config.quic.server.token_key.filename", 
"proxy.config.quic.server.token_key.filename");
+      !r) {
+    ret.note(r);
+  }
+
   return ret;
 }
 
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index 6cca71851d..3ad853798e 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -1431,6 +1431,8 @@ static constexpr RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.quic.server.stateless_retry_enabled", RECD_INT, 
"0", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.quic.server.token_key.filename", RECD_STRING, 
nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  ,
   {RECT_CONFIG, "proxy.config.quic.client.vn_exercise_enabled", RECD_INT, "0", 
RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.quic.client.cm_exercise_enabled", RECD_INT, "0", 
RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}

Reply via email to