fix(security): peers must not be able to grant themselves Trusted

Reported: "peers seem to be slipping into trusted status somehow which is
absolutely terrible for security". Two independent fail-open paths, both
granting Trusted with no operator decision anywhere in the loop.

1. federation.peer-joined is UNAUTHENTICATED (middleware's no-session
   list — federated peers call it over Tor without cookies) and reachable
   on /rpc/v1, which is peer-allowed. It does verify an ed25519 signature,
   but against THE PUBKEY THE CALLER SUPPLIED, so it proves the caller
   holds its own key and nothing about whether we ever invited it. A join
   presenting no invite_token fell through to

       None => TrustLevel::Trusted.min(claimed_trust)

   and claimed_trust itself defaults to Trusted when the field is absent.
   So anything able to reach the node could generate a keypair, omit the
   token, and be recorded as Trusted. Now capped at Observer: an invite
   WE minted is the only path to Trusted. `min` is kept so a peer's own
   lower claim is still honoured — this can only ever reduce trust.

2. merge_transitive_peers added every peer advertised by a Trusted source
   as Trusted. That makes trust viral rather than transitive-by-one-hop:
   the merged node is itself synced with, its peers merged in turn, so a
   single invite anywhere in the graph eventually marked the entire graph
   Trusted on every node. Now Observer — which is what this feature's own
   spec always said. NodeStateSnapshot.federated_peers is documented as
   "adds them as Observers on her side… doesn't auto-promote Observer-via-
   Bob to Trusted". The code contradicted the comment directly above it.

Observer is deliberate rather than Untrusted: the merge exists for
routing, and Observer still passes the `!= Untrusted` gates that
federation, DWN and messaging actually check, so a legacy peer degrades
instead of breaking. Per the operator's decision, existing peers are NOT
auto-demoted — silently rewriting live trust relationships across the
fleet would be worse than the bug.

Instead they are made auditable: FederatedNode.trust_source records WHY a
level was granted (invite | uninvited-join | transitive-merge | manual).
It deliberately has no default provenance — None means "recorded before
this existed", which is exactly the population worth reviewing.

The one failing test was asserting the vulnerable behaviour
(merge_transitive_peers_skips_source_and_local_node expected Trusted); it
now asserts the security property and says why, so the escalation cannot
be reintroduced by making a test go green.

Verified: 42/42 federation tests, cargo check --all-targets clean.

Still open, tracked in .planning/RELEASE-1.7.121-TASKS.md: surface
trust_source in the UI, and require the node password to grant Trusted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 10:31:16 -04:00
co-authored by Claude Opus 5
parent ff2cb5aa0e
commit c0cfc72a05
6 changed files with 101 additions and 8 deletions
+1 -1
View File
@@ -24,4 +24,4 @@ pub use storage::{
record_sync_result, remove_node, save_nodes, set_trust_level, update_node,
};
pub use sync::{build_local_state, deploy_to_peer, sync_with_peer, sync_with_peer_by_did};
pub use types::{AppStatus, FederatedNode, NodeStateSnapshot, TrustLevel};
pub use types::{AppStatus, FederatedNode, NodeStateSnapshot, TrustLevel, TrustSource};