Skip to main content

tf_types/
federation.rs

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