Skip to main content

tf_types/
relay.rs

1//! Relay model — Rust mirror of `tools/tf-types-ts/src/core/relay.ts`.
2//! Forwarding authority is strictly separate from action authority;
3//! `RelayHandler` only sees opaque ciphertext and routes it.
4
5use base64::engine::general_purpose::STANDARD;
6use base64::Engine;
7use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use sha2::{Digest, Sha256};
11
12use crate::canonicalize;
13use crate::expiration::{is_within_window, Window};
14
15#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
16pub struct RelayAuthority {
17    pub relay_authority_version: String,
18    pub relay: String,
19    pub trust_domain: String,
20    pub kinds: Vec<String>,
21    #[serde(skip_serializing_if = "Option::is_none", default)]
22    pub max_hop_count: Option<u32>,
23    #[serde(skip_serializing_if = "Option::is_none", default)]
24    pub rate_limit_per_minute: Option<u32>,
25    pub valid_from: String,
26    #[serde(skip_serializing_if = "Option::is_none", default)]
27    pub valid_until: Option<String>,
28    pub issuer: String,
29    #[serde(skip_serializing_if = "Option::is_none", default)]
30    pub constraints: Option<Vec<Value>>,
31    pub signature: SignatureEnvelope,
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
35pub struct SignatureEnvelope {
36    pub algorithm: String,
37    pub signer: String,
38    pub signature: String,
39}
40
41#[derive(Clone, Debug, Default)]
42pub struct RelayFrame {
43    pub ciphertext: Vec<u8>,
44    pub destination: String,
45    pub priority: Option<String>,
46    pub hop_count: u32,
47    pub expires_at: Option<String>,
48    pub source: Option<String>,
49}
50
51#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
52pub struct RelayForwardedEvent {
53    #[serde(rename = "type")]
54    pub kind: String,
55    pub relay: String,
56    pub destination: String,
57    #[serde(skip_serializing_if = "Option::is_none", default)]
58    pub source: Option<String>,
59    pub hop_count_in: u32,
60    pub hop_count_out: u32,
61    pub size_bytes: usize,
62    pub forwarded_at: String,
63    #[serde(skip_serializing_if = "Option::is_none", default)]
64    pub authority_id: Option<String>,
65    #[serde(skip_serializing_if = "Option::is_none", default)]
66    pub priority: Option<String>,
67}
68
69#[derive(Debug, thiserror::Error)]
70pub enum RelayPolicyError {
71    #[error("relay authority invalid: {0}")]
72    Authority(String),
73    #[error("authority window: {0}")]
74    Window(String),
75    #[error("frame expired: {0}")]
76    Expired(String),
77    #[error("hop count: {0}")]
78    HopCap(String),
79    #[error("rate limit: {0}")]
80    Rate(String),
81}
82
83pub struct RelayHandler {
84    authority: RelayAuthority,
85    issuer_pub: [u8; 32],
86    validated: std::cell::Cell<bool>,
87    rate_bucket_minute: std::cell::Cell<i64>,
88    rate_bucket_count: std::cell::Cell<u32>,
89}
90
91impl RelayHandler {
92    pub fn new(authority: RelayAuthority, issuer_public_key: [u8; 32]) -> Self {
93        RelayHandler {
94            authority,
95            issuer_pub: issuer_public_key,
96            validated: std::cell::Cell::new(false),
97            rate_bucket_minute: std::cell::Cell::new(-1),
98            rate_bucket_count: std::cell::Cell::new(0),
99        }
100    }
101
102    pub fn authority(&self) -> &RelayAuthority {
103        &self.authority
104    }
105
106    pub fn forward(
107        &self,
108        frame: &RelayFrame,
109        now: &str,
110    ) -> Result<(RelayFrame, RelayForwardedEvent), RelayPolicyError> {
111        if !self.validated.get() {
112            let v = verify_relay_authority(&self.authority, &self.issuer_pub);
113            if !v.ok {
114                return Err(RelayPolicyError::Authority(
115                    v.reason.unwrap_or_else(|| "unknown".into()),
116                ));
117            }
118            self.validated.set(true);
119        }
120        let window = Window {
121            valid_from: Some(self.authority.valid_from.as_str()),
122            valid_until: self.authority.valid_until.as_deref(),
123            ..Window::default()
124        };
125        if !is_within_window(&window, now) {
126            return Err(RelayPolicyError::Window(
127                "outside valid_from/valid_until".into(),
128            ));
129        }
130        if let Some(exp) = &frame.expires_at {
131            if exp.as_str() < now {
132                return Err(RelayPolicyError::Expired(format!(
133                    "frame expired at {}",
134                    exp
135                )));
136            }
137        }
138        if let Some(max) = self.authority.max_hop_count {
139            if frame.hop_count >= max {
140                return Err(RelayPolicyError::HopCap(format!(
141                    "hop count {} >= max {}",
142                    frame.hop_count, max
143                )));
144            }
145        }
146        if let Some(limit) = self.authority.rate_limit_per_minute {
147            let minute = parse_minute(now);
148            if minute != self.rate_bucket_minute.get() {
149                self.rate_bucket_minute.set(minute);
150                self.rate_bucket_count.set(0);
151            }
152            self.rate_bucket_count.set(self.rate_bucket_count.get() + 1);
153            if self.rate_bucket_count.get() > limit {
154                return Err(RelayPolicyError::Rate(format!(
155                    "rate limit {}/min exceeded",
156                    limit
157                )));
158            }
159        }
160        let mut outgoing = frame.clone();
161        outgoing.hop_count = frame.hop_count + 1;
162        let event = RelayForwardedEvent {
163            kind: "relay.forwarded".into(),
164            relay: self.authority.relay.clone(),
165            destination: frame.destination.clone(),
166            source: frame.source.clone(),
167            hop_count_in: frame.hop_count,
168            hop_count_out: outgoing.hop_count,
169            size_bytes: frame.ciphertext.len(),
170            forwarded_at: now.to_string(),
171            authority_id: Some(self.authority.relay.clone()),
172            priority: frame.priority.clone(),
173        };
174        Ok((outgoing, event))
175    }
176}
177
178fn parse_minute(now: &str) -> i64 {
179    // Lexicographic-friendly: "YYYY-MM-DDTHH:MM:SS..."; parse into unix
180    // seconds then divide by 60. If parsing fails, return 0 (which makes
181    // the rate limiter behave as a single-bucket counter).
182    let len = now.len().min(19);
183    if !now.is_char_boundary(len) || len < 19 {
184        return 0;
185    }
186    let trimmed = &now[..19];
187    let unix = parse_iso8601(trimmed).unwrap_or(0);
188    unix / 60
189}
190
191fn parse_iso8601(s: &str) -> Option<i64> {
192    if s.len() < 19 {
193        return None;
194    }
195    let year: i64 = s.get(0..4)?.parse().ok()?;
196    let month: u32 = s.get(5..7)?.parse().ok()?;
197    let day: u32 = s.get(8..10)?.parse().ok()?;
198    let hour: u32 = s.get(11..13)?.parse().ok()?;
199    let minute: u32 = s.get(14..16)?.parse().ok()?;
200    let second: u32 = s.get(17..19)?.parse().ok()?;
201    let y = if month <= 2 { year - 1 } else { year };
202    let era = if y >= 0 { y } else { y - 399 } / 400;
203    let yoe = (y - era * 400) as u64;
204    let m = if month > 2 { month - 3 } else { month + 9 };
205    let doy = (153 * m as u64 + 2) / 5 + day as u64 - 1;
206    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
207    let days = era * 146_097 + doe as i64 - 719_468;
208    Some(days * 86_400 + (hour as i64) * 3600 + (minute as i64) * 60 + second as i64)
209}
210
211pub fn relay_authority_signing_bytes(a: &RelayAuthority) -> [u8; 32] {
212    let mut value = serde_json::to_value(a).unwrap_or(Value::Null);
213    if let Value::Object(map) = &mut value {
214        map.remove("signature");
215    }
216    let canonical = canonicalize(&value).unwrap_or_default();
217    Sha256::digest(canonical.as_bytes()).into()
218}
219
220pub fn sign_relay_authority(
221    mut authority: RelayAuthority,
222    private_key: &[u8; 32],
223) -> RelayAuthority {
224    authority.signature = SignatureEnvelope {
225        algorithm: "ed25519".into(),
226        signer: authority.issuer.clone(),
227        signature: String::new(),
228    };
229    let digest = relay_authority_signing_bytes(&authority);
230    let signing = SigningKey::from_bytes(private_key);
231    let sig: Signature = signing.sign(&digest);
232    authority.signature.signature = STANDARD.encode(sig.to_bytes());
233    authority
234}
235
236#[derive(Debug)]
237pub struct VerifyRelayAuthorityResult {
238    pub ok: bool,
239    pub reason: Option<String>,
240}
241
242pub fn verify_relay_authority(
243    authority: &RelayAuthority,
244    issuer_public_key: &[u8; 32],
245) -> VerifyRelayAuthorityResult {
246    let rejected = |r: &str| VerifyRelayAuthorityResult {
247        ok: false,
248        reason: Some(r.to_string()),
249    };
250    if authority.relay_authority_version != "1" {
251        return rejected(&format!(
252            "unsupported version {}",
253            authority.relay_authority_version
254        ));
255    }
256    if authority.signature.algorithm != "ed25519" {
257        return rejected(&format!(
258            "unsupported signature algorithm {}",
259            authority.signature.algorithm
260        ));
261    }
262    if authority.signature.signer != authority.issuer {
263        return rejected("signature signer does not match authority issuer");
264    }
265    let digest = relay_authority_signing_bytes(authority);
266    let sig_bytes = match STANDARD.decode(&authority.signature.signature) {
267        Ok(b) => b,
268        Err(e) => return rejected(&format!("signature base64 decode: {}", e)),
269    };
270    let sig = match Signature::from_slice(&sig_bytes) {
271        Ok(s) => s,
272        Err(e) => return rejected(&format!("signature parse: {}", e)),
273    };
274    let vk = match VerifyingKey::from_bytes(issuer_public_key) {
275        Ok(v) => v,
276        Err(e) => return rejected(&format!("verifying key: {}", e)),
277    };
278    if vk.verify(&digest, &sig).is_err() {
279        return rejected("relay authority signature did not verify");
280    }
281    VerifyRelayAuthorityResult {
282        ok: true,
283        reason: None,
284    }
285}