1use blake3::Hasher;
2use ed25519_dalek::VerifyingKey;
3
4use crate::audit::{AuditEvent, AuditOutcome, AuditSink, NoopAuditSink};
5use crate::cert::DelegationCert;
6use crate::crypto::DOMAIN_CHAIN_FP;
7use crate::error::A1Error;
8use crate::intent::{IntentHash, MerkleProof};
9use crate::policy::PolicySet;
10use crate::registry::{NonceStore, RevocationStore};
11#[cfg(feature = "tracing")]
12use tracing::Instrument;
13
14pub trait Clock {
17 fn unix_now(&self) -> u64;
18}
19
20pub struct SystemClock;
21
22impl Clock for SystemClock {
23 fn unix_now(&self) -> u64 {
24 std::time::SystemTime::now()
25 .duration_since(std::time::UNIX_EPOCH)
26 .expect("system clock is before the Unix epoch")
27 .as_secs()
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub struct VerificationReceipt {
36 pub chain_depth: usize,
37 pub verified_scope_root: IntentHash,
38 pub intent: IntentHash,
39 pub verified_at_unix: u64,
40 pub chain_fingerprint: [u8; 32],
41 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
42 pub namespace: Option<String>,
43}
44
45impl VerificationReceipt {
46 pub fn fingerprint_hex(&self) -> String {
47 hex::encode(self.chain_fingerprint)
48 }
49
50 #[cfg(feature = "wire")]
51 pub(crate) fn canonical_bytes(&self) -> Vec<u8> {
52 let mut out = Vec::with_capacity(16 + 8 + 32 + 32 + 8 + 32 + 64);
53 out.extend_from_slice(b"a1_dyolo_v2.8.0:");
54 out.extend_from_slice(&(self.chain_depth as u64).to_be_bytes());
55 out.extend_from_slice(&self.verified_scope_root);
56 out.extend_from_slice(&self.intent);
57 out.extend_from_slice(&self.verified_at_unix.to_be_bytes());
58 out.extend_from_slice(&self.chain_fingerprint);
59 if let Some(ns) = &self.namespace {
60 out.extend_from_slice(&(ns.len() as u64).to_be_bytes());
61 out.extend_from_slice(ns.as_bytes());
62 } else {
63 out.extend_from_slice(&0u64.to_be_bytes());
64 }
65 out
66 }
67}
68
69#[derive(Debug)]
72pub struct BatchAuthorizeResult {
73 pub receipts: Vec<Option<VerificationReceipt>>,
74 pub errors: Vec<Option<A1Error>>,
75 pub all_authorized: bool,
76}
77
78impl BatchAuthorizeResult {
79 pub fn authorized_count(&self) -> usize {
80 self.receipts.iter().filter(|r| r.is_some()).count()
81 }
82}
83
84#[must_use]
87#[non_exhaustive]
88pub struct AuthorizedAction {
89 pub receipt: VerificationReceipt,
90}
91
92impl AuthorizedAction {
93 pub(crate) fn new(receipt: VerificationReceipt) -> Self {
94 Self { receipt }
95 }
96
97 pub fn receipt(&self) -> &VerificationReceipt {
98 &self.receipt
99 }
100}
101
102impl std::fmt::Debug for AuthorizedAction {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("AuthorizedAction")
105 .field("receipt", &self.receipt)
106 .finish()
107 }
108}
109
110struct ChainValidationResult {
113 depth: usize,
114 verified_scope_root: IntentHash,
115 verified_at_unix: u64,
116 seen_nonces: Vec<[u8; 16]>,
117 cert_fingerprints: Vec<[u8; 32]>,
118 chain_fingerprint: [u8; 32],
119}
120
121fn namespace_scope(namespace: &str, scope: &IntentHash) -> IntentHash {
124 let mut h = blake3::Hasher::new_derive_key("a1::dyolo::namespace::scope::v2.8.0");
125 h.update(&(namespace.len() as u64).to_le_bytes());
126 h.update(namespace.as_bytes());
127 h.update(scope);
128 h.finalize().into()
129}
130
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct DyoloChain {
135 pub principal_pk: VerifyingKey,
136 pub principal_scope: IntentHash,
137 certs: Vec<DelegationCert>,
138 pub drift_tolerance_secs: u64,
139 pub namespace: Option<String>,
140}
141
142impl DyoloChain {
143 pub fn new(principal_pk: VerifyingKey, principal_scope: IntentHash) -> Self {
144 Self {
145 principal_pk,
146 principal_scope,
147 certs: Vec::new(),
148 drift_tolerance_secs: 15,
149 namespace: None,
150 }
151 }
152
153 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
172 let ns = namespace.into();
173 self.principal_scope = namespace_scope(&ns, &self.principal_scope);
174 self.namespace = Some(ns);
175 self
176 }
177
178 pub fn with_drift_tolerance(mut self, secs: u64) -> Self {
179 self.drift_tolerance_secs = secs;
180 self
181 }
182
183 pub fn push(&mut self, cert: DelegationCert) -> &mut Self {
184 self.certs.push(cert);
185 self
186 }
187
188 pub fn len(&self) -> usize {
189 self.certs.len()
190 }
191 pub fn is_empty(&self) -> bool {
192 self.certs.is_empty()
193 }
194 pub fn certs(&self) -> &[DelegationCert] {
195 &self.certs
196 }
197
198 pub fn fingerprint(&self) -> [u8; 32] {
199 let mut h = Hasher::new_derive_key(DOMAIN_CHAIN_FP);
200 h.update(b"a1::dyolo::chain::v2.8.0");
201 h.update(self.principal_pk.as_bytes());
202 h.update(&self.principal_scope);
203 if let Some(ns) = &self.namespace {
204 h.update(&(ns.len() as u64).to_le_bytes());
205 h.update(ns.as_bytes());
206 } else {
207 h.update(&0u64.to_le_bytes());
208 }
209 self.certs
210 .iter()
211 .fold(h, |mut h, cert| {
212 h.update(&cert.fingerprint());
213 h
214 })
215 .finalize()
216 .into()
217 }
218
219 fn validate_structure(
222 &self,
223 agent_pk: &VerifyingKey,
224 intent: &IntentHash,
225 proof: &MerkleProof,
226 clock: &dyn Clock,
227 drift_tolerance: u64,
228 ) -> Result<ChainValidationResult, A1Error> {
229 if self.certs.is_empty() {
230 return Err(A1Error::EmptyChain);
231 }
232 if self.certs[0].delegator_pk != self.principal_pk {
233 return Err(A1Error::RootMismatch);
234 }
235
236 let now = clock.unix_now();
237 let tolerated_early = now.saturating_add(drift_tolerance);
238 let tolerated_late = now.saturating_sub(drift_tolerance);
239 let chain_len = self.certs.len();
240
241 if chain_len > 255 {
242 return Err(A1Error::MaxDepthExceeded(255, 255));
243 }
244
245 let mut current_scope = self.principal_scope;
246 let mut expected_delegator = self.principal_pk;
247 let mut depth: usize = 0;
248 let mut max_allowed_depth = u8::MAX;
249 let mut parent_expiry = u64::MAX;
250
251 let mut seen_nonces: Vec<[u8; 16]> = Vec::with_capacity(chain_len);
252 let mut cert_fingerprints: Vec<[u8; 32]> = Vec::with_capacity(chain_len);
253 let mut batch_sigs: Vec<ed25519_dalek::Signature> = Vec::with_capacity(chain_len);
254 let mut batch_pks: Vec<VerifyingKey> = Vec::with_capacity(chain_len);
255 let mut batch_msgs: Vec<Vec<u8>> = Vec::with_capacity(chain_len);
256
257 for (i, cert) in self.certs.iter().enumerate() {
258 if cert.delegator_pk != expected_delegator {
259 return Err(A1Error::BrokenLinkage(i));
260 }
261
262 if cert.version != crate::cert::CERT_VERSION {
263 return Err(A1Error::UnsupportedVersion {
264 expected: crate::cert::CERT_VERSION,
265 got: cert.version,
266 });
267 }
268
269 #[cfg(feature = "wire")]
270 let ext_commit = cert.extensions.commitment();
271 #[cfg(not(feature = "wire"))]
272 let ext_commit = cert.extensions_hash.unwrap_or_else(|| {
273 let mut h = crate::crypto::derive_key("a1::dyolo::cert::ext::v2.8.0", cert.version);
274 h.update(&0u64.to_le_bytes());
275 h.finalize().into()
276 });
277
278 batch_msgs.push(DelegationCert::signable_bytes(
279 cert.version,
280 &cert.delegator_pk,
281 &cert.delegate_pk,
282 &cert.scope_root,
283 &cert.scope_proof,
284 &cert.nonce,
285 cert.issued_at,
286 cert.expiration_unix,
287 cert.max_depth,
288 &ext_commit,
289 ));
290 batch_sigs.push(cert.signature);
291 batch_pks.push(cert.delegator_pk);
292
293 if tolerated_early < cert.issued_at {
294 return Err(A1Error::NotYetValid(i, cert.issued_at, now));
295 }
296 if cert.expiration_unix < tolerated_late {
297 return Err(A1Error::Expired(i, cert.expiration_unix, now));
298 }
299 if cert.expiration_unix > parent_expiry {
300 return Err(A1Error::TemporalViolation(
301 i,
302 cert.expiration_unix,
303 parent_expiry,
304 ));
305 }
306
307 depth += 1;
308 if depth > max_allowed_depth as usize {
309 return Err(A1Error::MaxDepthExceeded(i, max_allowed_depth));
310 }
311 if cert.max_depth < max_allowed_depth {
312 max_allowed_depth = cert.max_depth;
313 }
314
315 for seen in &seen_nonces {
316 if seen == &cert.nonce {
317 return Err(A1Error::NonceReplay);
318 }
319 }
320 seen_nonces.push(cert.nonce);
321 cert_fingerprints.push(cert.fingerprint());
322
323 let is_passthrough =
324 cert.scope_proof.subset_intents.is_empty() && cert.scope_proof.proofs.is_empty();
325 if is_passthrough {
326 use subtle::ConstantTimeEq;
327 if cert.scope_root.ct_eq(¤t_scope).unwrap_u8() == 0 {
328 return Err(A1Error::ScopeEscalation(i));
329 }
330 } else {
331 let derived = cert
332 .scope_proof
333 .verify_and_derive_root(¤t_scope)
334 .map_err(|_| A1Error::ScopeEscalation(i))?;
335 use subtle::ConstantTimeEq;
336 if derived.ct_eq(&cert.scope_root).unwrap_u8() == 0 {
337 return Err(A1Error::ScopeEscalation(i));
338 }
339 }
340
341 parent_expiry = cert.expiration_unix;
342 current_scope = cert.scope_root;
343 expected_delegator = cert.delegate_pk;
344 }
345
346 if expected_delegator != *agent_pk {
347 return Err(A1Error::UnauthorizedLeaf);
348 }
349
350 {
351 let msgs_refs: Vec<&[u8]> = batch_msgs.iter().map(|m| m.as_slice()).collect();
352 if ed25519_dalek::verify_batch(&msgs_refs, &batch_sigs, &batch_pks).is_err() {
353 for (i, cert) in self.certs.iter().enumerate() {
354 if !cert.verify_signature() {
355 return Err(A1Error::InvalidSignature(i));
356 }
357 }
358 return Err(A1Error::InvalidSignature(0));
359 }
360 }
361
362 let intent_authorized = if proof.siblings.is_empty() {
363 use subtle::ConstantTimeEq;
364 intent.ct_eq(¤t_scope).into()
365 } else {
366 proof.verify(intent, ¤t_scope)
367 };
368 if !intent_authorized {
369 return Err(A1Error::ScopeViolation);
370 }
371
372 Ok(ChainValidationResult {
373 depth,
374 verified_scope_root: current_scope,
375 verified_at_unix: now,
376 seen_nonces,
377 cert_fingerprints,
378 chain_fingerprint: self.fingerprint(),
379 })
380 }
381
382 pub fn authorize(
385 &self,
386 agent_pk: &VerifyingKey,
387 intent: &IntentHash,
388 proof: &MerkleProof,
389 clock: &(dyn Clock + Send + Sync),
390 revocation: &(dyn RevocationStore + Send + Sync),
391 nonces: &(dyn NonceStore + Send + Sync),
392 ) -> Result<AuthorizedAction, A1Error> {
393 self.authorize_with_options(
394 agent_pk,
395 intent,
396 proof,
397 clock,
398 revocation,
399 nonces,
400 None,
401 &NoopAuditSink,
402 )
403 }
404
405 #[allow(clippy::too_many_arguments)]
406 pub fn authorize_with_options(
407 &self,
408 agent_pk: &VerifyingKey,
409 intent_h: &IntentHash,
410 proof: &MerkleProof,
411 clock: &(dyn Clock + Send + Sync),
412 revocation: &(dyn RevocationStore + Send + Sync),
413 nonces: &(dyn NonceStore + Send + Sync),
414 policy: Option<&PolicySet>,
415 sink: &dyn AuditSink,
416 ) -> Result<AuthorizedAction, A1Error> {
417 #[cfg(feature = "tracing")]
418 let _span = tracing::info_span!("a1::authorize", chain_len = self.certs.len()).entered();
419
420 let principal_hex = hex::encode(self.principal_pk.as_bytes());
421 let executor_hex = hex::encode(agent_pk.as_bytes());
422
423 let result =
424 self.authorize_inner(agent_pk, intent_h, proof, clock, revocation, nonces, policy);
425
426 let outcome = match &result {
427 Ok(_) => AuditOutcome::Authorized,
428 Err(A1Error::PolicyViolation(_)) => AuditOutcome::PolicyViolation,
429 Err(e) if e.is_transient_storage_failure() => AuditOutcome::StorageError,
430 Err(_) => AuditOutcome::Denied,
431 };
432
433 let mut event = AuditEvent::new(
434 outcome,
435 principal_hex,
436 executor_hex,
437 self.certs.len(),
438 intent_h,
439 clock.unix_now(),
440 );
441
442 if let Ok(action) = &result {
443 event = event.with_fingerprint(action.receipt.chain_fingerprint);
444 #[cfg(feature = "tracing")]
445 tracing::info!(
446 chain_depth = action.receipt.chain_depth,
447 chain_fingerprint = %action.receipt.fingerprint_hex(),
448 "a1: authorization succeeded"
449 );
450 } else if let Err(e) = &result {
451 event = event.with_error(e.to_string());
452 #[cfg(feature = "tracing")]
453 tracing::warn!(error = %e, "a1: authorization failed");
454 }
455
456 sink.emit(event);
457 result
458 }
459
460 #[allow(clippy::too_many_arguments)]
461 fn authorize_inner(
462 &self,
463 agent_pk: &VerifyingKey,
464 intent_h: &IntentHash,
465 proof: &MerkleProof,
466 clock: &(dyn Clock + Send + Sync),
467 revocation: &(dyn RevocationStore + Send + Sync),
468 nonces: &(dyn NonceStore + Send + Sync),
469 policy: Option<&PolicySet>,
470 ) -> Result<AuthorizedAction, A1Error> {
471 if let Some(p) = policy {
472 p.check_chain(self)?;
473 }
474
475 let v =
476 self.validate_structure(agent_pk, intent_h, proof, clock, self.drift_tolerance_secs)?;
477
478 for fp in &v.cert_fingerprints {
479 if revocation.is_revoked(fp).map_err(A1Error::StorageFailure)? {
480 return Err(A1Error::Revoked);
481 }
482 }
483
484 if !nonces
485 .try_consume_batch(&v.seen_nonces)
486 .map_err(A1Error::StorageFailure)?
487 {
488 return Err(A1Error::NonceReplay);
489 }
490
491 Ok(AuthorizedAction::new(VerificationReceipt {
492 chain_depth: v.depth,
493 verified_scope_root: v.verified_scope_root,
494 intent: *intent_h,
495 verified_at_unix: v.verified_at_unix,
496 chain_fingerprint: v.chain_fingerprint,
497 namespace: self.namespace.clone(),
498 }))
499 }
500
501 pub fn authorize_batch(
504 &self,
505 agent_pk: &VerifyingKey,
506 intents: &[(IntentHash, MerkleProof)],
507 clock: &(dyn Clock + Send + Sync),
508 revocation: &(dyn RevocationStore + Send + Sync),
509 nonces: &(dyn NonceStore + Send + Sync),
510 ) -> BatchAuthorizeResult {
511 if intents.is_empty() {
512 return BatchAuthorizeResult {
513 receipts: Vec::new(),
514 errors: Vec::new(),
515 all_authorized: true,
516 };
517 }
518
519 let now = clock.unix_now();
520 let first_intent = &intents[0].0;
521 let first_proof = &intents[0].1;
522
523 let v = match self.validate_structure(
524 agent_pk,
525 first_intent,
526 first_proof,
527 clock,
528 self.drift_tolerance_secs,
529 ) {
530 Ok(v) => v,
531 Err(e) => {
532 let n = intents.len();
533 let msg = e.to_string();
534 return BatchAuthorizeResult {
535 receipts: vec![None; n],
536 errors: (0..n)
537 .map(|i| {
538 Some(A1Error::BatchItemFailed {
539 index: i,
540 reason: msg.clone(),
541 })
542 })
543 .collect(),
544 all_authorized: false,
545 };
546 }
547 };
548
549 for fp in &v.cert_fingerprints {
550 match revocation.is_revoked(fp) {
551 Ok(true) => {
552 let n = intents.len();
553 return BatchAuthorizeResult {
554 receipts: vec![None; n],
555 errors: (0..n).map(|_| Some(A1Error::Revoked)).collect(),
556 all_authorized: false,
557 };
558 }
559 Err(e) => {
560 let n = intents.len();
561 let msg = A1Error::StorageFailure(e).to_string();
562 return BatchAuthorizeResult {
563 receipts: vec![None; n],
564 errors: (0..n)
565 .map(|_| {
566 Some(A1Error::BatchItemFailed {
567 index: 0,
568 reason: msg.clone(),
569 })
570 })
571 .collect(),
572 all_authorized: false,
573 };
574 }
575 Ok(false) => {}
576 }
577 }
578
579 let mut receipts: Vec<Option<VerificationReceipt>> = Vec::with_capacity(intents.len());
580 let mut errors: Vec<Option<A1Error>> = Vec::with_capacity(intents.len());
581 let mut all_ok = true;
582
583 for (i, (intent_h, proof)) in intents.iter().enumerate() {
584 let intent_authorized = if proof.siblings.is_empty() {
585 use subtle::ConstantTimeEq;
586 intent_h.ct_eq(&v.verified_scope_root).into()
587 } else {
588 proof.verify(intent_h, &v.verified_scope_root)
589 };
590
591 if intent_authorized {
592 receipts.push(Some(VerificationReceipt {
593 chain_depth: v.depth,
594 verified_scope_root: v.verified_scope_root,
595 intent: *intent_h,
596 verified_at_unix: now,
597 chain_fingerprint: v.chain_fingerprint,
598 namespace: self.namespace.clone(),
599 }));
600 errors.push(None);
601 } else {
602 receipts.push(None);
603 errors.push(Some(A1Error::BatchItemFailed {
604 index: i,
605 reason: A1Error::ScopeViolation.to_string(),
606 }));
607 all_ok = false;
608 }
609 }
610
611 if !all_ok {
612 return BatchAuthorizeResult {
613 receipts,
614 errors,
615 all_authorized: false,
616 };
617 }
618
619 match nonces.try_consume_batch(&v.seen_nonces) {
620 Ok(true) => {}
621 Ok(false) => {
622 let n = intents.len();
623 return BatchAuthorizeResult {
624 receipts: vec![None; n],
625 errors: (0..n).map(|_| Some(A1Error::NonceReplay)).collect(),
626 all_authorized: false,
627 };
628 }
629 Err(e) => {
630 let n = intents.len();
631 let msg = A1Error::StorageFailure(e).to_string();
632 return BatchAuthorizeResult {
633 receipts: vec![None; n],
634 errors: (0..n)
635 .map(|_| {
636 Some(A1Error::BatchItemFailed {
637 index: 0,
638 reason: msg.clone(),
639 })
640 })
641 .collect(),
642 all_authorized: false,
643 };
644 }
645 }
646
647 BatchAuthorizeResult {
648 receipts,
649 errors,
650 all_authorized: true,
651 }
652 }
653
654 #[cfg(feature = "async")]
657 pub async fn authorize_async(
658 &self,
659 agent_pk: &VerifyingKey,
660 intent: &IntentHash,
661 proof: &MerkleProof,
662 clock: &(dyn Clock + Send + Sync),
663 revocation: &(dyn crate::registry::r#async::AsyncRevocationStore + Send + Sync),
664 nonces: &(dyn crate::registry::r#async::AsyncNonceStore + Send + Sync),
665 ) -> Result<AuthorizedAction, A1Error> {
666 self.authorize_async_with_options(
667 agent_pk,
668 intent,
669 proof,
670 clock,
671 revocation,
672 nonces,
673 None,
674 &NoopAuditSink,
675 )
676 .await
677 }
678
679 #[cfg(feature = "async")]
680 #[allow(clippy::too_many_arguments)]
681 pub async fn authorize_async_with_options(
682 &self,
683 agent_pk: &VerifyingKey,
684 intent_h: &IntentHash,
685 proof: &MerkleProof,
686 clock: &(dyn Clock + Send + Sync),
687 revocation: &(dyn crate::registry::r#async::AsyncRevocationStore + Send + Sync),
688 nonces: &(dyn crate::registry::r#async::AsyncNonceStore + Send + Sync),
689 policy: Option<&PolicySet>,
690 sink: &dyn AuditSink,
691 ) -> Result<AuthorizedAction, A1Error> {
692 let principal_hex = hex::encode(self.principal_pk.as_bytes());
693 let executor_hex = hex::encode(agent_pk.as_bytes());
694
695 #[cfg(feature = "tracing")]
696 let span = tracing::info_span!("a1::authorize_async", chain_len = self.certs.len());
697
698 let result = async {
699 if let Some(p) = policy {
700 p.check_chain(self)?;
701 }
702
703 let v = self.validate_structure(
704 agent_pk,
705 intent_h,
706 proof,
707 clock,
708 self.drift_tolerance_secs,
709 )?;
710
711 for fp in &v.cert_fingerprints {
712 if revocation
713 .is_revoked(fp)
714 .await
715 .map_err(A1Error::StorageFailure)?
716 {
717 return Err(A1Error::Revoked);
718 }
719 }
720
721 if !nonces
722 .try_consume_batch(&v.seen_nonces)
723 .await
724 .map_err(A1Error::StorageFailure)?
725 {
726 return Err(A1Error::NonceReplay);
727 }
728
729 Ok(AuthorizedAction::new(VerificationReceipt {
730 chain_depth: v.depth,
731 verified_scope_root: v.verified_scope_root,
732 intent: *intent_h,
733 verified_at_unix: v.verified_at_unix,
734 chain_fingerprint: v.chain_fingerprint,
735 namespace: self.namespace.clone(),
736 }))
737 };
738
739 #[cfg(feature = "tracing")]
740 let result = result.instrument(span).await;
741 #[cfg(not(feature = "tracing"))]
742 let result = result.await;
743
744 let outcome = match &result {
745 Ok(_) => AuditOutcome::Authorized,
746 Err(A1Error::PolicyViolation(_)) => AuditOutcome::PolicyViolation,
747 Err(e) if e.is_transient_storage_failure() => AuditOutcome::StorageError,
748 Err(_) => AuditOutcome::Denied,
749 };
750
751 let mut event = AuditEvent::new(
752 outcome,
753 principal_hex,
754 executor_hex,
755 self.certs.len(),
756 intent_h,
757 clock.unix_now(),
758 );
759
760 if let Ok(action) = &result {
761 event = event.with_fingerprint(action.receipt.chain_fingerprint);
762 } else if let Err(e) = &result {
763 event = event.with_error(e.to_string());
764 }
765
766 sink.emit(event);
767 result
768 }
769
770 #[cfg(feature = "async")]
771 pub async fn authorize_batch_async(
772 &self,
773 agent_pk: &VerifyingKey,
774 intents: &[(IntentHash, MerkleProof)],
775 clock: &(dyn Clock + Send + Sync),
776 revocation: &(dyn crate::registry::r#async::AsyncRevocationStore + Send + Sync),
777 nonces: &(dyn crate::registry::r#async::AsyncNonceStore + Send + Sync),
778 ) -> BatchAuthorizeResult {
779 if intents.is_empty() {
780 return BatchAuthorizeResult {
781 receipts: Vec::new(),
782 errors: Vec::new(),
783 all_authorized: true,
784 };
785 }
786
787 let now = clock.unix_now();
788 let first_intent = &intents[0].0;
789 let first_proof = &intents[0].1;
790
791 let v = match self.validate_structure(
792 agent_pk,
793 first_intent,
794 first_proof,
795 clock,
796 self.drift_tolerance_secs,
797 ) {
798 Ok(v) => v,
799 Err(e) => {
800 let n = intents.len();
801 let msg = e.to_string();
802 return BatchAuthorizeResult {
803 receipts: vec![None; n],
804 errors: (0..n)
805 .map(|i| {
806 Some(A1Error::BatchItemFailed {
807 index: i,
808 reason: msg.clone(),
809 })
810 })
811 .collect(),
812 all_authorized: false,
813 };
814 }
815 };
816
817 for fp in &v.cert_fingerprints {
818 match revocation.is_revoked(fp).await {
819 Ok(true) => {
820 let n = intents.len();
821 return BatchAuthorizeResult {
822 receipts: vec![None; n],
823 errors: (0..n).map(|_| Some(A1Error::Revoked)).collect(),
824 all_authorized: false,
825 };
826 }
827 Err(e) => {
828 let n = intents.len();
829 let msg = A1Error::StorageFailure(e).to_string();
830 return BatchAuthorizeResult {
831 receipts: vec![None; n],
832 errors: (0..n)
833 .map(|_| {
834 Some(A1Error::BatchItemFailed {
835 index: 0,
836 reason: msg.clone(),
837 })
838 })
839 .collect(),
840 all_authorized: false,
841 };
842 }
843 Ok(false) => {}
844 }
845 }
846
847 let mut receipts: Vec<Option<VerificationReceipt>> = Vec::with_capacity(intents.len());
848 let mut errors: Vec<Option<A1Error>> = Vec::with_capacity(intents.len());
849 let mut all_ok = true;
850
851 for (i, (intent_h, proof)) in intents.iter().enumerate() {
852 let intent_authorized = if proof.siblings.is_empty() {
853 use subtle::ConstantTimeEq;
854 intent_h.ct_eq(&v.verified_scope_root).into()
855 } else {
856 proof.verify(intent_h, &v.verified_scope_root)
857 };
858
859 if intent_authorized {
860 receipts.push(Some(VerificationReceipt {
861 chain_depth: v.depth,
862 verified_scope_root: v.verified_scope_root,
863 intent: *intent_h,
864 verified_at_unix: now,
865 chain_fingerprint: v.chain_fingerprint,
866 namespace: self.namespace.clone(),
867 }));
868 errors.push(None);
869 } else {
870 receipts.push(None);
871 errors.push(Some(A1Error::BatchItemFailed {
872 index: i,
873 reason: A1Error::ScopeViolation.to_string(),
874 }));
875 all_ok = false;
876 }
877 }
878
879 if !all_ok {
880 return BatchAuthorizeResult {
881 receipts,
882 errors,
883 all_authorized: false,
884 };
885 }
886
887 match nonces.try_consume_batch(&v.seen_nonces).await {
888 Ok(true) => {}
889 Ok(false) => {
890 let n = intents.len();
891 return BatchAuthorizeResult {
892 receipts: vec![None; n],
893 errors: (0..n).map(|_| Some(A1Error::NonceReplay)).collect(),
894 all_authorized: false,
895 };
896 }
897 Err(e) => {
898 let n = intents.len();
899 let msg = A1Error::StorageFailure(e).to_string();
900 return BatchAuthorizeResult {
901 receipts: vec![None; n],
902 errors: (0..n)
903 .map(|_| {
904 Some(A1Error::BatchItemFailed {
905 index: 0,
906 reason: msg.clone(),
907 })
908 })
909 .collect(),
910 all_authorized: false,
911 };
912 }
913 }
914
915 BatchAuthorizeResult {
916 receipts,
917 errors,
918 all_authorized: true,
919 }
920 }
921}
922
923impl Clone for DyoloChain {
924 fn clone(&self) -> Self {
925 Self {
926 principal_pk: self.principal_pk,
927 principal_scope: self.principal_scope,
928 certs: self.certs.clone(),
929 drift_tolerance_secs: self.drift_tolerance_secs,
930 namespace: self.namespace.clone(),
931 }
932 }
933}
934
935#[cfg(test)]
938mod tests {
939 use super::*;
940 #[allow(deprecated)]
941 use crate::{
942 cert::CertBuilder,
943 identity::DyoloIdentity,
944 intent::{intent_hash, IntentTree},
945 registry::{MemoryNonceStore, MemoryRevocationStore},
946 };
947
948 struct FixedClock(u64);
949 impl Clock for FixedClock {
950 fn unix_now(&self) -> u64 {
951 self.0
952 }
953 }
954
955 #[allow(deprecated)]
956 fn setup() -> (DyoloIdentity, DyoloIdentity, DyoloIdentity, IntentTree, u64) {
957 let human = DyoloIdentity::generate();
958 let agent_a = DyoloIdentity::generate();
959 let agent_b = DyoloIdentity::generate();
960 let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
961 let query = intent_hash("QUERY_PORTFOLIO", b"");
962 let tree = IntentTree::build(vec![trade, query]).unwrap();
963 let now = 1_700_000_000u64;
964 (human, agent_a, agent_b, tree, now)
965 }
966
967 #[allow(deprecated)]
968 fn two_hop_chain(
969 human: &DyoloIdentity,
970 a: &DyoloIdentity,
971 b: &DyoloIdentity,
972 scope: IntentHash,
973 now: u64,
974 ) -> DyoloChain {
975 let expiry = now + 3600;
976 let ca = CertBuilder::new(a.verifying_key(), scope, now, expiry).sign(human);
977 let cb = CertBuilder::new(b.verifying_key(), scope, now, expiry).sign(a);
978 let mut chain = DyoloChain::new(human.verifying_key(), scope);
979 chain.push(ca).push(cb);
980 chain
981 }
982
983 #[test]
984 #[allow(deprecated)]
985 fn full_delegation_chain_succeeds() {
986 let (human, a, b, tree, now) = setup();
987 let root = tree.root();
988 let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
989 let proof = tree.prove(&trade).unwrap();
990 let chain = two_hop_chain(&human, &a, &b, root, now);
991 let action = chain
992 .authorize(
993 &b.verifying_key(),
994 &trade,
995 &proof,
996 &FixedClock(now),
997 &MemoryRevocationStore::new(),
998 &MemoryNonceStore::new(),
999 )
1000 .unwrap();
1001 assert_eq!(action.receipt.chain_depth, 2);
1002 assert_eq!(action.receipt.intent, trade);
1003 assert!(action.receipt.namespace.is_none());
1004 }
1005
1006 #[test]
1007 #[allow(deprecated)]
1008 fn namespace_isolation_different_scopes() {
1009 let human = DyoloIdentity::generate();
1010 let agent = DyoloIdentity::generate();
1011 let now = SystemClock.unix_now();
1012
1013 let base_scope: IntentHash = intent_hash("trade", b"");
1015 let chain_a = DyoloChain::new(human.verifying_key(), base_scope).with_namespace("tenant-a");
1016 let chain_b = DyoloChain::new(human.verifying_key(), base_scope).with_namespace("tenant-b");
1017
1018 assert_ne!(
1019 chain_a.principal_scope, chain_b.principal_scope,
1020 "namespaced chains must have different effective scopes"
1021 );
1022 assert_ne!(chain_a.fingerprint(), chain_b.fingerprint());
1023
1024 let scope_a = chain_a.principal_scope;
1028 let scope_b = chain_b.principal_scope;
1029
1030 let cert_a =
1031 CertBuilder::new(agent.verifying_key(), scope_a, now, now + 86400).sign(&human);
1032 let cert_b =
1033 CertBuilder::new(agent.verifying_key(), scope_b, now, now + 86400).sign(&human);
1034
1035 let mut ca = DyoloChain::new(human.verifying_key(), scope_a);
1037 ca.push(cert_a);
1038 let ok_a = ca.authorize(
1039 &agent.verifying_key(),
1040 &scope_a,
1041 &MerkleProof::default(),
1042 &FixedClock(now),
1043 &MemoryRevocationStore::new(),
1044 &MemoryNonceStore::new(),
1045 );
1046 assert!(
1047 ok_a.is_ok(),
1048 "tenant-a cert should authorize under tenant-a scope: {:?}",
1049 ok_a.err()
1050 );
1051
1052 let mut cb = DyoloChain::new(human.verifying_key(), scope_b);
1054 cb.push(cert_b);
1055 let ok_b = cb.authorize(
1056 &agent.verifying_key(),
1057 &scope_b,
1058 &MerkleProof::default(),
1059 &FixedClock(now),
1060 &MemoryRevocationStore::new(),
1061 &MemoryNonceStore::new(),
1062 );
1063 assert!(
1064 ok_b.is_ok(),
1065 "tenant-b cert should authorize under tenant-b scope: {:?}",
1066 ok_b.err()
1067 );
1068
1069 let cert_b_wrong =
1072 CertBuilder::new(agent.verifying_key(), scope_b, now, now + 86400).sign(&human);
1073 let mut c_wrong = DyoloChain::new(human.verifying_key(), scope_a);
1074 c_wrong.push(cert_b_wrong);
1075 let result = c_wrong.authorize(
1076 &agent.verifying_key(),
1077 &scope_a,
1078 &MerkleProof::default(),
1079 &FixedClock(now),
1080 &MemoryRevocationStore::new(),
1081 &MemoryNonceStore::new(),
1082 );
1083 assert!(
1084 result.is_err(),
1085 "cert scoped to tenant-b must not work under tenant-a chain"
1086 );
1087 }
1088
1089 #[test]
1090 #[allow(deprecated)]
1091 fn batch_authorize_all_or_nothing() {
1092 let (human, a, b, tree, now) = setup();
1093 let root = tree.root();
1094 let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
1095 let query = intent_hash("QUERY_PORTFOLIO", b"");
1096 let t_proof = tree.prove(&trade).unwrap();
1097 let q_proof = tree.prove(&query).unwrap();
1098 let chain = two_hop_chain(&human, &a, &b, root, now);
1099 let result = chain.authorize_batch(
1100 &b.verifying_key(),
1101 &[(trade, t_proof), (query, q_proof)],
1102 &FixedClock(now),
1103 &MemoryRevocationStore::new(),
1104 &MemoryNonceStore::new(),
1105 );
1106 assert!(result.all_authorized);
1107 assert_eq!(result.authorized_count(), 2);
1108 }
1109
1110 #[test]
1111 #[allow(deprecated)]
1112 fn chain_fingerprint_stable() {
1113 let (human, a, b, tree, now) = setup();
1114 let chain = two_hop_chain(&human, &a, &b, tree.root(), now);
1115 assert_eq!(chain.fingerprint(), chain.clone().fingerprint());
1116 }
1117
1118 #[cfg(feature = "async")]
1119 #[tokio::test]
1120 #[allow(deprecated)]
1121 async fn authorize_async_succeeds() {
1122 use crate::registry::r#async::{SyncNonceAdapter, SyncRevocationAdapter};
1123 use std::sync::Arc;
1124 let (human, a, b, tree, now) = setup();
1125 let root = tree.root();
1126 let trade = intent_hash("TRADE_AAPL_100", b"limit=182.50");
1127 let proof = tree.prove(&trade).unwrap();
1128 let chain = two_hop_chain(&human, &a, &b, root, now);
1129 let rev = SyncRevocationAdapter(Arc::new(MemoryRevocationStore::new()));
1130 let nonces = SyncNonceAdapter(Arc::new(MemoryNonceStore::new()));
1131 let action = chain
1132 .authorize_async(
1133 &b.verifying_key(),
1134 &trade,
1135 &proof,
1136 &FixedClock(now),
1137 &rev,
1138 &nonces,
1139 )
1140 .await
1141 .unwrap();
1142 assert_eq!(action.receipt.chain_depth, 2);
1143 }
1144}