Drafts the Friend-of-Friend post-gating spec with crypto specifics marked TBD — OPUS for Opus to fill in. Six-layer implementation plan; each layer independently shippable. Includes README overview + six layer files: - Layer 1: V_me vouch primitive (keys, keyring, VouchGrant wire format) - Layer 2: Mode 2 — public post + FoF-gated comments - Layer 3: Mode 1 — FoFClosed (encrypted body via wrap_slots + prefilter) - Layer 4: per-post keypair rotation - Layer 5: unlock cache + prefilter optimization (perf-critical) - Layer 6: revocation (stub; likely deferred post-v1) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.2 KiB
Layer 4 — Per-Post Keypair Rotation
Scope: Graceful rotation of (priv_post, pub_post) when the author's FoF set changes (new vouches granted, V_me rotated, or a vouchee removed). Old comments remain verifiable under the old pub_post; new comments require the new pub_post.
Goal
- Author can update the FoF set of an existing post without deleting / recreating it.
- A
PostKeyRotationrecord, signed by author identity key, carries a new(priv_post', pub_post')wrapped under the current keyring. - Existing comments under old
pub_poststay cryptographically valid. - New comments must sign under
priv_post'. - Readers who were admitted under the OLD set but not the NEW one retain read access to the body (CEK didn't change) but can no longer produce accepted comments.
Lead decisions
- Body CEK is NOT rotated. Only the signing keypair rotates. Rotation's purpose is to narrow (or widen) the commenter set going forward, not revoke read access to the body. Read access of already-distributed content is non-recoverable by design — if an author needs that, they delete the post.
- Rotation is append-only. Rotation records accumulate;
pub_post_nis derived from the latest rotation record. Oldpub_postvalues are retained for verifying already-posted comments. - Rotation is optional. Simple case is a post with one immutable
pub_post. Layer 4 adds the escape hatch; most posts never rotate. - Author-signed. Only the post author (identity key) can rotate. Prevents an admitted commenter from rotating others out.
Data model
PostKeyRotation record
struct PostKeyRotation {
post_id: PostId,
rotation_index: u32, // monotonic; 0 = original keypair (implicit), 1 = first rotation, etc.
new_pub_post: [u8; 32],
new_wrap_slots: Vec<WrapSlot>, // wraps new priv_post under current keyring (same as post creation)
superseded_at: u64, // ms; rotation timestamp
sig: [u8; 64], // author identity-key signature over the above
}
Persisted as a sidecar to the post. TBD — OPUS: whether this lives in its own table (post_key_rotations) or as a serialized column on the post.
Comment verification after rotation
Each comment's group_sig is verified against a specific pub_post. Determination rule:
- If
comment.created_at < rotation_1.superseded_at→ verify againstpub_post_0(original). - If
rotation_n.superseded_at ≤ comment.created_at < rotation_{n+1}.superseded_at→ verify againstrotation_n.new_pub_post. - If
comment.created_at ≥ latest_rotation.superseded_at→ verify against latestnew_pub_post.
TBD — OPUS: time-based bucketing requires trusting comment.created_at. Alternative: comment carries an explicit pub_post_index field pointing at which keypair generation it's signed under. Lead leaning: explicit index field in comment, avoids clock-skew ambiguity.
Extend InlineComment (from Layer 2)
struct InlineComment {
// ... existing fields ...
#[serde(default)]
pub_post_index: u32, // 0 for original keypair, n for rotation n
group_sig: ...,
vouch_mac: ...,
}
Rotation flow (author side)
- Author changes FoF-relevant state (new vouch granted, someone un-vouched,
V_merotated). - Author decides to re-gate a specific post's comments: UI action "rotate comment keys for this post."
- Generate new
(priv_post', pub_post'). - Re-wrap
priv_post'under the author's CURRENT keyring (the same algorithm as initial post creation, Layer 3). - Build
PostKeyRotationrecord, sign, publish. - Rotation record propagates via normal CDN (it's a diff on the post, same mechanism as engagement diffs).
Reader/commenter side
- On receiving a
PostKeyRotationrecord, readers store it keyed by(post_id, rotation_index). - At comment-creation time: look up the latest rotation record for the parent post; trial-decrypt the new slots; if success, use
priv_post'to sign, setpub_post_indexto latest index. - At comment-verification time: look up the rotation referenced by
comment.pub_post_index; verify against thatpub_post.
If a reader can unwrap rotation_n.wrap_slots but NOT rotation_{n+1}.wrap_slots, they've been rotated out between n and n+1. They can still READ the body (CEK unchanged) and verify historical comments; they cannot author new comments after rotation n+1.
Propagation
Rotation records are signed post-deltas. They reuse the existing BlobHeaderDiff propagation mechanism — the post header gains the rotation's pub_post and wrap_slots; readers pull updated posts through the normal CDN.
TBD — OPUS: whether rotation records are part of the post's BlobHeader diff (natural fit) or a separate post-referencing record (cleaner separation but more protocol surface).
Edge cases
- Multiple rotations in short succession. Monotonic index + explicit
pub_post_indexon comments disambiguates. No clock dependency. - Comment authored against stale rotation. If a commenter creates a comment under rotation n, but by the time it propagates, rotation n+1 exists — the comment is still valid against rotation n. Readers verify against the rotation the comment declares.
- Attacker forges rotation. Rotation is signed by author identity key. Forgery == identity key compromise, which is outside FoF scope.
- Reader never sees rotation record but sees new comments. Until the rotation record arrives, new comments appear "unverified." Filter-out at render leaves them as pending until rotation arrives. Standard eventually-consistent behavior.
Open questions
- Rotation on
V_merotation. When a persona rotates their ownV_me, do we auto-rotate every one of their open FoFClosed posts? Cost is O(posts × eligible_keys). Lead leaning: no auto-rotation; user opts in per-post. Posts without rotation continue to accept comments from the old keyring (which still holds the oldV_me). - Garbage-collecting old rotation records. They're needed to verify historical comments. Never GC'd? Or keep only most recent N? Lead leaning: keep all; historical comments don't re-verify often and the data is cheap.
- UI for rotation. "Update who can comment" button on post. Simple. No scheduling / batch rotation UI in v1.
- Rotation without keyring change. Could be used to kick out a specific commenter by rotating and manually excluding their winning
V_x. But winningV_xisn't known to the author (wrap slots are anonymous). Practical effect: authors can widen or narrow the whole set, not surgically exclude one person, without additional support.
Ship criteria for Layer 4
PostKeyRotationrecord type exists end-to-end.- Author UI action: "Update who can comment on this post."
- Rotation records propagate via CDN.
- Comment signing uses latest rotation's
priv_post;pub_post_indexattached. - Comment verification routes to the correct
pub_postvia index. - Back-compat: posts without rotation records are handled as
pub_post_index = 0uniformly. - Integration test: A posts FoFClosed. B comments (admitted). A vouches for C, rotates. C comments (admitted). Old B-comment still verifies; new B-comment still verifies (B still in keyring); new D-comment (never admitted) rejected.