docs(design.html): add section 20a Friend-of-Friend Visibility
Public-facing architecture description of the FoF post-gating system specified in docs/fof-spec/. Sits between section 20 (Encryption) and section 21 (Delete Propagation). All subsections marked badge-planned. Covers the user-facing 4-level visibility model, V_me primitive, bio-post HPKE distribution, dual-mode operation (public-body+FoF- comments vs FoFClosed), CDN-level comment verification, bucketed padding scheme, revocation/rotation/key-burn lifecycle, PQ-readiness, and the five ship-able layers. Visibility variants table updated with the new FoFClosed row. Disambiguation note added at top of section 20a noting this "vouch" is the cryptographic V_me primitive, distinct from the directory vouches in section 27. Reciprocal disambiguation note added at top of section 27 pointing the other direction. TOC entry added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
73b1e24f9a
commit
d7ce2f734c
2 changed files with 133 additions and 0 deletions
|
|
@ -68,6 +68,7 @@
|
|||
<a href="#erasure-cdn">18b. Erasure-Coded CDN Replication</a>
|
||||
<a href="#sync">19. Sync Protocol</a>
|
||||
<a href="#encryption">20. Encryption</a>
|
||||
<a href="#fof">20a. Friend-of-Friend Visibility</a>
|
||||
<a href="#deletes">21. Delete Propagation</a>
|
||||
<a href="#privacy">22. Social Graph Privacy</a>
|
||||
<a href="#multidevice">23. Multi-Device Identity</a>
|
||||
|
|
@ -1304,6 +1305,7 @@ END</code></pre>
|
|||
<tr><td><code>Public</code></td><td>None</td><td>Unlimited</td></tr>
|
||||
<tr><td><code>Encrypted { recipients }</code></td><td>~60 bytes per recipient</td><td>~500 (256KB cap)</td></tr>
|
||||
<tr><td><code>GroupEncrypted { group_id, epoch, wrapped_cek }</code></td><td>~100 bytes total</td><td>Unlimited (one CEK wrap for the group)</td></tr>
|
||||
<tr><td><code>FoFClosed { pub_post_set, wrap_slots }</code> <span class="badge badge-planned">Planned</span></td><td>~154 bytes per admitted V_x, padded</td><td>Bucketed (8/16/32/64/128/256, then +128 steps)</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>PostId integrity</h3>
|
||||
|
|
@ -1350,6 +1352,105 @@ END</code></pre>
|
|||
<p>Different profile versions per circle, encrypted with the circle/group key. A peer sees the profile version for the most-privileged circle they belong to. <code>CircleProfileUpdate</code> (<code>0xB4</code>) wire message. Public profiles can be hidden (<code>public_visible=false</code> strips display_name/bio).</p>
|
||||
</section>
|
||||
|
||||
<!-- 20a. Friend-of-Friend Visibility -->
|
||||
<section id="fof">
|
||||
<h2>20a. Friend-of-Friend Visibility <span class="badge badge-planned">Planned</span></h2>
|
||||
|
||||
<div class="note">
|
||||
<strong>Distinct from directory vouches.</strong> The "FoF vouch" described here is a <em>cryptographic</em> primitive for post readership and comment gating (per-persona symmetric key <code>V_me</code>). It is unrelated to the <em>directory vouch</em> system in <a href="#directory">section 27</a>, which governs discovery-layer trust and bot-ring resistance. The two share vocabulary but operate at different layers.
|
||||
</div>
|
||||
|
||||
<h3>The problem</h3>
|
||||
<p>Existing visibility variants gate by explicit recipient lists (<code>Encrypted{recipients}</code>) or named-circle membership (<code>GroupEncrypted</code>). Neither expresses "people who are reachable through my social graph" without leaking the graph itself. FoF visibility fills that gap: posts whose readership emerges from <em>cryptographic reachability</em> through a unilateral vouch graph, with no recipient IDs on the wire and no centrally-computed membership lists.</p>
|
||||
|
||||
<h3>User-facing model</h3>
|
||||
<p>Authors pick one of four visibility levels at compose time:</p>
|
||||
<table>
|
||||
<tr><th>Level</th><th>Reaches</th></tr>
|
||||
<tr><td><strong>Public</strong></td><td>All readers (unchanged)</td></tr>
|
||||
<tr><td><strong>Friends-only</strong></td><td>Personas you have vouched for</td></tr>
|
||||
<tr><td><strong>Friends-of-Friends</strong></td><td>Your vouchees + every vouchee of anyone who vouched for you (emergent FoF)</td></tr>
|
||||
<tr><td><strong>Custom</strong> <span class="badge badge-planned">v2</span></td><td>Author-selected subset of held vouch keys</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Core primitives</h3>
|
||||
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
|
||||
<li><strong><code>V_me</code></strong>: a 32-byte symmetric key owned by each persona. Distributed to everyone the persona vouches for. Anyone holding <code>V_alice</code> can decrypt wrap slots Alice sealed under it.</li>
|
||||
<li><strong>Keyring</strong>: per-persona, holds the persona's own <code>V_me</code> plus every <code>V_x</code> received from vouchers. The union of these is what makes FoF reach emergent: an author wraps a post slot under every <code>V_x</code> they hold, and any reader whose keyring intersects with that set can decrypt.</li>
|
||||
<li><strong>Wrap slot</strong>: an anonymous AEAD ciphertext in the post header, sealed under one <code>V_x</code>. Carries the post's CEK plus a per-slot signing key. No recipient ID visible on the wire.</li>
|
||||
<li><strong>Prefilter tag</strong>: a 2-byte <code>HMAC(V_x, post_id)[:2B]</code> on each slot. Readers precompute tags for keys in their keyring and skip non-matching slots, cutting trial-decrypt cost ~65,000× per post.</li>
|
||||
<li><strong><code>pub_post_set</code></strong>: list of all admitted signing pubkeys for a post's FoF set. Inline in the post header, randomly ordered. Allows CDN-level verification of comment signatures without revealing membership identities.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Distribution: vouches ride bio posts</h3>
|
||||
<p>Vouches are NOT delivered via DM. Instead, the voucher publishes anonymous HPKE-sealed wrappers (one per recipient) inside their bio post. HPKE (RFC 9180) provides <strong>recipient anonymity</strong> — wrapper ciphertext reveals nothing about the recipient's pubkey. Each wrapper is 48 bytes (32-byte sealed <code>V_me</code> + 16-byte AEAD tag); one shared ephemeral pubkey per batch.</p>
|
||||
<p>Readers auto-scan bio posts of accounts they follow, trial-decrypting each wrapper against each of their personas. Cost is ~60µs per wrapper per persona on mobile — a 200-wrapper bio scanned against 3 personas is ~36 ms. The bio's <code>VouchGrantBatch</code> is padded with dummy wrappers and shuffled on every publish so observers see neither vouch-set size nor change-targets.</p>
|
||||
|
||||
<h3>Two modes</h3>
|
||||
<div class="card">
|
||||
<h3>Mode 2: Public body, FoF-gated comments</h3>
|
||||
<p>Body is plaintext in the CDN (indexable, cacheable, shardable — unchanged from existing public-post path). Comments are encrypted under a CEK derived from the wrap-slot CEK, so only FoF members can read them. Non-FoF observers see only ciphertext + signature fields. The compose UI exposes this via a new <code>CommentPolicy::FriendsOfFriends</code> variant.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Mode 1: <code>FoFClosed</code> (body + comments encrypted)</h3>
|
||||
<p>New <code>PostVisibility::FoFClosed</code> variant. Body is encrypted under CEK (same wrap-slot mechanism as comments). Both readership and comment authority emerge from keyring intersection with the post's <code>wrap_slots</code>.</p>
|
||||
</div>
|
||||
|
||||
<h3>CDN-level comment verification</h3>
|
||||
<p>Each wrap slot is dual-derived: one half yields the shared CEK (read capability), the other yields a per-V_x Ed25519 <em>signing</em> keypair (<code>priv_x</code> sealed inside the slot; the corresponding <code>pub_x</code> published in the post's <code>pub_post_set</code>). Comments declare which <code>pub_x</code> signed them and carry the signature.</p>
|
||||
<p>Propagation nodes verify three things before forwarding a comment:</p>
|
||||
<ol style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
|
||||
<li><code>pub_x_index</code> points to a valid entry in <code>pub_post_set</code>.</li>
|
||||
<li>That entry is not in the post's <code>revocation_list</code>.</li>
|
||||
<li>The comment's <code>group_sig</code> validates against that <code>pub_x</code>.</li>
|
||||
</ol>
|
||||
<p>Any failure → drop, do not forward. This kills the bandwidth-amplification attack that a single admitted-but-malicious FoF member could otherwise mount: their forgeries cannot pass the propagation gate.</p>
|
||||
|
||||
<h3>Privacy properties</h3>
|
||||
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
|
||||
<li><strong>Unilateral</strong>: vouching is a one-way act, no handshake. The FoF graph forms without bilateral negotiation.</li>
|
||||
<li><strong>Graph-private</strong>: wrap slots carry no recipient IDs. Observers cannot enumerate who can read a post.</li>
|
||||
<li><strong>Bucketed padding</strong>: slot count and body size are deterministically padded to fixed buckets (power-of-2 up to 256, then +128 steps for slots; same shape up to 256 KB then +256 KB steps for bodies). Observers learn the bucket, not the position within it.</li>
|
||||
<li><strong>Recipient anonymity on vouch distribution</strong>: HPKE key privacy ensures bio-post wrappers do not reveal recipients.</li>
|
||||
<li><strong>Per-post chain pseudonym</strong> (accepted tradeoff): the <code>pub_x_index</code> in a comment lets observers correlate "these N comments came through the same chain" within a single post. Cross-post correlation is broken because keys regenerate per post.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Revocation & key lifecycle</h3>
|
||||
<p>Three complementary mechanisms:</p>
|
||||
<div class="card">
|
||||
<h3>Per-post comment revocation (default)</h3>
|
||||
<p>The author signs a <code>RevocationEntry</code> for a specific <code>pub_x</code> on a specific post. Propagation nodes <strong>delete</strong> locally-stored comments by that signer, remove the entry from <code>pub_post_set</code>, append to <code>revocation_list</code>, and forward the diff. Retroactive: the mesh self-cleans as the diff sweeps through.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Persona-wide <code>V_me</code> rotation</h3>
|
||||
<p>To remove a vouchee, the persona generates <code>V_me_new</code> and issues it to every non-revoked vouchee via the next bio-post batch. Revoked vouchees retain <code>V_me_old</code>. Old posts (sealed under <code>V_me_old</code>) stay readable by anyone who holds the old key — <strong>grandfathered by default</strong>. The CDN does not auto-cascade revocations.</p>
|
||||
<p>Receivers append new <code>V_me</code> values to their keyring (chain), so newly-issued keys do not invalidate prior ones — the receiver keeps holding the old key for reading historical content.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Opt-in cascade + key burn (advanced)</h3>
|
||||
<p>If the author wants to cut off comment authority on old posts after a rotation, they cascade by publishing per-<code>pub_x</code> revocations on each affected post. Local-only <code>own_post_slot_provenance</code> table maps "which pub_x was sealed under which V_me" so the author can target precisely.</p>
|
||||
<p>For the rare case of a leaked <code>V_me</code>, an optional <code>KeyBurnDiff</code> primitive swaps the V_old wrap slot for a V_new wrap slot in-place on a specific post. Scrubs the leaked key from the CDN copy of old posts. Body CEK unchanged.</p>
|
||||
</div>
|
||||
|
||||
<h3>Performance budget</h3>
|
||||
<p>At realistic scale (~500 vouchees, ~500 wrap slots per post), reader-side decryption uses an unlock cache: the first time persona <code>P</code> decrypts a post from author <code>A</code> via key <code>V_x</code>, the <code>(P, V_x)</code> tuple is cached. Subsequent posts from <code>A</code> try that key first — one HMAC + one AEAD attempt in the hot path. Full scan only on cache miss; newly-received <code>V_x</code> triggers a retry sweep over the unreadable-posts table.</p>
|
||||
|
||||
<h3>Post-quantum readiness</h3>
|
||||
<p>Body encryption, wrap slots, and HKDF/HMAC are all symmetric — PQ-safe. Comment signing uses Ed25519 today; the spec shape is algorithm-agnostic so ML-DSA-65 (~2 KB pubkey, ~3.3 KB signature) can substitute, optionally with a Merkle-commit variant on <code>pub_post_set</code> to keep header size bounded.</p>
|
||||
|
||||
<h3>Implementation</h3>
|
||||
<p>Full crypto-level byte layouts, data models, wire-format additions, ship criteria, and integration tests are specified in <code>docs/fof-spec/</code>. The implementation is layered for bottom-up shipping:</p>
|
||||
<table>
|
||||
<tr><th>Layer</th><th>Scope</th><th>Status</th></tr>
|
||||
<tr><td>1</td><td>Vouch primitive (V_x keys, keyring, bio-post HPKE wrappers, scan policy)</td><td><span class="badge badge-planned">Planned</span></td></tr>
|
||||
<tr><td>2</td><td>Mode 2: public posts with FoF-gated comments, CDN-level verification</td><td><span class="badge badge-planned">Planned</span></td></tr>
|
||||
<tr><td>3</td><td>Mode 1: <code>FoFClosed</code> body + wrap slots + anonymous prefilter</td><td><span class="badge badge-planned">Planned</span></td></tr>
|
||||
<tr><td>4</td><td>Rotation, revocation, key lifecycle (grandfather + cascade + key-burn)</td><td><span class="badge badge-planned">Planned</span></td></tr>
|
||||
<tr><td>5</td><td>Unlock cache + prefilter optimization (perf-critical at scale)</td><td><span class="badge badge-planned">Planned</span></td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 21. Delete Propagation -->
|
||||
<section id="deletes">
|
||||
<h2>21. Delete Propagation</h2>
|
||||
|
|
@ -1578,6 +1679,10 @@ END</code></pre>
|
|||
<p><em>The directory is an opt-in convenience layer for discovery and creator protection. It is not node access — losing directory presence does not disconnect anyone from the network or from their existing connections. This asymmetry is load-bearing: humans with mature relationships shrug off directory loss; bots and content thieves depend on it entirely.</em></p>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>Distinct from FoF cryptographic vouches.</strong> The "vouch" described in this section is a <em>directory-layer</em> trust signal governing discovery and bot-ring resistance. It is unrelated to the <em>cryptographic vouch</em> (<code>V_me</code>) in <a href="#fof">section 20a</a>, which gates post readership and commenting via per-persona symmetric keys. The two share vocabulary but operate at different layers.
|
||||
</div>
|
||||
|
||||
<h3>Scope</h3>
|
||||
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
|
||||
<li><strong>Whitelist track</strong> — discoverability, vouch-based entry, graph-scoped visibility.</li>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue