Captures the decisions from the Layer 4 conversation with Scott: Default narrowing on a single post = Layer 2 revocation (existing). Advanced narrowing of read access = full re-issue with optional supersedes_post_id link (network-heavy, opt-in). V_me rotation = the persona-wide revocation primitive. Generate new V_me, distribute to non-revoked vouchees via next bio-post batch. Receiver-chain model: receivers append new V_me alongside old (not overwrite). Trial-unwrap iterates the chain. Grandfather by default: CDN is V_me-blind, so rotation does NOT auto-cascade comment deletions. Revoked vouchee retains comment authority on old posts unless author opts to cascade per-pub_x revocations. Per-post cascade is opt-in. Local-only own_post_slot_provenance table lets author query "which pub_x's in my posts were sealed under V_me_old?" and publish per-pub_x RevocationEntries. New optional KeyBurnDiff primitive (signed header-diff) swaps a V_me_old wrap_slot for a V_me_new one in-place on a specific post. For the leaked-V_me scenario. Body CEK unchanged. Skeleton's PostKeyRotation record removed entirely. Layer 1 updated: rotation is append-only at receivers; pointer to Layer 4. Multi-epoch bio-post-batch toggle hook added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Layer 4 — Rotation, Revocation, and Key Lifecycle
Scope: How an author narrows access on a published post, and how a persona rotates their own V_me. Most of the mechanism already exists in Layers 1–2. Layer 4's job is to lock in the policy and add the local-only provenance table that makes selective cascades possible.
Goal
- An author can narrow comment authority on a published post via Layer 2 revocation (default, cheap).
- An author can narrow read access by republishing the post with a new key and narrower wrap_slots (advanced, network-heavy, opt-in per-post).
- A persona rotates
V_meto remove a vouchee. The newV_meis issued to all non-revoked vouchees via the next bio-post wrapper batch. The revoked vouchee retains the oldV_me. - Old posts (sealed under any era of
V_me) stay readable by their original audience — no automatic rewrite. The CDN does not auto-cascade comment deletions on rotation. - Authors retain per-post discretion to cascade comment revocations onto old posts when they want to, by publishing per-pub_x revocation diffs against pub_x's they know were sealed under the old
V_me. - An optional key-burn primitive lets an author scrub a specific
V_mefrom a specific post's wrap_slots in-place, for the narrow case of a leakedV_me.
Lead decisions
- Default = Layer 2 revocation. Narrowing comment authority on a single published post: author signs a
RevocationEntryfor a specificpub_x, propagation nodes delete locally-stored comments by that signer, remove the entry frompub_post_set, and forward. No new wire primitive. - Advanced = full re-issue. Author publishes a fresh post with new
(CEK, pub_post_set, wrap_slots)under a narrower V_x set, optionally with asupersedes_post_idlink for engagement continuity. Old post stays at its oldpost_idor is locally deleted. Heavy: re-encrypts body, rebuilds engagement context, costs bandwidth. Used only when narrowing READ access matters more than retaining the post in its current form. V_merotation IS the persona-wide revocation primitive. No separate "kill V_me" wire message. To remove a vouchee, the persona generates a newV_me_newand distributes it to every current vouchee except the revoked one via the next bio-post batch (Layer 1 mechanism, unchanged). The revoked vouchee's only key remainsV_me_old.- Receiver keeps the chain (Option A). When a persona receives
V_B_newfrom Party B, they append it tovouch_keys_receivedrather than overwrite. They now hold{V_B_old, V_B_new}(and any earlier epochs). On any wrap_slot unwrap attempt, the client trials every epoch in the chain. UX: the "current" key for outgoing operations is the latest received; older epochs are archived but kept for reading historical content. - Old posts are grandfathered by default.
V_merotation does NOT automatically trigger CDN comment deletion. CDN's revocation primitive operates onpub_x, notV_me— and CDN is V_me-blind. After rotation, the revoked vouchee retains the oldV_me, retains read access to posts with V_me_old slots, and retains comment authority on those posts unless the author explicitly publishes per-pub_x revocations. - Per-post cascade is opt-in by the author. The author can choose to cascade a V_me rotation onto a specific old post by publishing a
RevocationEntryfor the pub_x's that were sealed under the old V_me. The author knows this mapping locally (seeown_post_slot_provenancebelow); the CDN does not. Cascade can be all posts (batch action) or a chosen subset. - Key burn is optional, narrow scope. For the case where
V_me_oldhas leaked (not just rotated for turnover), the author may publish a signed header-diff on a specific post that swaps the V_old wrap_slot for a V_new wrap_slot in-place. Same propagation primitive as revocation and access-grant. Scrubs V_old from the CDN copy of that post. Future observers acquiring leaked V_old can no longer fresh-decrypt the body. Locally-cached plaintext on existing readers' devices is unrecoverable by any wire mechanism (out of scope). vouch_keys_ownretains multi-epoch rows. OldV_meepochs are never deleted automatically. May be deleted by explicit user action with a prominent warning ("this prevents future re-keys / cascades on any post sealed under this epoch").
What goes away from the original skeleton
The skeleton's PostKeyRotation record (with rotation_index, pub_post_index on comments, time-bucketed signature verification) is removed entirely. Its job is done by:
- Layer 2's
RevocationEntryfor the comment-narrowing case. - Standard post-publish + optional
supersedes_post_idfor the re-issue case. - In-place wrap_slot swap as a new, optional key-burn diff (below).
What goes away:
PostKeyRotationrecord type.rotation_index/pub_post_indexfield on comments.- Time-bucketed cross-rotation signature verification.
What stays:
RevocationEntryfrom Layer 2.- Standard post-publish path.
vouch_keys_ownandvouch_keys_receivedfrom Layer 1, both with multi-epoch retention.
Data model additions
own_post_slot_provenance (local only, never on wire)
The author needs to know which slot in each of their posts was sealed under which V_x, so they can selectively cascade a V_me rotation onto old posts. This is author-local state, not transmitted.
own_post_slot_provenance(
author_persona_id BLOB,
post_id BLOB,
slot_index INTEGER, -- index into pub_post_set / wrap_slots
sealed_under_v_x_owner BLOB, -- which persona issued the V_x used (== author_persona_id for the author's own V_me slots)
sealed_under_v_x_epoch INTEGER,
pub_x BLOB(32), -- the pub_x in the post's pub_post_set for this slot
PRIMARY KEY (author_persona_id, post_id, slot_index)
)
Populated at post-publish time. Used at cascade time: SELECT pub_x FROM own_post_slot_provenance WHERE author_persona_id = ? AND sealed_under_v_x_owner = ? AND sealed_under_v_x_epoch = ? returns the pub_x list to revoke.
Optional supersedes_post_id field on PostHeader
struct PostHeader {
// ... existing fields ...
#[serde(default, skip_serializing_if = "Option::is_none")]
supersedes_post_id: Option<PostId>,
}
Used by the advanced re-issue path. Author identity sig covers it. Readers may display "this is a re-issued version of an earlier post."
KeyBurnDiff (header-diff type)
struct KeyBurnDiff {
post_id: PostId,
slot_index: u32, // which slot to swap
new_wrap_slot: WrapSlot, // sealed under V_x_new (typically author's V_me_new)
new_pub_x: [u8; 32], // corresponding pub_x for pub_post_set replacement
sealed_at_ms: u64,
author_sig: [u8; 64], // author identity-key sig over the above + parent_post_id
}
Propagation: same path as revocation diffs. File-holders apply by replacing wrap_slots[slot_index] and pub_post_set[slot_index] in their local copy of the post header. Forward to neighbors. Idempotent (re-applying with the same slot_index + same new pub_x is a no-op).
Author UX surfaces
- "Remove this commenter from this post" →
RevocationEntryfor that specificpub_x. Standard Layer 2 path. - "Rotate my vouch key" (Settings) → generates a new
V_meepoch invouch_keys_own, sets current. Old epoch retained. Then offers: "Issue the new key to your existing vouchees?" → if yes, queues a fresh bio-post batch wrapping V_me_new for all current targets except those marked revoked. Standard Layer 1 mechanism. - "Cascade this rotation onto my old posts" (offered after a rotation if it was triggered by a revoke action) → batch action that queries
own_post_slot_provenancefor the rotated-out epoch and publishes per-pub_x revocations on each affected post. Costs N RevocationEntries; can be done in background. Optional. Per-post selection allowed. - "Re-issue this post with narrower access" (advanced) → opens compose with body pre-filled and
supersedes_post_idset. New audience pickable. Old post optionally deleted on publish. - "Burn this leaked key from a post" (rare) → publishes a
KeyBurnDiffswapping the V_me_old slot for a V_me_new slot on a specific post. Offered when the user marksV_me_oldas leaked. Can be batched across all the author's posts via the provenance table.
Cascade decision tree (for the author)
| Scenario | Default action | Optional escalation |
|---|---|---|
| Vouchee unfollowed, casual cleanup | Rotate V_me. Old posts grandfathered. | None. |
| Vouchee misbehaving on one specific post | Layer 2 revocation on that post's pub_x. |
None. |
| Vouchee misbehaving across many posts | Rotate V_me. Cascade revocations onto every post they ever had access to. | Optional follow-up: key-burn if their continued read access is unacceptable. |
| V_me_old leaked | Rotate V_me. Cascade revocations onto all affected posts. | Key-burn V_me_old slots on every affected post. |
Open questions
- Bio-post batch contents during rotation. Default: wrap only the new V_me epoch in the next batch. Advanced UI option: wrap multiple epochs for vouchees who lost their device and need to re-bootstrap. Lead leaning: default to new-only; advanced toggle for multi-epoch.
supersedes_post_idbinding strength. The post-headerauthor_sigalready covers the field. Sufficient, or do we want a reciprocal "this post has been superseded" diff on the old post for symmetric discoverability? Lead leaning: one-way link is sufficient; old post being deleted or not is independent.- Key-burn vs. body re-encryption. Key-burn swaps wrap_slots but keeps the body ciphertext (still encrypted under the same CEK). A reader who unwrapped via V_me_old still has CEK cached locally; their local plaintext copy is unaffected. Key-burn only prevents fresh decryption of the wire ciphertext. Is that sufficient, or should key-burn also imply CEK rotation? CEK rotation = re-encrypting the body = essentially full re-issue. Lead leaning: key-burn does NOT rotate CEK; it's specifically the "scrub V_old from headers" primitive. Full re-issue remains available if CEK rotation is wanted.
own_post_slot_provenanceexport. Lost on device wipe means the author can't cascade-revoke after a fresh install. Lead leaning: include in identity export bundle.- Garbage-collecting
vouch_keys_ownancient epochs. Never auto-GC. User-explicit only, with warning.
Ship criteria for Layer 4
vouch_keys_ownretains multi-epoch rows without auto-deletion on rotation.vouch_keys_receivedretains multi-epoch rows; trial-unwrap iterates the chain per voucher.own_post_slot_provenancetable populated at every post-publish.- Author UI: "Rotate my vouch key" with optional follow-up "Issue to existing vouchees."
- Author UI: "Cascade revocations onto my old posts" as a post-rotation action.
- Author UI: "Re-issue this post with narrower access" (advanced).
- Author UI: "Burn leaked key" as a rare/explicit action.
KeyBurnDiffpropagation: same path as revocation diffs; idempotent application.supersedes_post_idfield onPostHeaderis wire-defined and back-compat (default None).- No
PostKeyRotationrecord exists. - Integration test: A vouches for B and C. A posts FoF post P sealed under V_a (and other V_x's). B and C read + comment. A rotates V_a → V_a' to remove C; issues V_a' to B only. C still holds V_a; A's new posts (sealed only under V_a') invisible to C, visible to B. C can still read P (V_a-sealed slot still in P's wrap_slots); A optionally cascades a revocation on P that removes C's pub_x and deletes C's comments on P. A optionally key-burns V_a from P, swapping the V_a slot for a V_a' slot — C can no longer fresh-decrypt P from the wire (already-cached plaintext on C's device unaffected, out of scope).