feat: E2E member key delivery — wrap-on-approve (ADR-037/038, #151 Phase 3b PR B) - #161
Merged
Conversation
…on-admit (ADR-038, #151 PR B) To wrap the content key to a member on approval the owner needs their Ed25519 PUBLIC key, but it holds only the fingerprint (a one-way hash). The daemon — which has the joiner's pubkey from the authenticated session — now records it in the pending request, which rides the kbc: broadcast the owner already mirrors. - `PendingRequest.pubkey: Option<[u8;32]>` + `PENDING_PUBKEY_KEY`; `add_pending` takes + writes it (hex), `pending()` reads it back. Backward-compatible: absent ⇒ None (a v1 pending record is unaffected). - Daemon `kb/join`: capture `session.peer_identity.pubkey` (threaded through handle_doc_request_inner as `auth_pubkey`) and pass it to `add_pending`. Test: a pending record round-trips the joiner's pubkey; a pubkey-less record reads back None. Both workspaces compile + clippy clean; the non-daemon callers pass None. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bkey storage (ADR-037/038, #151 PR B) The owner-side authoring for delivering the content key to a member: author a signed Admit op carrying the key wrapped to the member, mirror them into member_roles (role + epoch + their pubkey), all in one combined collection delta. The op's epoch == the member_roles epoch (the dual-write keeps the ADR-023 fence consistent for the hub flow). The member's pubkey is persisted (MEMBER_PUBKEY_KEY) so 3c can re-wrap on rotation; `member_pubkey()` reads it back. Adversarial test: after enable + admit, the member recovers the SAME content key (derive_content_key) on both the owner's and a peer's replica; the member's pubkey is stored; the signed Admit's epoch equals the member_roles epoch; a non-member recovers nothing. The editor-side KbApprove rewiring (route E2e approves through this + the pending pubkey) lands next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…key on E2e KB (ADR-037/038, #151 PR B) Routes `kb-approve` through the owner-authoring path for an E2e KB: the network task wraps the content key to the approved member (whose pubkey rode the pending request — ADR-038), authors a signed Admit via `KbCollectionDoc::author_member_admit` (dual-write op-log + member_roles + pubkey), and ships it via the key-blind `kb/collection_op`. The member then recovers the key from the relayed op-log. `CollabCommand::KbApprove` now carries the main thread's `collection_state` (the intent mapping attaches it). Unencrypted KBs (or, defensively, an E2e KB whose pending request lacks a pubkey) fall back to the legacy daemon `kb/approve_member`. Builds on the tested foundations (author_member_admit, PendingRequest.pubkey). Editor compiles + clippy/fmt clean. Deferred-wrap for bare-fingerprint add (KbMember) + the end-to-end approve test land next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inner test helpers The PendingRequest.pubkey commit added an `auth_pubkey` param to `handle_doc_request_inner`; the two daemon test dispatch helpers still passed 9 args, so the daemon lib-test crate didn't compile (the "daemon / check + test" + "Server-Client Integration" CI failures). Pass None (these helpers don't exercise key-wrapping). 215 daemon tests green. 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.
Phase 3b PR B — member key delivery (wrap-on-approve)
Completes the multi-user half of E2E KB sharing (#131/#151): a joining member now receives the per-KB content key, so the full enable → join → approve → decrypt flow works end to end, daemon key-blind throughout. Stacks on PR A (#160, merged).
The flow
kb-join; the daemon records their Ed25519 pubkey in the pending request (from the authenticated session) — it rides thekbc:broadcast the owner mirrors.kb-approve; the network task wraps the content key to that member's pubkey, authors a signedAdmitcarrying thewrapped_key(viaKbCollectionDoc::author_member_admit), and ships it via the key-blindkb/collection_op.Changes
PendingRequest.pubkey+PENDING_PUBKEY_KEY; daemonkb/joincapturessession.peer_identity.pubkey(threaded throughhandle_doc_request_innerasauth_pubkey). Backward-compatible (absent ⇒ None).KbCollectionDoc::author_member_admit— signed Admit + key wrapped to the member +member_rolesdual-write (role + epoch + the member's pubkey, stored for 3c re-wrap), in one combined delta. The op's epoch == themember_rolesepoch (keeps the ADR-023 fence consistent).member_pubkey()reader.CollabCommand::KbApprovecarriescollection_state; the send arm routes an E2e approve through the above (legacykb/approve_memberfallback for unencrypted KBs / a pubkey-less pending record).Tests (adversarial, Principle #14)
A pending record round-trips the joiner's pubkey (absent ⇒ None); after enable + admit, the member recovers the same content key on both the owner's and a peer's replica, their pubkey is stored, the signed Admit's epoch equals the
member_rolesepoch, and a non-member recovers nothing. 242 mae-sync tests green; editor + daemon compile + clippy clean.Follow-up
Direct add-by-bare-fingerprint (
kb-add-memberwith no prior join) has no pubkey to wrap; for an E2e KB that member is keyless until a re-wrap. The deferred-wrap (keyless Admit + a later wrap-only op, latest-wins) is the noted next step; the default invite→approve policy is fully supported here. The end-to-end approve test lands with 3d's docker e2e.🤖 Generated with Claude Code