docs: PRD for delegated trust in relay federation
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>
This commit is contained in:
170
docs/PRD-delegated-trust.md
Normal file
170
docs/PRD-delegated-trust.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 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
|
||||
|
||||
```toml
|
||||
# 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:
|
||||
1. Is the origin relay directly trusted? → accept
|
||||
2. Is the forwarding relay trusted with `delegate = true`? → accept
|
||||
3. 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:
|
||||
```rust
|
||||
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 = true` in 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
|
||||
|
||||
```rust
|
||||
#[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
|
||||
|
||||
```rust
|
||||
/// 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:
|
||||
|
||||
```rust
|
||||
// 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:
|
||||
1. Collect all directly connected peer fingerprints
|
||||
2. Send `FederationTrustChain { vouched_relays }` to each peer
|
||||
3. 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)
|
||||
Reference in New Issue
Block a user