Skip to main content

tf_types/
federation.rs

1//! Federation primitives — Rust mirror of TS `federation.ts`.
2
3use crate::encoding::STANDARD;
4use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use sha2::{Digest, Sha256};
8
9use crate::canonicalize;
10use crate::expiration::{is_within_window, Window};
11
12#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
13pub struct SignatureEnvelope {
14    pub algorithm: String,
15    pub signer: String,
16    pub signature: String,
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
20pub struct TrustBundleEntry {
21    pub kind: String,
22    pub value: String,
23    #[serde(skip_serializing_if = "Option::is_none", default)]
24    pub key_id: Option<String>,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
28pub struct FederationAttestation {
29    pub attestation_version: String,
30    pub attestation_id: String,
31    pub issuer_domain: String,
32    pub subject_domain: String,
33    #[serde(skip_serializing_if = "Option::is_none", default)]
34    pub subject_actor: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none", default)]
36    pub scope: Option<Vec<String>>,
37    #[serde(skip_serializing_if = "Option::is_none", default)]
38    pub trust_levels_granted: Option<Vec<String>>,
39    pub trust_bundle: Vec<TrustBundleEntry>,
40    #[serde(skip_serializing_if = "Option::is_none", default)]
41    pub constraints: Option<Vec<Value>>,
42    pub issued_at: String,
43    pub valid_until: String,
44    pub issuer: String,
45    pub signature: SignatureEnvelope,
46}
47
48pub fn attestation_signing_bytes(a: &FederationAttestation) -> [u8; 32] {
49    let mut value = serde_json::to_value(a).unwrap_or(Value::Null);
50    if let Value::Object(map) = &mut value {
51        map.remove("signature");
52    }
53    let canonical = canonicalize(&value).unwrap_or_default();
54    Sha256::digest(canonical.as_bytes()).into()
55}
56
57#[derive(Clone, Debug)]
58pub struct SignAttestationArgs {
59    pub attestation_id: String,
60    pub issuer_domain: String,
61    pub subject_domain: String,
62    pub subject_actor: Option<String>,
63    pub scope: Option<Vec<String>>,
64    pub trust_levels_granted: Option<Vec<String>>,
65    pub trust_bundle: Vec<TrustBundleEntry>,
66    pub constraints: Option<Vec<Value>>,
67    pub issued_at: Option<String>,
68    pub valid_until: String,
69    pub issuer: String,
70    pub private_key: [u8; 32],
71}
72
73pub fn sign_federation_attestation(
74    args: SignAttestationArgs,
75) -> Result<FederationAttestation, String> {
76    if args.trust_bundle.is_empty() {
77        return Err("trust_bundle must be non-empty".into());
78    }
79    let mut att = FederationAttestation {
80        attestation_version: "1".into(),
81        attestation_id: args.attestation_id,
82        issuer_domain: args.issuer_domain,
83        subject_domain: args.subject_domain,
84        subject_actor: args.subject_actor,
85        scope: args.scope.filter(|s| !s.is_empty()),
86        trust_levels_granted: args.trust_levels_granted.filter(|s| !s.is_empty()),
87        trust_bundle: args.trust_bundle,
88        constraints: args.constraints.filter(|s| !s.is_empty()),
89        issued_at: args.issued_at.unwrap_or_else(now_iso8601),
90        valid_until: args.valid_until,
91        issuer: args.issuer.clone(),
92        signature: SignatureEnvelope {
93            algorithm: "ed25519".into(),
94            signer: args.issuer,
95            signature: String::new(),
96        },
97    };
98    let digest = attestation_signing_bytes(&att);
99    let signing = SigningKey::from_bytes(&args.private_key);
100    let sig: Signature = signing.sign(&digest);
101    att.signature.signature = STANDARD.encode(sig.to_bytes());
102    Ok(att)
103}
104
105#[derive(Debug)]
106pub struct VerifyAttestationResult {
107    pub ok: bool,
108    pub reason: Option<String>,
109}
110
111pub fn verify_federation_attestation(
112    a: &FederationAttestation,
113    issuer_public_key: &[u8; 32],
114    now: Option<&str>,
115) -> VerifyAttestationResult {
116    let rejected = |r: &str| VerifyAttestationResult {
117        ok: false,
118        reason: Some(r.to_string()),
119    };
120    if a.attestation_version != "1" {
121        return rejected(&format!("unsupported version {}", a.attestation_version));
122    }
123    if a.signature.signer != a.issuer {
124        return rejected("signature signer does not match issuer");
125    }
126    if a.signature.algorithm != "ed25519" {
127        return rejected(&format!("unsupported algorithm {}", a.signature.algorithm));
128    }
129    let now_string = now.map(str::to_string).unwrap_or_else(now_iso8601);
130    let window = Window {
131        valid_from: Some(a.issued_at.as_str()),
132        valid_until: Some(a.valid_until.as_str()),
133        ..Window::default()
134    };
135    if !is_within_window(&window, &now_string) {
136        return rejected("attestation outside valid window");
137    }
138    let digest = attestation_signing_bytes(a);
139    let sig_bytes = match STANDARD.decode(&a.signature.signature) {
140        Ok(b) => b,
141        Err(e) => return rejected(&format!("signature base64: {}", e)),
142    };
143    let sig = match Signature::from_slice(&sig_bytes) {
144        Ok(s) => s,
145        Err(e) => return rejected(&format!("signature parse: {}", e)),
146    };
147    let vk = match VerifyingKey::from_bytes(issuer_public_key) {
148        Ok(v) => v,
149        Err(e) => return rejected(&format!("verifying key: {}", e)),
150    };
151    if vk.verify(&digest, &sig).is_err() {
152        return rejected("signature did not verify");
153    }
154    VerifyAttestationResult {
155        ok: true,
156        reason: None,
157    }
158}
159
160#[derive(Default)]
161pub struct FederatedTrustStore {
162    by_id: std::collections::HashMap<String, FederationAttestation>,
163}
164
165impl FederatedTrustStore {
166    pub fn new() -> Self {
167        FederatedTrustStore::default()
168    }
169
170    pub fn add(&mut self, att: FederationAttestation) {
171        self.by_id.insert(att.attestation_id.clone(), att);
172    }
173
174    pub fn remove(&mut self, attestation_id: &str) -> bool {
175        self.by_id.remove(attestation_id).is_some()
176    }
177
178    pub fn list(&self) -> Vec<&FederationAttestation> {
179        self.by_id.values().collect()
180    }
181
182    pub fn find_for(
183        &self,
184        actor: &str,
185        subject_domain: &str,
186        now: Option<&str>,
187    ) -> Option<&FederationAttestation> {
188        let now_string = now.map(str::to_string).unwrap_or_else(now_iso8601);
189        for a in self.by_id.values() {
190            if a.subject_domain != subject_domain {
191                continue;
192            }
193            if let Some(s) = &a.subject_actor {
194                if s != actor {
195                    continue;
196                }
197            }
198            let window = Window {
199                valid_from: Some(a.issued_at.as_str()),
200                valid_until: Some(a.valid_until.as_str()),
201                ..Window::default()
202            };
203            if !is_within_window(&window, &now_string) {
204                continue;
205            }
206            return Some(a);
207        }
208        None
209    }
210
211    pub fn verify_foreign(
212        &self,
213        actor: &str,
214        subject_domain: &str,
215        signed: Option<(&[u8], &[u8])>,
216        now: Option<&str>,
217    ) -> ForeignIdentityCheck {
218        let a = match self.find_for(actor, subject_domain, now) {
219            Some(a) => a,
220            None => {
221                return ForeignIdentityCheck {
222                    ok: false,
223                    reason: Some(format!(
224                        "no active attestation for {} in {}",
225                        actor, subject_domain
226                    )),
227                    matched_attestation_id: None,
228                    trust_levels: None,
229                    scope: None,
230                };
231            }
232        };
233        if let Some((message, sig_bytes)) = signed {
234            let sig = match Signature::from_slice(sig_bytes) {
235                Ok(s) => s,
236                Err(e) => {
237                    return ForeignIdentityCheck {
238                        ok: false,
239                        reason: Some(format!("foreign sig parse: {}", e)),
240                        matched_attestation_id: Some(a.attestation_id.clone()),
241                        trust_levels: None,
242                        scope: None,
243                    };
244                }
245            };
246            let mut matched = false;
247            for entry in &a.trust_bundle {
248                if entry.kind != "ed25519" {
249                    continue;
250                }
251                let pk_bytes = match STANDARD.decode(&entry.value) {
252                    Ok(b) => b,
253                    Err(_) => continue,
254                };
255                if pk_bytes.len() != 32 {
256                    continue;
257                }
258                let mut arr = [0u8; 32];
259                arr.copy_from_slice(&pk_bytes);
260                if let Ok(vk) = VerifyingKey::from_bytes(&arr) {
261                    if vk.verify(message, &sig).is_ok() {
262                        matched = true;
263                        break;
264                    }
265                }
266            }
267            if !matched {
268                return ForeignIdentityCheck {
269                    ok: false,
270                    reason: Some("no bundle key matched the foreign actor's signature".into()),
271                    matched_attestation_id: Some(a.attestation_id.clone()),
272                    trust_levels: None,
273                    scope: None,
274                };
275            }
276        }
277        ForeignIdentityCheck {
278            ok: true,
279            reason: None,
280            matched_attestation_id: Some(a.attestation_id.clone()),
281            trust_levels: a.trust_levels_granted.clone(),
282            scope: a.scope.clone(),
283        }
284    }
285}
286
287#[derive(Debug)]
288pub struct ForeignIdentityCheck {
289    pub ok: bool,
290    pub reason: Option<String>,
291    pub matched_attestation_id: Option<String>,
292    pub trust_levels: Option<Vec<String>>,
293    pub scope: Option<Vec<String>>,
294}
295
296fn now_iso8601() -> String {
297    let secs = std::time::SystemTime::now()
298        .duration_since(std::time::UNIX_EPOCH)
299        .unwrap_or_default()
300        .as_secs() as i64;
301    let (y, m, d, h, mi, s) = secs_to_ymdhms(secs);
302    format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, h, mi, s)
303}
304
305fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
306    let days = secs.div_euclid(86_400);
307    let time = secs.rem_euclid(86_400);
308    let hour = (time / 3600) as u32;
309    let minute = ((time % 3600) / 60) as u32;
310    let second = (time % 60) as u32;
311    let z = days + 719_468;
312    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
313    let doe = (z - era * 146_097) as u64;
314    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
315    let y = yoe as i64 + era * 400;
316    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
317    let mp = (5 * doy + 2) / 153;
318    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
319    let m = if mp < 10 {
320        (mp + 3) as u32
321    } else {
322        (mp - 9) as u32
323    };
324    let year = if m <= 2 { y + 1 } else { y };
325    (year as i32, m, d, hour, minute, second)
326}