Skip to content

ROT-3a: rotate the entity DEK on revoke (content forward secrecy)#128

Merged
th3-br41n merged 4 commits into
mainfrom
feat/rot-3-live-wiring
Jul 9, 2026
Merged

ROT-3a: rotate the entity DEK on revoke (content forward secrecy)#128
th3-br41n merged 4 commits into
mainfrom
feat/rot-3-live-wiring

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

What

Wires SharingEngine.revoke to 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):

  1. mint DEK′ (EntityDekStore.persist) — becomes the most-recent row, so emission flips to it.
  2. re-wrap DEK′ for survivors (ROT-1) — the now-inactive member is excluded by construction.
  3. publish wraps — append into the doc + emitWrapBootstrap to each survivor's inbox, so a live survivor installs DEK′ before the next frame.
  4. re-seal snapshot under DEK′ (full-state emit).
  5. rotate — a local no-op: the 10.11 token re-home is dormant in production (§dormancy), so no coordinator hop.

Scope boundary

  • Content forward secrecy — revoked member can't decrypt post-rotation content.
  • Metadata forward secrecy (ROT-3b) — revoked member still sees now-undecryptable traffic on the entity channel; gated on 10.11b (token layer activation).
  • Offline revoke records the removal but defers rotation (logged) — no relay to deliver wraps.

Proof

sharing-engine-rotate.test.ts drives the real SharingEngine over a persisted YDocStore + real EntityDekStore: 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.

⚠️ Merge gate

Do not merge before ROT-4: a dedicated /security-review + /pentester pass on the whole revoke→rotate→emit path (design 73 makes this mandatory, not optional).

🤖 Generated with Claude Code

th3-br41n and others added 2 commits July 9, 2026 08:20
…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>
@th3-br41n th3-br41n marked this pull request as draft July 9, 2026 06:32
@th3-br41n

Copy link
Copy Markdown
Contributor Author

🔴 ROT-4 review: BLOCKED — do not merge

An 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: docs/_review/2026-07-09-rot-3a-security-review.md (harness).

What holds: revoked member is provably excluded from the DEK′ wrap set (signature-verified active filter); owner-local emission flips to DEK′; no key material leaks.

Blockers:

  • F-ROT-1 (availability, reproduced): installEntityDek no-ops when any DEK already exists → a live survivor (who holds the old DEK) silently drops the DEK′ wrap and can't decrypt post-rotation content. The happy-path test in this PR passed only because it inspects owner-side wraps and never drives the survivor install path.
  • F-ROT-2 (prerequisite): MemberWrapPayload carries no DEK version — the install path can't tell a wrap is newer.
  • F-ROT-3 (confidentiality): DEK ordering is effectively receive-time (dek_id=randomUUID, version=1), so a replayed pre-rotation wrap could roll a survivor back onto the old DEK the revoked member still holds.

Required before merge — ROT-3a-i (versioned/monotonic DEK install): wrap carries a monotonic dekVersion; store ranks by explicit version; installEntityDek accepts strictly-newer / rejects ≤ (anti-rollback); extend the test to the real survivor installWrap→decrypt path. This also closes OQ-REV-6's stale-wrap-on-new-device.

Keeping this as a draft until ROT-3a-i lands. The owner-side test + orchestration wiring here are sound and stay.

…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>
@th3-br41n

Copy link
Copy Markdown
Contributor Author

✅ 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:

  • Member-wrap v2 carries the DEK rotation ordinal and binds it into the AEAD AAD — the ordinal is authenticated, so a relay can't replay/re-label an old wrap to a higher ordinal without breaking the tag. v1 wraps stay readable (v-dispatch).
  • EntityDekStore mints monotonic versions (maxVersion+1), exposes version on the handle, and getByEntityId ranks by version DESC — not receive time.
  • installEntityDek accepts a strictly-newer ordinal, no-ops on ≤: a rotated DEK upgrades a live survivor (F-ROT-1), a replayed pre-rotation wrap can't roll back (F-ROT-3), re-delivery stays idempotent. Version threaded through the share/cascade/rotate producers; create-time self-wraps stay v1 (ordinal 1).

Tests: member-wrap v2 round-trip + tamper-rejection; installEntityDek accept-newer/reject-rollback; and an end-to-end proof driving guest B's real installWrap→store path across a rotation (B upgrades to DEK′; replay rejected) — the coverage the owner-side-only test lacked. Full shell suite green (527 files / 5787 tests).

Still draft. The doc-73 gate needs a re-run /security-review on this diff + the /pentester two-shell real-relay dogfood before merge.

@th3-br41n

Copy link
Copy Markdown
Contributor Author

ROT-4 /security-review re-run — online path clean, one HIGH finding

Three independent adversarial lenses (crypto/AAD, rotation-ordering, auth-surface), each reading the real surrounding code. Full write-up: docs/_review/2026-07-09-rot-3a-i-security-review.md.

Clean (no findings ≥8): the versioned-wrap crypto (ordinal is AAD-authenticated; tampering breaks the tag; v1↔v2 downgrade fails; every consumer unwraps before trusting the version), the auth surface (no DEK′/wrap leakage to the revoked member or relay; parameterized SQL; DEKs zeroed everywhere; no auth bypass), and the version-primary ordering (replay/rollback rejected).

F-ROT-4 (HIGH, confidence 8) — blocks the "complete" claim: an offline revoke (or an online rotation that throws mid-publishWraps) never rotates the DEK, yet revoke() returns true, and the retry is structurally dead (revokeAccess returns false on a second call → if (revoked) skips rotation). No reconnect hook drains it. So the removed member keeps decrypting new content after reconnect — the exact thing rotation should prevent. Not a regression (revoke is policy-only today), but the feature isn't complete.

Fix → ROT-3a-ii: persist a durable rotation-pending marker whenever rotation can't complete; drain on relay-connect + boot; mint DEK′ locally even offline; don't report revoke() complete when deferred.

Staying draft until ROT-3a-ii lands, then the /pentester two-shell dogfood (online + offline-revoke-then-reconnect) at the 0.3.0 cut.

…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>
@th3-br41n th3-br41n marked this pull request as ready for review July 9, 2026 12:09
@th3-br41n

Copy link
Copy Markdown
Contributor Author

✅ ROT-3a-ii landed — F-ROT-4 closed. This PR is mergeable.

#rotateAfterRevoke now always runs its durable half online or off (mint DEK′ → re-wrap survivors → append wraps to the doc); only the wire delivery needs a relay. A durable pending_rotations table (entities.db v8) marks any entity whose delivery couldn't finish, and drainPendingRotations() — fired on relay-connect (on("state")) + session boot, self-guarded — re-derives the survivor wraps from the current DEK and finishes delivery. So a deferred rotation converges instead of being silently lost; revoke() no longer gives false assurance.

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 status

All 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 /pentester two-shell real-relay dogfood (revoked shell can't read a post-revoke edit — online + offline-revoke-then-reconnect) is a 0.3.0 release-cut gate, not a merge gate. Docs: harness PR #49.

@th3-br41n th3-br41n merged commit 26b0d59 into main Jul 9, 2026
2 of 3 checks passed
@th3-br41n th3-br41n deleted the feat/rot-3-live-wiring branch July 9, 2026 13:15
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.

1 participant