feat(fof-layer2): wire types — WrapSlot, FoFCommentGating, CommentPermission::FriendsOfFriends
Adds the on-wire shapes for FoF Mode 2 comment-gating per
docs/fof-spec/layer-2-mode2-fof-comments.md:
- WrapSlot: per-V_x slot with 2B prefilter_tag + 48B read_ciphertext
+ 48B sign_ciphertext (sealed CEK + sealed priv_x_seed). 98 bytes
total per slot. Receiver trial-decrypts via prefilter match.
- FoFCommentGating: author-published gating block embedded in
Post.fof_gating. Carries slot_binder_nonce (32B random; replaces
spec's circular "post_id in HKDF info"), pub_post_set (1:1 with
wrap_slots, includes dummy pubkeys), wrap_slots, and revocation_list
(initially empty; revocation diffs accumulate on the BlobHeader copy).
- RevocationEntry: author-signed entry triggering retroactive comment
delete + pub_post_set removal on every file-holder that receives it.
- CommentPermission gains FriendsOfFriends variant. Existing match arm
in connection.rs handle-incoming-diff path is extended with a
"drop pending CDN four-check verification" stub (full verify in a
later slice).
- InlineComment extended with three optional fields:
pub_x_index: index into parent post's pub_post_set
group_sig: 64B ed25519 sig under priv_x
encrypted_payload: ChaCha20-Poly1305 ciphertext under CEK_comments
All #[serde(default)] for back-compat. Old comments deserialize
cleanly with None.
- Post gains optional fof_gating field for the author-signed snapshot
at publish time. PostId = BLAKE3(Post) covers it, so any tampering
is detectable. Mutations (revocation, access-grant) arrive later as
diffs against the local BlobHeader copy.
All 21 existing Post construction sites + 4 existing InlineComment
sites updated via perl -0pe sweeps to pass None for the new fields.
Full test suite: 134/134 pass (4 new slot crypto + 130 existing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
74fec3b1fb
commit
0f5147a31c
11 changed files with 148 additions and 0 deletions
|
|
@ -143,6 +143,7 @@ pub fn build_announcement_post(
|
||||||
content: serde_json::to_string(&content).unwrap_or_default(),
|
content: serde_json::to_string(&content).unwrap_or_default(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6171,6 +6171,12 @@ impl ConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
crate::types::CommentPermission::Public => {}
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !crate::crypto::verify_comment_signature(
|
if !crate::crypto::verify_comment_signature(
|
||||||
&comment.author,
|
&comment.author,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ mod tests {
|
||||||
content: "hello world".to_string(),
|
content: "hello world".to_string(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: 1000,
|
timestamp_ms: 1000,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let id1 = compute_post_id(&post);
|
let id1 = compute_post_id(&post);
|
||||||
let id2 = compute_post_id(&post);
|
let id2 = compute_post_id(&post);
|
||||||
|
|
@ -36,12 +37,14 @@ mod tests {
|
||||||
content: "hello".to_string(),
|
content: "hello".to_string(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: 1000,
|
timestamp_ms: 1000,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let post2 = Post {
|
let post2 = Post {
|
||||||
author: [1u8; 32],
|
author: [1u8; 32],
|
||||||
content: "world".to_string(),
|
content: "world".to_string(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: 1000,
|
timestamp_ms: 1000,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
assert_ne!(compute_post_id(&post1), compute_post_id(&post2));
|
assert_ne!(compute_post_id(&post1), compute_post_id(&post2));
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +56,7 @@ mod tests {
|
||||||
content: "test".to_string(),
|
content: "test".to_string(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: 1000,
|
timestamp_ms: 1000,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let id = compute_post_id(&post);
|
let id = compute_post_id(&post);
|
||||||
assert!(verify_post_id(&id, &post));
|
assert!(verify_post_id(&id, &post));
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,7 @@ pub fn build_delete_control_post(
|
||||||
content: serde_json::to_string(&op).unwrap_or_default(),
|
content: serde_json::to_string(&op).unwrap_or_default(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,6 +183,7 @@ pub fn build_visibility_control_post(
|
||||||
content: serde_json::to_string(&op).unwrap_or_default(),
|
content: serde_json::to_string(&op).unwrap_or_default(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,6 +214,7 @@ mod tests {
|
||||||
content: "hello".to_string(),
|
content: "hello".to_string(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: 1000,
|
timestamp_ms: 1000,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let post_id = crate::content::compute_post_id(&post);
|
let post_id = crate::content::compute_post_id(&post);
|
||||||
s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap();
|
s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap();
|
||||||
|
|
@ -240,6 +243,7 @@ mod tests {
|
||||||
content: "hello".to_string(),
|
content: "hello".to_string(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: 1000,
|
timestamp_ms: 1000,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let post_id = crate::content::compute_post_id(&post);
|
let post_id = crate::content::compute_post_id(&post);
|
||||||
s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap();
|
s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ pub fn build_distribution_post(
|
||||||
content: ciphertext_b64,
|
content: ciphertext_b64,
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let post_id = compute_post_id(&post);
|
let post_id = compute_post_id(&post);
|
||||||
let visibility = PostVisibility::Encrypted { recipients: wrapped_keys };
|
let visibility = PostVisibility::Encrypted { recipients: wrapped_keys };
|
||||||
|
|
@ -241,6 +242,7 @@ mod tests {
|
||||||
content: ciphertext,
|
content: ciphertext,
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: 200,
|
timestamp_ms: 200,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let forged_vis = PostVisibility::Encrypted { recipients: wrapped };
|
let forged_vis = PostVisibility::Encrypted { recipients: wrapped };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,7 @@ pub async fn import_as_personas(
|
||||||
content: ep.content.clone(),
|
content: ep.content.clone(),
|
||||||
attachments: attachments.clone(),
|
attachments: attachments.clone(),
|
||||||
timestamp_ms: ep.timestamp_ms,
|
timestamp_ms: ep.timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preserve the original visibility intent from the export.
|
// Preserve the original visibility intent from the export.
|
||||||
|
|
@ -459,6 +460,7 @@ pub async fn import_public_posts(
|
||||||
content: ep.content.clone(),
|
content: ep.content.clone(),
|
||||||
attachments: attachments.clone(),
|
attachments: attachments.clone(),
|
||||||
timestamp_ms: ep.timestamp_ms,
|
timestamp_ms: ep.timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read blob data from archive
|
// Read blob data from archive
|
||||||
|
|
@ -685,6 +687,7 @@ pub async fn merge_with_key(
|
||||||
content: plaintext,
|
content: plaintext,
|
||||||
attachments: attachments.clone(),
|
attachments: attachments.clone(),
|
||||||
timestamp_ms: ep.timestamp_ms,
|
timestamp_ms: ep.timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read blob data from archive (may need decryption for encrypted posts)
|
// Read blob data from archive (may need decryption for encrypted posts)
|
||||||
|
|
|
||||||
|
|
@ -2253,6 +2253,7 @@ mod tests {
|
||||||
content: "test".to_string(),
|
content: "test".to_string(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: 1000,
|
timestamp_ms: 1000,
|
||||||
|
fof_gating: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -827,6 +827,7 @@ impl Node {
|
||||||
content: serde_json::to_string(&content).unwrap_or_default(),
|
content: serde_json::to_string(&content).unwrap_or_default(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: pi.created_at,
|
timestamp_ms: pi.created_at,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let post_id = crate::content::compute_post_id(&post);
|
let post_id = crate::content::compute_post_id(&post);
|
||||||
{
|
{
|
||||||
|
|
@ -1162,6 +1163,7 @@ impl Node {
|
||||||
content: final_content,
|
content: final_content,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: now,
|
timestamp_ms: now,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_id = compute_post_id(&post);
|
let post_id = compute_post_id(&post);
|
||||||
|
|
@ -3081,6 +3083,7 @@ impl Node {
|
||||||
content: new_content,
|
content: new_content,
|
||||||
attachments: post.attachments.clone(),
|
attachments: post.attachments.clone(),
|
||||||
timestamp_ms: post.timestamp_ms,
|
timestamp_ms: post.timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let new_post_id = compute_post_id(&new_post);
|
let new_post_id = compute_post_id(&new_post);
|
||||||
|
|
||||||
|
|
@ -4586,6 +4589,9 @@ impl Node {
|
||||||
signature,
|
signature,
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
ref_post_id,
|
ref_post_id,
|
||||||
|
pub_x_index: None,
|
||||||
|
group_sig: None,
|
||||||
|
encrypted_payload: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let storage = self.storage.get().await;
|
let storage = self.storage.get().await;
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ pub fn build_profile_post(
|
||||||
content: serde_json::to_string(&content).unwrap_or_default(),
|
content: serde_json::to_string(&content).unwrap_or_default(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -463,6 +464,7 @@ mod tests {
|
||||||
content: serde_json::to_string(&content).unwrap(),
|
content: serde_json::to_string(&content).unwrap(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply. Auto-scan should fire and store the unwrapped V_me.
|
// Apply. Auto-scan should fire and store the unwrapped V_me.
|
||||||
|
|
@ -531,6 +533,7 @@ mod tests {
|
||||||
content: serde_json::to_string(&content).unwrap(),
|
content: serde_json::to_string(&content).unwrap(),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
apply_profile_post_if_applicable(&s, &post, Some(&VisibilityIntent::Profile)).unwrap();
|
apply_profile_post_if_applicable(&s, &post, Some(&VisibilityIntent::Profile)).unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -893,6 +893,7 @@ impl Storage {
|
||||||
content: row.get(1)?,
|
content: row.get(1)?,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: row.get::<_, i64>(3)? as u64,
|
timestamp_ms: row.get::<_, i64>(3)? as u64,
|
||||||
|
fof_gating: None,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|
@ -919,6 +920,7 @@ impl Storage {
|
||||||
content: row.get(1)?,
|
content: row.get(1)?,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: row.get::<_, i64>(3)? as u64,
|
timestamp_ms: row.get::<_, i64>(3)? as u64,
|
||||||
|
fof_gating: None,
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
)))
|
)))
|
||||||
|
|
@ -1012,6 +1014,7 @@ impl Storage {
|
||||||
content,
|
content,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: timestamp_ms as u64,
|
timestamp_ms: timestamp_ms as u64,
|
||||||
|
fof_gating: None,
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
));
|
));
|
||||||
|
|
@ -1050,6 +1053,7 @@ impl Storage {
|
||||||
content,
|
content,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: timestamp_ms as u64,
|
timestamp_ms: timestamp_ms as u64,
|
||||||
|
fof_gating: None,
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
));
|
));
|
||||||
|
|
@ -1212,6 +1216,7 @@ impl Storage {
|
||||||
content,
|
content,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: timestamp_ms as u64,
|
timestamp_ms: timestamp_ms as u64,
|
||||||
|
fof_gating: None,
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
));
|
));
|
||||||
|
|
@ -1247,6 +1252,7 @@ impl Storage {
|
||||||
content,
|
content,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: timestamp_ms as u64,
|
timestamp_ms: timestamp_ms as u64,
|
||||||
|
fof_gating: None,
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
));
|
));
|
||||||
|
|
@ -2926,6 +2932,7 @@ impl Storage {
|
||||||
content: row.get(2)?,
|
content: row.get(2)?,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: row.get::<_, i64>(4)? as u64,
|
timestamp_ms: row.get::<_, i64>(4)? as u64,
|
||||||
|
fof_gating: None,
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
));
|
));
|
||||||
|
|
@ -5302,6 +5309,9 @@ impl Storage {
|
||||||
signature: sig,
|
signature: sig,
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
ref_post_id,
|
ref_post_id,
|
||||||
|
pub_x_index: None,
|
||||||
|
group_sig: None,
|
||||||
|
encrypted_payload: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|
@ -5341,6 +5351,9 @@ impl Storage {
|
||||||
signature: sig,
|
signature: sig,
|
||||||
deleted_at: del.map(|v| v as u64),
|
deleted_at: del.map(|v| v as u64),
|
||||||
ref_post_id,
|
ref_post_id,
|
||||||
|
pub_x_index: None,
|
||||||
|
group_sig: None,
|
||||||
|
encrypted_payload: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|
@ -6100,6 +6113,7 @@ mod tests {
|
||||||
content: format!("post at {}", ts),
|
content: format!("post at {}", ts),
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
timestamp_ms: ts,
|
timestamp_ms: ts,
|
||||||
|
fof_gating: None,
|
||||||
};
|
};
|
||||||
let id = blake3::hash(&serde_json::to_vec(&post).unwrap());
|
let id = blake3::hash(&serde_json::to_vec(&post).unwrap());
|
||||||
s.store_post(id.as_bytes(), &post).unwrap();
|
s.store_post(id.as_bytes(), &post).unwrap();
|
||||||
|
|
@ -6797,6 +6811,9 @@ mod tests {
|
||||||
signature: vec![0u8; 64],
|
signature: vec![0u8; 64],
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
ref_post_id: None,
|
ref_post_id: None,
|
||||||
|
pub_x_index: None,
|
||||||
|
group_sig: None,
|
||||||
|
encrypted_payload: None,
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
|
||||||
s.store_comment(&InlineComment {
|
s.store_comment(&InlineComment {
|
||||||
|
|
@ -6807,6 +6824,9 @@ mod tests {
|
||||||
signature: vec![1u8; 64],
|
signature: vec![1u8; 64],
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
ref_post_id: None,
|
ref_post_id: None,
|
||||||
|
pub_x_index: None,
|
||||||
|
group_sig: None,
|
||||||
|
encrypted_payload: None,
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
|
||||||
let comments = s.get_comments(&post_id).unwrap();
|
let comments = s.get_comments(&post_id).unwrap();
|
||||||
|
|
@ -6832,6 +6852,9 @@ mod tests {
|
||||||
signature: vec![9u8; 64],
|
signature: vec![9u8; 64],
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
ref_post_id: Some(ref_post),
|
ref_post_id: Some(ref_post),
|
||||||
|
pub_x_index: None,
|
||||||
|
group_sig: None,
|
||||||
|
encrypted_payload: None,
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
|
||||||
let live = s.get_comments(&post_id).unwrap();
|
let live = s.get_comments(&post_id).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,15 @@ pub struct Post {
|
||||||
pub attachments: Vec<Attachment>,
|
pub attachments: Vec<Attachment>,
|
||||||
/// Unix timestamp in milliseconds
|
/// Unix timestamp in milliseconds
|
||||||
pub timestamp_ms: u64,
|
pub timestamp_ms: u64,
|
||||||
|
/// FoF Layer 2: author-signed snapshot of the comment-gating
|
||||||
|
/// state at publish time. Carries wrap_slots, pub_post_set, and the
|
||||||
|
/// slot_binder_nonce. `None` on posts without FoF comment gating.
|
||||||
|
/// Covered by `PostId = BLAKE3(Post)` so any forgery is detectable.
|
||||||
|
/// Revocations and access-grants arrive later as engagement diffs
|
||||||
|
/// against the local BlobHeader copy; this field is the snapshot at
|
||||||
|
/// t=0, not the live mutable state.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fof_gating: Option<FoFCommentGating>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A reference to a media blob attached to a post
|
/// A reference to a media blob attached to a post
|
||||||
|
|
@ -918,6 +927,9 @@ pub struct InlineComment {
|
||||||
pub post_id: PostId,
|
pub post_id: PostId,
|
||||||
/// Either the full comment text (short comments) or a short preview of
|
/// Either the full comment text (short comments) or a short preview of
|
||||||
/// the referenced post (when `ref_post_id` is set).
|
/// the referenced post (when `ref_post_id` is set).
|
||||||
|
///
|
||||||
|
/// On FoF-policy posts this field is empty — the body lives encrypted
|
||||||
|
/// in `encrypted_payload`. Non-FoF readers see no text at all.
|
||||||
pub content: String,
|
pub content: String,
|
||||||
/// When the comment was created (ms)
|
/// When the comment was created (ms)
|
||||||
pub timestamp_ms: u64,
|
pub timestamp_ms: u64,
|
||||||
|
|
@ -932,6 +944,23 @@ pub struct InlineComment {
|
||||||
/// for the expanded view.
|
/// for the expanded view.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ref_post_id: Option<PostId>,
|
pub ref_post_id: Option<PostId>,
|
||||||
|
/// FoF Layer 2: index into the parent post's `pub_post_set`
|
||||||
|
/// identifying which voucher-chain signed this comment. `None` on
|
||||||
|
/// non-FoF comments. CDN propagation nodes verify `group_sig`
|
||||||
|
/// against `pub_post_set[pub_x_index]` before forwarding.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pub_x_index: Option<u32>,
|
||||||
|
/// FoF Layer 2: 64-byte ed25519 signature under priv_x over
|
||||||
|
/// `(encrypted_payload || parent_post_id || pub_x_index)`. Verified
|
||||||
|
/// at CDN-level against `pub_post_set[pub_x_index]`.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub group_sig: Option<Vec<u8>>,
|
||||||
|
/// FoF Layer 2: ChaCha20-Poly1305 ciphertext under CEK_comments
|
||||||
|
/// (derived from CEK via HKDF). Plaintext is the JSON-encoded
|
||||||
|
/// comment body + optional vouch_mac + optional parent_comment_id.
|
||||||
|
/// Non-FoF observers see only ciphertext + sigs — body is opaque.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub encrypted_payload: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Permission level for comments on a post
|
/// Permission level for comments on a post
|
||||||
|
|
@ -944,6 +973,13 @@ pub enum CommentPermission {
|
||||||
FollowersOnly,
|
FollowersOnly,
|
||||||
/// Comments disabled
|
/// Comments disabled
|
||||||
None,
|
None,
|
||||||
|
/// FoF Layer 2: commenter must hold one of the V_x keys in the
|
||||||
|
/// author's keyring (own V_me + every V_x they received). The author
|
||||||
|
/// publishes pub_post_set + wrap_slots in the post; commenters trial-
|
||||||
|
/// decrypt to unlock priv_x for signing. CDN nodes verify the
|
||||||
|
/// comment's group_sig + pub_x_index before forwarding — kills the
|
||||||
|
/// bandwidth-DoS attack a single admitted FoF member could mount.
|
||||||
|
FriendsOfFriends,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CommentPermission {
|
impl Default for CommentPermission {
|
||||||
|
|
@ -987,6 +1023,65 @@ impl Default for ModerationMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// FoF Layer 2: per-V_x wrap slot in a post header. Dual-derived so
|
||||||
|
/// one successful AEAD-open yields both the read CEK and the per-V_x
|
||||||
|
/// signing seed. Real slots and dummy padding slots are byte-identical
|
||||||
|
/// (98 bytes each); receivers identify "their" slot by successful
|
||||||
|
/// AEAD decryption, not by position.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WrapSlot {
|
||||||
|
/// 2-byte HMAC prefix. Receivers precompute one per held V_x; the
|
||||||
|
/// scan iterates only slots whose prefilter matches.
|
||||||
|
pub prefilter_tag: [u8; 2],
|
||||||
|
/// AEAD ciphertext under KDF(V_x, slot_binder_nonce, "read"); 48B
|
||||||
|
/// (32B sealed CEK + 16B tag).
|
||||||
|
pub read_ciphertext: Vec<u8>,
|
||||||
|
/// AEAD ciphertext under KDF(V_x, slot_binder_nonce, "sign"); 48B
|
||||||
|
/// (32B sealed priv_x ed25519 seed + 16B tag).
|
||||||
|
pub sign_ciphertext: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FoF Layer 2: author-signed revocation entry. When a post-holder
|
||||||
|
/// receives a valid revocation diff, it deletes all locally-stored
|
||||||
|
/// comments signed by `revoked_pub_x` AND removes the entry from its
|
||||||
|
/// local pub_post_set, then forwards the diff. Retroactive cleanup.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RevocationEntry {
|
||||||
|
/// The pub_x being revoked. Must be in the post's pub_post_set
|
||||||
|
/// at the time the diff is processed.
|
||||||
|
pub revoked_pub_x: [u8; 32],
|
||||||
|
/// ms since epoch.
|
||||||
|
pub revoked_at_ms: u64,
|
||||||
|
/// Opaque to CDN; used by author UI to display the reason.
|
||||||
|
pub reason_code: u8,
|
||||||
|
/// 64-byte ed25519 signature by the post author over
|
||||||
|
/// (post_id || revoked_pub_x || revoked_at_ms || reason_code).
|
||||||
|
pub author_sig: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FoF Layer 2: the author-published gating block embedded in a
|
||||||
|
/// FoF-comment-policy post. Carries the wrap slots + the matching
|
||||||
|
/// pub_post_set + the slot_binder_nonce. The `revocation_list` is
|
||||||
|
/// initially empty; revocation diffs append over the post's lifetime.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct FoFCommentGating {
|
||||||
|
/// Random 32B nonce. Plays the spec's "post_id in HKDF info" role
|
||||||
|
/// without circularity (PostId = BLAKE3(post) depends on this field).
|
||||||
|
pub slot_binder_nonce: [u8; 32],
|
||||||
|
/// All admitted pub_x's, 1:1 with `wrap_slots` (including dummies).
|
||||||
|
/// Order is randomized at publish; access-grants append at the tail
|
||||||
|
/// (Layer 3 resolved decision — pub_x_index stability matters more
|
||||||
|
/// than the small tail-positional-recency leak).
|
||||||
|
pub pub_post_set: Vec<[u8; 32]>,
|
||||||
|
/// Real wrap slots + dummy slots, shuffled at publish. 1:1 with
|
||||||
|
/// `pub_post_set`.
|
||||||
|
pub wrap_slots: Vec<WrapSlot>,
|
||||||
|
/// Initially empty. Receivers accumulate revocations as diffs
|
||||||
|
/// arrive; the on-wire t=0 snapshot is empty.
|
||||||
|
#[serde(default)]
|
||||||
|
pub revocation_list: Vec<RevocationEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Author-controlled engagement policy for a post
|
/// Author-controlled engagement policy for a post
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct CommentPolicy {
|
pub struct CommentPolicy {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue