feat(fof-layer2): CDN four-check verification on incoming FoF comments

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) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-05-14 14:06:34 -04:00
parent 00522f4c4b
commit 63ff5ad6eb

View file

@ -6172,11 +6172,49 @@ impl ConnectionManager {
} }
crate::types::CommentPermission::Public => {} crate::types::CommentPermission::Public => {}
crate::types::CommentPermission::FriendsOfFriends => { crate::types::CommentPermission::FriendsOfFriends => {
// FoF four-check verification gate lives // FoF Layer 2 CDN four-check accept rule:
// in a future slice; for now treat as // 1. parent post must carry fof_gating
// "drop until verified" (safest default). // (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; 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( if !crate::crypto::verify_comment_signature(
&comment.author, &comment.author,