Addresses the trust gap where a hub relay can forward media from unknown relays without the receiving relay's consent. Introduces delegate=true flag on [[trusted]] entries: when set, the relay accepts media forwarded through the trusted peer from relays it vouches for. Without delegate, only direct media is accepted. Covers: FederationTrustChain signal, origin authorization checks, TTL for chain depth limiting, anti-spam properties. 5 phases, ~3 days. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.1 KiB
PRD: Delegated Trust for Relay Federation
Problem
In the current federation model, when Relay 1 trusts Relay 2, and Relay 2 forwards media from Relay 3, Relay 1 has no way to know or control that Relay 3's traffic is reaching it. This is a trust gap — any relay in the chain can introduce untrusted traffic.
Example: Relay 1 (trusted zone) ←→ Relay 2 (hub) ←→ Relay 3 (unknown)
Relay 1 explicitly trusts Relay 2. But Relay 2 forwards Relay 3's media to Relay 1 without Relay 1's consent. Relay 1 receives media that originated from an entity it never approved.
Solution
Add a delegate flag to [[trusted]] entries. When delegate = true, the relay accepts media forwarded through the trusted peer from relays that the trusted peer vouches for. When delegate = false (default), only media originating from explicitly trusted/peered relays is accepted.
Trust Levels
| Config | Meaning |
|---|---|
[[peers]] |
"I connect to you and trust your identity" |
[[trusted]] |
"I accept connections from you" |
[[trusted]] delegate = true |
"I accept connections from you AND from relays you vouch for" |
| No entry | "I reject your connections and drop your forwarded media" |
Configuration
# Relay 1: trusts Relay 2 and delegates trust
[[trusted]]
fingerprint = "relay-2-tls-fingerprint"
label = "Relay 2 (Hub)"
delegate = true # Accept relays that Relay 2 forwards from
# Without delegate (default = false):
[[trusted]]
fingerprint = "relay-4-tls-fingerprint"
label = "Relay 4"
# delegate = false (implicit default)
# Only direct media from Relay 4 is accepted
Protocol Changes
Relay-to-Relay Media Authorization
When Relay 2 forwards media from Relay 3 to Relay 1, the datagram needs to carry origin information so Relay 1 can decide whether to accept it.
Option A: Origin tag in datagram (recommended)
Extend the federation datagram format:
[room_hash: 8 bytes][origin_relay_fp: 8 bytes][media_packet]
The 8-byte origin fingerprint identifies which relay originally produced the media. The forwarding relay (Relay 2) sets this to the source relay's fingerprint. Relay 1 checks:
- Is the origin relay directly trusted? → accept
- Is the forwarding relay trusted with
delegate = true? → accept - Otherwise → drop
Option B: Trust announcement signal
When Relay 2 connects to Relay 1, it sends a FederationTrustChain signal listing which relays it will forward from:
FederationTrustChain {
/// Fingerprints of relays this peer may forward media from
vouched_relays: Vec<String>,
}
Relay 1 checks each fingerprint against its policy:
- If Relay 2 has
delegate = truein Relay 1's config → accept all listed relays - If Relay 2 has
delegate = false→ reject, only accept direct media from Relay 2
Option B is simpler to implement (no datagram format change) but less granular.
Recommended: Option B for v1, Option A for v2
Option B is simpler — the trust chain is established at connection time, not per-datagram. The forwarding relay announces what it will forward, and the receiving relay approves or rejects upfront.
Implementation
Config Changes
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TrustedConfig {
pub fingerprint: String,
#[serde(default)]
pub label: Option<String>,
/// When true, also accept media forwarded through this relay from
/// relays it vouches for. Default: false.
#[serde(default)]
pub delegate: bool,
}
Federation Signal
/// Sent after FederationHello — lists relays this peer will forward from.
FederationTrustChain {
/// TLS fingerprints of relays whose media may be forwarded through us.
vouched_relays: Vec<String>,
}
Forwarding Authorization
In handle_datagram, before forwarding media to local participants:
// Check if we should accept this forwarded media
let is_authorized = if source_is_direct_peer {
true // Direct peer, always accepted
} else {
// Check if the forwarding peer has delegate=true
let forwarding_peer = fm.find_trusted_by_fingerprint(forwarding_peer_fp);
forwarding_peer.map(|t| t.delegate).unwrap_or(false)
};
if !is_authorized {
warn!("dropping forwarded media from unauthorized relay chain");
return;
}
Relay 2 (Hub) Behavior
When Relay 2 receives FederationTrustChain queries from peers:
- Collect all directly connected peer fingerprints
- Send
FederationTrustChain { vouched_relays }to each peer - When a new relay connects, update all peers' trust chains
Anti-Spam Properties
| Attack | Mitigation |
|---|---|
| Unknown relay connects to hub | Hub rejects (not in [[trusted]]) |
| Hub forwards spam relay's media | Receiving relay checks delegate flag, drops if false |
| Relay spoofs origin fingerprint | Origin tag is set by the forwarding relay, not the source. The forwarding relay is trusted, so if it lies about origin, the trust is misplaced at the config level. |
| Chain amplification (A→B→C→D→...) | TTL on forwarded datagrams (decrement at each hop, drop at 0). Default TTL=2 (one intermediate relay). |
TTL for Chain Length
Add a TTL byte to the federation datagram to limit chain depth:
[room_hash: 8 bytes][ttl: 1 byte][media_packet]
- Default TTL = 2 (allows one intermediate relay: A→B→C)
- Each forwarding relay decrements TTL
- When TTL = 0, don't forward further (only deliver to local participants)
- Configurable per-relay:
max_federation_hops = 2
Milestones
| Phase | Scope | Effort |
|---|---|---|
| 1 | Add delegate field to TrustedConfig |
0.5 day |
| 2 | FederationTrustChain signal + announcement |
1 day |
| 3 | Authorization check in handle_datagram |
0.5 day |
| 4 | TTL in federation datagrams | 0.5 day |
| 5 | Testing: authorized vs unauthorized forwarding | 0.5 day |
Non-Goals (v1)
- Per-room trust policies (trust Relay X only for room "android")
- Dynamic trust negotiation (relays negotiate trust level at runtime)
- Revocation (removing a relay from trust chain requires config edit + restart)
- Cryptographic proof of origin (signed datagrams from source relay)