docs: FoF-gating spec skeleton (hand-off to Opus)
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>
This commit is contained in:
parent
d118daee28
commit
1fdf9a94cc
8 changed files with 867 additions and 0 deletions
122
docs/fof-spec/layer-4-keypair-rotation.md
Normal file
122
docs/fof-spec/layer-4-keypair-rotation.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Layer 4 — Per-Post Keypair Rotation
|
||||
|
||||
**Scope**: Graceful rotation of `(priv_post, pub_post)` when the author's FoF set changes (new vouches granted, `V_me` rotated, or a vouchee removed). Old comments remain verifiable under the old `pub_post`; new comments require the new `pub_post`.
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
- Author can update the FoF set of an existing post without deleting / recreating it.
|
||||
- A `PostKeyRotation` record, signed by author identity key, carries a new `(priv_post', pub_post')` wrapped under the current keyring.
|
||||
- Existing comments under old `pub_post` stay cryptographically valid.
|
||||
- New comments must sign under `priv_post'`.
|
||||
- Readers who were admitted under the OLD set but not the NEW one retain read access to the body (CEK didn't change) but can no longer produce accepted comments.
|
||||
|
||||
---
|
||||
|
||||
## Lead decisions
|
||||
|
||||
- **Body CEK is NOT rotated.** Only the signing keypair rotates. Rotation's purpose is to narrow (or widen) the *commenter* set going forward, not revoke read access to the body. Read access of already-distributed content is non-recoverable by design — if an author needs that, they delete the post.
|
||||
- **Rotation is append-only.** Rotation records accumulate; `pub_post_n` is derived from the latest rotation record. Old `pub_post` values are retained for verifying already-posted comments.
|
||||
- **Rotation is optional.** Simple case is a post with one immutable `pub_post`. Layer 4 adds the escape hatch; most posts never rotate.
|
||||
- **Author-signed.** Only the post author (identity key) can rotate. Prevents an admitted commenter from rotating others out.
|
||||
|
||||
---
|
||||
|
||||
## Data model
|
||||
|
||||
### `PostKeyRotation` record
|
||||
|
||||
```rust
|
||||
struct PostKeyRotation {
|
||||
post_id: PostId,
|
||||
rotation_index: u32, // monotonic; 0 = original keypair (implicit), 1 = first rotation, etc.
|
||||
new_pub_post: [u8; 32],
|
||||
new_wrap_slots: Vec<WrapSlot>, // wraps new priv_post under current keyring (same as post creation)
|
||||
superseded_at: u64, // ms; rotation timestamp
|
||||
sig: [u8; 64], // author identity-key signature over the above
|
||||
}
|
||||
```
|
||||
|
||||
Persisted as a sidecar to the post. TBD — OPUS: whether this lives in its own table (`post_key_rotations`) or as a serialized column on the post.
|
||||
|
||||
### Comment verification after rotation
|
||||
|
||||
Each comment's `group_sig` is verified against a specific `pub_post`. Determination rule:
|
||||
|
||||
- If `comment.created_at < rotation_1.superseded_at` → verify against `pub_post_0` (original).
|
||||
- If `rotation_n.superseded_at ≤ comment.created_at < rotation_{n+1}.superseded_at` → verify against `rotation_n.new_pub_post`.
|
||||
- If `comment.created_at ≥ latest_rotation.superseded_at` → verify against latest `new_pub_post`.
|
||||
|
||||
TBD — OPUS: time-based bucketing requires trusting `comment.created_at`. Alternative: comment carries an explicit `pub_post_index` field pointing at which keypair generation it's signed under. Lead leaning: **explicit index field in comment**, avoids clock-skew ambiguity.
|
||||
|
||||
### Extend `InlineComment` (from Layer 2)
|
||||
|
||||
```rust
|
||||
struct InlineComment {
|
||||
// ... existing fields ...
|
||||
#[serde(default)]
|
||||
pub_post_index: u32, // 0 for original keypair, n for rotation n
|
||||
group_sig: ...,
|
||||
vouch_mac: ...,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation flow (author side)
|
||||
|
||||
1. Author changes FoF-relevant state (new vouch granted, someone un-vouched, `V_me` rotated).
|
||||
2. Author decides to re-gate a specific post's comments: UI action "rotate comment keys for this post."
|
||||
3. Generate new `(priv_post', pub_post')`.
|
||||
4. Re-wrap `priv_post'` under the author's CURRENT keyring (the same algorithm as initial post creation, Layer 3).
|
||||
5. Build `PostKeyRotation` record, sign, publish.
|
||||
6. Rotation record propagates via normal CDN (it's a diff on the post, same mechanism as engagement diffs).
|
||||
|
||||
---
|
||||
|
||||
## Reader/commenter side
|
||||
|
||||
- On receiving a `PostKeyRotation` record, readers store it keyed by `(post_id, rotation_index)`.
|
||||
- At comment-creation time: look up the **latest** rotation record for the parent post; trial-decrypt the new slots; if success, use `priv_post'` to sign, set `pub_post_index` to latest index.
|
||||
- At comment-verification time: look up the rotation referenced by `comment.pub_post_index`; verify against that `pub_post`.
|
||||
|
||||
If a reader can unwrap `rotation_n.wrap_slots` but NOT `rotation_{n+1}.wrap_slots`, they've been rotated out between n and n+1. They can still READ the body (CEK unchanged) and verify historical comments; they cannot author new comments after rotation n+1.
|
||||
|
||||
---
|
||||
|
||||
## Propagation
|
||||
|
||||
Rotation records are signed post-deltas. They reuse the existing `BlobHeaderDiff` propagation mechanism — the post header gains the rotation's `pub_post` and `wrap_slots`; readers pull updated posts through the normal CDN.
|
||||
|
||||
TBD — OPUS: whether rotation records are part of the post's BlobHeader diff (natural fit) or a separate post-referencing record (cleaner separation but more protocol surface).
|
||||
|
||||
---
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Multiple rotations in short succession.** Monotonic index + explicit `pub_post_index` on comments disambiguates. No clock dependency.
|
||||
- **Comment authored against stale rotation.** If a commenter creates a comment under rotation n, but by the time it propagates, rotation n+1 exists — the comment is still valid against rotation n. Readers verify against the rotation the comment declares.
|
||||
- **Attacker forges rotation.** Rotation is signed by author identity key. Forgery == identity key compromise, which is outside FoF scope.
|
||||
- **Reader never sees rotation record but sees new comments.** Until the rotation record arrives, new comments appear "unverified." Filter-out at render leaves them as pending until rotation arrives. Standard eventually-consistent behavior.
|
||||
|
||||
---
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Rotation on `V_me` rotation.** When a persona rotates their own `V_me`, do we auto-rotate every one of their open FoFClosed posts? Cost is O(posts × eligible_keys). Lead leaning: **no auto-rotation**; user opts in per-post. Posts without rotation continue to accept comments from the old keyring (which still holds the old `V_me`).
|
||||
- **Garbage-collecting old rotation records.** They're needed to verify historical comments. Never GC'd? Or keep only most recent N? Lead leaning: keep all; historical comments don't re-verify often and the data is cheap.
|
||||
- **UI for rotation.** "Update who can comment" button on post. Simple. No scheduling / batch rotation UI in v1.
|
||||
- **Rotation without keyring change.** Could be used to kick out a specific commenter by rotating and manually excluding their winning `V_x`. But winning `V_x` isn't known to the author (wrap slots are anonymous). Practical effect: authors can widen or narrow the whole set, not surgically exclude one person, without additional support.
|
||||
|
||||
---
|
||||
|
||||
## Ship criteria for Layer 4
|
||||
|
||||
- `PostKeyRotation` record type exists end-to-end.
|
||||
- Author UI action: "Update who can comment on this post."
|
||||
- Rotation records propagate via CDN.
|
||||
- Comment signing uses latest rotation's `priv_post`; `pub_post_index` attached.
|
||||
- Comment verification routes to the correct `pub_post` via index.
|
||||
- Back-compat: posts without rotation records are handled as `pub_post_index = 0` uniformly.
|
||||
- Integration test: A posts FoFClosed. B comments (admitted). A vouches for C, rotates. C comments (admitted). Old B-comment still verifies; new B-comment still verifies (B still in keyring); new D-comment (never admitted) rejected.
|
||||
Loading…
Add table
Add a link
Reference in a new issue