1313#include < folly/Conv.h>
1414#include < folly/Expected.h>
1515#include < folly/Range.h>
16- #include < openssl/sha.h>
16+ #include < folly/logging/xlog.h>
17+ #include < openssl/evp.h>
18+ #include < openssl/kdf.h>
1719
1820#include < algorithm>
1921#include < array>
2022#include < limits>
23+ #include < memory>
2124#include < span>
25+ #include < stdexcept>
2226#include < vector>
2327
2428using namespace moxygen ;
@@ -38,9 +42,45 @@ std::string canonicalNamespace(const TrackNamespace& ns) {
3842 return out;
3943}
4044
45+ // Derive a 256-bit HMAC key from the configured secret using HKDF-SHA256
46+ // (RFC 5869) rather than a bare hash. The fixed, non-secret salt and info
47+ // strings provide domain separation so the same configured secret can't yield
48+ // identical key material if reused for another purpose. (HKDF does not add
49+ // entropy: a low-entropy secret is still weak -- operators should use a long,
50+ // random secret.)
51+ constexpr std::string_view kHkdfSalt = " moqx-catapult-v1" ;
52+ constexpr std::string_view kHkdfInfo = " moqx-catapult-hmac-token-verify" ;
53+
4154std::vector<uint8_t > deriveHmacKey (std::string_view secret) {
42- std::vector<uint8_t > key (SHA256_DIGEST_LENGTH );
43- SHA256 (reinterpret_cast <const unsigned char *>(secret.data ()), secret.size (), key.data ());
55+ std::vector<uint8_t > key (32 ); // 256-bit output for HMAC-SHA256
56+
57+ auto * ctx = EVP_PKEY_CTX_new_id (EVP_PKEY_HKDF , nullptr );
58+ if (!ctx) {
59+ throw std::runtime_error (" EVP_PKEY_CTX_new_id(HKDF) failed" );
60+ }
61+ auto guard = std::unique_ptr<EVP_PKEY_CTX , decltype (&EVP_PKEY_CTX_free)>(ctx, EVP_PKEY_CTX_free);
62+
63+ size_t keyLen = key.size ();
64+ if (EVP_PKEY_derive_init (ctx) <= 0 || EVP_PKEY_CTX_set_hkdf_md (ctx, EVP_sha256 ()) <= 0 ||
65+ EVP_PKEY_CTX_set1_hkdf_salt (
66+ ctx,
67+ reinterpret_cast <const unsigned char *>(kHkdfSalt .data ()),
68+ static_cast <int >(kHkdfSalt .size ())
69+ ) <= 0 ||
70+ EVP_PKEY_CTX_set1_hkdf_key (
71+ ctx,
72+ reinterpret_cast <const unsigned char *>(secret.data ()),
73+ static_cast <int >(secret.size ())
74+ ) <= 0 ||
75+ EVP_PKEY_CTX_add1_hkdf_info (
76+ ctx,
77+ reinterpret_cast <const unsigned char *>(kHkdfInfo .data ()),
78+ static_cast <int >(kHkdfInfo .size ())
79+ ) <= 0 ||
80+ EVP_PKEY_derive (ctx, key.data (), &keyLen) <= 0 ) {
81+ throw std::runtime_error (" HKDF key derivation failed" );
82+ }
83+ key.resize (keyLen);
4484 return key;
4585}
4686
@@ -59,6 +99,15 @@ catapult::MoqtBinaryMatch toCatapultMatch(const std::vector<MatchRule>& rules) {
5999 if (rules.empty ()) {
60100 return catapult::MoqtBinaryMatch::any ();
61101 }
102+ // A Catapult CWT scope carries a single binary match per dimension, whereas a
103+ // MatchRule vector can express several ANDed rules (e.g. Prefix + Suffix). We
104+ // can only serialize the first; warn loudly rather than silently dropping the
105+ // rest, since that would widen the grant beyond what was configured.
106+ if (rules.size () > 1 ) {
107+ XLOG (WARN ) << " CWT scope supports a single match rule per dimension; dropping "
108+ << (rules.size () - 1 ) << " extra rule(s) -- the serialized grant will be broader "
109+ << " than the configured match rules" ;
110+ }
62111 const auto & rule = rules.front ();
63112 switch (rule.type ) {
64113 case MatchRule::Type::Exact:
@@ -152,7 +201,13 @@ Grants grantsFromToken(const catapult::CatToken& token) {
152201
153202} // namespace
154203
155- AuthTokenVerifier::AuthTokenVerifier (config::AuthConfig config) : config_(std::move(config)) {}
204+ AuthTokenVerifier::AuthTokenVerifier (config::AuthConfig config) : config_(std::move(config)) {
205+ // Derive each configured key once; verify() reuses these (see DerivedKey).
206+ derivedKeys_.reserve (config_.hmacKeys .size ());
207+ for (const auto & key : config_.hmacKeys ) {
208+ derivedKeys_.push_back (DerivedKey{.id = key.id , .key = deriveHmacKey (key.secret )});
209+ }
210+ }
156211
157212folly::Expected<Grants, AuthError> AuthTokenVerifier::verify (const AuthToken& token) const {
158213 if (!config_.enabled ) {
@@ -165,14 +220,18 @@ folly::Expected<Grants, AuthError> AuthTokenVerifier::verify(const AuthToken& to
165220 return folly::makeUnexpected (AuthError::Malformed);
166221 }
167222
223+ // Catapult's CWT API has no way to read the token's key id without first
224+ // validating against a key, so we trial-verify against each configured key.
225+ // Key derivation already happened at construction; only the HMAC check (one
226+ // per key until a match) repeats here. A true key-id short-circuit would need
227+ // a "peek the unprotected header" entry point in Catapult that doesn't exist
228+ // yet -- see the PR discussion.
168229 const auto tokenBytes = toBytes (token.tokenValue );
169- for (const auto & key : config_.hmacKeys ) {
230+ const auto span = std::span<const uint8_t >(tokenBytes.data (), tokenBytes.size ());
231+ for (const auto & derived : derivedKeys_) {
170232 try {
171- catapult::HmacSha256Algorithm hmac (deriveHmacKey (key.secret ));
172- auto cwt = catapult::Cwt::validateCwt (
173- std::span<const uint8_t >(tokenBytes.data (), tokenBytes.size ()),
174- hmac
175- );
233+ catapult::HmacSha256Algorithm hmac (derived.key );
234+ auto cwt = catapult::Cwt::validateCwt (span, hmac);
176235 auto grants = grantsFromToken (cwt.payload );
177236 if (grants.expiresAt <= std::chrono::system_clock::now ()) {
178237 return folly::makeUnexpected (AuthError::Expired);
0 commit comments