fix(security): authenticate gossip payloads (S2 — sign + verify, replay-bound)
The gossip `author` field was self-asserted, so an in-room member could forge it to impersonate (chat), force-evict (Leave), or poison presence + the address book (Announce). Now every GossipPayload is signed with the node's ed25519 secret key and verified on receipt; a forged author can't validate because the attacker lacks the victim's key. Done as the complete fix (forgery + replay): - GossipPayload gains `ts` (sender-stamped) + `sig` (iroh::Signature, serde-64B); Debug is hand-written since Signature has none. - Signature covers domain tag + room topic_id + author + ts + msg (`signable_bytes`): topic binding blocks cross-room replay, ts + a 2-min freshness window block temporal replay (within-window replays are byte- identical and de-duped by the swarm), author binding makes spoofing fail. - New pure seams `sign_gossip` / `verify_gossip` (+ `GossipReject`); all four outgoing broadcasts sign, the receive loop verifies-then-trusts (drops unauthenticated/stale before any peer-map / event / address-book action). - IrohGossipState now holds the node SecretKey + active topic bytes; callers (core, test_net) updated. NOTE: breaking gossip wire change — all peers must run this build (the staged friend release + dopedart need rebuild). Node identity is ephemeral, so no migration concern beyond rebuild. +5 unit tests (genuine accept; forged author, tampered msg, cross-room, stale/ future all rejected). 163 lib tests (was 158), clippy --all-targets clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-4
@@ -14,7 +14,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let lookup_a = iroh::address_lookup::memory::MemoryLookup::new();
|
||||
let secret_a = iroh::SecretKey::generate();
|
||||
let endpoint_a = Endpoint::builder(presets::N0)
|
||||
.secret_key(secret_a)
|
||||
.secret_key(secret_a.clone())
|
||||
.address_lookup(lookup_a.clone())
|
||||
.bind()
|
||||
.await?;
|
||||
@@ -27,13 +27,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.accept(iroh_gossip::net::GOSSIP_ALPN, gossip_a.clone())
|
||||
.spawn();
|
||||
|
||||
let room_a = IrohGossipState::new(endpoint_a.clone(), gossip_a.clone(), lookup_a.clone());
|
||||
let room_a = IrohGossipState::new(endpoint_a.clone(), gossip_a.clone(), lookup_a.clone(), secret_a);
|
||||
|
||||
// 2. Node B (Client) Setup
|
||||
let lookup_b = iroh::address_lookup::memory::MemoryLookup::new();
|
||||
let secret_b = iroh::SecretKey::generate();
|
||||
let endpoint_b = Endpoint::builder(presets::N0)
|
||||
.secret_key(secret_b)
|
||||
.secret_key(secret_b.clone())
|
||||
.address_lookup(lookup_b.clone())
|
||||
.bind()
|
||||
.await?;
|
||||
@@ -46,7 +46,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.accept(iroh_gossip::net::GOSSIP_ALPN, gossip_b.clone())
|
||||
.spawn();
|
||||
|
||||
let room_b = IrohGossipState::new(endpoint_b.clone(), gossip_b.clone(), lookup_b.clone());
|
||||
let room_b = IrohGossipState::new(endpoint_b.clone(), gossip_b.clone(), lookup_b.clone(), secret_b);
|
||||
|
||||
// 3. Create room on Node A
|
||||
let topic_id = rand::random();
|
||||
|
||||
Reference in New Issue
Block a user