From 5bab7e0b57814a7077b892cf5bc271e2254ac2c3 Mon Sep 17 00:00:00 2001 From: bneradt Date: Thu, 16 Jul 2026 13:29:34 -0500 Subject: [PATCH] Make QUIC token secrets configurable 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 | 136 +++++++++++ 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, 509 insertions(+), 69 deletions(-) create mode 100644 src/iocore/net/unit_tests/test_QUICTokenKeyConfig.cc diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index a554c554a2d..dae6ed43990 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -5130,8 +5130,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 @@ -5146,6 +5146,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 1ebec4f6aa0..d6e83a656c2 100644 --- a/include/iocore/net/quic/QUICConfig.h +++ b/include/iocore/net/quic/QUICConfig.h @@ -30,6 +30,47 @@ #include "iocore/net/SSLTypes.h" #include "mgmt/config/ConfigContext.h" +#include +#include +#include + +class QUICTokenKeyConfigParams : public ConfigInfo +{ +public: + static constexpr size_t KEY_LENGTH = 32; + using Key = std::array; + + ~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 &keys() const; + const std::string &filename() const; + +private: + std::vector 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; + +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 706e807cadb..4600dd449e8 100644 --- a/include/iocore/net/quic/QUICTypes.h +++ b/include/iocore/net/quic/QUICTypes.h @@ -321,13 +321,20 @@ class QUICStatelessResetToken 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 @@ class QUICAddressValidationToken // 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 ac3f12d7cc8..a3a7c71f7ef 100644 --- a/src/iocore/net/CMakeLists.txt +++ b/src/iocore/net/CMakeLists.txt @@ -144,6 +144,9 @@ if(BUILD_TESTING) unit_tests/unit_test_main.cc unit_tests/benchmark_TLSCertCompression.cc ) + 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 9905ae06889..8d35bcf552b 100644 --- a/src/iocore/net/quic/QUICConfig.cc +++ b/src/iocore/net/quic/QUICConfig.cc @@ -23,10 +23,16 @@ #include "iocore/net/quic/QUICConfig.h" +#include +#include #include #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" @@ -34,9 +40,138 @@ #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 < static_cast(KEY_LENGTH) || key_data_len % 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_len); + return false; + } + + m_keys.resize(key_data_len / KEY_LENGTH); + memcpy(m_keys.data(), key_data.get(), key_data_len); + OPENSSL_cleanse(key_data.get(), key_data_len); + m_filename = path; + return true; +} + +bool +QUICTokenKeyConfigParams::generate(ConfigContext ctx) +{ + m_keys.resize(1); + if (RAND_bytes(m_keys.front().data(), static_cast(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::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(configProcessor.get(_config_id)); +} + +void +QUICTokenKeyConfig::release(QUICTokenKeyConfigParams *params) +{ + if (_config_id > 0) { + configProcessor.release(_config_id, params); + } +} + SSL_CTX * quic_new_ssl_ctx() { @@ -448,6 +583,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 419c586a54f..9de55694fbf 100644 --- a/src/iocore/net/quic/QUICTypes.cc +++ b/src/iocore/net/quic/QUICTypes.cc @@ -27,13 +27,82 @@ #include #include "iocore/net/quic/QUICTypes.h" +#include "iocore/net/quic/QUICConfig.h" #include "iocore/net/quic/QUICIntUtil.h" -#include "tscore/CryptoHash.h" #include +#include #include 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(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(data), data_size); + size_t data_len = strlen(reinterpret_cast(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(data), data_size); + size_t data_len = strlen(reinterpret_cast(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(&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(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(data), sizeof(data)); - data_len = strlen(reinterpret_cast(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(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(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(data), sizeof(data)); - data_len = strlen(reinterpret_cast(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(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(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 00000000000..4e0a066c2a2 --- /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 + +#include +#include +#include +#include + +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(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 6928b40cc5e..192fd0b1996 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 7890f427ed1..9339e2920b3 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -1429,6 +1429,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}