Replace single per-post priv_post with per-V_x (pub_x, priv_x). Post header publishes pub_post_set; comments declare pub_x_index; CDN propagation nodes verify group_sig + identity_sig against named pubkey before forwarding. Kills bandwidth-amplification DoS from admitted-but- malicious FoF members. Dual-derivation wrap slot (read → CEK, sign → priv_x) with shared structure Layer 3 inherits. Comments encrypted under CEK_comments so Mode 2 comments are genuinely FoF-read-gated, not just FoF-sign-filtered. Author-signed revocation diff appended to post; CDN honors per-chain revocation. Tradeoff: pub_x_index is a per-post voucher-chain pseudonym, re-randomized across posts. Accepted. Layer 3 banner added noting wrap-slot structure is now superseded by Layer 2's canonical form; full reconciliation deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
8.4 KiB
Markdown
98 lines
8.4 KiB
Markdown
# Friend-of-Friend (FoF) Gating — Implementation Spec
|
||
|
||
**Status**: Skeleton drafted 2026-04-23 by Scott + Lead Claude. Crypto-level byte layouts and algorithm specifics are intentionally left as `TBD — OPUS` markers for Opus to fill in. See each layer file for scope.
|
||
|
||
---
|
||
|
||
## What this spec covers
|
||
|
||
FoF-gating is a new post-visibility mechanism that gates readership and commenting by **social reachability** rather than explicit membership lists. It complements, rather than replaces, the existing primitives (`Public`, per-recipient `Encrypted`, per-circle `GroupEncrypted`).
|
||
|
||
The central primitive is the **per-person vouch key** `V_me` — a symmetric key each persona owns, handed to everyone they vouch for. Cryptographic reachability for an author's FoF post emerges from the union of every `V_x` that author holds (own + received-from-vouchers), and every reader whose keyring intersects that set.
|
||
|
||
### User-facing model
|
||
|
||
Authors pick a post visibility from four levels:
|
||
|
||
| Level | Wrap slots include | Cryptographic reach |
|
||
|---|---|---|
|
||
| **Public** | (none) | All readers |
|
||
| **Friends-only** | `V_me` only | My vouchees |
|
||
| **Friends-of-Friends** | `V_me` + every `V_x` I hold | My vouchees + every vouchee of anyone who vouched for me (emergent FoF) |
|
||
| **Custom** | Author-selected subset of held `V_x` | Union of reach through each included key |
|
||
|
||
No centrally-computed membership list. Reach is a function of the wrap-slot set and which keys each reader holds.
|
||
|
||
### Key design properties
|
||
|
||
- **Unilateral**: vouching is a one-way act (Alice hands out `V_alice`). No bilateral handshake required for the FoF graph to form.
|
||
- **Graph-private**: wrap slots carry no recipient IDs. Observers cannot enumerate "who can read this post."
|
||
- **Anonymous prefilter**: each wrap slot carries a 2-byte `HMAC(V_x, post_id)[:2B]` tag. Readers precompute tags for keys in their ring and skip non-matching slots, cutting trial-decrypt cost ~65000× per post.
|
||
- **Slot count padding**: author pads wrap slots to power-of-2 buckets to blunt vouch-count leak.
|
||
- **Per-post keypair**: each FoF post generates a fresh `(priv_post, pub_post)`. Leak of one post's `priv_post` bounds blast radius to that post's comments; doesn't expose other posts or the author's identity key.
|
||
|
||
### Two modes
|
||
|
||
- **Mode 1 — `FOF_CLOSED`**: post body encrypted, comments encrypted. FoF-only readership + writership.
|
||
- **Mode 2 — `PUBLIC_FOF_COMMENT`**: post body public (indexable, cacheable, shardable), comments can be public OR FoF-encrypted per-comment.
|
||
|
||
---
|
||
|
||
## Layering / implementation order
|
||
|
||
Build and ship bottom-up. Each layer is independently shippable and exercised before moving to the next.
|
||
|
||
1. **[Layer 1](layer-1-vouch-primitive.md) — Vouch primitive.** `V_x` keys, per-persona keyring storage, epoch tag, HPKE-sealed anonymous-wrapper distribution via the voucher's bio post, scan-on-follow + scan cache, minimal UI. No FoF-gated posts yet.
|
||
2. **[Layer 2](layer-2-mode2-fof-comments.md) — Mode 2: public posts with FoF-gated comments.** Easier implementation path; reuses existing public-post CDN path; extends `CommentPolicy` with a new `GroupMembersOfFoF` variant.
|
||
3. **[Layer 3](layer-3-mode1-fof-closed.md) — Mode 1: `FOF_CLOSED` posts.** New `PostVisibility::FoFClosed` variant. Wrap slots, anonymous prefilter tag. Receive-path integration.
|
||
4. **[Layer 4](layer-4-keypair-rotation.md) — Per-post keypair rotation.** Graceful `(priv_post', pub_post')` rotation record re-wrapped to current FoF set. Old comments still verifiable under old `pub_post`; new comments require new.
|
||
5. **[Layer 5](layer-5-prefilter-and-cache.md) — Unlock cache + prefilter optimization.** Author-direct fast path, winning-V_x-per-author cache, unreadable-posts retry table, re-try-on-new-V_x trigger. Performance-critical at realistic keyring sizes (400–500 keys × 400–500 slots).
|
||
6. **[Layer 6](layer-6-revocation.md) — Revocation & rotation cascades.** Deferred; may not be in v1. Drafted as a stub for design review.
|
||
|
||
---
|
||
|
||
## Intentionally out of scope (v1)
|
||
|
||
- **First-contact DM gating.** Deferred; Scott has a separate approach to sketch later.
|
||
- **Spam / abuse mitigation beyond vouch_mac.** Rate limits, PoW, reputation, reporting — tracked as separate work.
|
||
- **Intra-circle anonymity.** Ring signatures over the vouch set so even other FoF members cannot tell which voucher's chain a comment arrived through. v2.
|
||
- **Zero-knowledge audience gating.** v2.
|
||
- **PQ migration** of `priv_post` / identity sigs. The spec shape above is algorithm-agnostic; PQ (ML-DSA ~1.3KB sigs) swaps in without structural change. Tracked separately.
|
||
|
||
---
|
||
|
||
## Glossary
|
||
|
||
- **Persona**: a user's posting identity. A device may hold multiple. Each persona has its own `V_me` and its own received-vouches keyring.
|
||
- **Vouch**: a unilateral act by persona P1 declaring trust in persona P2. P1 gives P2 a copy of `V_P1`.
|
||
- **Vouch key (`V_x`)**: a symmetric key owned by persona `x`, distributed by `x` to everyone `x` vouches for. `V_me` refers to the current persona's own vouch key.
|
||
- **Keyring**: for a given persona, the set of vouch keys held = `{V_me}` ∪ `{V_x : x vouched for me}`.
|
||
- **Wrap slot**: an anonymous ciphertext in a post header, carrying the post's private material encrypted under some `V_x`. Readers trial-decrypt slots whose 2-byte prefilter tag matches an owned key.
|
||
- **pub_x / priv_x**: per-`V_x` ed25519 keypair (Layer 2). `priv_x` is wrapped in the sign-slot of each `V_x`; `pub_x` is published in the post's `pub_post_set`. Comments reference a `pub_x_index` so propagation nodes can verify the comment signature without unwrapping. Replaces the earlier single `pub_post/priv_post` per-post keypair.
|
||
- **pub_post_set**: list of all `pub_x` for a post's FoF set, in randomized order. Inline in post header. Comments reference entries by index.
|
||
- **revocation_list**: author-appended signed entries that tell CDN propagation nodes to drop comments under a named `pub_x`. Stops compromised voucher-chains at the propagation layer.
|
||
- **Identity key**: persona's long-term ed25519 key, used to sign content as that persona. Distinct from `priv_post`. (In current ItsGoin terms: this is the persona's posting key. Worth naming-alignment review when specifying.)
|
||
- **Vouch MAC**: `HMAC(V_x, post_id || comment_hash)` — 16B truncated. Identifies which `V_x` a commenter holds. Inside encrypted payload (Mode 1) or alongside plaintext (Mode 2). Used by author strict-mode to verify the commenter is reachable via a known vouch chain.
|
||
|
||
---
|
||
|
||
## Integration with existing ItsGoin primitives
|
||
|
||
- **`Circle` + `GroupEncrypted`** remain as-is for named explicit-membership groups. FoF is a separate visibility class, not a replacement.
|
||
- **`PostVisibility`** gains one new variant (`FoFClosed`, from Layer 3). Mode 2 reuses `PostVisibility::Public` with extended `CommentPolicy`.
|
||
- **`CommentPolicy`** gains one new variant for Layer 2 (Mode 2 comment gating).
|
||
- **`InlineComment`** gets `pub_x_index`, `group_sig`, `identity_sig`, encrypted `ciphertext`, and inner `vouch_mac` (inside the ciphertext). Back-compat via `#[serde(default)]`, same pattern as Phase 2e `ref_post_id`.
|
||
- **Propagation-node accept rule** for comments on FoF posts: valid `pub_x_index` + not in `revocation_list` + `group_sig` verifies + `identity_sig` verifies. Any failure → drop without forwarding. Makes bandwidth-amplification DoS infeasible.
|
||
- **`control::receive_post`** gets new verify-gate branches for `FoFClosed` posts (author_sig + wrap_slots well-formedness) and FoF comments (group_sig verifies against `pub_post` from the referenced parent post).
|
||
- **Multi-persona**: keyrings are per-persona. Unlock attempts iterate personas; the persona that successfully unlocks is recorded and drives comment-authorship defaults. See Layer 3 for detail.
|
||
- **Bio post (`VisibilityIntent::Profile`)**: Layer 1 adds an optional `vouch_grants` field carrying an HPKE-sealed per-recipient wrapper batch. Existing bio-post CDN propagation carries vouch distribution — no new control-message type. See Layer 1 for wrapper format and scan policy.
|
||
|
||
---
|
||
|
||
## How to read this spec
|
||
|
||
- Every `TBD — OPUS` marker identifies a crypto specifier / byte layout / test vector that Opus will fill in.
|
||
- `Lead decisions` blocks capture commitments that came out of the Scott + Lead Claude conversation; these are not open to further design iteration.
|
||
- `Open questions` blocks flag things the Lead wants Opus's input on before proceeding.
|
||
|
||
Once Opus fills in the crypto layers, the Lead re-reviews + we move to implementation on a per-layer branch schedule.
|