feat: content-key rotation on member removal (ADR-037 §D3 Phase 3c, #152) - #164
Merged
Conversation
…removal (ADR-037 §D3, #152) The kb.rs authoring core for Phase 3c. On removing a member from an E2e KB, the owner authors ONE combined collection delta: a signed `Remove` of the departed member (+ member_roles mirror with #72 epoch tombstoning), then one owner-authored wrap-only `Admit` per REMAINING member carrying a freshly-wrapped new key. Design: - Re-key ops re-assert each member's CURRENT derived role/can_invite/epoch verbatim (a re-admit overwrites the derived entry — "later re-admit wins", membership.rs:855 — so preserving them avoids a silent downgrade). Attributes come from the authoritative op-log derivation, not the legacy mirror. - Epoch is NOT bumped: re-keying must not force the remaining members to rebase. The removed member is dropped from derived membership, so their stale lineage is refused regardless of epoch (the fence, #157). - The removed member receives no new wrapped op → find_wrapped_content_key returns only their OLD key. They decrypt pre-rotation content but no post-rotation ciphertext. - Daemon stays key-blind: it relays the owner-signed delta via kb/collection_op. Adversarial test (the §D3 SELECTIVE oracle): 3 members share k; remove B with fresh k'. Asserts the two remaining converge on k', the removed B keeps ONLY the old k (not k', not nothing — proving k' is denied specifically, not B's pipeline severed), B is dropped from membership while owner+C retain unchanged role/can_invite/epoch (the clobber guard), a pre-rotation replica applying the relayed delta agrees on all points, and the Remove op carries a verifiable owner signature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ADR-037 §D3, #152) Wires the editor side of Phase 3c onto author_rotate_on_remove. Removing a member from an E2e KB (one with a registered content key) now ROTATES the key instead of issuing a plain daemon-authored kb/remove_member: - The KbRemoveMember intent carries the main thread's cached collection replica (mirrors KbApprove / KbSetEncryption), so the network task — which holds the identity secret + the content key — can author the rotation. - The network task: derives the current members from the op-log, generates a fresh k', wraps it once per REMAINING member (the owner re-keys itself via its own pubkey; others via the pubkey stored on admit, ADR-038), authors the combined signed Remove + per-member re-key delta, ships it KEY-BLIND via kb/collection_op, then persists k' and registers it so the owner's next edits seal under it. - Members with no stored pubkey are skipped with a loud warn (they keep the OLD key until a re-share re-wraps them) — no silent coverage gap (principle #14). - Adds and non-E2e removes fall through to the legacy kb/add_member / kb/remove_member path unchanged. The daemon stays key-blind and re-derives membership (the signed Remove drops the member) from the relayed delta. Data-layer convergence + the §D3 security oracle (removed member stranded on the old key; remaining members converge on k') are covered by the author_rotate_on_remove adversarial test (a9c30e3); the full wired daemon e2e lands in 3d (#153, docker). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #152. Part of #131 (ADR-037 E2E). Builds on 3b (#160/#161).
What
Removing a member from an E2e KB now rotates the content key (§D3 eager re-wrap) instead of a plain
daemon-authored remove. The owner authors ONE combined, signed collection delta — a
Removeof the departedmember + a fresh-key re-wrap to every remaining member — and ships it key-blind via
kb/collection_op.The removed member gets no new wrap, so it keeps only the old key and cannot read post-rotation content.
Commits
a9c30e30(sync) —KbCollectionDoc::author_rotate_on_remove: the authoring core. Re-key ops re-asserteach member's current derived role/can_invite/epoch verbatim (a re-admit overwrites the entry — "later
re-admit wins" — so preserving them avoids a silent downgrade) and do not bump the epoch (no forced
rebase; the removed member is dropped from membership anyway).
bb68f088(collab) — editor wiring: the remove path derives members, generatesk', wraps once perremaining member (owner via its own key; others via the pubkey stored on admit), authors the delta, ships
it key-blind, persists
k'. Adds + non-E2e removes fall through unchanged. Members with no stored pubkeyare skipped with a loud warn (no silent gap).
Tests (principle #14 — adversarial, the §D3 SELECTIVE oracle)
rotate_on_remove_rekeys_remaining_members_and_strands_the_removed_one: 3 members sharek; remove B with afresh
k'. Asserts —k';k(notk', not nothing) — provingk'is denied specifically,not that B's pipeline was severed;
clobber guard) and C's epoch is not bumped;
Removeop carries a verifiable owner signature.The removed member's write-denial follows by composition (they become a non-member →
verify_content_oprejects, existing behavior). The full wired daemon e2e (two editors + daemon, docker) lands in 3d (#153).
Scope / safety
make ci-allshould stay green.release:none— no version bump (dev velocity; the deliverable bump comes when 3c/3d close the story).🤖 Generated with Claude Code