v0.4.1: Security hardening, lock contention fixes, data cleanup

Security:
- Reaction signatures: ed25519 sign/verify (sign_reaction, verify_reaction_signature)
  Backward-compatible — unsigned reactions from old nodes still accepted
- Comment signature verification: verify_comment_signature now called on receipt
- Reaction removal authorization: only reactor or post author can remove
- BlobHeader author verification: lookup actual author from storage, don't trust payload

Lock contention (4 fixes):
- ManifestPush discovery: cm lock released before PostFetch I/O
- Pull request handler: load under lock, filter without lock, brief re-lock for is_deleted
- Pull sender: split into two brief locks (store posts, then batch upstream+sync)
- Engagement checker: batch all chunk results, single lock for writes

Data cleanup:
- Post deletion cleans post_downstream, post_upstream, seen_engagement tables
- Added TODO-hardening.md documenting remaining DOS/security/lock/data issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-21 19:30:38 -04:00
parent bbaacf9b6c
commit bb6f2b64b0
11 changed files with 500 additions and 138 deletions

View file

@ -549,6 +549,7 @@ pub fn random_slot_noise(size: usize) -> Vec<u8> {
const REACTION_WRAP_CONTEXT: &str = "itsgoin/private-reaction/v1";
const COMMENT_SIGN_CONTEXT: &str = "itsgoin/comment-sig/v1";
const REACTION_SIGN_CONTEXT: &str = "itsgoin/reaction-sig/v1";
/// Encrypt a private reaction payload (only the post author can decrypt).
/// Uses X25519 DH between reactor and author, then ChaCha20-Poly1305.
@ -645,6 +646,47 @@ pub fn verify_comment_signature(
verifying_key.verify(digest.as_bytes(), &sig).is_ok()
}
/// Sign a reaction: ed25519 over BLAKE3(reactor || post_id || emoji || timestamp_ms).
pub fn sign_reaction(
seed: &[u8; 32],
reactor: &NodeId,
post_id: &PostId,
emoji: &str,
timestamp_ms: u64,
) -> Vec<u8> {
let signing_key = SigningKey::from_bytes(seed);
let mut hasher = blake3::Hasher::new_derive_key(REACTION_SIGN_CONTEXT);
hasher.update(reactor);
hasher.update(post_id);
hasher.update(emoji.as_bytes());
hasher.update(&timestamp_ms.to_le_bytes());
let digest = hasher.finalize();
signing_key.sign(digest.as_bytes()).to_bytes().to_vec()
}
/// Verify a reaction's ed25519 signature.
pub fn verify_reaction_signature(
reactor: &NodeId,
post_id: &PostId,
emoji: &str,
timestamp_ms: u64,
signature: &[u8],
) -> bool {
let Ok(verifying_key) = VerifyingKey::from_bytes(reactor) else {
return false;
};
let Ok(sig) = ed25519_dalek::Signature::from_slice(signature) else {
return false;
};
let mut hasher = blake3::Hasher::new_derive_key(REACTION_SIGN_CONTEXT);
hasher.update(reactor);
hasher.update(post_id);
hasher.update(emoji.as_bytes());
hasher.update(&timestamp_ms.to_le_bytes());
let digest = hasher.finalize();
verifying_key.verify(digest.as_bytes(), &sig).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;