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}