Skip to content

Add Media over QUIC (MoQ) CAT support (draft-ietf-moq-c4m) - #8

Open
zshenker wants to merge 5 commits into
alg-confusion-fixfrom
moqt-support
Open

zshenker wants to merge 5 commits into
alg-confusion-fixfrom
moqt-support

Conversation

@zshenker

@zshenker zshenker commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #6 (base: alg-confusion-fix). Merge after #6; GitHub will retarget this PR to main when #6 merges.

Summary

This PR adds the moqt and moqt-reval claims from draft-ietf-moq-c4m-01. With them, the library can issue and validate tokens for MoQ Transport relays, publishers, and clients.

  • New moqt module.
    • It has typed MoqtClaim / MoqtScope / BinMatch scopes, MoqtAction codes 0–8, and MoqtRequest, which is an action plus a Full Track Name.
    • Matching is bytewise and case-sensitive, with exact, prefix, and suffix matches. A trailing nil pins the namespace length.
    • Parsing is strict: a malformed claim fails with Error::InvalidMoqtClaim. It is never ignored, so it can't widen access.
  • Token API.
    • TokenBuilder::moqt / moqt_reval and Token::moqt_claim / moqt_reval, plus Token::moqt_claim_at / moqt_reval_at to read either claim at any label.
    • New VerificationOptions fields: verify_moqt, moqt_request, moqt_revalidation, and moqt_claim_labels (MoqtClaimLabels, defaulting to 327/328).
    • New errors: MoqtNotAuthorized and MoqtRevalidationUnsupported.
  • moqt-reval handling. As the draft requires, recipients that can't revalidate reject tokens with a non-zero interval. The default for moqt_revalidation is Unsupported. Recipients that can revalidate reject intervals below their supported minimum.
  • CborValue::Float. Floats are decoded at half, single, or double precision, and encoded at the shortest width that is exact. Without this, the draft's half-float moqt-reval test vector (f95cb0) fails to decode. It uses minicbor's half feature; the half crate is already in Cargo.lock through criterion.

Validator roles

The check is the same whoever does it, so there is one role-neutral API: verify the signature with verify_with_key and a typed VerifyingKey (from #6), then call verify_claims with a MoqtRequest. examples/moqt.rs covers three roles:

  1. An edge relay validating a subscriber.
  2. An ingest relay validating a publisher's PUBLISH_NAMESPACE and PUBLISH.
  3. A publisher validating a relay's SUBSCRIBE using only an ES256 public key.

A relay can keep a token from session setup and re-check it for later actions such as REQUEST_UPDATE. It can do that through verify_claims, or by calling MoqtClaim::permits on a cached claim.

MoqtScope::matches_name exposes the same name matcher without the action check, for re-checking names against scopes already selected by action (for example each track routed to a publisher after PUBLISH_NAMESPACE, or SUBSCRIBE_NAMESPACE discovery).

Interpretation choices (flagged in docs and FOLLOWUPS)

  • Which matches apply to which action. The draft doesn't say, so this PR decides:
    • Setup actions only need to appear in the scope's action list.
    • Namespace actions also check the namespace but skip the track match. Otherwise the draft's own publisher vector, [[2,6], ns, track], could never PUBLISH_NAMESPACE.
    • Track actions with no track always fail.
    • A request namespace shorter than the scope's is denied.
  • Absent moqt claim. With verify_moqt(true), a token without a moqt claim authorizes no MoQ action. Draft §2 says that once a token is presented, all actions are blocked unless a scope enables them. (require_moqt was removed as redundant.)
  • Claim keys. 327 and 328 are provisional values taken from the draft's test vectors; IANA lists them as TBD. Validators can override them with moqt_claim_labels to interoperate with issuers that use other labels.

Breaking changes (0.4.0 is unreleased)

  • CborValue is now #[non_exhaustive] and has a new Float variant.
  • VerificationOptions has new public fields, which breaks code that builds it with a struct literal.

Relationship to #6

This PR is based on #6 and uses its API: every signature check in the new tests, example, and README goes through verify_with_key with a typed VerifyingKey, so it adds no deprecation warnings. MoQ publishers and origins are expected to verify with public keys, which is exactly the algorithm-confusion case #6 fixes. test_moqt_hmac_forgery_over_public_key_is_rejected checks that an HMAC token forged over an ES256 public key is rejected before any moqt scope is consulted.

Deferred to FOLLOWUPS.md:

  • Composite or / and / nor / crit claims.
  • DPoP proof verification.
  • Spec ambiguities to revisit as the draft evolves.

Testing

🤖 Generated with Claude Code

zshenker and others added 4 commits October 19, 2026 15:48
Implement the moqt and moqt-reval claims so the library can issue and
validate tokens for MoQT relays, publishers, and clients:

- New moqt module: typed MoqtClaim/MoqtScope/BinMatch scopes,
  MoqtAction codes, and MoqtRequest, with strict parsing and bytewise
  Full Track Name matching.
- TokenBuilder::moqt/moqt_reval, Token::moqt_claim/moqt_reval, and
  VerificationOptions::{verify_moqt, require_moqt, moqt_request,
  moqt_revalidation}.
- CborValue::Float (now non_exhaustive), decoding half/single/double
  floats and encoding in the shortest exact width, as needed for the
  draft's half-float moqt-reval test vector.
- examples/moqt.rs covering edge relay, ingest relay, and publisher
  validation; README, CHANGELOG, and FOLLOWUPS updates.

Claim keys 327/328 are provisional, taken from the draft's test vectors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
draft-ietf-moq-c4m §2: "As soon as a token is provided, all actions are
explicitly blocked unless explicitly enabled." verify_moqt(true) now
rejects a token with no moqt claim with MoqtNotAuthorized, instead of
passing it unless require_moqt was set. require_moqt is removed as
redundant (0.4.0 is unreleased).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IANA still lists both labels as TBD, so issuers may use labels other
than the provisional 327/328 until registration. Add MoqtClaimLabels
(Default = moqt_keys::MOQT / MOQT_REVAL, so a derived
VerificationOptions::default() doesn't silently read label 0),
VerificationOptions::moqt_claim_labels, and Token::moqt_claim_at /
moqt_reval_at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Now that the branch is based on the typed VerifyingKey API, verify
tokens with verify_with_key instead of the deprecated verify(). The
ES256 MoQ test uses the fixed demo key pair instead of generating one
with rand_core, so it no longer depends on PS256-only dependencies.
Add a test that an HMAC forgery over an ES256 public key is rejected
before any moqt scope is consulted, and drop the completed FOLLOWUPS
item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zshenker
zshenker changed the base branch from main to alg-confusion-fix October 19, 2026 21:51
Add MoqtScope::matches_name(namespace, track), which applies a scope's
namespace field matches, trailing-nil length pin and (when a track is
given) track match without consulting its actions. permits() now uses
it, so callers that must re-check names against scopes they have
already selected by action -- e.g. each track later routed to a
publisher after PUBLISH_NAMESPACE, or namespaces returned for
SUBSCRIBE_NAMESPACE discovery -- share the exact matcher instead of
reimplementing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant