diff --git a/docs/specs/remote-security-model.md b/docs/specs/remote-security-model.md index 5eb932edf..dc43c0458 100644 --- a/docs/specs/remote-security-model.md +++ b/docs/specs/remote-security-model.md @@ -297,7 +297,7 @@ 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. ### Device verification @@ -305,4 +305,24 @@ Source of truth: `remote-lib-common/src/security/noise.ts`, `isEnrollment` / `pe ### 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. + +**A stage-1 revocation needs no passkey assertion**: the Burrow acts on the Relay's answer alone (rationale). diff --git a/docs/specs/remote-security-model.rationale.md b/docs/specs/remote-security-model.rationale.md index d537e4db8..e614d50a2 100644 --- a/docs/specs/remote-security-model.rationale.md +++ b/docs/specs/remote-security-model.rationale.md @@ -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. diff --git a/scripts/spec-word-budgets.json b/scripts/spec-word-budgets.json index 41f278f16..4264e1cc5 100644 --- a/scripts/spec-word-budgets.json +++ b/scripts/spec-word-budgets.json @@ -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,