docs: Layer 3 round 1 + unified hybrid padding rule
Hybrid padding rule (slots + body, same shape): - <=256 real units: pad to next power of 2 (8, 16, ..., 256) - >256 real units: pad to real + rand(0..=256) / nearest 256KB Replaces Layer 2 round 2's rand(32..=128). Small authors/posts get strong bucket-grouping; large authors/posts get probabilistic noise without 2x bandwidth waste of pure power-of-2 at scale. Layer 3 resolutions: - Custom mode deferred; v1 ships Public / Friends-only / FoF only - Slot dedup at V_x byte level (one slot per unique key) - Body-length padding adopted Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a79cab049f
commit
3ee20736aa
3 changed files with 51 additions and 13 deletions
|
|
@ -35,7 +35,7 @@ Cost: `pub_x_index` is a per-post pseudonym for the voucher-chain — leaks "the
|
|||
- **Comments are encrypted.** `CEK_comments = HKDF(CEK, "comments")`. Non-FoF observers see only ciphertext + signatures on comments. Read-gating is a side effect of the same slot unwrap.
|
||||
- **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.
|
||||
- **Random-count dummy padding, not fixed buckets.** Append `rand(32..=128)` dummy slots (each real-shape, indistinguishable). Observers know a random amount was added; they cannot infer the floor count across multiple posts from the same author. 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).
|
||||
- **Hybrid slot-count padding.** Up to 256 real slots: pad to next power of 2 (smallest authors get strong bucket-grouping; e.g., 5 real → 8 published, 100 real → 128 published, 200 real → 256 published). Above 256 real slots: pad to `real_count + rand(0..=256)` (large authors get probabilistic noise; power-of-2 jumps would waste up to 50%). 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.
|
||||
- **`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.
|
||||
|
|
@ -246,7 +246,7 @@ struct AccessGrantComment {
|
|||
| `prefilter_tag` | 2 |
|
||||
| **Subtotal per slot** | **~154** |
|
||||
|
||||
At 500 real vouchees + `rand(32..=128)` dummies: header payload is 532–628 slots × ~154B = ~82KB to ~97KB. Acceptable for a header carried once per post.
|
||||
At 500 real vouchees (above the 256 inflection point): pad to `500 + rand(0..=256)` = 500–756 slots × ~154B = ~77KB to ~117KB. At 200 real vouchees: pad to 256 = ~39KB. At 9 real vouchees: pad to 16 = ~2.5KB. Acceptable for a header carried once per post.
|
||||
|
||||
Per-comment CDN overhead (vs shared-keypair baseline): +64B (`group_sig`) + 4B (`pub_x_index`) + 64B (`identity_sig` — already in baseline) ≈ 68B additional. Negligible.
|
||||
|
||||
|
|
@ -266,7 +266,11 @@ Decision: **defer Merkle variant to PQ migration**. v1 ships inline.
|
|||
|
||||
The reason this is acceptable: the alternative is no propagation-level verification, which means any one admitted FoF member can DoS the mesh. Per-post pseudonym is the minimum viable disclosure for CDN-level filtering.
|
||||
|
||||
**Vouch-set size:** random `rand(32..=128)` dummy padding means an observer sees `real_count + rand(32..=128)` and cannot infer the true count. Across multiple posts from the same author, the observer also cannot establish a lower bound — each post's padding is independent, so the minimum observed size across many posts only converges to `real_count + 32`, not to `real_count`.
|
||||
**Vouch-set size:** hybrid padding behaves differently in two regimes.
|
||||
- **Small authors (≤256 real slots)** publish a power-of-2 count. Observer learns the bucket (e.g., `published == 128` → `real_count ∈ (64, 128]`). Bucket boundaries are 8, 16, 32, 64, 128, 256. Within-bucket count is fully hidden.
|
||||
- **Large authors (>256 real slots)** publish `real_count + rand(0..=256)`. Each post leaks an upper bound on real_count. Across many posts, an observer's best floor estimate is `min(published_i) - 256`, which doesn't converge tighter without independent side information.
|
||||
|
||||
Small-author bucket boundaries are a known cost; large authors trade off a coarser noise floor against bandwidth. Both regimes hide the true count within meaningful brackets.
|
||||
|
||||
**Non-FoF reader UX:** non-FoF readers see the public body + "Comments are private" affordance. Comment count is not shown (no engagement leak). Optional: a "Request access via DM" button that sends the author a note; author can respond by publishing an access-grant author comment (above) that retroactively admits the requester.
|
||||
|
||||
|
|
@ -302,6 +306,6 @@ The reason this is acceptable: the alternative is no propagation-level verificat
|
|||
- Revocation diff format + CDN honor path (retroactive delete + forward).
|
||||
- Access-grant author comment mechanism for post-hoc read widening.
|
||||
- Per-post ephemeral-keypair generation.
|
||||
- Random-count dummy padding `rand(32..=128)` on both `wrap_slots` and `pub_post_set`, shuffled together.
|
||||
- Hybrid dummy padding on both `wrap_slots` and `pub_post_set` (power-of-2 up to 256, then `count + rand(0..=256)`), shuffled together.
|
||||
- Back-compat: old clients can still render existing non-FoF posts; old comments on new FoF posts are rejected at CDN (missing required fields).
|
||||
- Integration test: 3-node FoF chain (A→B→C). A posts Mode 2 FoF. B comments (accepted, propagates). C comments (accepted via B's chain). D (unrelated) signs junk with fake `pub_x_index` pointing at a real entry (rejected at first-hop CDN: `group_sig` fails). D signs junk with `pub_x_index` pointing at a dummy (rejected: `group_sig` fails against a dummy random pubkey). A revokes B's `pub_x`: B's already-propagated comments are deleted on each file-holder as the diff sweeps through; B's subsequent comments rejected at first hop. A vouches for E after the post is live and publishes access-grant author comment; E can now read + comment retroactively.
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ Builds on Layer 2's `pub_post` / `priv_post` / `wrap_slot` primitives — same s
|
|||
## Lead decisions
|
||||
|
||||
- **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 `V_x` in the author's keyring.** For a Friends-only post, one slot under `V_me`. For FoF, N+1 slots (one per `V_x` the author holds + own `V_me`). For Custom, subset chosen by author.
|
||||
- **Slot count padded to power-of-2.** Prevents observers from counting vouchers the author has. TBD — OPUS: confirm padding up to next power of 2 with random dummy slots (non-decryptable ciphertext indistinguishable from real slots).
|
||||
- **Each slot carries both CEK and priv_post.** Wrapped together as a single plaintext. One successful unwrap gives reader everything they need to read body + sign comments.
|
||||
- **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).
|
||||
- **Hybrid slot-count padding.** Up to 256 real slots: pad to next power of 2 (smallest authors get strong bucket-grouping). Above 256: pad to `real_count + rand(0..=256)` (large authors get probabilistic noise; power-of-2 jumps would waste up to 50% bandwidth). 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).
|
||||
- **Hybrid body-size padding.** Same shape: up to 256KB of body ciphertext, pad to next power of 2 (1KB, 2KB, 4KB, …, 256KB). Above 256KB, round up to nearest 256KB. Aligns large posts with the storage chunking-block size; small posts get strong bucket-grouping against length-based classification.
|
||||
- **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.
|
||||
- **Order of slots is randomized.** No positional leak about which slot corresponds to which voucher.
|
||||
- **Order of slots is randomized.** No positional leak about which slot corresponds to which voucher. Re-shuffled on every header revision (including access-grant appends from Layer 2 — TBD whether append-only ordering is acceptable, or whether the entire set is re-shuffled at each grant).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -121,21 +122,28 @@ Ciphertext `FoFClosed` posts ride the same CDN propagation as other encrypted po
|
|||
|
||||
## Open questions
|
||||
|
||||
- **Slot size uniformity.** Real slots and dummy padding slots must be byte-identical-sized. Confirmed. TBD — OPUS: should we also pad the body length to a bucket to avoid length-based classification?
|
||||
- **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.
|
||||
- **Custom mode slot selection.** Does the author UI let them pick specific vouchers, specific groups of vouchers, or only "all held + own" vs "own only"? Lead leaning: initial UI = only the three preset levels (Friends-only / FoF / Public); custom ships as power-user option later.
|
||||
- **Deduplication of `V_x` across personas.** If multiple personas hold the same `V_x`, do we include one slot or one-per-persona? Lead leaning: dedup at the `V_x` bytes level; one slot per unique key.
|
||||
- **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**: hybrid scheme — up to 256, next power of 2; above 256, `real_count + rand(0..=256)`. Body-size padding follows the same shape with 256KB as the inflection point.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## Ship criteria for Layer 3
|
||||
|
||||
- `PostVisibility::FoFClosed` exists end-to-end.
|
||||
- Author creation path generates ephemeral keypair, wraps CEK+priv_post under each eligible `V_x`, pads to power-of-2.
|
||||
- Author creation path generates per-post keypairs, wraps CEK+priv_x under each unique `V_x` (deduped), and pads per the hybrid rule: power-of-2 up to 256 real slots, then `real_count + rand(0..=256)` above.
|
||||
- Body-size padded: power-of-2 up to 256KB, then nearest 256KB above.
|
||||
- Reader decryption path iterates personas × keyring with prefilter tag.
|
||||
- `receive_post` accepts FoFClosed ciphertext without decrypting.
|
||||
- UI surface: post composer has Public / Friends-only / FoF / Custom picker.
|
||||
- UI surface: post composer has three presets — Public / Friends-only / Friends-of-Friends. Custom subset is v2.
|
||||
- Integration test: A posts FoFClosed. B (direct vouchee) reads. C (FoF via B) reads. D (unrelated) gets ciphertext, cannot decrypt.
|
||||
- Performance: decryption completes within budget at 500-key keyring × 500-slot posts (see Layer 5 for the optimization work that makes this budget feasible).
|
||||
|
|
|
|||
26
sessions.md
26
sessions.md
|
|
@ -65,6 +65,32 @@ 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 1 + cross-cutting padding rule
|
||||
|
||||
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)`.
|
||||
|
||||
**Hybrid padding rule (applies to both slot count and body size)**:
|
||||
- ≤256 real units → pad to next power of 2 (1, 2, 4, …, 256). Strong bucket-grouping for small authors / small bodies.
|
||||
- >256 real units → pad to `real + rand(0..=256)` (slots) / round up to nearest 256KB (bodies). Probabilistic noise for large authors / large bodies; avoids 2× bandwidth waste of pure power-of-2 at scale.
|
||||
|
||||
Applies uniformly to slot count and body size.
|
||||
|
||||
**Other resolved Layer 3 questions**:
|
||||
- Custom mode UI deferred. v1 ships three presets only: Public / Friends-only / FoF.
|
||||
- Slot dedup at `V_x` byte level. One slot per unique key.
|
||||
- Body-length padding adopted.
|
||||
|
||||
**Files touched**:
|
||||
- `docs/fof-spec/layer-3-mode1-fof-closed.md`: Lead decisions updated (hybrid padding, dedup, three-preset UI); open questions split into still-open + Resolved; ship criteria updated.
|
||||
- `docs/fof-spec/layer-2-mode2-fof-comments.md`: padding rule promoted to hybrid scheme; size budget rewritten with three regime examples; privacy section rewritten for two-regime analysis; Resolved bullet superseded with pointer.
|
||||
- `sessions.md`: this entry.
|
||||
|
||||
**Still-open Layer 3 questions worth flagging to Scott**:
|
||||
1. Access-grant ordering — does appending a new slot re-shuffle the full `wrap_slots` / `pub_post_set` (preserves the random-order privacy property but invalidates `pub_x_index` values in already-stored comments), or is it append-only (`pub_x_index` is stable but tail-positional leak says "these are recent grants")? Lead leaning: **append-only**; index stability matters for revocation and stored-comment verification.
|
||||
2. Minimum slot-count floor for tiny authors. Power-of-2-of-1 = 1, which leaks "this persona has one vouch (probably just themselves)." Lead leaning: minimum bucket of 8.
|
||||
|
||||
**Pending**: Layers 4–6 iterations. Scott to confirm two flagged questions.
|
||||
|
||||
### Update 2026-04-24 — Layer 2 round 2 (Scott answers all 5 questions)
|
||||
|
||||
Scott resolved all five open questions:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue