diff --git a/docs/fof-spec/layer-2-mode2-fof-comments.md b/docs/fof-spec/layer-2-mode2-fof-comments.md index 4bb837d..00c7cfe 100644 --- a/docs/fof-spec/layer-2-mode2-fof-comments.md +++ b/docs/fof-spec/layer-2-mode2-fof-comments.md @@ -36,7 +36,7 @@ Cost: `pub_x_index` is a per-post pseudonym for the voucher-chain — leaks "the - **CDN propagation verifies before forwarding.** Four-check accept rule: valid `pub_x_index`, not in `revocation_list`, `group_sig` validates, `identity_sig` validates. Any failure → drop, do not propagate. - **Revocation is retroactive.** Revocation diff is author-signed, appends a revoked `pub_x` entry. Every file-holder that receives the diff **deletes all comments on this post signed by that `pub_x`** from local storage, then forwards the diff. Comments in flight before the diff arrived get deleted when it catches up. Stronger than "stop forwarding" — prior garbage also goes away. - **Bucketed slot-count padding.** Deterministic buckets throughout. Up to 256 real slots: next power of 2 (8, 16, 32, 64, 128, 256). Above 256: next +128-step bucket (384, 512, 640, 768, …). Author publishes `next_bucket(real_count)` slots; dummy slots fill the gap. Dummy `pub_x` entries are included in `pub_post_set` 1:1 so `pub_post_set.len() == wrap_slots.len()`; dummy pubkeys are 32B random bytes (no one holds the matching `priv_x`, so `group_sig` verify against a dummy always fails — benign). -- **Post-hoc read-access grant via author comment.** Author can widen the read-set of an already-published post by publishing an author-signed special comment that appends a new `WrapSlot` (and its `pub_post_set` entry) for a newly-vouched persona. No rotation, no republish; retroactive inclusion. See "Access-grant author comment" below. +- **Post-hoc read-access grant via author comment.** Author can widen the read-set of an already-published post by publishing an author-signed special comment that **appends at the tail** a new `WrapSlot` + `pub_post_set` entry for a newly-vouched persona. `pub_x_index` values in already-stored comments stay valid (positions 0..N-1 unchanged). Cost: observers see "tail entries are the most recent grants" — small positional-recency leak; accepted tradeoff for index stability. No rotation, no republish; retroactive inclusion. See "Access-grant author comment" below. - **`vouch_mac` retained.** Inside ciphertext. Enables author render-time and intra-circle attribution complementary to CDN-level `pub_x_index` attribution. - **Author has their own `pub_me/priv_me`.** Treated as one of the entries in `pub_post_set`. Author signs their own comments through the same path. - **`V_me` handed to vouchees already implies `priv_me` handoff.** No — correction: `priv_x` is wrapped in the per-`V_x` sign-slot of every post, not handed out at vouch time. Only people who can unwrap the slot (= people holding `V_x`) receive `priv_x`. This is what makes per-post rotation (Layer 4) a coherent revocation surface. diff --git a/docs/fof-spec/layer-3-mode1-fof-closed.md b/docs/fof-spec/layer-3-mode1-fof-closed.md index a06cbbf..b74ba89 100644 --- a/docs/fof-spec/layer-3-mode1-fof-closed.md +++ b/docs/fof-spec/layer-3-mode1-fof-closed.md @@ -23,7 +23,7 @@ Builds on Layer 2's `pub_post` / `priv_post` / `wrap_slot` primitives — same s - **New variant, not extended Encrypted.** `PostVisibility::FoFClosed` is its own variant. Existing `Encrypted{recipients}` wraps per-recipient NodeIds — visible on the wire. FoF wraps anonymously under symmetric keys — no NodeIds. - **One wrap slot per unique `V_x`.** Dedup at the `V_x` byte level — if multiple personas hold the same `V_x`, include one slot. Friends-only post: one slot under `V_me`. FoF post: own `V_me` + every distinct `V_x` the author holds. Custom: subset chosen by author (deferred to v2 power-user UI; v1 ships three presets only: Public / Friends-only / Friends-of-Friends). -- **Bucketed slot-count padding.** Deterministic bucket boundaries throughout — observers learn the bucket, not the position within it. Buckets: 8, 16, 32, 64, 128, 256 (power-of-2 up to 256), then 384, 512, 640, 768, … (linear +128 steps above 256). Author publishes `next_bucket(real_count)` slots with random dummies filling the gap. Power-of-2 sub-256 keeps small-author overhead bounded; +128 steps above 256 avoid the 2× waste of pure power-of-2 at scale. Dummy slots are byte-identical to real ones, AEAD-fails on any `V_x`. Dummy entries also added to `pub_post_set` 1:1 (see Layer 2). +- **Bucketed slot-count padding.** Deterministic bucket boundaries throughout — observers learn the bucket, not the position within it. Buckets: 8, 16, 32, 64, 128, 256 (power-of-2 from a minimum of 8 up to 256), then 384, 512, 640, 768, … (linear +128 steps above 256). Author publishes `next_bucket(real_count)` slots with random dummies filling the gap. Minimum bucket of 8 means a brand-new persona's first post still publishes 8 slots — no "this persona has no vouchees" signal. Power-of-2 sub-256 keeps small-author overhead bounded; +128 steps above 256 avoid the 2× waste of pure power-of-2 at scale. Dummy slots are byte-identical to real ones, AEAD-fails on any `V_x`. Dummy entries also added to `pub_post_set` 1:1 (see Layer 2). - **Bucketed body-size padding.** Same shape applied to body ciphertext bytes: power-of-2 buckets up to 256KB (1KB, 2KB, 4KB, …, 256KB), then 256KB-step buckets above (512KB, 768KB, 1024KB, …). 256KB above is the future storage chunk size — once large enough to chunk, padding aligns to chunk boundaries naturally. - **Each slot carries both CEK and priv_x (Layer 2 dual-derivation).** Layer 2's `WrapSlot` dual-derivation (read → CEK, sign → priv_x) is the canonical form. Mode 1 simply also uses the CEK to encrypt the body, where Mode 2 leaves the body plaintext. - **Prefilter tag is `HMAC(V_x, post_id)[:2B]`.** Readers precompute a 2-byte tag for each key in their keyring and skip slots that don't match. Cuts trial-decrypt cost by ~2^16 on average. @@ -125,12 +125,11 @@ Ciphertext `FoFClosed` posts ride the same CDN propagation as other encrypted po - **Prefilter false-positive cost.** 1/65536 false positive per slot. With 500 slots × reader iterating 500 keys, expected ~3.8 false-positive AEAD attempts per post. Acceptable. - **Prefilter collision on legitimate hits.** Two different `V_x` could produce the same `prefilter_tag` for the same `post_id`. Reader just tries both. No correctness issue. - **Slot-reuse across posts.** If the same `V_x` is used across many posts, an attacker can observe prefilter tags recur. Since `post_id` is in the HMAC input, tags differ per post. No leak. -- **Access-grant re-shuffle vs append-only.** When the author publishes a Layer 2 access-grant comment, do we re-shuffle the entire `wrap_slots` + `pub_post_set` (preserves the random-order property for the full set but invalidates `pub_x_index` values in already-propagated comments), or append-only (`pub_x_index` is stable across the post's lifetime, but the newest entries are always at the tail — small positional leak that grants are recent)? Lead leaning: **append-only**; `pub_x_index` stability is load-bearing for revocation and comment verification on already-stored comments. -- **Padding floor for small authors.** Power-of-2 padding on 1 real slot → 1, 2, or 4? Power-of-2-of-1 is 1, but that's no padding. Probably enforce a minimum bucket of 4 or 8 so that a brand-new persona with one vouch doesn't publish a singleton. Lead leaning: minimum 8. ## Resolved (2026-04-24) -- **Slot count padding**: bucketed throughout. Power-of-2 buckets to 256 (8, 16, 32, 64, 128, 256), then linear +128 buckets (384, 512, 640, …). Body-size padding follows the same shape with 256KB as the power-of-2 ceiling and 256KB linear steps above. +- **Slot count padding**: bucketed throughout. Power-of-2 buckets from 8 to 256 (minimum bucket is 8 — singleton/tiny-set posts pad up to 8 to avoid leaking "new persona with no vouchees"), then linear +128 buckets (384, 512, 640, …). Body-size padding follows the same shape with 256KB as the power-of-2 ceiling and 256KB linear steps above. +- **Access-grant ordering**: **append at tail** (Scott's call). New entries land at the end of `pub_post_set` + `wrap_slots`. `pub_x_index` values in already-stored comments stay valid. Small positional-recency leak (tail = recent grants) is the accepted cost. - **Custom mode UI**: deferred. v1 ships only the three presets (Public / Friends-only / FoF). Power-user custom-subset UI is v2. - **Slot deduplication**: dedup at the `V_x` byte level. One slot per unique key. - **Body length padding**: yes — pad to next power of 2 up to 256KB, then 256KB chunks above. diff --git a/sessions.md b/sessions.md index d349278..e4d5219 100644 --- a/sessions.md +++ b/sessions.md @@ -65,6 +65,18 @@ See `CONTRIBUTING.md` for the protocol. See `AGENTS.md` for the Claude-specific **Stopping point**: commit `b8b38a6` (Layer 1) + new commit for Layer 2 both on branch; not merged. Awaiting Scott. +### Update 2026-04-24 — Layer 3 round 2 (last two open questions) + +Two follow-up questions resolved: + +- **Access-grant slot ordering**: **append at tail** (not re-shuffle). I'd initially overcorrected to a "switch comments from index to pub_x bytes so shuffles are free" change; Scott reverted that and clarified the choice. Append-at-tail preserves `pub_x_index` stability across the post's lifetime — already-stored comments stay verifiable, no write amplification on grant. Accepted positional-recency leak (tail = newest grants). +- **Minimum slot bucket**: **8**. Singleton/tiny-set posts pad up to 8 slots. Brand-new personas don't publish "I have no vouchees" headers. + +**Files touched**: +- `docs/fof-spec/layer-2-mode2-fof-comments.md`: access-grant lead decision made explicit about append-at-tail and index stability. +- `docs/fof-spec/layer-3-mode1-fof-closed.md`: minimum-8 floor added to padding lead decision; both open questions moved to Resolved. +- `sessions.md`: this entry. + ### Update 2026-04-24 — Layer 3 round 1 + cross-cutting padding rule (corrected) Scott talked to Opus and resolved Layer 3 open questions + introduced a unified padding rule that supersedes Layer 2 round 2's `rand(32..=128)`.