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>
130 lines
6.5 KiB
Markdown
130 lines
6.5 KiB
Markdown
# 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.
|