Skip to content

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

Open

Description

@dezren39

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_genesis never checks that the signing key is one of agreement.players. A third party can sign the agreement and produce a well-formed attestation.genesis event 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 only first == second (duplicate seat) before serializing and calling chain::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 test pkgs/id/tests/probe_attestation_sign.rs, cargo test -p id --test probe_attestation_sign -- --nocapture, 1 passed): sign_genesis by a third key naming neither seat → Ok (event produced); sign_claim by the same third key → Ok; compute_state over 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_genesis refuses to sign when the signing key's player_id is not one of agreement.players, surfacing a typed SignError variant (e.g. NotAPlayer); no event is produced.
  • sign_claim gains a way to refuse a non-player signer — e.g. it takes the GenesisAgreement (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 the sign_unchecked pattern 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 existing attestation_poc.rs demonstration passes unchanged).
  • compute_state behavior 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

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    on Nov 6, 2026
  2. developing-today commented on Nov 6, 2026

    @developing-today
    Contributor
  3. added
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Nov 6, 2026
  4. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  5. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  6. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triage verdict — CONFIRMED, with the API-shape decision made

    Verdict: CONFIRMED at laptop main 1dae17e457.

    Fresh source verification (pkgs/id/src/attestation.rs):

    • sign_genesis checks only the duplicate-seat invariant (first == second → SignError::DuplicateSeat); it never compares player_id(key) against agreement.players.
    • sign_claim checks nothing at all — and structurally cannot: its parameters are (key, seq_num, backlink, claim, after). A ResultClaim carries only the agreement hash, not the player set, so signer membership is undecidable at this call site without an API change.
    • compute_state counts 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-player sign_genesis Ok, non-player sign_claim Ok, stranger-only claim computes Proposed) is consistent with this source read.

    API-shape decision (the triage call the packet asked for):

    1. Add a typed SignError::NotAPlayer variant, returned when player_id(key) is not one of agreement.players.
    2. sign_genesis: refuse with NotAPlayer after the existing duplicate-seat check; no event produced.
    3. sign_claim: change the signature to also take agreement: &GenesisAgreement (mirroring sign_genesis), refuse with NotAPlayer when the signer is not a player, and refuse when claim.agreement != agreement.agreement_hash() (a claim answering a different agreement is equally dead on arrival in compute_state; with the agreement in hand this check is free). A checked sibling that leaves the current unchecked public sign_claim in place is rejected: the silent path would remain public.
    4. Cost is low and bounded: the only in-repo callers of sign_claim are 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_state behavior 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, the attestation_poc demonstration still computing Agreed, and cargo test -p id attestation coverage.

  7. 65 remaining items

  8. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · PR opened, parked for CI

    • Agent: repo-worker-code (OpenChamber scheduled lane, lane bf419306), task 10870
    • Task: Implemented sign-time refusal (NotAPlayer / AgreementMismatch, crate-private sign_claim_unchecked) in PR fix(id): refuse non-player signers in sign_genesis/sign_claim (#6424) #6559. 20 attestation tests pass locally. PR also fixes two str/String comparisons in world_net.rs that stopped the id lib compiling on main 9c06d0c.
    • Next: parked 20 min; resume reads CI fresh and merges if green.
  9. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  11. added
    queue/claimedQueue worker lifecycle state
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    queue/claimedQueue worker lifecycle state
    on Nov 6, 2026
  12. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  13. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/idPackage/app: id (pkgs/ or apps/ in this repo)kind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/parkedQueue task is parked (waiting on CI, time, or an event)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/parkedDeliberately paused with a recorded resume conditionstatus:in-reviewtask/small-fixQueue packet kind: bounded fix with reproduction and targeted teststask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions