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.6 KiB
Layer 2 — Mode 2: Public Posts with FoF-Gated Comments
Scope: Extend CommentPolicy with a FriendsOfFriends variant. Post body is public (indexable, cacheable, shardable via existing CDN). Comments on the post must prove FoF-reachability to the author before being accepted.
This layer ships before Mode 1 because it reuses the existing public-post path and only adds a verification gate on comments. No new PostVisibility variant needed.
Goal
- An author creates a public post with
comment_policy = FriendsOfFriends. - Body is encrypted to no one — plaintext in the CDN, same as a normal public post today.
- Comments on the post include a proof artifact that lets readers (and the author in strict mode) verify the commenter is reachable through the author's FoF graph.
- Non-FoF readers can still READ the post. They cannot post an accepted comment.
Lead decisions
- Mode 2 does not gate readership. The post body is genuinely public. Only comments are gated. This is intentional — it preserves CDN shardability for the body and gives authors a lightweight "comments to my circle, body to the world" mode.
pub_postis included in the public post header. Every FoF-eligible post (both modes) has a per-post ephemeral ed25519 keypair.pub_postin the header,priv_postin wrap slots (Mode 1) or directly in a control record (Mode 2 — TBD below).- Comments carry a
group_sigsigned withpriv_post. Any device that can unwrappriv_post(i.e., holds a matchingV_x) can sign.group_sigverification againstpub_postis the cryptographic proof of FoF-reachability. Verifiable by any observer holding the public post. - Optional
vouch_macfor strict mode.HMAC(V_x, post_id || comment_hash)identifies which voucher's chain a commenter holds. Author can run in strict mode and reject comments whosevouch_macdoesn't match aV_xthe author has a record of distributing. Non-strict mode accepts any validgroup_sig. - Comment still signed by commenter's identity key as well.
group_sigproves FoF-membership; identity sig binds the comment to a specific persona for display / abuse reporting / author-side blocklist. - Non-FoF devices can still render the post. Read path is unchanged — comments missing
group_sig(or failing verification) are filtered out in the feed renderer rather than hard-rejected in storage. This leaves forensic traces and is cheaper than re-verifying in the renderer vs at ingest. TBD open question below.
Data model
Extend CommentPolicy
pub enum CommentPolicy {
Public,
FollowersOnly,
None,
FriendsOfFriends, // NEW — Layer 2
}
Post header additions (for posts with comment_policy = FriendsOfFriends)
struct PostHeader {
// ... existing fields ...
// NEW for Mode 2 comment gating:
pub_post: Option<[u8; 32]>, // ed25519 public key of per-post ephemeral keypair
wrap_slots: Vec<WrapSlot>, // wraps priv_post under each V_x in author's keyring
// TBD — OPUS: WrapSlot byte layout (same type as Layer 3 uses)
}
Note: Mode 2 posts carry wrap_slots even though the body is public, because commenters need priv_post to sign. The wrap slot set IS the FoF membership definition.
Extend InlineComment
struct InlineComment {
// ... existing fields ...
#[serde(default, skip_serializing_if = "Option::is_none")]
group_sig: Option<[u8; 64]>, // ed25519 signature over comment_hash, verifies against parent post's pub_post
#[serde(default, skip_serializing_if = "Option::is_none")]
vouch_mac: Option<[u8; 16]>, // HMAC(V_x, post_id || comment_hash), truncated
}
Back-compat: old comments without these fields are treated as "not FoF-signed" — accepted on non-FoF posts, rejected on FoF posts.
Comment creation (author of the comment)
- Commenter fetches parent post. Reads
pub_postfrom header. - Commenter iterates their keyring. For each
V_xheld, computesHMAC(V_x, post_id)[:2B]. Compares against eachWrapSlot.prefilter_tag. On match, trial-decrypts the slot to getpriv_post. - If any slot unwraps successfully: commenter now holds
priv_post. Signscomment_hashto producegroup_sig. Computesvouch_mac = HMAC(V_x_winner, post_id || comment_hash). Attaches both to comment. - Publishes comment through normal comment-propagation path.
If NO slot unwraps: commenter is not in the author's FoF set. UI reports "You can't comment on this post." Pure client-side enforcement — no wire attempt.
Comment verification (reader side)
At feed render time, for every comment on a FriendsOfFriends-policy post:
- If
group_sigis missing → filter out (strict) or show with "unverified" badge (permissive). Lead leaning: filter out. - Verify
group_sigagainst parent post'spub_postovercomment_hash. If fail → filter out. - Verify identity sig of comment as normal. If fail → filter out.
At author side (strict mode, optional):
- Recompute expected
vouch_maccandidates from the set ofV_xthe author has distributed. Ifcomment.vouch_macdoesn't match any → discard at ingest (don't store, don't propagate).
Propagation
No changes to comment propagation path. Comments still flow via existing BlobHeaderDiff / engagement-diff mechanism. Verification is done at display time (reader) and optionally at ingest (author-strict).
Open questions
- Filter-out vs reject-at-storage for bad
group_sig. Filter-out preserves forensic trail and avoids double-verify cost. Reject-at-storage saves disk and sync bandwidth. Lead leaning: filter-out at render, reject-at-storage only on author-side strict mode. priv_postdistribution for Mode 2. In Mode 1,priv_postis insidewrap_slots. In Mode 2, body is plaintext, but commenters still needpriv_post. Options: (A)wrap_slotsstill present in Mode 2 headers, carrying onlypriv_post; (B) separatecomment_priv_keycontrol record distributed out of band. Lead leaning: A (uniform structure with Mode 1).- Wrap slot prefilter tag. 2B = 65536 buckets, false-positive 1/65536. For Mode 2 this also defines the commenter-eligibility filter. TBD — OPUS: confirm 2B is enough for realistic keyring sizes.
- Author's own comments. Author has
priv_postdirectly (generated it). Do they self-vouch-mac againstV_me? Leaning: yes, so strict mode uniformly verifies all comments. - Displaying vouch path to the author. Mode 2 strict mode knows which
V_xa commenter arrived through. Should the author's UI show "comment arrived via your vouch to Alice" or keep it opaque? Lead leaning: opaque by default; optional power-user setting. - Rate-limiting / spam. A malicious FoF member can flood comments.
vouch_macidentifies the chain, so author can block a chain. Out-of-scope for Layer 2 ship (tracked separately).
Ship criteria for Layer 2
CommentPolicy::FriendsOfFriendsvariant exists end-to-end (storage, protocol, UI picker).- Authors can create public posts with FoF-gated comments.
pub_post/priv_post/wrap_slotsgenerated on post creation, wrapped under author's full keyring.- Commenters: client-side check of FoF eligibility before offering comment box;
group_sig+vouch_macattached on send. - Readers: filter-out comments failing
group_sigverification. - Author strict-mode: optional ingress rejection on unknown
vouch_mac. - Back-compat: old clients see FoF posts as readable (body is public) but can't comment (missing
priv_post); old comments on new posts are filtered at render. - Integration test: 3-node FoF chain (A→B→C). A posts Mode 2. B comments (reachable). C comments (reachable via B). D (unrelated) cannot.