Skip to content

id(directory_auth): a signed request replays at exactly at+SIGNED_WINDOW_MS — the replay guard evicts the nonce 1 ms before the verifier's window closes #1769

Closed

Description

@dezren39

Problem

A captured signed directory request can be replayed successfully at exactly one instant: signed.at + SIGNED_WINDOW_MS (5 minutes after its timestamp). At that millisecond the replay guard has already forgotten the nonce, while the verifier still considers the request inside its time window — so the same signed request is accepted twice.

Evidence (verified this run on main 9874fe59)

  • pkgs/id/src/directory_auth.rs:28 — pub const SIGNED_WINDOW_MS: u64 = 5 * 60 * 1000;
  • Verifier gate (directory_auth.rs:327, in verify_signed): let window_ok = signed.at.abs_diff(now) <= SIGNED_WINDOW_MS; — the boundary instant is inclusive: at now == at + SIGNED_WINDOW_MS the request is still in-window.
  • Replay guard eviction (directory_auth.rs:249-250, in ReplayGuard::accept): self.seen.retain(|_, seen_at| *seen_at + SIGNED_WINDOW_MS > now); — the entry is exclusive: at now == seen_at + SIGNED_WINDOW_MS the nonce is already evicted. (seen_at is the original request's signed.at, stored at directory_auth.rs:266.)
  • accept is called only after the signature verifies (directory_auth.rs:352), and a replay carries the original valid signature, so nothing else stops the second acceptance.

Probe (verbatim mirror, run on the laptop)

Mirror of ReplayGuard::accept + the verify_signed window gate (comparisons and the retain predicate character-exact; directory_auth.rs sha256 fab8803d… on main 9874fe59). Probe: ~/git/fleet/probe-2026-10-28-laneB-replay-invite/ (laptop), copy + log in the goal bookkeeping hidden_files/probe-2026-10-28-laneB-replay-invite/:

P1 first use at at:             Ok(())
P1 replay at at+WINDOW-1:       Err("request was already used")
P1 replay at at+WINDOW (exact): Ok(())  <-- replay accepted
P1 replay at at+WINDOW+1:       Err("signed request is outside its time window")

The controls bracket the bug exactly: refused 1 ms before the boundary (nonce still remembered) and 1 ms after it (window closed), accepted only at the boundary millisecond.

Why it matters

The replay guard is the only thing standing between a captured signed request (method + target + body hash are all in the signed message) and a second execution of it inside its validity window. The window is narrow — one millisecond per captured request — but it is deterministic: an attacker who captured a request knows its at exactly and can flood replays around at + 300000 so one lands on the boundary millisecond on the server's clock. The two boundary conventions (<= in the verifier, > in the guard) were clearly meant to describe the same interval; they disagree at exactly one point.

Acceptance criteria

  • One convention, both places: a signed request is acceptable at a given now iff its nonce is still remembered at that now — e.g. the guard retains while seen_at + SIGNED_WINDOW_MS >= now, or the verifier uses < — with the choice pinned and commented so the two cannot drift apart again.
  • Regression test in directory_auth.rs tests: first use at at, replay at at + SIGNED_WINDOW_MS - 1, at at + SIGNED_WINDOW_MS, and at at + SIGNED_WINDOW_MS + 1 — only the first use succeeds.
  • Existing directory_auth tests still pass (laptop or CI).

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane B, slug new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run, 2026-10-28 ~18:28Z
    • Found via: filed this run
    • Task: Triage the signed-request replay boundary mismatch — pin one boundary convention shared by verify_signed's window gate and ReplayGuard's nonce eviction, with a bracketing regression test.
    • Expectation: effort/s — done looks like the triage verdict pinned on this issue and a fix packet enqueued (or the fix itself, if triage finds the one-operator alignment mechanical).
    • Links: evidence in the issue body; verbatim-mirror probe ~/git/fleet/probe-2026-10-28-laneB-replay-invite/ (laptop)
  4. 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 1, 2026
  5. 4 remaining items

  6. added
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (PR opened)

    • Agent: queue worker (repo-worker-code), OpenChamber scheduled lane
    • Session / ID: OpenCode session ses_e5e683b7eb6ffe5yJi13wNrMpp; queue task 4801
    • Found via: queue claim
    • Task: Align replay nonce retention with the inclusive SIGNED_WINDOW_MS verifier window.
    • Expectation: complete — PR fix(id): retain signed nonces through window edge #3765 opened; local directory_auth tests pass (14/14). No CI workflow checks ran; CodeRabbit reported NEUTRAL/skipped, so the PR remains open pending green checks.
    • Links: fix(id): retain signed nonces through window edge #3765; task 4801 (repo-health-code-pilot)
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)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:in-reviewtask/small-fixQueue packet kind: bounded fix with reproduction and targeted teststask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions