Skip to content

id(hosted_binding): create_pairing_code does not supersede earlier codes — multiple live pairing codes per challenge, none revocable #4871

Closed

Description

@dezren39

Problem

In pkgs/id/src/hosted_binding.rs (own-node ceremony variant, §3.3b), create_pairing_code (hosted_binding.rs:813) mints a fresh 6-digit pairing code for a challenge without invalidating any previously minted codes for the same challenge. Each code is stored independently in pairing_codes (insert at hosted_binding.rs:851-857; PairingCode records the challenge_id, hosted_binding.rs:390-397), and exchange_pairing_code (hosted_binding.rs:870) honours every live code — exchanging a code does not consume the challenge or any sibling code.

So one pending ceremony can have many simultaneously valid bearer codes (bounded only by MAX_PAIRINGS_PER_HOUR = 10 per account), each able to fetch the full challenge (sub, iroh_id, nonce) until the challenge's 5-minute expiry. The §3.3b flow describes a short-lived pairing code tied to the pending ceremony; nothing in the module documents multiple live codes per ceremony, and there is no way to revoke an earlier code (e.g. one displayed to the wrong screen, or mistyped into the wrong node) short of abandoning the whole challenge.

Evidence (verified this run)

Re-verified by new-issue-creator lane A at origin/main 2194bd4 (laptop worktree ~/git/fleet/probe-2026-11-03-laneA-hosted, since removed; temporary integration test pkgs/id/tests/probe_hosted_a.rs, log /tmp/probe-laneA-hosted.log on the laptop):

  • One challenge, two create_pairing_code calls: two distinct live codes (probe printed 096628 vs 017728). Exchanging the first succeeded, exchanging the second also succeeded, and both returned the same challenge — the first code was not superseded or invalidated by minting the second, and neither exchange affected the other code.

Code reading at the same commit: create_pairing_code (hosted_binding.rs:813) never removes or marks existing pairing_codes entries whose challenge_id matches; the collision loop only avoids reusing a code string that is currently present. Originally found by lane B (creator-b) at 550b611; its filing was blocked by a GitHub secondary rate limit, so lane A re-verified and filed it.

Why it matters

The pairing code is the bearer handoff between the authenticated browser session and the user's node. Keeping every previously minted code valid for the challenge's whole lifetime multiplies the exposure of a 6-digit (10^6) secret and defeats the natural user recovery action — "that code went to the wrong place, give me a new one" — because the old code keeps working. Binding still requires the root key's signature, so this is a hardening/contract gap rather than a direct takeover; it is cheap to fix now, in the slice that introduced the flow (#3808), before the Worker API slice (#3811) exposes it over HTTP.

Acceptance criteria

  • Minting a pairing code for a challenge supersedes (invalidates) all previously minted, unexchanged codes for the same challenge_id — or the module docs are amended in the same change to state that multiple live codes per ceremony are intended, with the reasoning recorded.
  • Regression test: code A minted, code B minted for the same challenge, exchanging A fails (UnknownChallenge), exchanging B succeeds and returns the challenge.
  • Existing behaviour preserved: a single code still exchanges once and only once; per-account pairing rate limit unchanged; expired codes still return ChallengeExpired on exchange.
  • Existing hosted_binding tests still pass (cargo test -p id --lib hosted_binding).

Found by new-issue-creator lane B (creator-b) during a scan of the newly landed #3808 module, re-verified and filed by lane A; not covered by #3808/#3809/#3811 as filed.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    source/creatorFiled by the new-issue-creator job
    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 3, 2026
  2. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: reviewed (coverage loop-back)
    Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 7529 (small_fix, queued, priority 50) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
    Found via: coverage loop-back 2026-11-03

  4. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note: design-architect pass 45 (2026-11-03) ADOPTED this issue as Phase 2 of design #5047 ("A name is claimed once — the hosted binding lifecycle contract"; design doc files/designs/code-id-hosted-binding-lifecycle.md). Linked as a sub-issue of #5047. Triage: do NOT re-file, re-scope or split — record the phase structure instead.

    Recorded ruling on this issue's either/or: minting supersedes (the first acceptance bullet). The multiple-live-codes-are-intended alternative was considered and lost: the code is a bearer handoff of the full challenge with a 10^6 space, and a mint that does not revoke makes the user's only recovery action meaningless. See #5047.

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

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. added 2 commits that reference this issue on Nov 4, 2026
  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    What landed

    • create_pairing_code (pkgs/id/src/hosted_binding.rs) supersedes every earlier unexchanged code for the same challenge: the replacement is generated while the old codes are still in the map (so the collision loop can never re-issue a just-revoked string — Codex P2 on PR fix(id): create_pairing_code supersedes earlier codes for the same challenge (#4871) #5223, fixed in 3c72277 and replied on its thread), then the retain removes the superseded entries, then the new code is inserted. A rate-limited mint still errors before any supersede.
    • Regression test minting_a_pairing_code_supersedes_earlier_codes_for_the_challenge: A minted, B minted → A exchanges as UnknownChallenge, B succeeds once, and a different challenge's code is untouched.
    • Verified on the laptop: cargo test -p id --lib hosted_binding — 22 passed, 0 failed (run twice: before and after the Codex fix). PR check stayed QUEUED (CI runs on main only per the standing merge authority); merge was on verified tests + diff + Codex disposition.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions