1use 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}