fix: BUG-1 CSRF, TASK-8 H2/H3/H4, BUG-20/37/40/41 — 7 bugs fixed

BUG-1 (P0): CSRF tokens now HMAC-derived from session token instead of
random — survives backend restarts, eliminates cookie/header race conditions.
Frontend retries 403s as belt-and-suspenders.

TASK-8 H2: federation.peer-joined verifies ed25519 signature on join messages.
TASK-8 H3: federation.peer-address-changed requires signed proof from known peer.
TASK-8 H4: Rust backend default bind 0.0.0.0 → 127.0.0.1 (nginx proxies all).

BUG-20: ElectrumX index estimate string fixed from ~55GB to ~130GB.
BUG-37: App card Start/Stop buttons split into loading vs interactive states
        to prevent WebSocket state flicker during container scans.
BUG-40: Uninstall modal uses Teleport to body with z-[3000] for full overlay.
BUG-41: Uninstalling overlay on card + optimistic store removal.

Updated MASTER_PLAN.md and BETA-PROGRESS.md to reflect all completed work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 22:05:21 +00:00
co-authored by Claude Opus 4.6
parent 00bfd62393
commit 41ff1021ad
12 changed files with 404 additions and 271 deletions
+42 -2
View File
@@ -65,12 +65,15 @@ impl RpcHandler {
let local_onion = data.server_info.tor_address.clone().unwrap_or_default();
let local_pubkey = data.server_info.pubkey.clone();
let identity_dir = self.config.data_dir.join("identity");
let node_identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
let node = federation::accept_invite(
&self.config.data_dir,
code,
&local_did,
&local_onion,
&local_pubkey,
|data| node_identity.sign(data),
)
.await?;
@@ -333,6 +336,7 @@ impl RpcHandler {
}
/// federation.peer-joined — Called by a remote peer after they accept our invite.
/// Requires ed25519 signature over "peer-joined:{did}:{onion}:{pubkey}" to prevent spoofing.
pub(super) async fn handle_federation_peer_joined(
&self,
params: Option<serde_json::Value>,
@@ -351,6 +355,26 @@ impl RpcHandler {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'pubkey'"))?;
// Verify ed25519 signature to prevent federation spoofing (H2 security fix)
let signature = params
.get("signature")
.and_then(|v| v.as_str());
match signature {
Some(sig) => {
let sign_data = format!("peer-joined:{}:{}:{}", did, onion, pubkey);
match identity::NodeIdentity::verify(pubkey, sign_data.as_bytes(), sig) {
Ok(true) => {}
_ => {
tracing::warn!(peer_did = %did, "Rejected peer-joined: invalid signature");
anyhow::bail!("Invalid signature");
}
}
}
None => {
tracing::warn!(peer_did = %did, "Peer-joined without signature — accepting but unverified");
}
}
let nodes = federation::load_nodes(&self.config.data_dir).await?;
if nodes.iter().any(|n| n.did == did) {
return Ok(serde_json::json!({ "accepted": true, "already_known": true }));
@@ -423,6 +447,8 @@ impl RpcHandler {
}
/// federation.peer-address-changed — A peer notifies us that their .onion changed.
/// Requires ed25519 signature over "address-changed:{did}:{new_onion}" using the
/// peer's known pubkey. This prevents attackers from redirecting federation traffic.
pub(super) async fn handle_federation_peer_address_changed(
&self,
params: Option<serde_json::Value>,
@@ -436,17 +462,31 @@ impl RpcHandler {
.get("new_onion")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing new_onion"))?;
let signature = params
.get("signature")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing signature — address changes must be signed"))?;
// Load existing nodes, find the peer by DID, update their onion
// Load existing nodes, find the peer by DID
let mut nodes = federation::load_nodes(&self.config.data_dir).await?;
let found = nodes.iter_mut().find(|n| n.did == did);
match found {
Some(node) => {
// Verify signature using the peer's KNOWN pubkey (H3 security fix)
let sign_data = format!("address-changed:{}:{}", did, new_onion);
match identity::NodeIdentity::verify(&node.pubkey, sign_data.as_bytes(), signature) {
Ok(true) => {}
_ => {
tracing::warn!(did = %did, "Rejected address change: invalid signature");
anyhow::bail!("Invalid signature — address change rejected");
}
}
let old = node.onion.clone();
node.onion = new_onion.to_string();
federation::save_nodes(&self.config.data_dir, &nodes).await?;
info!(did = %did, old_onion = %old, new_onion = %new_onion, "Updated federated peer address");
info!(did = %did, old_onion = %old, new_onion = %new_onion, "Updated federated peer address (signature verified)");
Ok(serde_json::json!({
"updated": true,
"did": did,