Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions docs/specs/remote-security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,32 @@ Source of truth: `remote-lib-common/src/security/noise.ts`, `isEnrollment` / `pe

## Future

**Scope: remote-trust-followups** — [Device verification](#device-verification) and [Revocation propagation](#revocation-propagation).
**Scope: remote-trust-followups** — [Device verification](#device-verification), [Revocation propagation](#revocation-propagation), then [Account-wide devices](#account-wide-devices), which builds on it.

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.

The ordering here goes in a circle. This line says Account-wide devices builds on Revocation propagation, but Revocation propagation now says Account-wide devices "stages both", and Account-wide devices opens with "One design with Revocation propagation, not two". If they are one design, the scope can list it once:

Suggested change
**Scope: remote-trust-followups** — [Device verification](#device-verification), [Revocation propagation](#revocation-propagation), then [Account-wide devices](#account-wide-devices), which builds on it.
**Scope: remote-trust-followups** — [Device verification](#device-verification), and [Account-wide devices](#account-wide-devices), which stages [Revocation propagation](#revocation-propagation).


### Device verification

`getUserMedia` inside a Home Screen web app is observable only on a real iOS device, and without it the install has only the paste field. The Client-static storage format surviving an app and phone restart is no longer open: the v3 Home Screen report is in [Client statics](#client-statics)'s rationale and in [pocket-app.rationale.md](./pocket-app.rationale.md).

### Revocation propagation

The Relay pushing revocations to Burrows. A Burrow revokes only from its own Settings and `dor` ([Revocation](#revocation)); no relay frame or protocol-v1 message carries one yet, and each would call `BurrowService.revoke`.
The Relay pushing revocations to Burrows. A Burrow revokes only from its own Settings and `dor` ([Revocation](#revocation)); no relay frame or protocol-v1 message carries one yet; [Account-wide devices](#account-wide-devices) stages both.

### Account-wide devices

One design with [Revocation propagation](#revocation-propagation), not two: a list nobody can act on and a revocation with no list are halves of one feature. Each stage is a new caller of `BurrowService.revoke` under a requester of its own ([Revocation](#revocation)).

**The managed identities are the two the model has; nothing merges them into a "device"** (rationale):

| Identity | Held by | Removing it cuts |
| --- | --- | --- |
| Passkey | the Relay, per account; synchronized onto many devices ([Passkeys](#passkeys)) | every browser that signs in with it, on every Burrow |
| ACL record | one Burrow, for one Client static ([Client statics](#client-statics)) | that browser on that Burrow |

One Pocket profile is a record per Burrow, and a phone's tab and its Home Screen install are two profiles. Pocket may mark the records that are its own, from its `KnownBurrowV1`s; nothing else is grouped.

**The Relay will hold no device directory**: no record's label, `deliveryId`, approval time, or `deviceIdFor` id reaches the Relay, which sees only stage 2's ciphertext ([Burrow Authorization](#burrow-authorization); rationale). The account view is assembled from what each side already holds, in two stages:

1. **Passkey removal reaches the ACL.** Hosted's account page already removes a passkey from the Relay, ending its sessions and sign-ins (`docs/specs/hosted.md` -> "Burrow enrollment"), while every Burrow keeps its records active. On its `burrowToken`, at socket open and on a nudge frame after any removal, the Burrow will send the credential ids its active records hold, and the Relay answer which of them its account no longer holds; the Burrow revokes each by its passkey target, an offline one at its next connection. **A Relay-named revocation may only revoke** — never create, restore, or edit a record — so a lying Relay costs re-pairing, which is availability ([Goals](#goals); rationale).
2. **Per-record list and revoke from Pocket**, on each online Burrow it is paired with, over protocol-v1 (`docs/specs/remote-api.md` -> "Future"): each record's mapped label, `deviceIdFor` id, approval time, last seen ([Access log](#access-log)), and whether it is the caller's, revoked by that id as `dor remote revoke` is, never by a static or `deliveryId`. **Revoking takes a fresh presence proof, never a window ride.** An unreachable Burrow is shown as one, stage 1 being the lever that reaches it.

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.

Stage 2 sends list and revoke messages over protocol-v1 and points at docs/specs/remote-api.md -> "Future", but that section (remote-api-remainder) has no stage for them. It covers browser surfaces, command replay, grants, and VR, and nothing about the ACL or revocation. Meanwhile remote-api's protocol-v1 scope says "Must restrict it to terminal listing, one attachment per session, and terminal input/resize", with everything else staged in its Future. So a reader following the pointer finds nothing, and the one spec that owns what a Client speaks has no record of these two methods. Add a stage to remote-api-remainder that names them and links back here, or rewrite the pointer to say the messages aren't staged there yet.


**A stage-1 revocation needs no passkey assertion**: the Burrow acts on the Relay's answer alone (rationale).
12 changes: 12 additions & 0 deletions docs/specs/remote-security-model.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,15 @@ Loss stays recoverable by re-pairing, so this changes the advice, not the model.
**Why keystroke timing is accepted rather than closed.** Closing it needs batching or cover traffic on the Client→Burrow direction: either holding keystrokes for a fixed quantum — latency a terminal user feels immediately, on the one interaction the product exists for — or sending padding frames forever, which a phone pays for in battery and a relay in bandwidth. The party that observes the timing is the one the user chose to run. A deployment that considers timing part of its threat model needs a different transport, not a different padding policy.

**Why a shared push endpoint correlates across Burrows.** One service-worker scope holds exactly one `PushSubscription`, and the Relay stores one row per `(burrowId, deliveryId)` against the endpoint that phone presented, so every `deliveryId` a Pocket profile registers lands on the same endpoint string and the Relay can read off the set of Burrows one phone is paired with — information the ACL deliberately keeps on each Burrow. Per-Burrow endpoints are not available: the browser mints subscriptions per scope, and a scope per Burrow would mean a service worker per Burrow.

## Account-wide devices

**Why no Relay-held directory.** It is the one place a list could live for a Burrow that is offline, but it would need a per-record handle the Relay can list to a session: a `deliveryId` is a push bearer capability the Relay never lists ([Burrow Authorization](./remote-security-model.md#burrow-authorization)), and the Client static is what IK keeps from the Relay. Its labels would say little, `PAIRING_DEVICE_LABELS` having two members. The offline case needs a lever rather than a list, and the passkey is already account-wide on the Relay.

**Why Pocket alone is not enough.** Pocket reaches only the Burrows it is paired with, and only while they are online; "cut this phone off everywhere" is wanted most when a Burrow is asleep.

**Why nothing is grouped.** Per-Burrow statics are unlinkable by construction, and iOS gives a Safari tab and the Home Screen install separate storage. The one cross-Burrow link, a profile's shared push endpoint, is an accepted leak ([Residual metadata](#residual-metadata)) that covers only profiles with push on — not evidence to build on.

**Why the Burrow asks about its own ids.** The Relay names removed ids out of the ones the Burrow sends, rather than handing over the account's live set, so a Relay answering nothing revokes nothing, and a self-host `account.json` hand-edit needs no tombstone.

**Why a revocation needs no signature.** Revocation fails safe, unlike approving a connection, which fails dangerously (Ned's decision, 2026-10): a forged revocation denies access and never grants it. A Relay can already refuse every session; making a refusal durable adds only the cost of re-pairing. A signed request would need an assertion for the passkeys' RP ID, the relay origin's hostname, which the account page on `hosted.` cannot make, so the lost-phone case would still need Pocket on another device. Passkey granularity has its own cost: a synchronized passkey lives on the user's other devices too, so removing it cuts them as well, and each pairs every Burrow again under a new passkey; stage 2 is the finer lever wherever a paired Burrow is reachable.
2 changes: 1 addition & 1 deletion scripts/spec-word-budgets.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"docs/specs/relay.md": 5750,
"docs/specs/remote-api.md": 4450,
"docs/specs/remote-network.md": 2400,
"docs/specs/remote-security-model.md": 6200,
"docs/specs/remote-security-model.md": 6600,
"docs/specs/reopen.md": 1200,
"docs/specs/security-audit.md": 5400,
"docs/specs/security-ci.md": 3400,
Expand Down
Loading