fix(federation): end the perpetual peer-joined "Invalid signature" storm
Root cause observed live 2026-08-16: onboarding/seed-restore rewrite identity/node_key on disk but server_info.pubkey is only seeded at boot, so until the next restart every peer-joined advertised the stale boot key while signing with the new seed-derived key — deterministically rejected by every receiver, once per 90s heal tick, forever. - seed.generate / seed.restore now refresh server_info.pubkey in the live snapshot immediately (mirrors the DID-rotation handler). - The 90s heal loop advertises the SAME key it signs with (disk identity, like federation sync already did) instead of the boot snapshot. - notify_join no longer logs "delivered" for an HTTP-200 JSON-RPC rejection; in-band errors are terminal (identical signed bytes can never succeed on retry). - The heal loop backs off per peer (doubling toward a daily re-assert) instead of re-notifying every 90s forever — Observer-held peers never appear in Trusted-only exported hints, so they_list_us could never become true for them. - Receiver now binds the DID to the advertised pubkey (the old check was self-referential) and logs malformed signatures distinctly from genuine mismatches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
809f7649a4
commit
519fa68c72
@@ -696,6 +696,22 @@ impl RpcHandler {
|
||||
anyhow::bail!("Refusing to peer with self");
|
||||
}
|
||||
|
||||
// Bind the DID to the advertised pubkey. Without this the signature
|
||||
// check below is self-referential (the caller signs over a pubkey it
|
||||
// also supplies), so a consistent-but-unrelated keypair would pass.
|
||||
// The DID-rotation handler already enforces the same invariant.
|
||||
match identity::did_key_from_pubkey_hex(pubkey) {
|
||||
Ok(derived) if derived == did => {}
|
||||
Ok(derived) => {
|
||||
tracing::warn!(peer_did = %did, derived_did = %derived, "Rejected peer-joined: DID does not match pubkey");
|
||||
anyhow::bail!("DID does not match pubkey");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(peer_did = %did, error = %e, "Rejected peer-joined: invalid pubkey");
|
||||
anyhow::bail!("Invalid pubkey");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify ed25519 signature to prevent federation spoofing (H2 security fix)
|
||||
let signature = params.get("signature").and_then(|v| v.as_str());
|
||||
match signature {
|
||||
@@ -703,10 +719,16 @@ impl RpcHandler {
|
||||
let sign_data = format!("peer-joined:{}:{}:{}", did, onion, pubkey);
|
||||
match identity::NodeIdentity::verify(pubkey, sign_data.as_bytes(), sig) {
|
||||
Ok(true) => {}
|
||||
_ => {
|
||||
Ok(false) => {
|
||||
tracing::warn!(peer_did = %did, "Rejected peer-joined: invalid signature");
|
||||
anyhow::bail!("Invalid signature");
|
||||
}
|
||||
Err(e) => {
|
||||
// Malformed hex / wrong length — distinguish from a
|
||||
// genuine mismatch so the log tells us which it was.
|
||||
tracing::warn!(peer_did = %did, error = %e, "Rejected peer-joined: malformed signature");
|
||||
anyhow::bail!("Invalid signature");
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
||||
Reference in New Issue
Block a user