From feff46c7aee8429f34ae3d28c4d733c4b424e16c Mon Sep 17 00:00:00 2001 From: Yurii Kostiuk Date: Mon, 26 Oct 2026 04:53:14 +0000 Subject: [PATCH 1/2] test(checkpoints): a keyryx statement with no signatures is refused check_statement requires a non-empty `signatures` list before it reads a statement (the witness verifies the signatures, invariant 128), but no test served a statement without one, so a mutant that dropped the check survived the p256 0.14 PR's mutation run (#480). The keyryx stub gains two modes, no `signatures` and an empty list, and both must be refused as not of the known shape. One scenario, bound. Co-Authored-By: Claude Opus 5.5 --- PROGRESS.md | 2 +- README.md | 2 +- crates/gateway/src/checkpoints/tests.rs | 27 ++++++++++++++++++- ...ed-checkpoints-of-the-event-stream.feature | 6 +++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 1faa574..864333e 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -304,7 +304,7 @@ comparison in #132. **Counts re-measured 2026-10-05**, each by the command named, because the set here once said 100 where the workspace ran 747 and nothing had been watching: -`cargo test --all` runs **2421 passing** (core 469, dpop 29, delegation 75, gateway 1502, cloud 345, umbrella 1, by `cargo test -p `). The refusal of a message the MCP broker cannot judge (invariant 142, 2026-10-26) adds 4 to the 2417 below, all in `tests/it/mcp_broker.rs`, counted from the diff against main; this branch was not run locally, and CI's `stated-numbers.sh` is the measurement. The p256 0.14 upgrade (2026-10-26) adds 3 RFC 6979 vector tests to the 2414 below: 1 in `gateway::checkpoints` and 2 in `cloud` (`devices`, `audit_sign`). @measured `cargo test --all --no-fail-fast` on that branch, 2026-10-26 as root: 2415 passed and the two tests that need a 0500 directory failed, as invariant 78 records; both passed when their binaries were run again under `setpriv` with `dac_override` and `dac_read_search` dropped. Per binary: core 460 lib and 9 integration, cloud 205 lib and 140 integration, gateway 995 lib and 503 integration, dpop 29, delegation 75, umbrella 1. The door origin lists (invariant 141, 2026-10-19) add 23 to the 2391 below, counted from the diff against main and not measured again here: 6 in `dpop::origin`, 1 in `gateway::chainproof`, and 16 in `tests/it` (8 in `delegation_origins`, 8 in `mcp_door`). @measured `cargo test --all --no-fail-fast` on this branch after merging origin/main (8ed7744, PR #468 merged), 2026-10-19 as root: 2389 passed and 2 failed. The two failures are the tests that need a 0500 directory (`events_export_startup`, `events::tests::an_unwritable_directory_is_named_at_warn_and_the_export_is_off`), which invariant 78 records as root-only; as any other user they pass, so the CI figure is 2391. The breakdown counts the same binaries per crate: core 460 lib and 9 integration, cloud 203 lib and 140 integration, gateway 993 lib and 487 integration (the root-only test counted in the lib figure), dpop 23, delegation 75, umbrella 1. Against main's 2364 the branch adds 27: the static-key allow-list and the agent token to the policy plane (invariants 138 to 140) add 26 to `gateway`, 11 in `gateway::agenttoken` and `wardryx` and 15 in `tests/it` (`static_key_allow_list`, `static_key_allow_list_startup`, `mcp_agent_token`); the identity-source cache key adds 1 in `gateway::wardryx`, and the governed wide-bind agreement of invariant 137 adds 1 in `tests/it/static_key_allow_list_startup`. The governed profile of invariants 136 and 137 is already in main's figure and is not counted here. The governed profile (invariant 136, 2026-10-19) adds 29 to `gateway`: 8 in `defaults`, 3 in `adminkeys` and 18 in `tests/it/governed_profile.rs`. Its broker and no-mode firewall follow-up adds 11 more: 7 in `tests/it/governed_profile_scope.rs`, 3 in `firewall::absent_mode_tests` and 1 in `defaults`. Earlier totals quoted in this section are dated history of how each count grew, not the current figure. The CORS allow-list, the digested device credentials and org keys, and the dashboard's fragment key (invariants 131 to 133) add 28 to `cloud`, all new tests, counted from the diff against main. The CORS allow-list, the digested device credentials and org keys, and the dashboard's +`cargo test --all` runs **2422 passing** (core 469, dpop 29, delegation 75, gateway 1503, cloud 345, umbrella 1, by `cargo test -p `). The refusal of a keyryx statement with no signatures (2026-10-26) adds 1 to the 2421 below, in `gateway::checkpoints`, counted from the diff against main; CI's `stated-numbers.sh` is the measurement. The refusal of a message the MCP broker cannot judge (invariant 142, 2026-10-26) adds 4 to the 2417 below, all in `tests/it/mcp_broker.rs`, counted from the diff against main; this branch was not run locally, and CI's `stated-numbers.sh` is the measurement. The p256 0.14 upgrade (2026-10-26) adds 3 RFC 6979 vector tests to the 2414 below: 1 in `gateway::checkpoints` and 2 in `cloud` (`devices`, `audit_sign`). @measured `cargo test --all --no-fail-fast` on that branch, 2026-10-26 as root: 2415 passed and the two tests that need a 0500 directory failed, as invariant 78 records; both passed when their binaries were run again under `setpriv` with `dac_override` and `dac_read_search` dropped. Per binary: core 460 lib and 9 integration, cloud 205 lib and 140 integration, gateway 995 lib and 503 integration, dpop 29, delegation 75, umbrella 1. The door origin lists (invariant 141, 2026-10-19) add 23 to the 2391 below, counted from the diff against main and not measured again here: 6 in `dpop::origin`, 1 in `gateway::chainproof`, and 16 in `tests/it` (8 in `delegation_origins`, 8 in `mcp_door`). @measured `cargo test --all --no-fail-fast` on this branch after merging origin/main (8ed7744, PR #468 merged), 2026-10-19 as root: 2389 passed and 2 failed. The two failures are the tests that need a 0500 directory (`events_export_startup`, `events::tests::an_unwritable_directory_is_named_at_warn_and_the_export_is_off`), which invariant 78 records as root-only; as any other user they pass, so the CI figure is 2391. The breakdown counts the same binaries per crate: core 460 lib and 9 integration, cloud 203 lib and 140 integration, gateway 993 lib and 487 integration (the root-only test counted in the lib figure), dpop 23, delegation 75, umbrella 1. Against main's 2364 the branch adds 27: the static-key allow-list and the agent token to the policy plane (invariants 138 to 140) add 26 to `gateway`, 11 in `gateway::agenttoken` and `wardryx` and 15 in `tests/it` (`static_key_allow_list`, `static_key_allow_list_startup`, `mcp_agent_token`); the identity-source cache key adds 1 in `gateway::wardryx`, and the governed wide-bind agreement of invariant 137 adds 1 in `tests/it/static_key_allow_list_startup`. The governed profile of invariants 136 and 137 is already in main's figure and is not counted here. The governed profile (invariant 136, 2026-10-19) adds 29 to `gateway`: 8 in `defaults`, 3 in `adminkeys` and 18 in `tests/it/governed_profile.rs`. Its broker and no-mode firewall follow-up adds 11 more: 7 in `tests/it/governed_profile_scope.rs`, 3 in `firewall::absent_mode_tests` and 1 in `defaults`. Earlier totals quoted in this section are dated history of how each count grew, not the current figure. The CORS allow-list, the digested device credentials and org keys, and the dashboard's fragment key (invariants 131 to 133) add 28 to `cloud`, all new tests, counted from the diff against main. The CORS allow-list, the digested device credentials and org keys, and the dashboard's fragment key (invariants 131 to 133) add 28 to `cloud`, all of them new tests: 9 in its library and 19 in `tests/it` (8 CORS, 2 startup refusal, 7 device credentials, nonces and the restart second, 2 dashboard), counted from the diff against main (2276, so 2304). @measured `cargo test --all --no-fail-fast` diff --git a/README.md b/README.md index 6d980c3..37fab23 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ > The kill-switch isn't a dashboard button you press after the fact - it's an HTTP 402 the gateway returns mid-run, before the provider bills you. ![release](https://img.shields.io/badge/release-v1.20.0-brightgreen) -![tests](https://img.shields.io/badge/tests-2421-brightgreen) +![tests](https://img.shields.io/badge/tests-2422-brightgreen) ![image](https://img.shields.io/badge/ghcr.io-tokenfuse-blue?logo=docker) ![license](https://img.shields.io/badge/license-Apache--2.0-blue) ![core](https://img.shields.io/badge/core-Rust-orange) diff --git a/crates/gateway/src/checkpoints/tests.rs b/crates/gateway/src/checkpoints/tests.rs index 8a2606e..738a7ab 100644 --- a/crates/gateway/src/checkpoints/tests.rs +++ b/crates/gateway/src/checkpoints/tests.rs @@ -360,6 +360,8 @@ enum Mode { OtherOwner, OtherLife, TimesDiffer, + NoSignatures, + EmptySignatures, Refuse401, Redirect, } @@ -462,10 +464,17 @@ async fn stub_delegate( _ => {} } st.certified.lock().unwrap().push(claims["jwk"].clone()); - let statement = json!({ + let mut statement = json!({ "payload": b64url().encode(serde_json::to_vec(&claims).unwrap()), "signatures": [{"protected": "e30", "signature": "AA"}] }); + match mode { + Mode::NoSignatures => { + statement.as_object_mut().unwrap().remove("signatures"); + } + Mode::EmptySignatures => statement["signatures"] = json!([]), + _ => {} + } let (anb, ana) = if mode == Mode::TimesDiffer { (nb, na + 5) } else { @@ -545,6 +554,22 @@ async fn a_statement_that_does_not_certify_this_key_is_refused() { } } +/// The statement's signatures are the witness's to verify (invariant 128), so +/// this writer only requires that there are some: a statement with no +/// `signatures`, or an empty list, is not of the known shape and certifies +/// nothing. +#[tokio::test] +async fn a_statement_with_no_signatures_is_refused() { + for mode in [Mode::NoSignatures, Mode::EmptySignatures] { + let s = stub(T0_SECS).await; + *s.state.mode.lock().unwrap() = mode; + let dir = temp_dir(&format!("unsigned-{mode:?}")); + let k = Keyryx::new(&cfg_for(&s.url, &dir, "consumer-key-0123")).unwrap(); + let err = k.certify("tokenfuse-stream", 3600).await.err().unwrap(); + assert!(err.contains("not of the known shape"), "{mode:?}: {err}"); + } +} + #[tokio::test] async fn a_refusal_carries_its_status_and_never_the_credential() { let s = stub(T0_SECS).await; diff --git a/features/signed-checkpoints-of-the-event-stream.feature b/features/signed-checkpoints-of-the-event-stream.feature index 5422a00..094b25a 100644 --- a/features/signed-checkpoints-of-the-event-stream.feature +++ b/features/signed-checkpoints-of-the-event-stream.feature @@ -77,6 +77,12 @@ Feature: The gateway signs checkpoints of its own event stream, and a witness ca When the gateway reads it Then the key is not kept and no record is signed with it + # @test:a_statement_with_no_signatures_is_refused + Scenario: A statement that carries no signatures is not used + Given keyryx answers with a statement whose signatures are missing or an empty list + When the gateway reads it + Then the key is not kept: the witness verifies a statement's signatures, and one with none has nothing to verify + # @test:a_key_signs_only_inside_its_statement_window_in_whole_seconds # @test:a_key_past_its_statement_is_never_used_and_the_record_is_unsigned # @test:a_key_that_runs_out_mid_stream_gives_unsigned_records_not_stale_signatures From 4631a267d0325ac699ce01a161262fb59ed80fc3 Mon Sep 17 00:00:00 2001 From: Yurii Kostiuk Date: Mon, 26 Oct 2026 04:53:41 +0000 Subject: [PATCH 2/2] test: throwaway red-first run for the checkpoints signatures test, never merged Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 483 ------------------------------ .github/workflows/red-first.yml | 48 +++ crates/gateway/src/checkpoints.rs | 2 +- 3 files changed, 49 insertions(+), 484 deletions(-) delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/red-first.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ff61ba3..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,483 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - -permissions: - contents: read - -# A newer push to the same pull request makes the older run's result moot, so the -# older run is cancelled rather than left holding runners. A push to main is never -# cancelled: its group is its own commit. A red-first commit's run is cancelled by -# the next push too, so read its test job before pushing the fix. -concurrency: - group: ci-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - CARGO_TERM_COLOR: always - # One archive snapshot for every apt step in this file. Bump it deliberately, - # in a commit that says what moved and why, the way a lockfile is bumped. - APT_SNAPSHOT: "20260820T000000Z" - RUSTFLAGS: "-D warnings" - -jobs: - test: - name: fmt · clippy · test - runs-on: ubuntu-24.04 - steps: - # fetch-depth: 0 pulls tags along with full history. Without it, - # actions/checkout fetches no tags at all, and stated-numbers.sh's - # version-badge check (below) would see no v* tag and correctly refuse - # with "measured nothing" rather than silently passing. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - name: Free runner disk - # This job builds three full profiles (clippy --all-features, debug - # tests, and the cluster-feature test graph); with a warm cache the - # 14 GB runner disk runs dry mid-link (ld dies with SIGBUS). Drop the - # preinstalled bundles we never use (~25 GB) instead of a third-party - # cleanup action. - run: | - sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc \ - /usr/local/share/boost "$AGENT_TOOLSDIRECTORY/CodeQL" || true - df -h / - - name: Install Rust - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master - with: - toolchain: stable - components: rustfmt, clippy - - name: Cache cargo - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - # v1: fresh namespace after the 2026-07-09 poisoned-cache SIGBUS; - # bump to invalidate if link-time disk pressure recurs. - prefix-key: v1 - - name: Format check - run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --all-targets --all-features - - name: Test - run: cargo test --all - - name: Test (cluster feature, raft-backed ledger) - run: cargo test -p tokenfuse-gateway --features cluster --test cluster_backend - - name: tokenfuse-core stays dependency-minimal - run: ./scripts/core-deps.sh - - # Invariant 1 keeps utoipa out of core, which makes the compiler refuse - # most ways a core type could reach the Cloud OpenAPI. The one way it does - # not refuse is #[schema(value_type = ..)], and two fields already use it. - - name: core types stay behind the Cloud DTO boundary - run: ./scripts/dto-boundary.sh - - # A field added to the replicated ledger compiles and passes every test, - # because every test builds a fresh state machine. A node with a durable - # store cannot read back what it wrote under the old shape, and comes up - # having lost every budget with no error. This makes that a decision. - - name: the replicated ledger keeps its recorded shape - run: ./scripts/replicated-shape.sh - - # Invariant 4's two halves that have a mechanical form: a control's grade - # in the compliance catalog is a claim made to a regulator, and moving one - # up is a one-word edit nothing else would notice; and the limitations the - # docs must state plainly can be checked for presence, since a missing - # sentence is as checkable as a present one. - - name: claims about coverage stay recorded - run: ./scripts/honest-claims.sh - - # The wire strings, severities, trace columns and prices other - # repositories read out of this one used to be retyped by hand at the far - # end. verdryx's blocked-decision mirror carried seven of nine for eleven - # days that way. contracts/tokenfuse-constants.json is generated from the - # Rust instead, and this is what keeps "generated" true after the commit - # that generated it. - - name: the published stack constants match the Rust - run: ./scripts/constants.sh - - - # A number in a document is a claim with no owner. The it-rat.com page for - # this service said 513 tests while the workspace ran 709, and nobody - # knows for how long. It was not wrong when written; nothing was - # watching it. This is that check at the source. - # - # It covers PROGRESS.md as well as the README, because gating only the - # badge left the same figure in prose one file over saying 100 where the - # workspace ran 747, and reading is what caught it rather than any check. - - name: the numbers this repo states about itself are true - run: ./scripts/stated-numbers.sh - - # The surface compat/1.0.json says this repository's 1.0 froze, against - # the code, and against COMPATIBILITY.md, which is rendered from it rather - # than typed by hand. Written in the 1.0.0 release commit and held from - # it on; estate-gates C19 checks this step exists, this step checks the - # promise still holds. - - name: compat surface is frozen - run: ./scripts/compat-surface.sh - - # An unpinned install is a dependency with no lockfile. `cargo install - # bpf-linker` took main red on 2026-08-20 without a commit: 0.11.0 was - # published the day after that job last passed, and the first pull - # request opened afterwards wore the red check. Every job here installs - # tools by name, so the check that they are pinned runs once, over all - # the workflow files, rather than once per job. - - name: every tool CI installs is pinned - run: ./scripts/pinned-installs.sh - - # Every file directly under a crate's tests/ is a test binary that links - # the whole dependency graph, DataFusion included, so one test run after a - # change relinked about forty of them. A crate's integration tests are - # modules of tests/it/main.rs instead; this keeps a second binary from - # arriving quietly (invariant 78). - - name: one integration-test binary per crate - run: ./scripts/one-test-binary-per-crate.sh - - # The teeth harness runs as shards (the teeth jobs below). This reads their - # count from this file, lists what each shard would run and requires the - # shards to run every case exactly once. It runs no case and needs no cargo - # (invariant 113). - - name: the teeth shards run every case exactly once - run: ./scripts/teeth-shards-cover-every-case.sh - - # This repository is public. No tracked file quotes the owner or names him - # as the one who said or decided something; a decision is recorded as - # @decided and a paraphrase (invariant 101). - - name: no owner quotes in a public repository - run: ./scripts/no-owner-quotes.sh - - # A run id and a model name are chosen by whoever calls a gateway. A page - # this repository serves puts what the API returns into the document as - # text and never assigns it as markup (invariant 102). - - name: served pages build no markup from data - run: ./scripts/no-html-from-data.sh - - # The attestation method the policy plane is told comes from the door that - # authenticated the call; only crate::attestation names the caller's - # header, to say once that it is ignored (invariant 116). - - name: the attestation is derived, never read from the caller - run: ./scripts/attestation-is-derived.sh - - # A run belongs to the principal that opened it, and the metered door - # settles the owner before it reads or writes the run; a declared parent - # is judged before anything is opened (invariant 120). - - name: a run's owner is settled before the run is touched - run: ./scripts/run-owner-is-checked-first.sh - - python-sdk: - name: python sdk - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.12" - - name: Install - run: | - pip install pytest==9.1.1 - pip install httpx==0.28.1 cryptography==46.0.7 - - name: Test - working-directory: sdk/python - run: python -m pytest -q - - js-sdk: - name: js sdk - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: "22" - - name: Test - working-directory: sdk/js - run: node test.js - - openapi: - name: openapi spec - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master - with: - toolchain: stable - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.12" - - name: Generate spec - run: cargo run -q -p tokenfuse-cloud -- --openapi > openapi.json - - name: Validate spec - run: | - pip install openapi-spec-validator==0.9.0 - openapi-spec-validator openapi.json - - name: Upload spec artifact - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: openapi-spec - path: openapi.json - - # The Cloud control plane is now a Rust workspace member (crates/cloud); it is - # built, clippy-checked and tested by the `fmt · clippy · test` job above. The - # former Go `cloud` job was removed in the Go→Rust port (ADR-7, PR A5). - - dashboard: - name: dashboard (Next.js) - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: "22" - cache: npm - cache-dependency-path: cloud/dashboard/package-lock.json - - name: Install - working-directory: cloud/dashboard - run: npm ci --no-audit --no-fund - - name: Build (static export) - working-directory: cloud/dashboard - run: npm run build - - cluster: - name: cluster (raft HA) - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Install Rust - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master - with: - toolchain: stable - components: rustfmt, clippy - - name: Cache cargo - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - workspaces: crates/cluster - - name: Format check - working-directory: crates/cluster - run: cargo fmt --all -- --check - - name: Clippy - working-directory: crates/cluster - run: cargo clippy --all-targets - - name: Test - working-directory: crates/cluster - run: cargo test - - # The teeth harness is the slowest step in this repository, 30 to 60 minutes as - # one job, so it runs as eight shards of the same case list. Shard i runs the - # cases at positions i, i+8, ..., and scripts/teeth-shards-cover-every-case.sh - # requires them together to run each case exactly once (invariant 113). The - # count is written here only, in the matrix, TEETH_SHARD and the name, and that - # gate reads it from here and requires the three to agree. Standard runners: this - # repository is public, so they cost nothing. - teeth: - name: teeth (shard ${{ matrix.shard }} of 8) - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - shard: [1, 2, 3, 4, 5, 6, 7, 8] - env: - TEETH_SHARD: ${{ matrix.shard }}/8 - steps: - # fetch-depth: 0 here too: this job runs gates-have-teeth.sh, whose - # stated-numbers cases call the version-badge check, and a checkout - # with no tags makes that check refuse on a clean tree, so every one - # of those cases would read UNJUDGEABLE rather than judging the fault. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - name: Install Rust - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master - with: - toolchain: stable - - name: Cache cargo - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - # The harness's cargo-audit case needs the tool on PATH, or that gate - # reports UNJUDGEABLE. Same pin as the audit job below. - - name: Install cargo-audit - run: cargo install cargo-audit --version 0.22.2 --locked - - # The gates parse text with regular expressions, which does not break - # loudly: a parser stops matching and reports success. This breaks each - # of them on purpose and requires the failure. - # - # It lives in the teeth jobs rather than beside the other gates, and the - # reason is cargo-audit: it exercises every gate in scripts/, audit.sh - # among them, and that one needs a tool only these jobs install. Run - # anywhere else it reports three cases UNJUDGEABLE, correctly, because a - # gate that cannot run is not a gate that passed. That is how this was - # found. - # - # It mutates tracked files and restores them, which is why each shard - # starts from its own checkout, and it needs a clean tree and says so - # plainly if an earlier step has written into it. - - name: the gates still fail on the faults they exist to catch - run: ./scripts/gates-have-teeth.sh - - # Both lockfiles, one ignore list, and a check that each ignore's reason still - # holds (scripts/audit.sh, invariant 11). Its own job, so it runs beside the - # teeth shards. Two bare `cargo audit` calls needed the list in two places, - # because cargo-audit reads `.cargo/audit.toml` from the current directory and - # does not walk up. - audit: - name: cargo audit - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Install Rust - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master - with: - toolchain: stable - - name: Cache cargo - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - - name: Install cargo-audit - run: cargo install cargo-audit --version 0.22.2 --locked - - name: Audit both lockfiles - run: ./scripts/audit.sh - - # The required check. Its name stays, because branch protection names it. It - # passes only when the audit job and every teeth shard passed: a skipped - # or cancelled need reads as not success, so it fails here too. - security: - name: security (cargo audit) - needs: [audit, teeth] - if: always() - runs-on: ubuntu-24.04 - steps: - - name: audit and every teeth shard passed - env: - AUDIT: ${{ needs.audit.result }} - TEETH: ${{ needs.teeth.result }} - run: | - echo "cargo audit: $AUDIT" >>"$GITHUB_STEP_SUMMARY" - echo "teeth, every shard: $TEETH" >>"$GITHUB_STEP_SUMMARY" - test "$AUDIT" = success - test "$TEETH" = success - - # Decides whether the radar job below has anything to test. radar is its own - # nested workspace with no path dependency on the rest of the repository, so - # a change outside crates/radar, this file and the apt probe cannot change - # what it builds. Until 2026-09-08 the job ran on every pull request anyway, - # and on that day snapshot.ubuntu.com was down for hours: every open pull - # request, most of them touching docs, the dashboard or an SDK, wore a red - # `radar (eBPF build)` for an outage none of them could cause or fix. The - # job stays a required check; when it is skipped here it reports "skipped", - # which branch protection counts as satisfied, and the reason is in this - # job's summary rather than implied by a green tick. - # - # Decided against, and why: a `paths:` filter on the workflow (a required - # check that never reports blocks the merge instead of skipping), a - # third-party paths-filter action (one more pinned dependency for four - # lines of git), and any live-archive fallback for the snapshot itself - # (@decided 2026-09-08: that trade is closed, and the pin stays.) - changes: - name: what changed - runs-on: ubuntu-24.04 - outputs: - radar: ${{ steps.radar.outputs.radar }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - name: radar-relevant paths in this change - id: radar - env: - BASE: ${{ github.event.pull_request.base.sha || github.event.before }} - run: | - # Any doubt runs the job: a base we cannot diff against (a first push, - # a force-push, a shallow clone) is a reason to run, never to skip. - relevant=true; reason="could not diff against base '$BASE'; running to be safe" - if [ -n "$BASE" ] && ! printf '%s' "$BASE" | grep -Eq '^0+$' && git cat-file -e "$BASE^{commit}" 2>/dev/null; then - if files=$(git diff --name-only "$BASE" "$GITHUB_SHA"); then - if printf '%s\n' "$files" | grep -Eq '^(crates/radar/|\.github/workflows/ci\.yml$|scripts/apt-snapshot-probe\.sh$)'; then - relevant=true; reason="radar, this workflow or the apt probe changed" - else - relevant=false; reason="nothing under crates/radar, this workflow or the apt probe changed ($(printf '%s\n' "$files" | grep -c .) file(s) elsewhere)" - fi - fi - fi - echo "radar=$relevant" >>"$GITHUB_OUTPUT" - echo "radar (eBPF build): $([ "$relevant" = true ] && echo runs || echo skipped), $reason" | tee -a "$GITHUB_STEP_SUMMARY" - - radar: - name: radar (eBPF build) - needs: changes - if: needs.changes.outputs.radar == 'true' - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: System deps - # A hard ceiling for a flapping snapshot service; the probe catches - # "down", apt's per-fetch bounds do not add up to a bound on the step. - timeout-minutes: 6 - run: | - # The snapshot service is served from archive.ubuntu.com. The runner - # ships pointing at the azure mirror, which does not serve snapshots - # and has its own long record of missing content. - for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources \ - /etc/apt/sources.list.d/*.list; do - # `if`, not `[ -f ] &&`: GitHub runs these blocks under `bash -e`, and a - # glob that matches nothing makes the && list exit 1 and kill the step. - if [ -f "$f" ]; then - sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g' "$f" - fi - done - ./scripts/apt-snapshot-probe.sh - sudo apt-get update --snapshot "$APT_SNAPSHOT" - sudo apt-get install -y --snapshot "$APT_SNAPSHOT" clang llvm libelf-dev - - name: Install Rust (stable + nightly + rust-src) - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master - with: - toolchain: nightly - components: rust-src - - run: rustup toolchain install stable - - name: Cache cargo - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - workspaces: crates/radar - # Pinned, and the pin is the point. This step read `cargo install - # bpf-linker` until 2026-08-20, which resolves to whatever is newest at - # the moment the job runs. bpf-linker 0.11.0 was published on 2026-08-12, - # the day after the last green run of this job, and it links against - # system LLVM dynamically: the step then died with `rust-lld: error: - # unable to find library -lLLVM`, because `System deps` above installs - # `llvm` and not `llvm-dev`. Nothing in this repository had changed, and - # main went red on its own. - # - # 0.10.4 is the last 0.10.x and ships its own Cargo.lock, so `--locked` - # holds bpf-linker's transitive dependencies too and not only its - # version. Moving to 0.11.0 is a separate decision and needs the LLVM - # dev package chosen deliberately rather than inherited from whatever - # the runner image happens to carry, which is the coupling that broke - # this in the first place. - - name: Install bpf-linker - run: cargo install bpf-linker --version 0.10.4 --locked - # --locked, because a committed lockfile that CI does not enforce is half - # a pin: cargo would quietly resolve something newer and update the file, - # which is invariant 22's own failure one level down from the tool - # installs it already covers. With it, a dependency that would move fails - # here and somebody decides. - - name: Build radar - working-directory: crates/radar - run: cargo build --locked - # The userspace crate only: radar-ebpf is `no_std`/`no_main` and has - # nothing to run. Added with the first tests this crate ever had; until - # 2026-08-08 the job built and stopped, so a test placed here would have - # been compiled by nobody and run by nobody while reading as coverage. - - name: Test radar (userspace) - working-directory: crates/radar - run: cargo test --locked -p radar - - apns: - name: cloud apns (feature build) - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master - with: - toolchain: stable - components: clippy - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - - name: Build + clippy the real APNs sender - run: cargo clippy -p tokenfuse-cloud --all-targets --features apns -- -D warnings - diff --git a/.github/workflows/red-first.yml b/.github/workflows/red-first.yml new file mode 100644 index 0000000..0f66801 --- /dev/null +++ b/.github/workflows/red-first.yml @@ -0,0 +1,48 @@ +# Throwaway: the new checkpoints test must fail when the presence check on +# `signatures` is disabled. This branch is never merged. +name: red-first + +on: pull_request + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + +jobs: + test: + name: red first (presence check disabled) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Free runner disk + run: | + sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc \ + /usr/local/share/boost "$AGENT_TOOLSDIRECTORY/CodeQL" || true + - name: Install Rust + uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master + with: + toolchain: stable + components: rustfmt, clippy + - name: Cache cargo + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + prefix-key: v1 + save-if: false + - name: The new test against the disabled check + run: | + set +e + cargo test --workspace --lib -- checkpoints::tests::a_statement_with_no_signatures_is_refused >out.txt 2>&1 + rc=$? + set -e + sed 's/\x1b\[[0-9;]*m//g' out.txt >plain.txt + tail -30 plain.txt + if [ "$rc" -ne 0 ] && grep -q 'test checkpoints::tests::a_statement_with_no_signatures_is_refused ... FAILED' plain.txt; then + echo "RED as required" | tee -a "$GITHUB_STEP_SUMMARY" + grep -E 'panicked at|NoSignatures|EmptySignatures' plain.txt | head -4 | tee -a "$GITHUB_STEP_SUMMARY" + else + echo "NOT RED: rc=$rc" | tee -a "$GITHUB_STEP_SUMMARY" + exit 1 + fi diff --git a/crates/gateway/src/checkpoints.rs b/crates/gateway/src/checkpoints.rs index 4513490..6167701 100644 --- a/crates/gateway/src/checkpoints.rs +++ b/crates/gateway/src/checkpoints.rs @@ -530,7 +530,7 @@ impl Keyryx { let signed = statement["signatures"] .as_array() .is_some_and(|s| !s.is_empty()); - if payload.is_empty() || !signed { + if payload.is_empty() || (!signed && false) { return Err("keyryx's statement is not of the known shape".into()); } let raw = base64::engine::general_purpose::URL_SAFE_NO_PAD