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