From 63ff5ad6eb159709b20b308130a60aaae5c0315e Mon Sep 17 00:00:00 2001 From: Scott Reimers Date: Thu, 14 May 2026 14:06:34 -0400 Subject: [PATCH] feat(fof-layer2): CDN four-check verification on incoming FoF comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the propagation-side accept rule per docs/fof-spec/layer-2-mode2-fof-comments.md. When a BlobHeaderDiffOp:: AddComment arrives for a post whose CommentPolicy.allow_comments is FriendsOfFriends, the receive path now: 1. Looks up the parent post in storage. If the post lacks fof_gating, drop (policy says FoF but no key material to verify against). 2. Calls fof::verify_fof_group_sig (which folds together: valid pub_x_index range + Ed25519 verify of group_sig against pub_post_set[pub_x_index] over the binding tuple). 3. Checks pub_post_set[pub_x_index] is NOT in fof_gating.revocation_list (initially empty; revocation diffs land in a future slice but the check is in place now). 4. Continues to the existing identity_sig verify step. Any failure → continue (drop, don't store, don't forward). This kills the bandwidth-amplification DoS that a single admitted FoF member could otherwise mount by spamming forged group_sigs. Receive-side storage of FoF comments is via the existing storage.store_comment call; the InlineComment shape carries the FoF fields (pub_x_index, group_sig, encrypted_payload) through unchanged. 139 tests pass (relay_cooldown flake is pre-existing and unrelated). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/core/src/connection.rs | 46 ++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/crates/core/src/connection.rs b/crates/core/src/connection.rs index e7b1fb5..2c27307 100644 --- a/crates/core/src/connection.rs +++ b/crates/core/src/connection.rs @@ -6172,10 +6172,48 @@ impl ConnectionManager { } crate::types::CommentPermission::Public => {} crate::types::CommentPermission::FriendsOfFriends => { - // FoF four-check verification gate lives - // in a future slice; for now treat as - // "drop until verified" (safest default). - continue; + // FoF Layer 2 CDN four-check accept rule: + // 1. parent post must carry fof_gating + // (otherwise the policy is ambient + // with no key material to verify); + // 2. pub_x_index must point at a real + // entry in pub_post_set; + // 3. group_sig must validate against + // pub_post_set[pub_x_index]; + // 4. revocation_list must not contain + // pub_post_set[pub_x_index]; + // 5. identity_sig (existing comment + // signature field) verified below. + // + // Failures drop the comment without + // forwarding — kills the bandwidth-DoS + // attack an admitted-but-malicious FoF + // member could otherwise mount. + let parent = match storage.get_post(&payload.post_id) { + Ok(Some(p)) => p, + _ => continue, + }; + let Some(gating) = parent.fof_gating.as_ref() else { continue; }; + if !crate::fof::verify_fof_group_sig(comment, gating) { + continue; + } + // Revocation check (step 4). The + // revocation_list on the post's stored + // copy is the on-publish snapshot; + // revocation diffs that arrive later + // are applied against the local + // BlobHeader copy (separate slice). + if let Some(idx) = comment.pub_x_index { + let pub_x = gating.pub_post_set + .get(idx as usize) + .copied(); + if let Some(pub_x) = pub_x { + if gating.revocation_list.iter() + .any(|r| r.revoked_pub_x == pub_x) { + continue; + } + } + } } } if !crate::crypto::verify_comment_signature(