Repository navigation
Description
Problem
In pkgs/id/src/hosted_binding.rs (hosted account + ID binding ceremony, slice 1 of #3808), the handling of dead challenges (expired or consumed) contradicts the module's own §3.4 failure-semantics table in two places:
-
An expired challenge is recorded as consumed on first presentation. The module header (hosted_binding.rs:43-44) says "expired challenges die without state", and
CeremonyError::ChallengeExpired's own doc (hosted_binding.rs:415) says "State change: none; the challenge is dead." But the expiry branch ofsubmit_proof(hosted_binding.rs:674-678) removes the challenge and inserts its ID intoconsumed_challenges. Observable consequence: presenting the same expired challenge a second time returnsChallengeReplay("already presented") instead ofChallengeExpired— even though expiry was specified as creating no state. It also makes the consumed set retain IDs whose only sin was expiring. -
create_pairing_code'sChallengeReplaybranch is unreachable. Its doc comment (hosted_binding.rs:807-812) promisesChallengeReplayfor a dead (consumed) challenge. But the function first looks the challenge up inself.challengesand returnsUnknownChallengeif absent (hosted_binding.rs:818-822), and only then checksconsumed_challenges(hosted_binding.rs:823-825). Consumed challenges are always removed fromself.challenges(consume(), hosted_binding.rs:769-772; the expiry branch above), so a consumed ID can never reach the replay check — the branch is dead code, and callers getUnknownChallengefor a challenge that was in fact consumed.
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):
- Expired challenge presented twice: first
Err(ChallengeExpired), secondErr(ChallengeReplay)— expiry did create state (the consumed entry), contradicting the "state change: none" specification. - Challenge consumed by a bad-signature submit (
Err(BadSignature)), thencreate_pairing_codefor it:Err(UnknownChallenge)— never the documentedChallengeReplay. - Control:
create_pairing_codefor a live challenge succeeds.
Code reading at the same commit confirms the mechanism for both: expiry inserts into consumed_challenges (hosted_binding.rs:676); create_pairing_code checks the live map (hosted_binding.rs:820) before the consumed set (hosted_binding.rs:824), and consume() (hosted_binding.rs:769-772) always removes from the live map first. 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
This module is the security ceremony that binds a Google identity to an Iroh root key. Its error taxonomy is the contract the §3.4 table specifies for clients and for the follow-up slices (rebind #3809, Worker API #3811): "expired" and "replayed" drive different client behaviour (plain retry vs. replay handling/logging). Today the two are conflated for expired challenges, one documented error can never be returned, and the consumed-ID set grows with expired IDs it was not specified to hold. The divergence is in brand-new code (landed today in #3808), so fixing the contract now is cheap; after slice 2/3 build on it, it is not.
Acceptance criteria
- Presenting an expired challenge repeatedly yields the §3.4-specified result for expired challenges on every presentation (per the table, expiry records no consumed state), OR the §3.4 docs/header/
ChallengeExpireddoc are amended in the same change to specify the recorded-as-consumed behaviour — code and docs agree either way, with the chosen semantics stated on the issue/PR. -
create_pairing_codefor a consumed challenge returns the error its documentation promises (ChallengeReplay), i.e. the consumed check is reachable — regression test covers consumed, expired, unknown, and live challenges. - Regression tests in
hosted_binding.rs: expired challenge presented twice; pairing-code creation against a challenge consumed by a failed submit. - 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
dezren39 commentedon Nov 3, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 3, 2026
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 7527 (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
dezren39 commentedon Nov 3, 2026
agent-note: design-architect pass 45 (2026-11-03) ADOPTED this issue as Phase 1 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.
Phase 1 scope addition from the design audit (same transition, second gate): verify_challenge_proof (hosted_binding.rs:1171-1205, the rebind paths' shared challenge gate) also consumes on expiry, and RebindError::ChallengeExpired's doc ("it is consumed and dead") contradicts CeremonyError's ("State change: none"). The recorded ruling covers both surfaces: expiry creates no state anywhere; the RebindError doc is amended in the same change. The docs-amended-the-other-way alternative in this issue's acceptance was considered and lost — replay is a security signal, expiry is not; see #5047.
4 remaining items
dezren39 commentedon Nov 4, 2026
🤖 Agent note · parked
queue/7527-4868-hosted-binding-dead-challengedezren39 commentedon Nov 4, 2026
🤖 Agent note · picked up
Merge pull request #5224 from developing-today/queue/7527-4868-hosted…
dezren39 commentedon Nov 4, 2026
🤖 Agent note · done