Skip to content

Commit f724e37

Browse files
committed
fix(review): address PR #286 auth feedback from suhasHere
- auth: derive each configured HMAC key once at AuthTokenVerifier construction and reuse it in verify(), instead of re-deriving per key on every call. A true key-id short-circuit isn't possible yet (Catapult has no API to read the CWT kid without first validating), so verify() still trial-verifies, but the expensive derivation no longer repeats. - auth: derive the HMAC key via HKDF-SHA256 (RFC 5869) with domain-separated salt/info instead of a bare SHA-256 of the secret. Sign and verify share the derivation, so this is self-consistent. - auth: warn loudly in toCatapultMatch when a scope has >1 match rule, since a CWT scope carries a single binary match per dimension and the extras are dropped (which would widen the grant). - test: add RejectsGarbageBytesAsMalformedOrBadSig to restore negative-path coverage for non-empty, structurally-invalid tokens.
1 parent fbeb446 commit f724e37

3 files changed

Lines changed: 93 additions & 10 deletions

File tree

src/auth/Auth.cpp

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
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

2428
using 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+
4154
std::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

157212
folly::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);

src/auth/Auth.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,16 @@ class AuthTokenVerifier {
7474
signForTest(std::string_view keyID, std::string_view secret, const Grants& grants);
7575

7676
private:
77+
// HMAC key material derived once at construction. The configured keys never
78+
// change after that, so the (relatively expensive) key derivation is hoisted
79+
// out of verify(), which only re-runs the per-key HMAC check.
80+
struct DerivedKey {
81+
std::string id;
82+
std::vector<uint8_t> key;
83+
};
84+
7785
config::AuthConfig config_;
86+
std::vector<DerivedKey> derivedKeys_;
7887
};
7988

8089
std::optional<moxygen::AuthToken>

test/AuthTest.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,21 @@ TEST(AuthTest, RejectsEmptyTokenAsMalformed) {
196196
EXPECT_EQ(emptyRes.error(), AuthError::Malformed);
197197
}
198198

199+
// Non-empty garbage that isn't a valid COSE/CWT structure must be rejected
200+
// cleanly (no crash). Either Malformed (CBOR/COSE decode fails) or BadSignature
201+
// (decodes but no key validates) is acceptable.
202+
TEST(AuthTest, RejectsGarbageBytesAsMalformedOrBadSig) {
203+
AuthTokenVerifier verifier(makeConfig());
204+
AuthToken token{
205+
.tokenType = 77,
206+
.tokenValue = std::string("\xde\xad\xbe\xef", 4),
207+
.alias = AuthToken::DontRegister,
208+
};
209+
auto result = verifier.verify(token);
210+
ASSERT_TRUE(result.hasError());
211+
EXPECT_TRUE(result.error() == AuthError::Malformed || result.error() == AuthError::BadSignature);
212+
}
213+
199214
TEST(AuthTest, RejectsExpiredToken) {
200215
auto expired = makeGrants({Action::ClientSetup}, {}, {});
201216
expired.expiresAt = std::chrono::system_clock::time_point(std::chrono::seconds(1'735'689'600));

0 commit comments

Comments
 (0)