ROT-3a: rotate the entity DEK on revoke (content forward secrecy)#128
Conversation
…rd secrecy) Wire SharingEngine.revoke to the ROT-2 rotate-on-revoke orchestrator: after a successful signed revoke, mint DEK′, re-wrap it for the SURVIVORS only (ROT-1 excludes the now-inactive member), publish those wraps to survivors' inboxes, and re-seal the snapshot under DEK′. The revoked member's key can no longer decrypt new content — the forward-secrecy guarantee. The 10.11 token re-home (metadata forward secrecy) stays dormant in production (design 73 §dormancy), so `rotate` is a local no-op: emission already flips to DEK′ because the store's most-recent-by-created_at row IS DEK′. The residual metadata gap (revoked member still sees now-undecryptable traffic) is ROT-3b, gated on 10.11b. An offline revoke records the removal but defers rotation. In-process test proves it cryptographically over a real YDocStore + EntityDekStore: the DEK rotates, the surviving guest unwraps DEK′ from the doc, and the revoked guest has no wrap of DEK′ anywhere (best it recovers is the superseded old DEK). Ships behind the ROT-4 gate: a dedicated /security-review + /pentester pass on the whole path before this reaches a release. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ll lock-out (F-ROT-1) The rotate-on-revoke test asserts the OWNER-side invariant only; it does not drive the survivor installWrap path, which no-ops on an already-installed DEK and drops DEK'. See the ROT-4 review. ROT-3a is blocked on ROT-3a-i (versioned DEK install) before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🔴 ROT-4 review: BLOCKED — do not mergeAn adversarial pass on the revoke→rotate→emit path found the orchestration is correct but sits on a receive-side DEK primitive that isn't rotation-aware. Full writeup: What holds: revoked member is provably excluded from the DEK′ wrap set (signature-verified Blockers:
Required before merge — Keeping this as a draft until |
…T-1/2/3) The ROT-4 review found rotate-on-revoke's orchestration correct but the receive-side DEK primitive not rotation-aware. This makes it monotonic end-to-end: - Member-wrap format v2 carries the DEK's rotation `version` and BINDS it into the AEAD AAD, so the ordinal is authenticated — a relay that replays or re-labels an old wrap can't forge a higher ordinal without breaking the tag. v1 wraps stay readable (unwrap dispatches on `v`). - EntityDekStore mints monotonic versions (maxVersion+1), exposes `version` on the handle, and `getByEntityId` ranks by `version DESC` (not receive-time), so the highest ordinal is current — not the most-recently-written row. - installEntityDek accepts a STRICTLY-NEWER ordinal and no-ops on equal/older: a rotated DEK now upgrades a live survivor (F-ROT-1), a replayed pre-rotation wrap can't roll them back (F-ROT-3), re-delivery stays idempotent. - Version threaded through the share/cascade/rotate wrap producers; create-time self-wraps stay v1 (ordinal 1, the entity's first DEK). Tests: member-wrap v2 round-trip + tamper-rejection; installEntityDek accept-newer/reject-rollback; and an END-TO-END proof driving guest B's real install path across a rotation (B upgrades to DEK′; a replay is rejected) — the coverage the owner-side-only ROT-3a test lacked. Full shell suite green (527 files / 5787 tests). Still behind the ROT-4 gate: re-run /security-review + /pentester two-shell dogfood before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ ROT-3a-i landed — F-ROT-1/2/3 closed (still draft, pending ROT-4 re-run)The receive-side is now version-monotonic end-to-end:
Tests: member-wrap v2 round-trip + tamper-rejection; Still draft. The doc-73 gate needs a re-run |
ROT-4
|
…ROT-4) The ROT-4 review found that an offline revoke (or an online rotation that threw mid-publish) never rotated the DEK, revoke() returned success anyway, and the retry was structurally dead — so the removed member kept reading new content. This makes rotate-on-revoke converge: - #rotateAfterRevoke now always does its DURABLE half online or off — mint DEK′ (so emission flips even for offline post-revoke edits), re-wrap survivors, append the wraps into the doc. Only the WIRE half (the survivor inbox WrapBootstrap + full-state re-emit) needs a relay. - New durable `pending_rotations` table (entities.db v8) + repo. When the wire delivery can't complete, the entity is marked pending; hardDelete cleans it (pragma-independent, like entity_deks). - drainPendingRotations() re-derives each pending entity's survivor wraps from its CURRENT DEK and re-emits; self-guarded + single-in-flight. Wired to fire on relay "state" (reconnect) and onActiveVaultSessionChanged (boot), mirroring drainAssetUploads. Tests: pending-rotations repo (mark/upsert/order/hardDelete-cleanup); an in-process offline-revoke → durable-mint + wrap-in-doc + pending → reconnect drain delivers + clears; and online-revoke-leaves-nothing-pending. Full shell suite green (528 files / 5792 tests); lint gate clean. Still behind the ROT-4 gate's remaining item: the /pentester two-shell dogfood (online + offline-revoke-then-reconnect) at the 0.3.0 cut. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ ROT-3a-ii landed — F-ROT-4 closed. This PR is mergeable.
Tests: pending-rotations repo (mark/upsert/order/hardDelete-cleanup); in-process offline-revoke → durable mint + wrap-in-doc + pending → reconnect drain delivers + clears; online-revoke-leaves-nothing-pending. Full shell suite green (528 files / 5792 tests), lint gate clean. Merge statusAll findings closed — F-ROT-1/2/3 (ROT-3a-i) and F-ROT-4 (ROT-3a-ii). The security review's online path was crypto/auth/injection clean across three lenses; the ROT-3a-ii diff got a focused self-review (no new crypto — reuses reviewed primitives). Marking ready to merge. The remaining |
What
Wires
SharingEngine.revoketo the ROT-2 rotate-on-revoke orchestrator so a revoke is cryptographically effective: after the signed removal appends, the entity DEK rotates and DEK′ is re-wrapped for the survivors only. The revoked member's key can no longer decrypt new content.Sequence (design 73, fail-closed):
EntityDekStore.persist) — becomes the most-recent row, so emission flips to it.emitWrapBootstrapto each survivor's inbox, so a live survivor installs DEK′ before the next frame.Scope boundary
Proof
sharing-engine-rotate.test.tsdrives the realSharingEngineover a persistedYDocStore+ realEntityDekStore: DEK rotates, the surviving guest unwraps DEK′ from the doc, the revoked guest has no wrap of DEK′ anywhere (best it recovers is the superseded old DEK). Full collab (14 files) + sharing-service + integration suites green.Do not merge before ROT-4: a dedicated
/security-review+/pentesterpass on the whole revoke→rotate→emit path (design 73 makes this mandatory, not optional).🤖 Generated with Claude Code