From 62cc184dcae15c655ad77b1c611a2a8c88ab5c7b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 12:18:53 -0700 Subject: [PATCH 1/2] test token wire compatibility floor --- .github/workflows/smoke.yml | 8 +++++-- README.md | 4 ++-- freshness.sh | 17 +++++++++++---- justfile | 7 +++--- token.sh | 43 +++++++++++++++++++++++++++++++++++-- 5 files changed, 66 insertions(+), 13 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 73b5871..86c056a 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -271,8 +271,9 @@ jobs: # cross-verifies. `moq auth` rides the same channel as moq-relay/moq # (cargo/apt/brew/nix, on PATH or MOQ_BIN); @moq/auth comes from npm and # runs under both node and bun; rust-docker runs `auth` in the - # moqdev/moq image. The negative pass inside token.sh confirms each verifier rejects - # tampered tokens and the wrong key. + # moqdev/moq image. The cargo cell also installs the 2026-07-22 compatibility + # floor as rust-legacy. The negative pass inside token.sh confirms each + # verifier rejects tampered tokens and the wrong key. - name: Token interop # Independent of the media matrix, so a red media cell mustn't hide it. if: ${{ !cancelled() }} @@ -288,6 +289,9 @@ jobs: # The Docker cell needs a daemon; GitHub's macOS runners have none, so # it's Linux-only. (Docker is preinstalled on the Linux runners.) if [ "$RUNNER_OS" = "Linux" ]; then impls="$impls,rust-docker"; fi + # One cell is enough to hold the wire-format compatibility floor; + # repeating this pinned build for apt and nix adds no coverage. + if [ "${{ matrix.channel }}" = "cargo" ]; then impls="$impls,rust-legacy"; fi fi ./token.sh --generators "$impls" --verifiers "$impls" diff --git a/README.md b/README.md index 4f6a9d1..a76c776 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This repo installs the public [moq](https://github.com/moq-dev/moq) packages and | Public relays | `just relays` | Rust publisher and subscriber through every relay in the [moq-interop-runner](https://github.com/englishm/moq-interop-runner) registry. CI publishes with Rust and the browser (`js-web`) and subscribes with Rust, `js-web`, and `js-bun`. On those relays `js-web` and `js-bun` run only on WebTransport (`@moq/web-transport` has no raw QUIC mode); a client that does not belong is omitted. WebSocket is its own column for the relays in `WEBSOCKET_KEYS` (`moq-dev-rs` and `stitcher-moq`): each of their `http(s)` endpoints also runs as `ws(s)`, and every other relay keeps that fallback off. That run is what the [results page](https://moq-dev.github.io/smoke/) shows. Only `--required` relays fail the run; the default is moq-dev's `cdn.moq.dev`, including its WebSocket endpoint. | | Cloudflare | `just cloudflare` | Relays built from the default branches of [cloudflare/moq-rs](https://github.com/cloudflare/moq-rs) and moq-dev/moq, driven by a Cloudflare client over WebTransport and raw QUIC. | | moxygen | `just moxygen` | Meta's [`moxygen`](https://github.com/facebookexperimental/moxygen) interop client through the latest moq-dev relay. Needs Linux Docker. | -| Tokens | `just token`, `just token-full` | `moq auth` and npm [`@moq/auth`](https://www.npmjs.com/package/@moq/auth) mint and verify each other's JWTs, and each verifier rejects a tampered token. | +| Tokens | `just token`, `just token-full` | `moq auth`, npm [`@moq/auth`](https://www.npmjs.com/package/@moq/auth), and the oldest supported pre-pattern `moq-token` release mint and verify each other's JWKs and JWTs. Each verifier also rejects a tampered token. | Hang media, Cloudflare, and moxygen each have their own lane. @@ -57,7 +57,7 @@ just check # shfmt, shellcheck, actionlint, freshness ## Versions -Packages resolve to latest on every run: `@moq/*` on the `latest` tag, PyPI `moq-rs`, `moq.dev/moq`, `moqdev/*` images, and GitHub release tarballs. `cloudflare.sh` and `moxygen.sh` build from unpinned Git HEAD. The repo commits no package lock files. `flake.lock` pins the dev toolchain. +Packages resolve to latest on every run: `@moq/*` on the `latest` tag, PyPI `moq-rs`, `moq.dev/moq`, `moqdev/*` images, and GitHub release tarballs. `cloudflare.sh` and `moxygen.sh` build from unpinned Git HEAD. The token lane intentionally pins `moq-token-cli` 0.5.38, the 2026-07-22 compatibility floor, as `rust-legacy`. The repo commits no package lock files. `flake.lock` pins the dev toolchain. npm `playwright` is pinned to the flake's `PLAYWRIGHT_VERSION`. `just freshness` fails when that pin drifts or a lock file is committed. Bump the toolchain with `nix flake update` and the pin together. diff --git a/freshness.sh b/freshness.sh index 0165ce8..313acf0 100755 --- a/freshness.sh +++ b/freshness.sh @@ -5,9 +5,8 @@ # fine: it pins the dev toolchain, not the moq packages, and the moq "nix" # channel references the moq flake ad-hoc so the moq version is never locked; # 2. the moq packages under test are requested as "latest", never pinned; -# 3. the one unavoidable pin (npm `playwright`, which must match the toolchain's -# Chromium build) equals what the toolchain ships, so a toolchain bump can't -# quietly leave it stale. +# 3. forced pins are deliberate: npm `playwright` must match the toolchain's +# Chromium build, while `moq-token-cli` stays at the compatibility floor. # # Run standalone (`just freshness`) or as the opening step of smoke.sh. set -euo pipefail @@ -51,6 +50,16 @@ if [[ "$ver" == "latest" ]]; then note ok "@moq/auth -> \"$ver\""; else note FAIL "@moq/auth pinned to \"$ver\" (want \"latest\")" fail=1 fi +# The legacy token CLI is deliberately fixed at the first supported release. +# Moving it forward would silently stop checking the compatibility floor. +# shellcheck disable=SC2016 # matching the literal defaulted variable in token.sh +if grep -qF 'LEGACY_TOKEN_VERSION="${LEGACY_TOKEN_VERSION:-0.5.38}"' token.sh && + grep -qF 'moq-token-cli' token.sh; then + note ok "moq-token-cli -> 0.5.38 (compatibility floor)" +else + note FAIL "token.sh no longer pins moq-token-cli 0.5.38 as its compatibility floor" + fail=1 +fi # The token Docker image must be the unpinned (:latest) tag, pulled fresh each run. # shellcheck disable=SC2016 # grepping for these literal strings in token.sh; the $vars must NOT expand here if grep -qF 'DOCKER_TOKEN_IMAGE:-moqdev/moq}' token.sh && grep -qF '"$DOCKER" pull "$DOCKER_TOKEN_IMAGE"' token.sh; then @@ -159,7 +168,7 @@ else fail=1 fi -echo "== forced pin (npm playwright) tracks the toolchain ==" +echo "== forced pins ==" pin=$(json_dep playwright) if [[ "$pin" == ^* || "$pin" == "~"* || "$pin" == "latest" || "$pin" == *"x" || "$pin" == *"*"* ]]; then note FAIL "playwright must be an exact version matching the toolchain's Chromium, got \"$pin\"" diff --git a/justfile b/justfile index dd00a62..2fdf142 100644 --- a/justfile +++ b/justfile @@ -46,10 +46,11 @@ token *args: ./token.sh {{ args }} # Full token matrix: every implementation mints and verifies every other's -# tokens. rust-docker pulls the published moqdev/moq image (needs a -# container runtime; set TOKEN_DOCKER=podman to use podman instead of docker). +# tokens. rust-legacy installs the 2026-07-22 compatibility floor. rust-docker +# pulls the published moqdev/moq image (needs a container runtime; set +# TOKEN_DOCKER=podman to use podman instead of docker). token-full: - ./token.sh --generators rust,js-node,js-bun,rust-docker --verifiers rust,js-node,js-bun,rust-docker + ./token.sh --generators rust,js-node,js-bun,rust-docker,rust-legacy --verifiers rust,js-node,js-bun,rust-docker,rust-legacy # The "nix" channel: get moq-relay + moq from the moq flake itself # (a public distribution channel, `nix run github:moq-dev/moq#moq`), instead diff --git a/token.sh b/token.sh index 7986f04..0bf2a1e 100755 --- a/token.sh +++ b/token.sh @@ -5,6 +5,7 @@ # in several flavours from several registries: # # - rust : `moq auth` from moq-cli (cargo / brew / apt) or the moq flake (nix) +# - rust-legacy: the first supported pre-pattern `moq-token` CLI release # - js-node : the @moq/auth npm package's `moq-auth` CLI, run under node # - js-bun : the same published npm package, run under bun # @@ -38,6 +39,13 @@ ALGORITHMS="${TOKEN_ALGORITHMS:-HS256,EdDSA,ES256,RS256}" # (moq-dev/moq#3684). TOKEN_BIN overrides the whole prefix. TOKEN="${TOKEN_BIN:-${MOQ_BIN:-moq} auth}" +# Compatibility floor from 2026-07-22, when moq-token 0.7.0 shipped. Unlike +# every current implementation, this is intentionally pinned: moving it would +# stop testing whether new JWK/JWT formats still load in the oldest supported +# verifier. LEGACY_TOKEN_BIN can supply an already-installed command instead. +LEGACY_TOKEN_VERSION="${LEGACY_TOKEN_VERSION:-0.5.38}" +LEGACY_TOKEN="${LEGACY_TOKEN_BIN:-}" + # The published Docker image for the `rust-docker` cell. Untagged = :latest, the # tag the release pipeline moves to the newest version; pulled fresh each run. DOCKER_TOKEN_IMAGE="${DOCKER_TOKEN_IMAGE:-moqdev/moq}" @@ -138,6 +146,7 @@ cli_for() { # callers expand it unquoted. case "$1" in rust) echo "$TOKEN" ;; + rust-legacy) echo "$LEGACY_TOKEN" ;; # Mount TMP at its real path so the in-container CLI reads/writes the same # key/token files token.sh hands it. The image bundles the nix store, so # the binary's libiconv deps resolve (the brew bottle's bug doesn't apply). @@ -157,7 +166,7 @@ gen() { return 1 } case "$impl" in - rust | rust-docker) + rust | rust-docker | rust-legacy) if [[ "$algo" == HS* ]]; then # shellcheck disable=SC2086 # cli is a deliberate multi-word prefix $cli generate --algorithm "$algo" --out "$dir/sign.jwk" @@ -199,7 +208,7 @@ verify() { return 1 } case "$impl" in - rust | rust-docker) + rust | rust-docker | rust-legacy) # Rust verify reads the token from --in and ignores root (it just # decodes); it prints a debug dump of the claims on success. # shellcheck disable=SC2086 @@ -220,6 +229,11 @@ rust_probe() { $TOKEN generate --algorithm HS256 --out "$TMP/rust-probe.jwk" >"$TMP/rust-probe.log" 2>&1 } +legacy_probe() { + # shellcheck disable=SC2086 # LEGACY_TOKEN is a deliberate command prefix + $LEGACY_TOKEN generate --algorithm HS256 --out "$TMP/legacy-probe.jwk" >"$TMP/legacy-probe.log" 2>&1 +} + "$SMOKE_DIR/freshness.sh" || echo "WARN: freshness check failed (see above); continuing" >&2 if needs rust; then @@ -238,6 +252,31 @@ if needs rust; then fi fi +if needs rust-legacy; then + if [[ -z "$LEGACY_TOKEN" ]]; then + if ! have cargo; then + mark_broken rust-legacy "cargo not found (needed to install moq-token-cli $LEGACY_TOKEN_VERSION)" + elif cargo install --quiet --locked --version "$LEGACY_TOKEN_VERSION" \ + --root "$TMP/rust-legacy" moq-token-cli >"$TMP/legacy-install.log" 2>&1; then + LEGACY_TOKEN="$TMP/rust-legacy/bin/moq-token" + else + mark_broken rust-legacy "moq-token-cli $LEGACY_TOKEN_VERSION failed to install" + sed 's/^/ /' "$TMP/legacy-install.log" >&2 || true + fi + fi + + if ! is_broken rust-legacy; then + if ! have "${LEGACY_TOKEN%% *}"; then + mark_broken rust-legacy "${LEGACY_TOKEN%% *} not found" + elif legacy_probe; then + echo "rust-legacy: moq-token-cli $LEGACY_TOKEN_VERSION ($LEGACY_TOKEN)" + else + mark_broken rust-legacy "$LEGACY_TOKEN won't run (see below)" + sed 's/^/ /' "$TMP/legacy-probe.log" >&2 || true + fi + fi +fi + if needs rust-docker; then if ! have "$DOCKER"; then mark_broken rust-docker "$DOCKER not found" From 85ce56346dd348e8f36121c583fe1467d8b1a133 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 15:21:54 -0700 Subject: [PATCH 2/2] Test subtree grants across legacy token verifiers --- README.md | 2 +- token.sh | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a76c776..85cfa20 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This repo installs the public [moq](https://github.com/moq-dev/moq) packages and | Public relays | `just relays` | Rust publisher and subscriber through every relay in the [moq-interop-runner](https://github.com/englishm/moq-interop-runner) registry. CI publishes with Rust and the browser (`js-web`) and subscribes with Rust, `js-web`, and `js-bun`. On those relays `js-web` and `js-bun` run only on WebTransport (`@moq/web-transport` has no raw QUIC mode); a client that does not belong is omitted. WebSocket is its own column for the relays in `WEBSOCKET_KEYS` (`moq-dev-rs` and `stitcher-moq`): each of their `http(s)` endpoints also runs as `ws(s)`, and every other relay keeps that fallback off. That run is what the [results page](https://moq-dev.github.io/smoke/) shows. Only `--required` relays fail the run; the default is moq-dev's `cdn.moq.dev`, including its WebSocket endpoint. | | Cloudflare | `just cloudflare` | Relays built from the default branches of [cloudflare/moq-rs](https://github.com/cloudflare/moq-rs) and moq-dev/moq, driven by a Cloudflare client over WebTransport and raw QUIC. | | moxygen | `just moxygen` | Meta's [`moxygen`](https://github.com/facebookexperimental/moxygen) interop client through the latest moq-dev relay. Needs Linux Docker. | -| Tokens | `just token`, `just token-full` | `moq auth`, npm [`@moq/auth`](https://www.npmjs.com/package/@moq/auth), and the oldest supported pre-pattern `moq-token` release mint and verify each other's JWKs and JWTs. Each verifier also rejects a tampered token. | +| Tokens | `just token`, `just token-full` | `moq auth`, npm [`@moq/auth`](https://www.npmjs.com/package/@moq/auth), and the oldest supported pre-pattern `moq-token` release mint and verify each other's JWKs and JWTs. Each verifier also rejects a tampered token and a wrong key; the legacy CLI refuses an exact-pattern token. | Hang media, Cloudflare, and moxygen each have their own lane. diff --git a/token.sh b/token.sh index 0bf2a1e..1ed5c94 100755 --- a/token.sh +++ b/token.sh @@ -190,15 +190,18 @@ gen() { } sign() { - local impl="$1" signkey="$2" algo="$3" cli + local impl="$1" signkey="$2" algo="$3" cli suffix="/**" cli=$(cli_for "$impl") || { echo "unknown signer: $impl" >&2 return 1 } + # The legacy CLI takes prefixes; current CLIs take patterns. A subtree has + # the same meaning in both formats and can be written as put/get on the wire. + if [[ "$impl" == rust-legacy || "${4:-}" == exact ]]; then suffix=""; fi # Same flags for the PATH binary and the Docker image; JS uses the same ones too. # shellcheck disable=SC2086 $cli sign --key "$signkey" --root "$ROOT" \ - --publish "pub-canary-$algo" --subscribe "sub-canary-$algo" + --publish "pub-canary-$algo$suffix" --subscribe "sub-canary-$algo$suffix" } verify() { @@ -442,6 +445,43 @@ else done fi +# Exact patterns cannot be represented by legacy put/get prefixes. Prove the +# legacy verifier accepts this key with a legacy subtree token first. +if needs rust-legacy && ! is_broken rust-legacy; then + exact_gen="" + for g in "${GEN_LIST[@]}"; do + if [[ "$g" != rust-legacy ]] && ! is_broken "$g"; then + exact_gen="$g" + break + fi + done + if [[ -n "$exact_gen" ]]; then + algo="${ALGO_LIST[0]}" + keydir="$TMP/exact-$exact_gen-$algo" + subtree="$keydir/subtree.jwt" + exact="$keydir/exact.jwt" + legacy_out="$keydir/legacy.log" + current_out="$keydir/current.log" + if ! gen "$exact_gen" "$algo" "$keydir" >"$keydir.gen.log" 2>&1 || + ! sign rust-legacy "$keydir/sign.jwk" "$algo" >"$subtree" 2>"$keydir.sign.log" || + ! verify rust-legacy "$keydir/verify.jwk" "$subtree" >"$legacy_out" 2>&1 || + ! claims_ok "$legacy_out" "$algo"; then + echo " FAIL reject(rust-legacy, exact): subtree setup failed" + overall=1 + elif ! sign "$exact_gen" "$keydir/sign.jwk" "$algo" exact >"$exact" 2>"$keydir.exact.log" || + ! verify "$exact_gen" "$keydir/verify.jwk" "$exact" >"$current_out" 2>&1 || + ! claims_ok "$current_out" "$algo"; then + echo " FAIL reject(rust-legacy, exact): current token setup failed" + overall=1 + elif verify rust-legacy "$keydir/verify.jwk" "$exact" >"$legacy_out" 2>&1; then + echo " FAIL reject(rust-legacy, exact): accepted an exact-pattern token" + overall=1 + else + echo " PASS reject(rust-legacy, exact): exact-pattern token refused" + fi + fi +fi + if [[ "$overall" -eq 0 ]]; then echo "token: all checks passed" else