itsgoin/docs/fof-spec/layer-1-vouch-primitive.md
Scott Reimers 1fdf9a94cc docs: FoF-gating spec skeleton (hand-off to Opus)
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>
2026-04-23 23:20:56 -04:00

6.5 KiB

Layer 1 — Vouch Primitive

Scope: Introduce V_x vouch keys, per-persona keyring storage, and a distribution/exchange mechanism. No post-gating yet — this layer ships first so the primitive is in place and UI-exercised before any post encryption depends on it.


Goal

After Layer 1 ships:

  • Each persona owns a current V_me symmetric key. New personas auto-generate one at creation.
  • Each persona has a keyring of received vouch keys: {V_x : x has vouched for me}.
  • Users can view who they've vouched for, who has vouched for them, and revoke/rotate V_me.
  • Wire protocol can transfer V_me from voucher to vouchee inside an existing encrypted channel.
  • No post encryption or comment gating depends on Layer 1 yet — that arrives in Layer 2/3.

Lead decisions

  • V_me is symmetric, not asymmetric. Every vouchee holds the same V_me. This is the property that makes FoF reach emergent (wrap under V_x, anyone x vouched for decrypts).
  • Distribution is unilateral. Alice vouching for Bob = Alice gives Bob a copy of V_alice. No request/accept handshake. Bob can immediately read FoF posts that include V_alice as a wrap slot.
  • Revocation = rotate V_me. There is no per-vouchee revocation. To un-vouch someone, the persona generates a new V_me' and re-distributes to every remaining vouchee. Layer 6 stub.
  • Epoch tag is part of the key. Every V_x has an associated (owner_id, epoch) so receivers can tell fresh from stale when multiple copies arrive (e.g., if voucher rotated).
  • Keyring is per-persona, not per-device. Multi-persona users have independent keyrings. Layer 3 reader logic iterates personas when trial-decrypting.

Data model

vouch_keys_own table

Per-persona, stores the persona's own V_me history (current + recent-past for graceful rotation).

TBD — OPUS: exact column types, key size in bytes, how many past epochs to retain.

vouch_keys_own(
    persona_id         BLOB,
    epoch              INTEGER,
    key_material       BLOB,      -- TBD — OPUS: symmetric key bytes (32B for a 256-bit key?)
    created_at_ms      INTEGER,
    is_current         INTEGER,   -- 1 for the active V_me, 0 for retained past epochs
    PRIMARY KEY (persona_id, epoch)
)

vouch_keys_received table

Per-persona, stores vouch keys received from other personas (one row per (owner_id, epoch) currently held).

vouch_keys_received(
    holder_persona_id  BLOB,      -- whose keyring this entry belongs to
    owner_id           BLOB,      -- the persona who owns (issued) this V_x
    epoch              INTEGER,
    key_material       BLOB,
    received_at_ms     INTEGER,
    PRIMARY KEY (holder_persona_id, owner_id, epoch)
)

Readers compute their keyring as SELECT key_material FROM vouch_keys_received WHERE holder_persona_id = ? UNION SELECT key_material FROM vouch_keys_own WHERE persona_id = ? AND is_current = 1.


Key generation

TBD — OPUS: specify the primitive. Candidates:

  • Random 32B from CSPRNG (simplest; V_me is pure symmetric secret)
  • HKDF-derived from persona identity key + epoch counter (allows deterministic re-derivation; couples key rotation to identity key exposure)

Lead leaning: random 32B CSPRNG, stored encrypted at rest alongside identity key material. Identity-key coupling offers no defensive benefit given the keyring is already indexed per-persona in the same DB.


Wire format for vouch distribution

A VouchGrant message carries a copy of the voucher's current V_me from voucher to vouchee.

TBD — OPUS: exact byte layout. Target shape:

VouchGrant {
    voucher_persona_id:  NodeId,        -- sender's persona
    epoch:               u32,
    key_material:        [u8; 32],      -- V_voucher at this epoch
    issued_at_ms:        u64,
    sig:                 [u8; 64],      -- voucher's identity-key signature over the above
}

Delivery: wrap VouchGrant inside the existing Direct (encrypted-to-one-recipient) post primitive. Do not introduce a new top-level control message. Content-type byte distinguishes vouch grants from regular DMs. Recipient's receive-path decodes, verifies signature, inserts into vouch_keys_received.

TBD — OPUS: decide whether vouch grants should use a reserved visibility intent (VisibilityIntent::VouchGrant) for filtering out of the Messages tab, or whether they ride under Direct and are suppressed client-side by content-type.


UI / UX

Minimum viable surface for Layer 1 ship:

  • Persona screen: "Vouch for someone" button. Picker of contacts. Hands them a V_me.
  • Persona screen: "Who has vouched for me" list (reads vouch_keys_received grouped by owner_id).
  • Persona screen: "People I've vouched for" list (local-only; TBD — OPUS on whether to track this explicitly or derive from DM-sent history — see open question below).
  • Settings: "Rotate my vouch key" button → generates new epoch, queues re-distribution to tracked vouchees.

Layer 1 ships without any post/comment behavior change. Vouches are visible in UI but don't gate content yet.


Open questions

  • Do we track outbound vouches explicitly? Option A: local vouches_issued(recipient_persona_id, epoch_at_grant) table. Option B: derive from DM-sent history searching for VouchGrant payloads. A is simpler + survives DM history loss; B avoids a redundant record. Lead leaning: A.
  • Re-distribution on rotation. When persona rotates V_me, do we auto-queue VouchGrant DMs to every tracked vouchee, or require user-initiated re-vouch? Lead leaning: auto-queue with a confirmation summary ("rotate will re-send vouch to N people").
  • Key size. 256-bit symmetric key is standard and matches our ChaCha20-Poly1305 usage. Confirm with Opus there's no reason to prefer 192 or 128 bits.
  • Epoch granularity. Monotonic counter per persona, or wall-clock-based? Counter is simpler; wall-clock aids debugging. Lead leaning: counter.
  • Should V_me ever be exported in the persona backup bundle? Losing V_me means every FoF post gated under it becomes permanently unreadable for every vouchee. Lead leaning: yes, include in identity export.

Ship criteria for Layer 1

  • All personas auto-generate V_me at creation.
  • Users can vouch and receive vouches end-to-end via DM-wrapped VouchGrant.
  • UI shows received and issued vouches per persona.
  • V_me rotation works; re-distribution to tracked vouchees is demonstrated.
  • No change to post visibility / comment behavior.
  • Integration test: two personas on two devices, Alice vouches for Bob, Bob's vouch_keys_received contains V_alice with correct signature.