Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
483 changes: 0 additions & 483 deletions .github/workflows/ci.yml

This file was deleted.

48 changes: 48 additions & 0 deletions .github/workflows/red-first.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <crate>`). 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 <crate>`). 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`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion crates/gateway/src/checkpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion crates/gateway/src/checkpoints/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ enum Mode {
OtherOwner,
OtherLife,
TimesDiffer,
NoSignatures,
EmptySignatures,
Refuse401,
Redirect,
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions features/signed-checkpoints-of-the-event-stream.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading