1#[cfg(not(feature = "std"))]
14use alloc::{format, string::String, string::ToString, vec, vec::Vec};
15
16use crate::canonical::{
17 encode_bool, encode_bytes_b64, encode_f64, encode_hybrid_pub_key, encode_hybrid_sig, encode_i32,
18 encode_i64, encode_str, encode_str_array,
19};
20use crate::crypto::{sign_both, verify_both};
21use crate::types::{
22 DelegationCert, HybridPrivateKey, HybridPublicKey, HybridSignature, PolicyVerdict, ProofBundle,
23 VerificationReceipt, VerifierContext, VerifyResult,
24};
25
26use hmac::{Hmac, Mac};
27use sha2::{Digest, Sha256};
28
29type HmacSha256 = Hmac<Sha256>;
30
31fn encode_delegation_for_hash(d: &DelegationCert, out: &mut String) {
38 use crate::canonical::encode_constraints;
39 out.push('{');
40 out.push_str("\"cert_id\":"); encode_str(&d.cert_id, out);
41 out.push_str(",\"constraints\":"); encode_constraints(&d.constraints, out);
42 out.push_str(",\"expires_at\":"); encode_i64(d.expires_at, out);
43 out.push_str(",\"issued_at\":"); encode_i64(d.issued_at, out);
44 out.push_str(",\"issuer_id\":"); encode_str(&d.issuer_id, out);
45 out.push_str(",\"issuer_pub_key\":"); encode_hybrid_pub_key(&d.issuer_pub_key, out);
46 out.push_str(",\"scope\":"); encode_str_array(&d.scope, out);
47 out.push_str(",\"signature\":"); encode_hybrid_sig(&d.signature, out);
48 out.push_str(",\"subject_id\":"); encode_str(&d.subject_id, out);
49 out.push_str(",\"subject_pub_key\":"); encode_hybrid_pub_key(&d.subject_pub_key, out);
50 out.push_str(",\"version\":"); encode_i32(d.version, out);
51 out.push('}');
52}
53
54pub fn bundle_hash(bundle: &ProofBundle) -> Result<Vec<u8>, String> {
63 let mut out = String::new();
64 out.push('{');
68 out.push_str("\"agent_id\":"); encode_str(&bundle.agent_id, &mut out);
69 out.push_str(",\"agent_pub_key\":"); encode_hybrid_pub_key(&bundle.agent_pub_key, &mut out);
70 out.push_str(",\"challenge\":"); encode_bytes_b64(&bundle.challenge, &mut out);
72 out.push_str(",\"challenge_at\":"); encode_i64(bundle.challenge_at, &mut out);
73 out.push_str(",\"challenge_sig\":"); encode_hybrid_sig(&bundle.challenge_sig, &mut out);
74 out.push_str(",\"delegations\":[");
76 for (i, d) in bundle.delegations.iter().enumerate() {
77 if i > 0 {
78 out.push(',');
79 }
80 encode_delegation_for_hash(d, &mut out);
81 }
82 out.push(']');
83 out.push_str(",\"session_context\":"); encode_bytes_b64(&bundle.session_context, &mut out);
84 out.push_str(",\"stream_id\":"); encode_bytes_b64(&bundle.stream_id, &mut out);
85 out.push_str(",\"stream_seq\":"); encode_i64(bundle.stream_seq, &mut out);
86 out.push('}');
87 let bytes = out.into_bytes();
88 Ok(Sha256::digest(&bytes).to_vec())
89}
90
91pub fn verification_receipt_sign_bytes_buf(
95 r: &VerificationReceipt,
96) -> Result<Vec<u8>, String> {
97 verification_receipt_sign_bytes(r)
98}
99
100fn verification_receipt_sign_bytes(r: &VerificationReceipt) -> Result<Vec<u8>, String> {
101 let mut out = String::new();
107 out.push('{');
108
109 let mut sep = "";
112
113 if !r.agent_id.is_empty() {
114 out.push_str(sep); sep = ",";
115 out.push_str("\"agent_id\":"); encode_str(&r.agent_id, &mut out);
116 }
117 out.push_str(sep); sep = ",";
118 out.push_str("\"bundle_hash\":"); encode_bytes_b64(&r.bundle_hash, &mut out);
119 out.push_str(sep);
120 out.push_str("\"decision\":"); encode_str(&r.decision, &mut out);
121 if !r.error_reason.is_empty() {
122 out.push_str(sep);
123 out.push_str("\"error_reason\":"); encode_str(&r.error_reason, &mut out);
124 }
125 if !r.granted_scope.is_empty() {
126 let mut scope = r.granted_scope.clone();
127 scope.sort();
128 out.push_str(sep);
129 out.push_str("\"granted_scope\":"); encode_str_array(&scope, &mut out);
130 }
131 if !r.human_id.is_empty() {
132 out.push_str(sep);
133 out.push_str("\"human_id\":"); encode_str(&r.human_id, &mut out);
134 }
135 out.push_str(sep);
136 out.push_str("\"prev_hash\":"); encode_bytes_b64(&r.prev_hash, &mut out);
137 out.push_str(sep);
138 out.push_str("\"verified_at\":"); encode_i64(r.verified_at, &mut out);
139 out.push_str(sep);
140 out.push_str("\"verifier_id\":"); encode_str(&r.verifier_id, &mut out);
141 out.push_str(sep);
142 out.push_str("\"verifier_pub\":"); encode_hybrid_pub_key(&r.verifier_pub, &mut out);
143 out.push_str(sep);
144 out.push_str("\"version\":"); encode_i32(r.version, &mut out);
145 out.push('}');
146 Ok(out.into_bytes())
147}
148
149pub fn issue_verification_receipt(
152 bundle: &ProofBundle,
153 result: &VerifyResult,
154 verifier_id: &str,
155 verifier_pub: &HybridPublicKey,
156 verifier_priv: &HybridPrivateKey,
157 prev_hash: Option<&[u8]>,
158 verified_at: i64,
159) -> Result<VerificationReceipt, String> {
160 let prev = match prev_hash {
161 Some(p) if p.len() == 32 => p.to_vec(),
162 None => vec![0u8; 32],
163 Some(p) => return Err(format!("prev_hash must be 32 bytes, got {}", p.len())),
164 };
165 let mut r = VerificationReceipt {
166 version: 1,
167 verifier_id: verifier_id.to_string(),
168 verifier_pub: verifier_pub.clone(),
169 bundle_hash: bundle_hash(bundle)?,
170 decision: result.identity_status.as_str().to_string(),
171 human_id: result.human_id.clone(),
172 agent_id: result.agent_id.clone(),
173 granted_scope: result.granted_scope.clone(),
174 error_reason: result.error_reason.clone(),
175 verified_at,
176 prev_hash: prev,
177 signature: HybridSignature {
178 ed25519: Vec::new(),
179 ml_dsa_65: Vec::new(),
180 },
181 };
182 let signable = verification_receipt_sign_bytes(&r)?;
183 r.signature = sign_both(&signable, verifier_priv);
184 Ok(r)
185}
186
187pub fn verify_verification_receipt(r: &VerificationReceipt) -> Result<(), String> {
191 if r.version != 1 {
192 return Err(format!("unsupported version {}", r.version));
193 }
194 if r.bundle_hash.len() != 32 {
195 return Err(format!("bundle_hash must be 32 bytes, got {}", r.bundle_hash.len()));
196 }
197 if r.prev_hash.len() != 32 {
198 return Err(format!("prev_hash must be 32 bytes, got {}", r.prev_hash.len()));
199 }
200 let signable = verification_receipt_sign_bytes(r)?;
201 verify_both(&signable, &r.signature, &r.verifier_pub)
202}
203
204pub fn receipt_hash(r: &VerificationReceipt) -> Result<Vec<u8>, String> {
207 let signable = verification_receipt_sign_bytes(r)?;
208 Ok(Sha256::digest(&signable).to_vec())
209}
210
211pub fn verifier_context_hash(ctx: &VerifierContext) -> Result<Vec<u8>, String> {
222 let has_amount = ctx.requested_amount.is_some();
223 let has_location = ctx.current_lat.is_some() && ctx.current_lon.is_some();
224 let has_speed = ctx.current_speed_mps.is_some();
225 let mut out = String::new();
226 out.push('{');
227 out.push_str("\"current_alt_m\":"); encode_f64(ctx.current_alt_m.unwrap_or(0.0), &mut out);
228 out.push_str(",\"current_lat\":"); encode_f64(ctx.current_lat.unwrap_or(0.0), &mut out);
229 out.push_str(",\"current_lon\":"); encode_f64(ctx.current_lon.unwrap_or(0.0), &mut out);
230 out.push_str(",\"current_speed_mps\":"); encode_f64(ctx.current_speed_mps.unwrap_or(0.0), &mut out);
231 out.push_str(",\"has_amount\":"); encode_bool(has_amount, &mut out);
232 out.push_str(",\"has_location\":"); encode_bool(has_location, &mut out);
233 out.push_str(",\"has_speed\":"); encode_bool(has_speed, &mut out);
234 out.push_str(",\"requested_amount\":"); encode_f64(ctx.requested_amount.unwrap_or(0.0), &mut out);
235 out.push_str(",\"requested_currency\":"); encode_str(ctx.requested_currency.as_deref().unwrap_or(""), &mut out);
236 out.push('}');
237 let bytes = out.into_bytes();
238 Ok(Sha256::digest(&bytes).to_vec())
239}
240
241pub fn policy_verdict_sign_bytes_buf(v: &PolicyVerdict) -> Result<Vec<u8>, String> {
246 policy_verdict_sign_bytes(v)
247}
248
249fn policy_verdict_sign_bytes(v: &PolicyVerdict) -> Result<Vec<u8>, String> {
250 let mut out = String::new();
251 out.push('{');
252 out.push_str("\"agent_id\":"); encode_str(&v.agent_id, &mut out);
253 out.push_str(",\"allow\":"); encode_bool(v.allow, &mut out);
254 out.push_str(",\"context_hash\":"); encode_bytes_b64(&v.context_hash, &mut out);
255 out.push_str(",\"issued_at\":"); encode_i64(v.issued_at, &mut out);
256 out.push_str(",\"scope\":"); encode_str(&v.scope, &mut out);
257 out.push_str(",\"valid_until\":"); encode_i64(v.valid_until, &mut out);
258 out.push_str(",\"verdict_id\":"); encode_str(&v.verdict_id, &mut out);
259 out.push_str(",\"version\":"); encode_i32(v.version, &mut out);
260 out.push('}');
261 Ok(out.into_bytes())
262}
263
264pub fn issue_policy_verdict(
266 verdict_id: &str,
267 agent_id: &str,
268 scope: &str,
269 allow: bool,
270 context_hash: &[u8],
271 issued_at: i64,
272 valid_until: i64,
273 policy_secret: &[u8],
274) -> Result<PolicyVerdict, String> {
275 if policy_secret.is_empty() {
276 return Err("policy_secret must not be empty".into());
277 }
278 if verdict_id.is_empty() {
279 return Err("verdict_id must not be empty".into());
280 }
281 if agent_id.is_empty() {
282 return Err("agent_id must not be empty".into());
283 }
284 if scope.is_empty() {
285 return Err("scope must not be empty".into());
286 }
287 if context_hash.len() != 32 {
288 return Err(format!("context_hash must be 32 bytes, got {}", context_hash.len()));
289 }
290 if valid_until <= issued_at {
291 return Err("valid_until must be strictly after issued_at".into());
292 }
293 let mut v = PolicyVerdict {
294 version: 1,
295 verdict_id: verdict_id.to_string(),
296 agent_id: agent_id.to_string(),
297 scope: scope.to_string(),
298 allow,
299 context_hash: context_hash.to_vec(),
300 issued_at,
301 valid_until,
302 mac: Vec::new(),
303 };
304 let signable = policy_verdict_sign_bytes(&v)?;
305 let mut mac = HmacSha256::new_from_slice(policy_secret).map_err(|e| e.to_string())?;
306 mac.update(&signable);
307 v.mac = mac.finalize().into_bytes().to_vec();
308 Ok(v)
309}
310
311pub fn verify_policy_verdict(
315 v: &PolicyVerdict,
316 policy_secret: &[u8],
317 expected_agent_id: &str,
318 expected_scope: &str,
319 expected_context_hash: &[u8],
320 now: i64,
321) -> Result<(), String> {
322 if policy_secret.is_empty() {
323 return Err("policy_secret must not be empty".into());
324 }
325 if v.version != 1 {
326 return Err(format!("unsupported version {}", v.version));
327 }
328 if v.context_hash.len() != 32 {
329 return Err(format!("context_hash must be 32 bytes, got {}", v.context_hash.len()));
330 }
331 if v.mac.len() != 32 {
332 return Err(format!("mac must be 32 bytes, got {}", v.mac.len()));
333 }
334 let signable = policy_verdict_sign_bytes(v)?;
335 let mut mac = HmacSha256::new_from_slice(policy_secret).map_err(|e| e.to_string())?;
336 mac.update(&signable);
337 mac.verify_slice(&v.mac)
338 .map_err(|_| "policy_verdict MAC invalid".to_string())?;
339 if now < v.issued_at {
340 return Err("policy_verdict not yet valid".into());
341 }
342 if now > v.valid_until {
343 return Err("policy_verdict expired".into());
344 }
345 if v.agent_id != expected_agent_id {
346 return Err("policy_verdict agent_id mismatch".into());
347 }
348 if v.scope != expected_scope {
349 return Err("policy_verdict scope mismatch".into());
350 }
351 if v.context_hash != expected_context_hash {
352 return Err("policy_verdict context_hash mismatch".into());
353 }
354 if !v.allow {
355 return Err(format!(
356 "policy_verdict_denied: cached deny for scope \"{}\"",
357 v.scope
358 ));
359 }
360 Ok(())
361}