Skip to content

Have a removed passkey revoke its pairings on every Burrow - #1527

Merged
nedtwigg merged 5 commits into
mainfrom
issue-331-stage1-passkey-revocation
Nov 3, 2026
Merged

nedtwigg merged 5 commits into
mainfrom
issue-331-stage1-passkey-revocation

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Removing a passkey ended its sign-in at the Relay while every Burrow kept the pairing, the known gap in docs/specs/security.md. This is stage 1 of the design merged in #1473: a removed passkey now reaches each Burrow's ACL.

Burrow. lib/src/remote/burrow/passkey-sync.ts asks at each relay socket's open and on each passkeys-changed frame. It sends only active records' credential ids, ignores any id it did not ask about, and revokes through BurrowService.revoke under a new requester, relay, so the access log says who asked. No command can claim relay, because the requesters a message may name are now a smaller set than the log's.

The Relay's answer can only revoke, and needs no passkey assertion: a lying Relay costs re-pairing, which is availability. A failed or refused ask revokes nothing. An older Relay answers 404 and nothing happens; an older Burrow ignores the frame. Asks run one at a time at least 5 seconds apart, so a hostile Relay's nudges cost a bounded rate.

Both Relays serve POST /api/burrow/passkeys/removed on the burrowToken, answering only asked ids the account no longer holds. It is HTTP because the RelayRoom opens no database connection and reads no Burrow frame beyond the routing envelope.

  • Hosted nudges through a new nudgeBurrows RPC after the delete commits, with the RelayRoom's hourly sweep as the backstop.
  • Selfhost's only removal is a hand edit of account.json, so its minute sweep compares the file with what it last read. An absent file names nothing.

Stage 1 moves above the fold in remote-security-model.md as "Relay-named revocation"; stage 2 stays in ## Future. New rules S300 and S301, with a Guarantee row. A computer that cannot reach its Relay cannot hear of a removal, which moves to What is not defended. Six specs' word budgets are ratcheted.

Verified: lib remote suites (1269), relay (310), remote-lib-common (360), and the Hosted suites on Docker Postgres; tsc in lib, standalone, vscode-ext and hosted; the specs, md, e2e, outbound, loopback and public-docs lints. 29 of 29 mutations went red. Not verified: a live Relay, a real phone, or a real Cloudflare deploy. Burrow and Relay have only run against a fake of each other.

Deploy: any order is safe. Prefer the relay Worker before the account Worker; otherwise the RPC fails, is logged, and the removal still answers 204.

For review: the 5-second spacing, the 3 retries, and the sweep nudge, which adds one small request per Burrow per hour, are the implementor's calls. A named passkey cuts all its synced devices.

Argos: the 33 changed snapshots in each browser should be the account page's reworded Passkeys help, which is one line longer. Exactly 33 of the 125 Hosted stories render it, counted by rendering each story's args. Nothing here was checked in Argos itself, so the diffs still want a look. No story has a revoked log entry, so the Settings log copy moves none.

Refs #331. Stage 2 stacks on this.

🤖 Generated with Claude Code

nedtwigg and others added 3 commits November 2, 2026 17:47
A passkey removed from a Relay's account left every Burrow's ACL record
active, because nothing on the wire could say it had gone. Adds the Relay half
of stage 1 of #331: POST /api/burrow/passkeys/removed on the burrowToken, which
answers only those of the asked ids the account does not hold, and a
passkeys-changed frame telling connected Burrows to ask.

Both Relays can lose a passkey, so both serve it. Hosted nudges from the
account Worker after the delete commits, whatever sessions the passkey had,
with the RelayRoom's hourly sweep as the backstop to a lost RPC. The self-host
Relay's only removal is a hand edit of account.json, so its minute sweep
compares the file with what it last read; an absent file answers nothing rather
than "every passkey went".

The question is HTTP because the RelayRoom opens no database connection and
reads no Burrow frame beyond the routing envelope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removing a phone's passkey ended its sign-in at the Relay while every computer
kept the pairing, the known gap in security.md. The Burrow now asks at each
relay socket's open and on each passkeys-changed frame, and revokes each named
credential through BurrowService.revoke under a requester of its own, `relay`,
so the access log says who asked.

The Relay's answer can only revoke, and needs no passkey assertion: a lying
Relay costs re-pairing. Only credential ids of active records reach it. An id
the Burrow did not ask about is ignored, and a failed or refused ask revokes
nothing, which is also how a Relay older than the route is handled; an older
Burrow ignores the frame. No command can claim the Relay asked, since the
requester a message may name is now a smaller set than the log's.

Asks run one at a time and at least five seconds apart on the Burrow's own
clock, so a nudge racing an ask asks once more and a hostile Relay's nudges cost
a bounded rate.

Promotes stage 1 above the fold in remote-security-model.md and keeps the
per-record list under Future. New rules S300 and S301.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…og copy

A computer that cannot reach its Relay cannot hear of a removal. That is
inherent, so it belongs with the accepted limits rather than among gaps the
project means to close. Also pins the Settings log's copy for a Relay-named
revocation, and rewraps a comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4ab7750
Status: ✅  Deploy successful!
Preview URL: https://be900f79.mouseterm.pages.dev
Branch Preview URL: https://issue-331-stage1-passkey-rev.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small thing in the access-log copy, inline.

Comment thread lib/src/components/PairedDevices.tsx Resolved
nedtwigg and others added 2 commits November 2, 2026 18:04
Exporting burrowRequest for passkey-sync.ts changed the context line the
burrow-fetch-global-fallback seed patches, and the header comment moved its
hunks by a line, so the seed no longer applied and test:audit failed on a stale
pool. The seed plants the same defect, a fallback to the global fetch: planted,
the test it deletes goes red if restored, and outbound-lint stays green under
its reclassified entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REVOKED_BY landed between ACCESS_EVENT_COPY's doc comment and its declaration,
so the comment documented the wrong thing. It moves above that comment and is
typed Record<RevocationRequester, string>, so a new requester fails to compile
rather than logging a bare "revoked".

The same insertion had put the self-host route between the setup-token banner
and the route that banner names, and Hosted's under the banner for discovery and
setup tokens. Each route now has a banner of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg marked this pull request as ready for review November 3, 2026 03:57
@nedtwigg
nedtwigg merged commit fedb131 into main Nov 3, 2026
21 checks passed
@nedtwigg
nedtwigg deleted the issue-331-stage1-passkey-revocation branch November 3, 2026 03:58
nedtwigg added a commit that referenced this pull request Nov 3, 2026
#1527 added the Relay requester on top of the dor verbs this branch removes. Keep the Relay's: RevocationRequester is 'settings' | 'relay', and the service names it, so no message can. Drop COMMAND_REQUESTERS, revocationRequester, and REVOKED_BY's dor copy, and the dor remote mentions #1527 added to the specs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nedtwigg added a commit that referenced this pull request Nov 3, 2026
S300 audited revocationRequester, which this branch removes, so it now names what holds the rule: #revokeDevice takes no requester from a message and logs settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants