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