docs: Layer 2 round 2 — resolve 5 questions + access-grant primitive
Fold in Scott's answers: - Per-post (pub_x, priv_x); confirmed. - Random rand(32..=128) dummy padding replaces power-of-2 buckets; dummy pubkeys in pub_post_set so .len() == wrap_slots.len(). Floor count is unrecoverable across multiple posts. - Non-FoF UX: "Comments are private" + optional "Request access via DM" button. No count leak. - Author's own (pub_me, priv_me) in pub_post_set; confirmed. - Revocation is retroactive delete + forward: file-holders delete locally-stored comments signed by revoked pub_x on diff arrival, then propagate. Stronger than stop-forwarding. New primitive: access-grant author comment. Author appends a WrapSlot + pub_post_set entry for a newly-vouched persona via a signed special comment — retroactive read widening without republish. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
553fbd3a20
commit
a79cab049f
2 changed files with 92 additions and 24 deletions
|
|
@ -29,12 +29,14 @@ Cost: `pub_x_index` is a per-post pseudonym for the voucher-chain — leaks "the
|
|||
|
||||
## Lead decisions
|
||||
|
||||
- **Per-`V_x` signing keypair `(pub_x, priv_x)`.** Generated when `V_x` is generated (Layer 1). Rotated as a bundle when `V_x` rotates.
|
||||
- **Per-`V_x` signing keypair `(pub_x, priv_x)` is per-post.** Author generates a fresh `(pub_x, priv_x)` for each slot when assembling a post, wraps `priv_x` in that slot's sign-part. No coordination with Layer 1. Per-post rotation (Layer 4) just re-generates all keypairs and re-wraps. Not stable across posts.
|
||||
- **`pub_post_set` is inline in the post header.** List of all admitted `pub_x` for this post's FoF set, random-order per publish. Comments reference entries by index.
|
||||
- **Dual-derivation wrap slots.** Each slot yields BOTH a shared `CEK` (read) and a per-`V_x` `priv_x` (sign). One wrap slot unwrapped = reader gets both capabilities.
|
||||
- **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 author-signed append-only diff to the post.** Appends a revoked `pub_x` entry. Propagation nodes honor on sync. Comments under revoked `pub_x` stop propagating.
|
||||
- **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).
|
||||
- **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.
|
||||
- **`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.
|
||||
|
|
@ -61,10 +63,9 @@ pub enum CommentPolicy {
|
|||
struct PostHeader {
|
||||
// ... existing fields ...
|
||||
|
||||
pub_post_set: Vec<[u8; 32]>, // all admitted pub_x, random-order
|
||||
wrap_slots: Vec<WrapSlot>, // one per V_x (padded to power-of-2 bucket)
|
||||
pub_post_set: Vec<[u8; 32]>, // real pub_x + dummy pubkeys, random-order; .len() == wrap_slots.len()
|
||||
wrap_slots: Vec<WrapSlot>, // real slots + rand(32..=128) dummy slots, shuffled
|
||||
revocation_list: Vec<RevocationEntry>, // initially empty; appended via signed diffs
|
||||
slot_count: u32, // padded count (for bucket alignment)
|
||||
author_sig: [u8; 64], // ed25519 sig over the header
|
||||
}
|
||||
```
|
||||
|
|
@ -186,9 +187,50 @@ Reader side (non-FoF): sees comment ciphertext + signature fields only. Body unr
|
|||
|
||||
1. Author's client detects abuse (via `vouch_mac` attribution, or repeated `pub_x_index` with bad behavior).
|
||||
2. Author builds `RevocationEntry { revoked_pub_x, timestamp, reason_code, author_sig }`, publishes as a header-diff on the post (same propagation primitive as engagement diffs).
|
||||
3. Propagation nodes merge `revocation_list` on next sync. Subsequent comments under the revoked `pub_x` fail step (2) of the accept rule.
|
||||
4. Comments already propagated remain in storage unless GC'd separately (future work).
|
||||
5. If the attacker's voucher-chain is broadly compromised, author can escalate to full `V_x` rotation (Layer 1), which affects every post using that chain — coarse but definitive.
|
||||
3. On receiving a revocation diff, every file-holder of the affected post:
|
||||
- **Deletes** all locally-stored comments on this post where `pub_x_index` points to the revoked `pub_x`.
|
||||
- Appends the entry to its local copy of `post.revocation_list`.
|
||||
- Forwards the revocation diff to neighbors.
|
||||
- Rejects any subsequent incoming comments matching the revoked `pub_x` at step (2) of the accept rule.
|
||||
4. Idempotent: re-receiving a revocation diff is a no-op (the entry is already present; deletion has already happened).
|
||||
5. Retroactive: comments that propagated before the diff existed get deleted as the diff catches up to each holder. There's a propagation-latency window where deleted comments may still be visible on yet-to-receive holders, but the garbage is bounded and self-cleaning.
|
||||
6. If the attacker's voucher-chain is broadly compromised, author can escalate to full `V_x` rotation (Layer 1), which affects every post using that chain — coarse but definitive.
|
||||
|
||||
---
|
||||
|
||||
## Access-grant author comment (post-hoc widening)
|
||||
|
||||
When the author vouches for a new persona AFTER publishing a FoF post, they may want that persona to retroactively gain read access. Full republish would rewrite the post ID and lose engagement history. Instead, the author publishes a special access-grant comment.
|
||||
|
||||
### `AccessGrantComment` (a distinguished `InlineComment` variant)
|
||||
|
||||
```rust
|
||||
struct AccessGrantComment {
|
||||
parent_post_id: PostId,
|
||||
new_pub_x: [u8; 32],
|
||||
new_wrap_slot: WrapSlot, // read + sign parts for the new V_x
|
||||
granted_at_ms: u64,
|
||||
commenter_id: NodeId, // must match post.author
|
||||
identity_sig: [u8; 64], // author identity-key sig over (parent_post_id || new_pub_x || new_wrap_slot || granted_at_ms)
|
||||
}
|
||||
```
|
||||
|
||||
### Propagation-node handling
|
||||
|
||||
- Accept iff `commenter_id == post.author` AND `identity_sig` validates against `post.author`. No `group_sig` / `pub_x_index` required — this is an author-signed header extension disguised as a comment.
|
||||
- On accept: append `new_pub_x` to local `pub_post_set`, append `new_wrap_slot` to local `wrap_slots`. Forward the access-grant to neighbors.
|
||||
- Subsequent comments from the newly-admitted persona reference the extended-set index (base_set.len() + grant_index).
|
||||
|
||||
### Reader-side handling
|
||||
|
||||
- New persona receives the access-grant via normal comment propagation. Unwraps `new_wrap_slot` with their `V_x` → gets `CEK` + `priv_x_new`. Can now read the body's comments and author their own.
|
||||
- Existing readers see the access-grant as an informational entry (optional UI: "Author granted access to a new friend").
|
||||
|
||||
### Open question
|
||||
|
||||
- **Does the UI expose this as a discrete action?** "Share this post with my new vouchee Bob" — yes, natural. Lead leaning: surface as a per-post affordance in author's post-detail view.
|
||||
- **Does the access-grant also carry a signing capability for the new persona?** Yes — the `WrapSlot`'s sign-part wraps a fresh `priv_x_new`. New persona can comment going forward.
|
||||
- **Does revocation apply to access-grant `pub_x` entries?** Yes, uniformly. Author can revoke a post-hoc-granted chain same as an originally-granted chain.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -204,7 +246,7 @@ Reader side (non-FoF): sees comment ciphertext + signature fields only. Body unr
|
|||
| `prefilter_tag` | 2 |
|
||||
| **Subtotal per slot** | **~154** |
|
||||
|
||||
At 500 vouchees: ~77KB header. Padded to 512 bucket ≈ 79KB. Acceptable for a header carried once per post.
|
||||
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.
|
||||
|
||||
Per-comment CDN overhead (vs shared-keypair baseline): +64B (`group_sig`) + 4B (`pub_x_index`) + 64B (`identity_sig` — already in baseline) ≈ 68B additional. Negligible.
|
||||
|
||||
|
|
@ -220,27 +262,32 @@ Decision: **defer Merkle variant to PQ migration**. v1 ships inline.
|
|||
|
||||
## Privacy tradeoff (accepted)
|
||||
|
||||
`pub_x_index` leaks per-post voucher-chain pseudonym to CDN + public observers. "All comments signed under slot 42 came through the same chain" — within a single post. Cross-post, the index-to-chain mapping re-randomizes (new `pub_post_set` order on each publish).
|
||||
`pub_x_index` leaks per-post voucher-chain pseudonym to CDN + public observers. "All comments signed under slot 42 came through the same chain" — within a single post. Cross-post, the index-to-chain mapping re-randomizes (new `pub_post_set` order on each publish), so cross-post correlation is broken.
|
||||
|
||||
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`.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Reader-only clients can skip the sign-slot.** They can short-circuit AEAD on sign-slot and save some work. Worth implementing? Lead leaning: yes, minor perf win; `read` slot succeeding already proves FoF-admission.
|
||||
- **Author's own comments.** Author holds `priv_me` directly (generated alongside `V_me` + `pub_me` at `V_me` genesis — OR regenerated per-post as part of the FoF set assembly — see TBD below). `pub_me` is one of the entries in `pub_post_set`. Author comments pass the same accept rule. Strict mode optional.
|
||||
- **When is `(pub_x, priv_x)` generated?** Two options:
|
||||
- (A) At `V_x` genesis (Layer 1): generated once per `V_x`, stable across all posts. `pub_x` published via bio-post alongside `V_x` wrappers; `priv_x` held by holders of `V_x`.
|
||||
- (B) Per-post: author generates fresh `(pub_x, priv_x)` for each slot when assembling a post, wraps `priv_x` in the sign-slot.
|
||||
|
||||
**(B) is cleaner** because per-post rotation (Layer 4) already re-wraps slots; no coordination with Layer 1 genesis. **(A) is cheaper** because keypair generation is once-per-vouch-grant instead of once-per-post-per-vouchee. Lead leaning: **(B) per-post**, matches the anonymous-slot model and keeps Layer 1 untouched.
|
||||
- **`revocation_list` propagation ordering.** Comments that arrive at a propagation node before the revocation diff arrives will propagate. The revocation applies from its sync-time forward. Acceptable window; not a correctness issue but a propagation-latency consideration.
|
||||
- **Dummy padding slots.** Real slot = prefilter + read + sign ≈ 154B. Dummy must be byte-identical in size. Random bytes that AEAD-fail on any `V_x`. Confirmed indistinguishable structurally.
|
||||
- **`pub_post_set` padding.** Currently sized 1:1 with `wrap_slots` (padded together). Confirm alignment is correct — `pub_x_index` indexes into `pub_post_set` only; dummy slots need dummy pubkeys? Or: `pub_post_set` carries only real pubkeys, `wrap_slots` is padded. A comment's `pub_x_index` would always point at real entries; dummies exist only in wrap_slots to hide the vouch count. Needs resolution.
|
||||
- **Rate limiting.** Operational knob. Per-`commenter_id` + per-`pub_x_index` caps in propagation-node config. Out of spec.
|
||||
- **Non-FoF rendering of comment count.** Do non-FoF readers see "42 FoF comments" or nothing? Privacy: the count leaks engagement. Lead leaning: show count (same as existing Public comment counts; engagement is already a public signal).
|
||||
- **Author's copy of CEK.** Author generated CEK at post creation; does author store it locally keyed by post_id, or unwrap via their own slot like any reader? Lead leaning: local-cache at creation; unwrap-fallback if cache lost.
|
||||
- **Access-grant dedup.** If the author accidentally publishes the same access-grant twice (or two devices race), propagation nodes must handle it idempotently. Keying by `(post_id, new_pub_x)` — duplicate = no-op.
|
||||
- **Revocation of a `pub_x` that has no comments yet.** Fine: future comments under that `pub_x` will fail the accept rule, and the deletion step is a no-op. Confirmed harmless.
|
||||
- **GC of `revocation_list`.** Grows unbounded across a post's lifetime. Realistically capped by vouch-set size. No GC needed in v1.
|
||||
|
||||
## Resolved
|
||||
|
||||
- **`(pub_x, priv_x)` lifecycle**: **per-post** (Scott confirmed). Author regenerates for each post's slot assembly. Matches the anonymous-slot model; no Layer 1 coordination.
|
||||
- **`pub_post_set` vs `wrap_slots` alignment**: **1:1 with dummy pubkeys** (Scott's direction). Both padded together with `rand(32..=128)` dummies so observers can't infer vouch count, and can't infer a floor across multiple posts either.
|
||||
- **Author's own entry**: **yes** (Scott confirmed). Author has their own `pub_me/priv_me` entry in `pub_post_set`; own comments pass the same accept rule.
|
||||
- **Non-FoF comment visibility**: front-end shows "Comments are private" when no key unlocks. Optional "Request access via DM" affordance. Count NOT shown. Author can respond to a request by publishing an access-grant author comment.
|
||||
- **Revocation semantics**: **retroactive delete + forward** (Scott's correction). File-holders delete comments signed by the revoked `pub_x` on arrival of the diff, then propagate.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -248,10 +295,13 @@ The reason this is acceptable: the alternative is no propagation-level verificat
|
|||
|
||||
- `CommentPolicy::FriendsOfFriends` end-to-end (storage, protocol, UI picker).
|
||||
- Dual-derivation wrap slot: read → CEK, sign → priv_x. AEAD with `post_id` AAD.
|
||||
- `pub_post_set` inline in header; `pub_x_index` on comments.
|
||||
- `pub_post_set` inline in header, 1:1 with `wrap_slots`, real + dummy entries.
|
||||
- `pub_x_index` on comments.
|
||||
- CEK_comments derived via HKDF; all comment bodies encrypted.
|
||||
- Propagation nodes enforce four-check accept rule before forwarding.
|
||||
- Revocation diff format + CDN honor path.
|
||||
- Per-post ephemeral-keypair generation (option B above, pending decision).
|
||||
- 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.
|
||||
- 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` (rejected at first-hop CDN). A revokes B's `pub_x`. C's subsequent comments (which went through B's chain) fail CDN verification and stop propagating.
|
||||
- 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue