Repository navigation
Description
id(attestation): sign_genesis / sign_claim never check the signer is a player — a non-player's signed genesis or claim is dead on arrival, and signing succeeds silently
Summary
pkgs/id/src/attestation.rs enforces its authority rules in the viewer: compute_state (attestation.rs:258) counts a genesis only when each of the agreement's two players has signed the identical agreement, and counts a claim only when its author is one of those players ("Claims answering a different agreement, from non-players, or with an outcome other than … are ignored", attestation.rs:254-255). The signing entry points enforce only one viewer invariant — sign_genesis (attestation.rs:171) refuses a duplicate-seat agreement, mirroring the viewer's malformed-agreement rule from #2863. Neither signer checks the other authority rule:
sign_genesisnever checks that the signing key is one ofagreement.players. A third party can sign the agreement and produce a well-formedattestation.genesisevent that no viewer will ever count toward the genesis pair.sign_claim(attestation.rs:208) never checks that the signing key is one of the players of the agreement the claim answers (it cannot check from the claim alone — see acceptance criteria for the shape this implies).
So a caller holding the wrong key — the realistic failure: a harness or wallet layer selects the wrong identity, or an operator signs with a witness/service key — gets a successful signature, a chain event, and an event id to hand out, and the object simply never exists for any viewer. That is the same failure shape closed for currency in #6422 ("creation reports success, consumption silently ignores") and for artifacts in #5623: a signed object should be refused at creation when the module's own consumption rules make it unreachable, so the mistake surfaces where the caller can fix it.
Evidence (verified this run, origin/main a2b4345e80)
- Code:
sign_genesis(attestation.rs:171-205) checks onlyfirst == second(duplicate seat) before serializing and callingchain::sign;sign_claim(attestation.rs:208-232) performs no check at all.compute_state's genesis rule (agreement.players.iter().all(|player| genesis.iter().any(|(author, signed)| author == player && …))) and per-player claim collection (author == player) make non-player objects unreachable by construction. - Probe (laptop, worktree of origin/main at
a2b4345e80,~/git/fleet/probe-attestation-wt, integration testpkgs/id/tests/probe_attestation_sign.rs,cargo test -p id --test probe_attestation_sign -- --nocapture, 1 passed):sign_genesisby a third key naming neither seat → Ok (event produced);sign_claimby the same third key → Ok;compute_stateover both players' genuine genesis events plus only the stranger's claim → Proposed (the stranger's claim is ignored, exactly as documented). Controls in the same probe: both players' sign calls succeed, and the existing duplicate-seat refusal errors as documented.
Why it matters
The attestation PoC's own module doc states the principle: "Reconstruction is admission, not deserialization: derive conclusions from signature-verified authors and compare claims with the selected agreement. Reject, ignore, or dispute inconsistent input according to the API's rules; never silently repair it or let an input's own claims establish authority." The viewer side honors that; the signer side is the remaining half. This module is explicitly the shape later production attestation APIs would be built on (#2039 design, epic #2056 review gate), so the sign-side contract should be settled while it is still a PoC surface. The duplicate-seat refusal in sign_genesis already establishes the house shape: viewer invariants that make an object meaningless are refused at sign time with a typed SignError.
Acceptance criteria
-
sign_genesisrefuses to sign when the signing key'splayer_idis not one ofagreement.players, surfacing a typedSignErrorvariant (e.g.NotAPlayer); no event is produced. -
sign_claimgains a way to refuse a non-player signer — e.g. it takes theGenesisAgreement(or its player set) alongside the claim, or a sibling checked entry point is added and the unchecked one is crate-private/test-only, mirroring thesign_uncheckedpattern in artifact.rs. The chosen shape is the fixer's call; the requirement is that the public signing path cannot silently produce a claim no viewer will count. - Tests: a non-player genesis signing and a non-player claim signing both error; both players' genuine genesis + claim flow still computes
Agreed(the existingattestation_poc.rsdemonstration passes unchanged). -
compute_statebehavior is unchanged: non-player objects from other/legacy signers are still ignored, never counted. - Out of scope: the witness slot (present and unused by design in this PoC) and objection handling (id: investigate: match audit — netcode log format, replay validation, and cheating signals #2038/id: design: match attestation + audit v1 #2039).
Notes
- Related: id(attestation): compute_state never checks the two players are distinct — players=[A,A] attests Agreed from one key's single genesis + single claim #2863 (duplicate-seat: the sign-time refusal precedent in this same function), id(attestation): compute_state never validates the claim outcome's domain — claims naming a third party (or any string) as winner compute Agreed #2868 (claim outcome domain, viewer side), id(currency): sign_authorisation never calls validate — a signed authorisation naming another issuer or with rate_den = 0 is dead on arrival, and signing succeeds silently #6422 (the currency sign-side validation gap — same failure shape, different module and rule; complementary, not a duplicate), id(artifact): validate known-kind bodies in generic signing helpers #5623 (artifact sign-side precedent), Design: reconstruction is admission — validate authority-bearing inputs where they are rebuilt (pkgs/id) #2898 (the reconstruction-is-admission design epic this module's doc cites).
- Found by Lane B (new-issue-creator-code-b), weighting away from Lane A's recent fleet-config/queue filings and Lane B's own recent dead-test and currency filings.
Activity
developing-today commentedon Nov 6, 2026
CODE-1924
dezren39 commentedon Nov 6, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 6, 2026
🤖 Agent note · triage verdict — CONFIRMED, with the API-shape decision made
direct, so this lane triaged it.Verdict: CONFIRMED at laptop main
1dae17e457.Fresh source verification (
pkgs/id/src/attestation.rs):sign_genesischecks only the duplicate-seat invariant (first == second→SignError::DuplicateSeat); it never comparesplayer_id(key)againstagreement.players.sign_claimchecks nothing at all — and structurally cannot: its parameters are(key, seq_num, backlink, claim, after). AResultClaimcarries only the agreement hash, not the player set, so signer membership is undecidable at this call site without an API change.compute_statecounts genesis only from the two players and ignores claims whose author is not a seated player — so a non-player's signed genesis/claim is dead on arrival, exactly as filed. The creator's laptop probe (non-playersign_genesisOk, non-playersign_claimOk, stranger-only claim computesProposed) is consistent with this source read.API-shape decision (the triage call the packet asked for):
SignError::NotAPlayervariant, returned whenplayer_id(key)is not one ofagreement.players.sign_genesis: refuse withNotAPlayerafter the existing duplicate-seat check; no event produced.sign_claim: change the signature to also takeagreement: &GenesisAgreement(mirroringsign_genesis), refuse withNotAPlayerwhen the signer is not a player, and refuse whenclaim.agreement != agreement.agreement_hash()(a claim answering a different agreement is equally dead on arrival incompute_state; with the agreement in hand this check is free). A checked sibling that leaves the current unchecked publicsign_claimin place is rejected: the silent path would remain public.sign_claimare the PoC tests (pkgs/id/tests/attestation_poc.rs, the two calls at ~:87/:95), and the module is explicitly a proof of concept with no production API built on it.compute_statebehavior is unchanged — non-player objects from peers are still ignored.Follow-up: small_fix task enqueued from this triage (task id in the completion note below / queue dedupe key
repo-health:v1:fix:developing-today/code:issue:6424) covering the refusal implementations, both new refusal tests, theattestation_pocdemonstration still computingAgreed, andcargo test -p idattestation coverage.65 remaining items
fix(id): refuse non-player signers in sign_genesis/sign_claim (#6424)
dezren39 commentedon Nov 6, 2026
🤖 Agent note · PR opened, parked for CI
dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
handed_off; the laptop opencode-claimant launches the bound session.dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
opencode— handed off to the opencode claimant, which launches the bound opencode session; this lane does not work it directly.dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
opencodebyfleet queue laneper worker instruction §1b; this lane does not work it directly. Issue id(attestation): sign_genesis / sign_claim never check the signer is a player — a non-player's signed genesis or claim is dead on arrival, and signing succeeds silently #6424 verified OPEN with no held, user/owned, do-not-merge, or do-not-auto-edit labels (fresh gh check 2026-11-06T17:57Z).dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off