1use super::approvals::{ApprovalError, ApprovalProvider};
2use super::authority::{PresentedAuthority, ReceivedAuthorization};
3use super::call::Call;
4use super::clock::{Clock, SystemClock};
5use super::decision::{
6 Decision, DecisionMetadata, Denial, DenialReporting, GuardError, Retryability, SdkDenialKind,
7};
8use super::delegation::{DelegationError, DelegationProfile};
9use super::diagnostics::Diagnostics;
10use super::signer::PopSigningRequest;
11use crate::approval::{ApprovalRequest, SignedApproval};
12use crate::constraints::ConstraintValue;
13use crate::crypto::{PublicKey, Signature};
14use crate::planes::Authorizer;
15use crate::revocation_tracker::RevocationTracker;
16use crate::verification::{
17 RevocationSnapshot, RevocationState, VerificationContext, VerificationInstant,
18};
19use crate::warrant::Warrant;
20use chrono::{DateTime, Utc};
21use std::collections::HashMap;
22use std::fmt;
23use std::sync::Arc;
24use std::time::Duration;
25
26#[cfg(feature = "receipts")]
27use super::evidence::{sign_payload, EvidencePolicy, ReceiptSigner, ReceiptSink, ReceiptSinkError};
28#[cfg(feature = "receipts")]
29use crate::approval::compute_request_hash;
30#[cfg(feature = "receipts")]
31use crate::receipt::ReceiptPayload;
32#[cfg(feature = "receipts")]
33use crate::wire::{encode_stack, WarrantStack};
34
35#[derive(Clone, Debug)]
40#[non_exhaustive]
41pub enum RevocationMode {
42 TtlOnly {
53 max_lifetime: Duration,
55 },
56 SignedSrl,
66 TtlUntilSrl {
71 max_lifetime: Duration,
73 },
74}
75
76#[derive(Clone)]
78pub struct Guard {
79 authorizer: Authorizer,
80 revocation: ResolvedRevocation,
81 denial_reporting: DenialReporting,
82 clock: Arc<dyn Clock>,
83 approval_provider: Option<Arc<dyn ApprovalProvider>>,
84 #[cfg(feature = "async")]
85 async_revocation: Option<Arc<dyn super::async_api::AsyncRevocationProvider>>,
86 #[cfg(feature = "receipts")]
87 evidence: EvidenceConfig,
88 #[cfg(feature = "filesystem")]
89 filesystem: Option<Arc<super::filesystem::Workspace>>,
90}
91
92#[derive(Clone)]
93enum ResolvedRevocation {
94 TtlOnly {
95 max_lifetime: Duration,
96 },
97 Snapshot(Arc<RevocationSnapshot>),
98 Tracker(std::sync::Arc<RevocationTracker>),
99 TtlUntilSrl {
101 max_lifetime: Duration,
102 tracker: std::sync::Arc<RevocationTracker>,
103 },
104}
105
106#[cfg(feature = "receipts")]
107pub(crate) fn verified_received_pop<'a>(
108 denial: &Denial,
109 pop: &'a Signature,
110) -> Option<&'a Signature> {
111 match denial.protocol_code() {
114 Some(
115 crate::ErrorCode::ConstraintViolation
116 | crate::ErrorCode::ApprovalRequired
117 | crate::ErrorCode::InsufficientApprovals
118 | crate::ErrorCode::ApprovalInvalid
119 | crate::ErrorCode::ApproverNotAuthorized
120 | crate::ErrorCode::ApprovalExpired
121 | crate::ErrorCode::UnsupportedApprovalVersion
122 | crate::ErrorCode::ApprovalPayloadInvalid
123 | crate::ErrorCode::ApprovalRequestHashMismatch,
124 ) => Some(pop),
125 _ => None,
126 }
127}
128
129impl Guard {
130 pub fn builder() -> GuardBuilder {
132 GuardBuilder::default()
133 }
134
135 pub fn check(
140 &self,
141 authority: &PresentedAuthority,
142 call: &Call<'_>,
143 ) -> Result<Decision, Denial> {
144 self.check_attempt(authority, AuthorizationAttempt::new(call))
145 }
146
147 pub fn check_attempt(
151 &self,
152 authority: &PresentedAuthority,
153 attempt: AuthorizationAttempt<'_, '_>,
154 ) -> Result<Decision, Denial> {
155 match self.try_authorize_holder(authority, &attempt) {
156 Ok(authorized) => self.complete_allow(&authorized),
157 Err((denial, pop)) => {
158 self.record_deny(&denial);
159 #[cfg(feature = "receipts")]
160 self.emit_deny_receipt(authority.chain(), attempt.call, &denial, pop.as_ref());
161 Err(denial)
162 }
163 }
164 }
165
166 pub fn guard<T, E>(
201 &self,
202 authority: &PresentedAuthority,
203 call: &Call<'_>,
204 op: impl FnOnce(&AuthorizedCall<'_>) -> Result<T, E>,
205 ) -> Result<Guarded<T>, GuardError<E>> {
206 self.guard_attempt(authority, AuthorizationAttempt::new(call), op)
207 }
208
209 pub fn guard_attempt<T, E>(
211 &self,
212 authority: &PresentedAuthority,
213 attempt: AuthorizationAttempt<'_, '_>,
214 op: impl FnOnce(&AuthorizedCall<'_>) -> Result<T, E>,
215 ) -> Result<Guarded<T>, GuardError<E>> {
216 let authorized =
217 self.try_authorize_holder(authority, &attempt)
218 .map_err(|(denial, pop)| {
219 self.record_deny(&denial);
220 #[cfg(feature = "receipts")]
221 self.emit_deny_receipt(authority.chain(), attempt.call, &denial, pop.as_ref());
222 GuardError::Denied(denial)
223 })?;
224 let decision = self
225 .complete_allow(&authorized)
226 .map_err(GuardError::Denied)?;
227 let value = op(&authorized).map_err(GuardError::Operation)?;
228 Ok(Guarded { value, decision })
229 }
230
231 pub fn check_received(
236 &self,
237 received: &ReceivedAuthorization<'_>,
238 call: &Call<'_>,
239 ) -> Result<Decision, Denial> {
240 match self.authorize_received(received, call) {
241 Ok(authorized) => self.complete_allow(&authorized),
242 Err(denial) => {
243 self.record_deny(&denial);
244 #[cfg(feature = "receipts")]
245 self.emit_deny_receipt(
246 received.chain(),
247 call,
248 &denial,
249 verified_received_pop(&denial, received.signature()),
250 );
251 Err(denial)
252 }
253 }
254 }
255
256 pub fn guard_received<T, E>(
258 &self,
259 received: &ReceivedAuthorization<'_>,
260 call: &Call<'_>,
261 op: impl FnOnce(&AuthorizedCall<'_>) -> Result<T, E>,
262 ) -> Result<Guarded<T>, GuardError<E>> {
263 let authorized = self.authorize_received(received, call).map_err(|denial| {
264 self.record_deny(&denial);
265 #[cfg(feature = "receipts")]
266 self.emit_deny_receipt(
267 received.chain(),
268 call,
269 &denial,
270 verified_received_pop(&denial, received.signature()),
271 );
272 GuardError::Denied(denial)
273 })?;
274 let decision = self
275 .complete_allow(&authorized)
276 .map_err(GuardError::Denied)?;
277 let value = op(&authorized).map_err(GuardError::Operation)?;
278 Ok(Guarded { value, decision })
279 }
280
281 pub fn diagnostics<'a>(&'a self, authority: &'a PresentedAuthority) -> Diagnostics<'a> {
283 Diagnostics::holder(self, authority)
284 }
285
286 pub fn diagnostics_received<'a>(
288 &'a self,
289 received: &'a ReceivedAuthorization<'a>,
290 ) -> Diagnostics<'a> {
291 Diagnostics::received(self, received)
292 }
293
294 pub fn delegate(
299 &self,
300 parent: &PresentedAuthority,
301 profile: &DelegationProfile,
302 ) -> Result<PresentedAuthority, DelegationError> {
303 self.confirm_parent_live(parent)?;
304 parent.delegate_local(profile)
305 }
306
307 pub fn delegate_to(
309 &self,
310 parent: &PresentedAuthority,
311 child_holder: &PublicKey,
312 profile: &DelegationProfile,
313 ) -> Result<Vec<Warrant>, DelegationError> {
314 self.confirm_parent_live(parent)?;
315 parent.delegate_to(child_holder, profile)
316 }
317
318 fn confirm_parent_live(&self, parent: &PresentedAuthority) -> Result<(), DelegationError> {
319 let instant = VerificationInstant::new(self.now());
320 let loaded = self
321 .load_revocation(instant.as_datetime())
322 .map_err(|denial| {
323 self.record_deny(&denial);
324 DelegationError::Denied(denial)
325 })?;
326 let context = VerificationContext::new(instant.as_datetime(), loaded.as_state());
327 self.authorizer
328 .verify_chain_with_context(parent.chain(), &context)
329 .map_err(|err| {
330 let denial = Denial::from_core(err);
331 self.record_deny(&denial);
332 DelegationError::Denied(denial)
333 })?;
334 Ok(())
335 }
336
337 #[allow(dead_code)]
338 pub(crate) fn denial_reporting(&self) -> DenialReporting {
339 self.denial_reporting
340 }
341
342 #[cfg(feature = "async")]
343 pub(crate) fn pop_window_config(&self) -> (i64, u32) {
344 self.authorizer.pop_window_config()
345 }
346
347 pub(crate) fn snapshot_for_observe(&self) -> Self {
348 let cloned = self.clone();
349 #[cfg(feature = "receipts")]
350 {
351 let mut cloned = cloned;
352 cloned.evidence = EvidenceConfig::disabled();
353 cloned
354 }
355 #[cfg(not(feature = "receipts"))]
356 cloned
357 }
358
359 pub(crate) fn complete_allow(
360 &self,
361 authorized: &AuthorizedCall<'_>,
362 ) -> Result<Decision, Denial> {
363 #[cfg(feature = "otel")]
364 super::telemetry::record_authorize("allow", "allowed", false);
365 #[cfg(feature = "receipts")]
366 {
367 let mut decision = authorized.as_decision();
368 decision.receipt = self.attach_receipt(authorized)?;
369 Ok(decision)
370 }
371 #[cfg(not(feature = "receipts"))]
372 Ok(authorized.as_decision())
373 }
374
375 pub(crate) fn now(&self) -> DateTime<Utc> {
376 self.clock.now()
377 }
378
379 pub fn resolve_approvals(
381 &self,
382 request: &ApprovalRequest,
383 ) -> Result<Vec<SignedApproval>, ApprovalError> {
384 let provider = self
385 .approval_provider
386 .as_ref()
387 .ok_or(ApprovalError::NoProvider)?;
388 provider.approvals_for(request)
389 }
390
391 #[cfg(feature = "async")]
392 pub async fn resolve_approvals_async(
397 &self,
398 request: &ApprovalRequest,
399 control: &super::async_api::AttemptControl,
400 ) -> Result<Vec<SignedApproval>, ApprovalError> {
401 if control.is_cancelled() {
402 return Err(ApprovalError::Unavailable);
403 }
404 self.resolve_approvals(request)
405 }
406
407 pub(crate) fn record_deny(&self, denial: &Denial) {
408 match self.denial_reporting {
409 DenialReporting::Error => {
410 eprintln!("tenuo deny [{}]: {}", denial.code(), denial);
411 }
412 DenialReporting::Warn => {
413 eprintln!("tenuo deny [{}] (warn): {}", denial.code(), denial);
414 }
415 DenialReporting::Debug => {}
416 }
417 #[cfg(feature = "otel")]
418 super::telemetry::record_authorize("deny", denial.code(), false);
419 }
420
421 #[cfg(feature = "receipts")]
422 fn attach_receipt(
423 &self,
424 authorized: &AuthorizedCall<'_>,
425 ) -> Result<Option<crate::receipt::Receipt>, Denial> {
426 if self.evidence.policy == EvidencePolicy::Disabled {
427 return Ok(None);
428 }
429 let stack = encode_stack(&WarrantStack(authorized.chain().to_vec())).map_err(|_| {
430 Denial::sdk(
431 SdkDenialKind::EvidenceUnavailable,
432 Retryability::AfterBackoff,
433 "receipt chain encoding failed",
434 )
435 })?;
436 let leaf = authorized.chain().last().ok_or_else(|| {
437 Denial::sdk(
438 SdkDenialKind::AuthorityMalformed,
439 Retryability::No,
440 "authority chain is empty",
441 )
442 })?;
443 let mut payload = ReceiptPayload::allow(
444 stack,
445 format!("tool:{}", authorized.capability()),
446 authorized.instant().as_datetime().timestamp(),
447 authorized.invocation_id(),
448 authorized.pop_signature().to_bytes(),
449 );
450 self.commit_receipt_links(&mut payload);
451 payload.request_hash = Some(compute_request_hash(
452 &leaf.id().to_string(),
453 authorized.capability(),
454 authorized.pop_args(),
455 Some(leaf.authorized_holder()),
456 ));
457 self.seal_linked_receipt(payload).map(Some)
458 }
459
460 #[cfg(feature = "receipts")]
461 fn commit_receipt_links(&self, payload: &mut ReceiptPayload) {
462 let srl = self
463 .tracker_srl()
464 .or_else(|| self.authorizer.installed_revocation_list().cloned());
465 if let Some(srl) = srl {
466 payload.srl_version = Some(srl.version());
467 if let Ok(bytes) = srl.to_bytes() {
468 payload.srl_hash = Some(crate::srl_commitment_digest(&bytes));
469 }
470 }
471 let roots: Vec<[u8; 32]> = self
472 .authorizer
473 .trusted_root_keys()
474 .iter()
475 .map(|k| k.to_bytes())
476 .collect();
477 payload.trusted_roots_hash = Some(crate::trusted_roots_digest(&roots));
478 }
479
480 #[cfg(feature = "receipts")]
481 fn recover_receipt_link(&self) -> std::sync::MutexGuard<'_, Option<[u8; 32]>> {
482 self.evidence
483 .last_receipt_hash
484 .lock()
485 .unwrap_or_else(|poisoned| poisoned.into_inner())
486 }
487
488 #[cfg(feature = "receipts")]
489 fn rollback_receipt_link(&self, digest: [u8; 32], prev: Option<[u8; 32]>) {
490 let mut link = self.recover_receipt_link();
491 if *link == Some(digest) {
492 *link = prev;
493 }
494 }
495
496 #[cfg(feature = "receipts")]
497 fn seal_linked_receipt(
498 &self,
499 mut payload: ReceiptPayload,
500 ) -> Result<crate::receipt::Receipt, Denial> {
501 let Some(signer) = self.evidence.signer.as_ref() else {
502 return Err(Denial::sdk(
503 SdkDenialKind::EvidenceUnavailable,
504 Retryability::AfterBackoff,
505 "receipt signer unavailable",
506 ));
507 };
508 let (receipt, digest, prev) = {
509 let mut link = self.recover_receipt_link();
510 let prev = *link;
511 payload.prev_receipt_hash = prev;
512 let bytes = payload.to_cbor().map_err(|_| {
513 Denial::sdk(
514 SdkDenialKind::EvidenceUnavailable,
515 Retryability::AfterBackoff,
516 "receipt payload encoding failed",
517 )
518 })?;
519 let receipt = sign_payload(&bytes, signer.as_ref()).map_err(|_| {
520 Denial::sdk(
521 SdkDenialKind::EvidenceUnavailable,
522 Retryability::AfterBackoff,
523 "receipt signer unavailable",
524 )
525 })?;
526 let digest = receipt.digest().ok();
527 if let Some(digest) = digest {
528 *link = Some(digest);
529 }
530 (receipt, digest, prev)
531 };
532 let Some(sink) = &self.evidence.sink else {
533 return Ok(receipt);
534 };
535 match sink.persist(&receipt) {
536 Ok(_) => Ok(receipt),
537 Err(ReceiptSinkError::Unavailable)
538 if self.evidence.policy == EvidencePolicy::BestEffort =>
539 {
540 if let Some(digest) = digest {
541 self.rollback_receipt_link(digest, prev);
542 }
543 Ok(receipt)
544 }
545 Err(_) => {
546 if let Some(digest) = digest {
547 self.rollback_receipt_link(digest, prev);
548 }
549 Err(Denial::sdk(
550 SdkDenialKind::EvidenceUnavailable,
551 Retryability::AfterBackoff,
552 "receipt persistence failed",
553 ))
554 }
555 }
556 }
557
558 fn tracker_srl(&self) -> Option<crate::revocation::SignedRevocationList> {
559 let tracker = match &self.revocation {
560 ResolvedRevocation::Tracker(tracker)
561 | ResolvedRevocation::TtlUntilSrl { tracker, .. } => tracker,
562 _ => return None,
563 };
564 tracker
565 .latest(self.now())
566 .ok()
567 .map(|snap| snap.srl().clone())
568 }
569
570 #[cfg(feature = "receipts")]
571 pub(crate) fn emit_deny_receipt(
572 &self,
573 chain: &[Warrant],
574 call: &Call<'_>,
575 denial: &Denial,
576 pop: Option<&Signature>,
577 ) {
578 if self.evidence.policy == EvidencePolicy::Disabled {
579 return;
580 }
581 let Some(signer) = self.evidence.signer.as_ref() else {
582 return;
583 };
584 let Ok(stack) = encode_stack(&WarrantStack(chain.to_vec())) else {
585 return;
586 };
587 let mut payload = if let Some(pop) = pop {
588 let mut payload = ReceiptPayload::deny(
589 stack,
590 format!("tool:{}", call.capability()),
591 self.now().timestamp(),
592 format!("deny:{}", denial.code()),
593 denial.code().to_string(),
594 pop.to_bytes(),
595 );
596 if let Some(leaf) = chain.last() {
597 payload.request_hash = Some(compute_request_hash(
598 &leaf.id().to_string(),
599 call.capability(),
600 call.pop_args(),
601 Some(leaf.authorized_holder()),
602 ));
603 }
604 payload
605 } else {
606 ReceiptPayload::deny_before_pop(
607 stack,
608 format!("tool:{}", call.capability()),
609 self.now().timestamp(),
610 format!("deny:{}", denial.code()),
611 denial.code().to_string(),
612 )
613 };
614 self.commit_receipt_links(&mut payload);
615 let (receipt, digest, prev) = {
616 let mut link = self.recover_receipt_link();
617 let prev = *link;
618 payload.prev_receipt_hash = prev;
619 let Ok(bytes) = payload.to_cbor() else {
620 return;
621 };
622 let Ok(receipt) = sign_payload(&bytes, signer.as_ref()) else {
623 return;
624 };
625 let digest = receipt.digest().ok();
626 if let Some(digest) = digest {
627 *link = Some(digest);
628 }
629 (receipt, digest, prev)
630 };
631 match self.evidence.sink.as_ref() {
632 None => {}
633 Some(sink) if sink.persist(&receipt).is_ok() => {}
634 Some(_) => {
635 if let Some(digest) = digest {
636 self.rollback_receipt_link(digest, prev);
637 }
638 }
639 }
640 }
641
642 pub(crate) fn authorize_holder<'a>(
643 &'a self,
644 authority: &'a PresentedAuthority,
645 attempt: &AuthorizationAttempt<'a, 'a>,
646 ) -> Result<AuthorizedCall<'a>, Denial> {
647 self.try_authorize_holder(authority, attempt)
648 .map_err(|(denial, _)| denial)
649 }
650
651 #[allow(clippy::result_large_err)]
652 fn try_authorize_holder<'a>(
653 &'a self,
654 authority: &'a PresentedAuthority,
655 attempt: &AuthorizationAttempt<'a, 'a>,
656 ) -> Result<AuthorizedCall<'a>, (Denial, Option<Signature>)> {
657 let as_of = self.now();
658 let instant = VerificationInstant::new(as_of);
659
660 if !authority.signer_matches_leaf() {
661 return Err((
662 Denial::sdk(
663 SdkDenialKind::SignerUnavailable,
664 Retryability::AfterBackoff,
665 "holder signer does not match leaf",
666 ),
667 None,
668 ));
669 }
670
671 let call = attempt.call;
672 let leaf = authority.leaf();
673 let (window_secs, _) = self.authorizer.pop_window_config();
674 let preimage = leaf
675 .pop_preimage(
676 call.capability(),
677 call.pop_args(),
678 as_of.timestamp(),
679 window_secs,
680 )
681 .map_err(|e| (Denial::from_core(e), None))?;
682 let request = PopSigningRequest::new(preimage, call.capability(), leaf.id().to_string());
683 let pop_signature = authority.signer().sign_pop(&request).map_err(|_| {
684 (
685 Denial::sdk(
686 SdkDenialKind::SignerUnavailable,
687 Retryability::AfterBackoff,
688 "holder signer unavailable",
689 ),
690 None,
691 )
692 })?;
693
694 self.decide(
695 authority.chain(),
696 pop_signature.clone(),
697 attempt.approvals,
698 call,
699 instant,
700 )
701 .map_err(|denial| (denial, Some(pop_signature)))
702 }
703
704 #[cfg(feature = "async")]
705 #[allow(clippy::result_large_err)]
706 pub(crate) async fn authorize_holder_async<'a>(
707 &'a self,
708 authority: &'a super::async_api::PresentedAsyncAuthority,
709 attempt: &AuthorizationAttempt<'a, 'a>,
710 control: &super::async_api::AttemptControl,
711 ) -> Result<AuthorizedCall<'a>, (Denial, Option<Signature>)> {
712 let as_of = self.now();
713 let instant = VerificationInstant::new(as_of);
714
715 if !authority.signer_matches_leaf() {
716 return Err((
717 Denial::sdk(
718 SdkDenialKind::SignerUnavailable,
719 Retryability::AfterBackoff,
720 "holder signer does not match leaf",
721 ),
722 None,
723 ));
724 }
725
726 let call = attempt.call;
727 let leaf = authority.leaf();
728 let (window_secs, _) = self.authorizer.pop_window_config();
729 let preimage = leaf
730 .pop_preimage(
731 call.capability(),
732 call.pop_args(),
733 as_of.timestamp(),
734 window_secs,
735 )
736 .map_err(|e| (Denial::from_core(e), None))?;
737 let request = PopSigningRequest::new(preimage, call.capability(), leaf.id().to_string());
738 let pop_signature = authority
739 .signer()
740 .sign_pop(&request, control)
741 .await
742 .map_err(|_| {
743 (
744 Denial::sdk(
745 SdkDenialKind::SignerUnavailable,
746 Retryability::AfterBackoff,
747 "holder signer unavailable",
748 ),
749 None,
750 )
751 })?;
752
753 self.decide_async(
754 authority.chain(),
755 pop_signature.clone(),
756 attempt.approvals,
757 call,
758 instant,
759 control,
760 )
761 .await
762 .map_err(|denial| (denial, Some(pop_signature)))
763 }
764
765 pub(crate) fn authorize_received<'a>(
766 &'a self,
767 received: &'a ReceivedAuthorization<'a>,
768 call: &'a Call<'a>,
769 ) -> Result<AuthorizedCall<'a>, Denial> {
770 let instant = VerificationInstant::new(self.now());
771 self.decide(
772 received.chain(),
773 received.signature().clone(),
774 received.approvals(),
775 call,
776 instant,
777 )
778 }
779
780 fn decide<'a>(
781 &'a self,
782 chain: &'a [Warrant],
783 pop_signature: Signature,
784 approvals: &'a [SignedApproval],
785 call: &'a Call<'a>,
786 instant: VerificationInstant,
787 ) -> Result<AuthorizedCall<'a>, Denial> {
788 let loaded = self.load_revocation(instant.as_datetime())?;
789 self.finish_decide(chain, pop_signature, approvals, call, instant, &loaded)
790 }
791
792 #[cfg(feature = "async")]
793 async fn decide_async<'a>(
794 &'a self,
795 chain: &'a [Warrant],
796 pop_signature: Signature,
797 approvals: &'a [SignedApproval],
798 call: &'a Call<'a>,
799 instant: VerificationInstant,
800 control: &super::async_api::AttemptControl,
801 ) -> Result<AuthorizedCall<'a>, Denial> {
802 let loaded = self
803 .load_revocation_async(instant.as_datetime(), control)
804 .await?;
805 self.finish_decide(chain, pop_signature, approvals, call, instant, &loaded)
806 }
807
808 fn finish_decide<'a>(
809 &'a self,
810 chain: &'a [Warrant],
811 pop_signature: Signature,
812 approvals: &'a [SignedApproval],
813 call: &'a Call<'a>,
814 instant: VerificationInstant,
815 loaded: &LoadedRevocation,
816 ) -> Result<AuthorizedCall<'a>, Denial> {
817 let _ = self.denial_reporting;
818 let context = VerificationContext::new(instant.as_datetime(), loaded.as_state());
819
820 self.authorizer
821 .check_chain_with_context(
822 chain,
823 call.capability(),
824 call.pop_args(),
825 call.constraint_args(),
826 Some(&pop_signature),
827 approvals,
828 &context,
829 )
830 .map_err(Denial::from_core)?;
831
832 let invocation_id = uuid::Uuid::new_v4().to_string();
833 let decision_id = uuid::Uuid::new_v4().to_string();
834 let leaf = chain.last().ok_or_else(|| {
835 Denial::sdk(
836 SdkDenialKind::AuthorityMalformed,
837 Retryability::No,
838 "authority chain is empty",
839 )
840 })?;
841 let dedup_key = leaf.dedup_key(call.capability(), call.pop_args());
842
843 Ok(AuthorizedCall {
844 invocation_id,
845 dedup_key,
846 decision_id,
847 capability: call.capability(),
848 instant,
849 execution_args: call.constraint_args(),
850 pop_args: call.pop_args(),
851 pop_signature,
852 chain,
853 approvals,
854 #[cfg(feature = "filesystem")]
855 filesystem: self.filesystem.clone(),
856 })
857 }
858
859 fn load_revocation(&self, at: DateTime<Utc>) -> Result<LoadedRevocation, Denial> {
860 match &self.revocation {
861 ResolvedRevocation::TtlOnly { max_lifetime } => Ok(LoadedRevocation::TtlOnly {
862 max_lifetime: *max_lifetime,
863 }),
864 ResolvedRevocation::Snapshot(snapshot) => {
865 if !snapshot.is_fresh_at(at) {
866 return Err(Denial::sdk(
867 SdkDenialKind::RevocationStateUnavailable,
868 Retryability::AfterBackoff,
869 "revocation snapshot is stale",
870 ));
871 }
872 Ok(LoadedRevocation::Snapshot(snapshot.clone()))
873 }
874 ResolvedRevocation::Tracker(tracker) => Self::snapshot_from_tracker(tracker, at),
875 ResolvedRevocation::TtlUntilSrl {
876 max_lifetime,
877 tracker,
878 } => match tracker.latest(at) {
879 Ok(snapshot) => {
880 if !snapshot.is_fresh_at(at) {
881 return Err(Denial::sdk(
882 SdkDenialKind::RevocationStateUnavailable,
883 Retryability::AfterBackoff,
884 "revocation snapshot is stale",
885 ));
886 }
887 Ok(LoadedRevocation::Snapshot(snapshot))
888 }
889 Err(crate::revocation_tracker::RevocationError::Unavailable)
890 | Err(crate::revocation_tracker::RevocationError::UnavailableAt { .. }) => {
891 Ok(LoadedRevocation::TtlOnly {
892 max_lifetime: *max_lifetime,
893 })
894 }
895 Err(_) => Err(Denial::sdk(
896 SdkDenialKind::RevocationStateUnavailable,
897 Retryability::AfterBackoff,
898 "revocation state unavailable",
899 )),
900 },
901 }
902 }
903
904 fn snapshot_from_tracker(
905 tracker: &RevocationTracker,
906 at: DateTime<Utc>,
907 ) -> Result<LoadedRevocation, Denial> {
908 let snapshot = tracker.latest(at).map_err(|_| {
909 Denial::sdk(
910 SdkDenialKind::RevocationStateUnavailable,
911 Retryability::AfterBackoff,
912 "revocation state unavailable",
913 )
914 })?;
915 if !snapshot.is_fresh_at(at) {
916 return Err(Denial::sdk(
917 SdkDenialKind::RevocationStateUnavailable,
918 Retryability::AfterBackoff,
919 "revocation snapshot is stale",
920 ));
921 }
922 Ok(LoadedRevocation::Snapshot(snapshot))
923 }
924
925 #[cfg(feature = "async")]
926 async fn load_revocation_async(
927 &self,
928 at: DateTime<Utc>,
929 control: &super::async_api::AttemptControl,
930 ) -> Result<LoadedRevocation, Denial> {
931 match self.load_revocation(at) {
932 Ok(loaded) => Ok(loaded),
933 Err(denial)
934 if matches!(
935 denial.sdk_kind(),
936 Some(SdkDenialKind::RevocationStateUnavailable)
937 ) =>
938 {
939 let Some(provider) = &self.async_revocation else {
940 return Err(denial);
941 };
942 if control.is_cancelled() {
943 return Err(Denial::sdk(
944 SdkDenialKind::Cancelled,
945 Retryability::AfterBackoff,
946 "authorization cancelled",
947 ));
948 }
949 if control
950 .deadline()
951 .is_some_and(|deadline| self.now() >= deadline)
952 {
953 return Err(Denial::sdk(
954 SdkDenialKind::DeadlineExceeded,
955 Retryability::AfterBackoff,
956 "authorization deadline exceeded",
957 ));
958 }
959 let ResolvedRevocation::Tracker(tracker) = &self.revocation else {
960 return Err(denial);
961 };
962 let update = provider.fetch(control).await.map_err(|_| {
963 Denial::sdk(
964 SdkDenialKind::RevocationStateUnavailable,
965 Retryability::AfterBackoff,
966 "revocation state unavailable",
967 )
968 })?;
969 let snapshot = tracker.accept(update, at).map_err(|_| {
970 Denial::sdk(
971 SdkDenialKind::RevocationStateUnavailable,
972 Retryability::AfterBackoff,
973 "revocation state unavailable",
974 )
975 })?;
976 if !snapshot.is_fresh_at(at) {
977 return Err(Denial::sdk(
978 SdkDenialKind::RevocationStateUnavailable,
979 Retryability::AfterBackoff,
980 "revocation snapshot is stale",
981 ));
982 }
983 Ok(LoadedRevocation::Snapshot(snapshot))
984 }
985 Err(denial) => Err(denial),
986 }
987 }
988}
989
990enum LoadedRevocation {
991 TtlOnly { max_lifetime: Duration },
992 Snapshot(Arc<RevocationSnapshot>),
993}
994
995impl LoadedRevocation {
996 fn as_state(&self) -> RevocationState<'_> {
997 match self {
998 Self::TtlOnly { max_lifetime } => RevocationState::TtlOnly {
999 max_lifetime: *max_lifetime,
1000 },
1001 Self::Snapshot(snapshot) => RevocationState::Snapshot(snapshot.as_ref()),
1002 }
1003 }
1004}
1005
1006#[cfg(feature = "receipts")]
1007#[derive(Clone)]
1008struct EvidenceConfig {
1009 policy: EvidencePolicy,
1010 signer: Option<std::sync::Arc<dyn ReceiptSigner>>,
1011 sink: Option<std::sync::Arc<dyn ReceiptSink>>,
1012 last_receipt_hash: std::sync::Arc<std::sync::Mutex<Option<[u8; 32]>>>,
1013}
1014
1015#[cfg(feature = "receipts")]
1016impl EvidenceConfig {
1017 fn disabled() -> Self {
1018 Self {
1019 policy: EvidencePolicy::Disabled,
1020 signer: None,
1021 sink: None,
1022 last_receipt_hash: std::sync::Arc::new(std::sync::Mutex::new(None)),
1023 }
1024 }
1025}
1026
1027pub struct AuthorizationAttempt<'attempt, 'args> {
1035 call: &'attempt Call<'args>,
1036 approvals: &'attempt [SignedApproval],
1037}
1038
1039impl<'attempt, 'args> AuthorizationAttempt<'attempt, 'args> {
1040 pub fn new(call: &'attempt Call<'args>) -> Self {
1042 Self {
1043 call,
1044 approvals: &[],
1045 }
1046 }
1047
1048 pub fn with_approvals(
1050 call: &'attempt Call<'args>,
1051 approvals: &'attempt [SignedApproval],
1052 ) -> Self {
1053 Self { call, approvals }
1054 }
1055}
1056
1057pub struct AuthorizedCall<'a> {
1059 invocation_id: String,
1060 dedup_key: String,
1061 decision_id: String,
1062 capability: &'a str,
1063 instant: VerificationInstant,
1064 execution_args: &'a HashMap<String, ConstraintValue>,
1065 pop_args: &'a HashMap<String, ConstraintValue>,
1066 pop_signature: Signature,
1067 chain: &'a [Warrant],
1068 approvals: &'a [SignedApproval],
1069 #[cfg(feature = "filesystem")]
1070 filesystem: Option<Arc<super::filesystem::Workspace>>,
1071}
1072
1073impl<'a> AuthorizedCall<'a> {
1074 pub fn invocation_id(&self) -> &str {
1076 &self.invocation_id
1077 }
1078
1079 pub fn dedup_key(&self) -> &str {
1082 &self.dedup_key
1083 }
1084
1085 pub fn decision_id(&self) -> &str {
1087 &self.decision_id
1088 }
1089
1090 pub fn capability(&self) -> &str {
1092 self.capability
1093 }
1094
1095 pub fn instant(&self) -> VerificationInstant {
1097 self.instant
1098 }
1099
1100 pub fn execution_args(&self) -> &HashMap<String, ConstraintValue> {
1102 self.execution_args
1103 }
1104
1105 pub fn pop_args(&self) -> &HashMap<String, ConstraintValue> {
1107 self.pop_args
1108 }
1109
1110 pub fn pop_signature(&self) -> &Signature {
1112 &self.pop_signature
1113 }
1114
1115 pub fn chain(&self) -> &[Warrant] {
1117 self.chain
1118 }
1119
1120 pub fn approvals(&self) -> &[SignedApproval] {
1122 self.approvals
1123 }
1124
1125 #[cfg(feature = "filesystem")]
1132 #[cfg_attr(docsrs, doc(cfg(feature = "filesystem")))]
1133 pub fn open(
1134 &self,
1135 argument: &str,
1136 options: super::filesystem::OpenOptions,
1137 ) -> Result<super::filesystem::OpenedFile, super::filesystem::FilesystemError> {
1138 let workspace = self
1139 .filesystem
1140 .as_ref()
1141 .ok_or(super::filesystem::FilesystemError::WorkspaceMissing)?;
1142 workspace.open_authorized(
1143 argument,
1144 self.capability,
1145 self.execution_args,
1146 self.chain,
1147 &options,
1148 )
1149 }
1150
1151 fn as_decision(&self) -> Decision {
1152 Decision {
1153 metadata: DecisionMetadata {
1154 decision_id: self.decision_id.clone(),
1155 invocation_id: self.invocation_id.clone(),
1156 dedup_key: self.dedup_key.clone(),
1157 capability: self.capability.to_string(),
1158 chain_depth: self.chain.len(),
1159 timestamp: self.instant.as_datetime(),
1160 },
1161 receipt: None,
1162 }
1163 }
1164}
1165
1166#[must_use]
1168pub struct Guarded<T> {
1169 pub value: T,
1171 pub decision: Decision,
1173}
1174
1175impl<T> Guarded<T> {
1176 pub fn into_inner(self) -> T {
1178 self.value
1179 }
1180}
1181
1182impl<T: fmt::Debug> fmt::Debug for Guarded<T> {
1183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1184 f.debug_struct("Guarded")
1185 .field("value", &self.value)
1186 .field("capability", &self.decision.metadata.capability)
1187 .field("decision_id", &self.decision.metadata.decision_id)
1188 .finish_non_exhaustive()
1189 }
1190}
1191
1192#[derive(Default)]
1194pub struct GuardBuilder {
1195 authorizer: Option<Authorizer>,
1196 revocation: Option<RevocationMode>,
1197 tracker: Option<std::sync::Arc<RevocationTracker>>,
1198 denial_reporting: DenialReporting,
1199 clock: Option<Arc<dyn Clock>>,
1200 approval_provider: Option<Arc<dyn ApprovalProvider>>,
1201 #[cfg(feature = "async")]
1202 async_revocation: Option<Arc<dyn super::async_api::AsyncRevocationProvider>>,
1203 #[cfg(feature = "receipts")]
1204 evidence: EvidencePolicy,
1205 #[cfg(feature = "receipts")]
1206 receipt_signer: Option<std::sync::Arc<dyn ReceiptSigner>>,
1207 #[cfg(feature = "receipts")]
1208 receipt_sink: Option<std::sync::Arc<dyn ReceiptSink>>,
1209 #[cfg(feature = "receipts")]
1210 receipt_link: Option<std::sync::Arc<std::sync::Mutex<Option<[u8; 32]>>>>,
1211 #[cfg(feature = "async")]
1212 declared_sync_deadline: bool,
1213 #[cfg(feature = "filesystem")]
1214 filesystem: Option<Arc<super::filesystem::Workspace>>,
1215}
1216
1217impl GuardBuilder {
1218 pub fn authorizer(mut self, authorizer: Authorizer) -> Self {
1220 self.authorizer = Some(authorizer);
1221 self
1222 }
1223
1224 pub fn revocation(mut self, mode: RevocationMode) -> Self {
1226 self.revocation = Some(mode);
1227 self
1228 }
1229
1230 pub fn revocation_tracker(mut self, tracker: std::sync::Arc<RevocationTracker>) -> Self {
1232 self.tracker = Some(tracker);
1233 self
1234 }
1235
1236 pub(crate) fn ttl_until_signed_srl(
1238 mut self,
1239 max_lifetime: Duration,
1240 tracker: std::sync::Arc<RevocationTracker>,
1241 ) -> Self {
1242 self.revocation = Some(RevocationMode::TtlUntilSrl { max_lifetime });
1243 self.tracker = Some(tracker);
1244 self
1245 }
1246
1247 #[cfg(feature = "filesystem")]
1251 #[cfg_attr(docsrs, doc(cfg(feature = "filesystem")))]
1252 pub fn filesystem(mut self, workspace: super::filesystem::Workspace) -> Self {
1253 self.filesystem = Some(Arc::new(workspace));
1254 self
1255 }
1256
1257 pub fn denial_reporting(mut self, reporting: DenialReporting) -> Self {
1262 self.denial_reporting = reporting;
1263 self
1264 }
1265
1266 pub fn approval_provider(mut self, provider: Arc<dyn ApprovalProvider>) -> Self {
1268 self.approval_provider = Some(provider);
1269 self
1270 }
1271
1272 #[cfg(feature = "async")]
1275 pub fn async_revocation_provider(
1276 mut self,
1277 provider: Arc<dyn super::async_api::AsyncRevocationProvider>,
1278 ) -> Self {
1279 self.async_revocation = Some(provider);
1280 self
1281 }
1282
1283 #[cfg(feature = "test-utils")]
1285 pub fn clock(mut self, clock: Arc<dyn Clock>) -> Self {
1286 self.clock = Some(clock);
1287 self
1288 }
1289
1290 #[cfg(feature = "receipts")]
1291 pub fn evidence_policy(mut self, policy: EvidencePolicy) -> Self {
1293 self.evidence = policy;
1294 self
1295 }
1296
1297 #[cfg(feature = "receipts")]
1298 pub fn receipt_signer(mut self, signer: std::sync::Arc<dyn ReceiptSigner>) -> Self {
1300 self.receipt_signer = Some(signer);
1301 self
1302 }
1303
1304 #[cfg(feature = "receipts")]
1305 pub fn receipt_sink(mut self, sink: std::sync::Arc<dyn ReceiptSink>) -> Self {
1307 self.receipt_sink = Some(sink);
1308 self
1309 }
1310
1311 #[cfg(feature = "receipts")]
1313 pub(crate) fn receipt_link(
1314 mut self,
1315 link: std::sync::Arc<std::sync::Mutex<Option<[u8; 32]>>>,
1316 ) -> Self {
1317 self.receipt_link = Some(link);
1318 self
1319 }
1320
1321 #[cfg(feature = "async")]
1323 pub fn deadline(mut self, _deadline: Duration) -> Self {
1324 self.declared_sync_deadline = true;
1325 self
1326 }
1327
1328 pub fn build(self) -> Result<Guard, GuardBuildError> {
1330 let authorizer = self.authorizer.ok_or(GuardBuildError::MissingAuthorizer)?;
1331 if !authorizer.has_trusted_roots() {
1332 return Err(GuardBuildError::EmptyTrustStore);
1333 }
1334 let mode = self
1335 .revocation
1336 .ok_or(GuardBuildError::MissingRevocationMode)?;
1337 #[cfg(feature = "async")]
1338 if self.declared_sync_deadline {
1339 return Err(GuardBuildError::DeadlineNotEnforceable);
1340 }
1341 #[cfg(feature = "receipts")]
1342 let evidence = {
1343 let last_receipt_hash = self
1344 .receipt_link
1345 .unwrap_or_else(|| std::sync::Arc::new(std::sync::Mutex::new(None)));
1346 match self.evidence {
1347 EvidencePolicy::Disabled => EvidenceConfig::disabled(),
1348 EvidencePolicy::BestEffort => {
1349 if self.receipt_signer.is_none() {
1350 return Err(GuardBuildError::MissingReceiptSigner);
1351 }
1352 EvidenceConfig {
1353 policy: EvidencePolicy::BestEffort,
1354 signer: self.receipt_signer,
1355 sink: self.receipt_sink,
1356 last_receipt_hash,
1357 }
1358 }
1359 EvidencePolicy::RequiredBeforeExecution => {
1360 if self.receipt_signer.is_none() {
1361 return Err(GuardBuildError::MissingReceiptSigner);
1362 }
1363 if self.receipt_sink.is_none() {
1364 return Err(GuardBuildError::MissingReceiptSink);
1365 }
1366 EvidenceConfig {
1367 policy: EvidencePolicy::RequiredBeforeExecution,
1368 signer: self.receipt_signer,
1369 sink: self.receipt_sink,
1370 last_receipt_hash,
1371 }
1372 }
1373 }
1374 };
1375 let revocation = match mode {
1376 RevocationMode::TtlOnly { max_lifetime } => {
1377 ResolvedRevocation::TtlOnly { max_lifetime }
1378 }
1379 RevocationMode::TtlUntilSrl { max_lifetime } => {
1380 let tracker = self.tracker.ok_or(GuardBuildError::SignedSrlUnavailable)?;
1381 ResolvedRevocation::TtlUntilSrl {
1382 max_lifetime,
1383 tracker,
1384 }
1385 }
1386 RevocationMode::SignedSrl => {
1387 if let Some(tracker) = self.tracker {
1388 ResolvedRevocation::Tracker(tracker)
1389 } else {
1390 let list = authorizer
1391 .installed_revocation_list()
1392 .cloned()
1393 .ok_or(GuardBuildError::SignedSrlUnavailable)?;
1394 ResolvedRevocation::Snapshot(Arc::new(RevocationSnapshot::from_accepted_list(
1395 list,
1396 )))
1397 }
1398 }
1399 };
1400 Ok(Guard {
1401 authorizer,
1402 revocation,
1403 denial_reporting: self.denial_reporting,
1404 clock: self.clock.unwrap_or_else(|| Arc::new(SystemClock)),
1405 approval_provider: self.approval_provider,
1406 #[cfg(feature = "async")]
1407 async_revocation: self.async_revocation,
1408 #[cfg(feature = "receipts")]
1409 evidence,
1410 #[cfg(feature = "filesystem")]
1411 filesystem: self.filesystem,
1412 })
1413 }
1414}
1415
1416impl Guard {
1417 #[cfg(feature = "filesystem")]
1423 #[cfg_attr(docsrs, doc(cfg(feature = "filesystem")))]
1424 pub fn with_filesystem(mut self, workspace: super::filesystem::Workspace) -> Self {
1425 self.filesystem = Some(Arc::new(workspace));
1426 self
1427 }
1428}
1429
1430#[derive(Debug, Clone, PartialEq, Eq)]
1432#[non_exhaustive]
1433pub enum GuardBuildError {
1434 MissingAuthorizer,
1436 EmptyTrustStore,
1438 MissingRevocationMode,
1440 SignedSrlUnavailable,
1442 #[cfg(feature = "receipts")]
1443 MissingReceiptSigner,
1445 #[cfg(feature = "receipts")]
1446 MissingReceiptSink,
1448 #[cfg(feature = "async")]
1449 DeadlineNotEnforceable,
1452}
1453
1454impl fmt::Display for GuardBuildError {
1455 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1456 match self {
1457 Self::MissingAuthorizer => write!(f, "guard requires an authorizer"),
1458 Self::EmptyTrustStore => write!(f, "guard authorizer has an empty trust store"),
1459 Self::MissingRevocationMode => write!(f, "guard requires an explicit revocation mode"),
1460 Self::SignedSrlUnavailable => {
1461 write!(f, "SignedSrl requires a revocation list on the authorizer")
1462 }
1463 #[cfg(feature = "receipts")]
1464 Self::MissingReceiptSigner => {
1465 write!(f, "receipts require an explicit ReceiptSigner")
1466 }
1467 #[cfg(feature = "receipts")]
1468 Self::MissingReceiptSink => {
1469 write!(f, "RequiredBeforeExecution requires a local ReceiptSink")
1470 }
1471 #[cfg(feature = "async")]
1472 Self::DeadlineNotEnforceable => {
1473 write!(f, "the synchronous guard cannot enforce a deadline")
1474 }
1475 }
1476 }
1477}
1478
1479impl std::error::Error for GuardBuildError {}
1480
1481#[cfg(test)]
1482mod tests {
1483 use super::*;
1484 use crate::constraints::ConstraintSet;
1485 use crate::crypto::SigningKey;
1486 use crate::error::ErrorCode;
1487 use crate::sdk::signer::{HolderSigner, LocalSigner, SignerError};
1488 use crate::sdk::{Call, PresentedAuthority, ReceivedAuthorization, VerifiedProjection};
1489 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1490 use std::sync::Arc;
1491
1492 fn mint(issuer: &SigningKey, holder: &SigningKey, tool: &str) -> Warrant {
1493 Warrant::builder()
1494 .capability(tool, ConstraintSet::new())
1495 .holder(holder.public_key())
1496 .ttl(Duration::from_secs(300))
1497 .build(issuer)
1498 .expect("mint")
1499 }
1500
1501 fn guard_for(issuer: &SigningKey) -> Guard {
1502 let mut authorizer = Authorizer::new();
1503 authorizer.add_trusted_root(issuer.public_key());
1504 Guard::builder()
1505 .authorizer(authorizer)
1506 .revocation(RevocationMode::TtlOnly {
1507 max_lifetime: Duration::from_secs(3600),
1508 })
1509 .build()
1510 .expect("guard")
1511 }
1512
1513 fn authority(chain: Vec<Warrant>, holder: SigningKey) -> PresentedAuthority {
1514 PresentedAuthority::new(chain, Arc::new(LocalSigner::new(holder))).expect("authority")
1515 }
1516
1517 #[test]
1518 fn allow_runs_operation_once() {
1519 let issuer = SigningKey::generate();
1520 let holder = SigningKey::generate();
1521 let warrant = mint(&issuer, &holder, "read");
1522 let authority = authority(vec![warrant], holder);
1523 let guard = guard_for(&issuer);
1524 let args = HashMap::new();
1525 let call = Call::borrowed("read", &args);
1526 let runs = AtomicUsize::new(0);
1527
1528 let guarded = guard
1529 .guard(&authority, &call, |_| {
1530 runs.fetch_add(1, Ordering::SeqCst);
1531 Ok::<_, &str>(7)
1532 })
1533 .expect("allow");
1534
1535 assert_eq!(guarded.value, 7);
1536 assert_eq!(runs.load(Ordering::SeqCst), 1);
1537 assert_eq!(guarded.decision.metadata.capability, "read");
1538 assert_eq!(guarded.decision.metadata.chain_depth, 1);
1539 let expected = authority.leaf().dedup_key("read", &args);
1540 assert_eq!(guarded.decision.metadata.dedup_key, expected);
1541 let again = guard.check(&authority, &call).expect("second allow");
1542 assert_eq!(again.metadata.dedup_key, expected);
1543 assert_ne!(
1544 again.metadata.invocation_id,
1545 guarded.decision.metadata.invocation_id
1546 );
1547 }
1548
1549 #[test]
1550 fn deny_does_not_run_operation() {
1551 let issuer = SigningKey::generate();
1552 let holder = SigningKey::generate();
1553 let warrant = mint(&issuer, &holder, "read");
1554 let authority = authority(vec![warrant], holder);
1555 let guard = guard_for(&issuer);
1556 let args = HashMap::new();
1557 let call = Call::borrowed("write", &args);
1558 let runs = AtomicUsize::new(0);
1559
1560 let err = guard
1561 .guard(&authority, &call, |_| {
1562 runs.fetch_add(1, Ordering::SeqCst);
1563 Ok::<_, &str>(())
1564 })
1565 .expect_err("deny");
1566
1567 assert_eq!(runs.load(Ordering::SeqCst), 0);
1568 match err {
1569 GuardError::Denied(denial) => {
1570 assert_eq!(denial.protocol_code(), Some(ErrorCode::ToolNotAuthorized));
1571 assert!(!denial.is_infrastructure());
1572 }
1573 GuardError::Operation(_) => panic!("operation must not run"),
1574 }
1575 }
1576
1577 #[test]
1578 fn empty_chain_rejected() {
1579 let holder = SigningKey::generate();
1580 let err = PresentedAuthority::new(vec![], Arc::new(LocalSigner::new(holder)))
1581 .expect_err("empty chain must be rejected");
1582 assert_eq!(err, crate::sdk::AuthorityError::EmptyChain);
1583 }
1584
1585 #[test]
1586 fn signer_mismatch_rejected_at_construction() {
1587 let issuer = SigningKey::generate();
1588 let holder = SigningKey::generate();
1589 let other = SigningKey::generate();
1590 let warrant = mint(&issuer, &holder, "read");
1591 let err = PresentedAuthority::new(vec![warrant], Arc::new(LocalSigner::new(other)))
1592 .expect_err("signer mismatch must be rejected");
1593 assert_eq!(err, crate::sdk::AuthorityError::SignerMismatch);
1594 }
1595
1596 #[test]
1597 fn empty_trust_store_rejected() {
1598 let err = Guard::builder()
1599 .authorizer(Authorizer::new())
1600 .revocation(RevocationMode::TtlOnly {
1601 max_lifetime: Duration::from_secs(60),
1602 })
1603 .build()
1604 .err()
1605 .expect("empty trust");
1606 assert_eq!(err, GuardBuildError::EmptyTrustStore);
1607 }
1608
1609 #[test]
1610 fn missing_revocation_mode_rejected() {
1611 let issuer = SigningKey::generate();
1612 let mut authorizer = Authorizer::new();
1613 authorizer.add_trusted_root(issuer.public_key());
1614 let err = Guard::builder()
1615 .authorizer(authorizer)
1616 .build()
1617 .err()
1618 .expect("missing mode");
1619 assert_eq!(err, GuardBuildError::MissingRevocationMode);
1620 }
1621
1622 #[test]
1623 fn signed_srl_without_list_rejected() {
1624 let issuer = SigningKey::generate();
1625 let mut authorizer = Authorizer::new();
1626 authorizer.add_trusted_root(issuer.public_key());
1627 let err = Guard::builder()
1628 .authorizer(authorizer)
1629 .revocation(RevocationMode::SignedSrl)
1630 .build()
1631 .err()
1632 .expect("no srl");
1633 assert_eq!(err, GuardBuildError::SignedSrlUnavailable);
1634 }
1635
1636 #[test]
1637 fn same_authority_denied_by_other_trust_store() {
1638 let issuer = SigningKey::generate();
1639 let other_root = SigningKey::generate();
1640 let holder = SigningKey::generate();
1641 let warrant = mint(&issuer, &holder, "read");
1642 let authority = authority(vec![warrant], holder);
1643 let args = HashMap::new();
1644 let call = Call::borrowed("read", &args);
1645
1646 assert!(guard_for(&issuer).check(&authority, &call).is_ok());
1647
1648 let mut stranger = Authorizer::new();
1649 stranger.add_trusted_root(other_root.public_key());
1650 let other_guard = Guard::builder()
1651 .authorizer(stranger)
1652 .revocation(RevocationMode::TtlOnly {
1653 max_lifetime: Duration::from_secs(3600),
1654 })
1655 .build()
1656 .unwrap();
1657 let denial = other_guard
1658 .check(&authority, &call)
1659 .err()
1660 .expect("untrusted");
1661 assert!(
1662 matches!(
1663 denial.protocol_code(),
1664 Some(ErrorCode::UntrustedRoot | ErrorCode::SignatureInvalid)
1665 ),
1666 "expected a protocol denial, got {:?}",
1667 denial.protocol_code()
1668 );
1669 }
1670
1671 struct FlipSigner {
1672 matching: SigningKey,
1673 other: SigningKey,
1674 flip: AtomicBool,
1675 }
1676
1677 impl HolderSigner for FlipSigner {
1678 fn public_key(&self) -> crate::crypto::PublicKey {
1679 if self.flip.load(Ordering::SeqCst) {
1680 self.other.public_key()
1681 } else {
1682 self.matching.public_key()
1683 }
1684 }
1685
1686 fn sign_pop(
1687 &self,
1688 request: &crate::sdk::PopSigningRequest<'_>,
1689 ) -> Result<Signature, SignerError> {
1690 Ok(self.matching.sign_raw(&request.final_signing_bytes()))
1691 }
1692
1693 fn sign_delegation(
1694 &self,
1695 request: &crate::sdk::DelegationSigningRequest<'_>,
1696 ) -> Result<Signature, SignerError> {
1697 Ok(self.matching.sign_raw(request.final_signing_bytes()))
1698 }
1699 }
1700
1701 #[test]
1702 fn signer_mismatch_at_call_time_denies_without_running() {
1703 let issuer = SigningKey::generate();
1704 let holder = SigningKey::generate();
1705 let other = SigningKey::generate();
1706 let warrant = mint(&issuer, &holder, "read");
1707 let signer = Arc::new(FlipSigner {
1708 matching: holder,
1709 other,
1710 flip: AtomicBool::new(false),
1711 });
1712 let authority = PresentedAuthority::new(vec![warrant], signer.clone()).expect("construct");
1713 signer.flip.store(true, Ordering::SeqCst);
1714
1715 let guard = guard_for(&issuer);
1716 let args = HashMap::new();
1717 let call = Call::borrowed("read", &args);
1718 let runs = AtomicUsize::new(0);
1719 let err = guard
1720 .guard(&authority, &call, |_| {
1721 runs.fetch_add(1, Ordering::SeqCst);
1722 Ok::<_, &str>(())
1723 })
1724 .expect_err("deny");
1725 assert_eq!(runs.load(Ordering::SeqCst), 0);
1726 match err {
1727 GuardError::Denied(denial) => {
1728 assert_eq!(denial.sdk_kind(), Some(SdkDenialKind::SignerUnavailable));
1729 assert_eq!(denial.retryability(), Retryability::AfterBackoff);
1730 }
1731 GuardError::Operation(_) => panic!("operation must not run"),
1732 }
1733 }
1734
1735 struct CountingSigner {
1736 inner: LocalSigner,
1737 pops: AtomicUsize,
1738 }
1739
1740 impl HolderSigner for CountingSigner {
1741 fn public_key(&self) -> crate::crypto::PublicKey {
1742 self.inner.public_key()
1743 }
1744
1745 fn sign_pop(
1746 &self,
1747 request: &crate::sdk::PopSigningRequest<'_>,
1748 ) -> Result<Signature, SignerError> {
1749 self.pops.fetch_add(1, Ordering::SeqCst);
1750 self.inner.sign_pop(request)
1751 }
1752
1753 fn sign_delegation(
1754 &self,
1755 request: &crate::sdk::DelegationSigningRequest<'_>,
1756 ) -> Result<Signature, SignerError> {
1757 self.inner.sign_delegation(request)
1758 }
1759 }
1760
1761 #[test]
1762 fn holder_path_always_signs_locally() {
1763 let issuer = SigningKey::generate();
1764 let holder = SigningKey::generate();
1765 let warrant = mint(&issuer, &holder, "read");
1766 let signer = Arc::new(CountingSigner {
1767 inner: LocalSigner::new(holder),
1768 pops: AtomicUsize::new(0),
1769 });
1770 let authority = PresentedAuthority::new(vec![warrant], signer.clone()).unwrap();
1771 let guard = guard_for(&issuer);
1772 let args = HashMap::new();
1773 let call = Call::borrowed("read", &args);
1774 guard.check(&authority, &call).expect("allow");
1775 assert_eq!(signer.pops.load(Ordering::SeqCst), 1);
1776 }
1777
1778 #[test]
1779 fn denial_reporting_does_not_change_execution() {
1780 let issuer = SigningKey::generate();
1781 let holder = SigningKey::generate();
1782 let warrant = mint(&issuer, &holder, "read");
1783 let authority = authority(vec![warrant], holder);
1784 let args = HashMap::new();
1785 let call = Call::borrowed("write", &args);
1786
1787 for reporting in [
1788 DenialReporting::Error,
1789 DenialReporting::Warn,
1790 DenialReporting::Debug,
1791 ] {
1792 let mut authorizer = Authorizer::new();
1793 authorizer.add_trusted_root(issuer.public_key());
1794 let guard = Guard::builder()
1795 .authorizer(authorizer)
1796 .revocation(RevocationMode::TtlOnly {
1797 max_lifetime: Duration::from_secs(3600),
1798 })
1799 .denial_reporting(reporting)
1800 .build()
1801 .unwrap();
1802 let runs = AtomicUsize::new(0);
1803 let err = guard
1804 .guard(&authority, &call, |_| {
1805 runs.fetch_add(1, Ordering::SeqCst);
1806 Ok::<_, &str>(())
1807 })
1808 .err()
1809 .unwrap();
1810 assert_eq!(runs.load(Ordering::SeqCst), 0);
1811 match err {
1812 GuardError::Denied(denial) => {
1813 assert_eq!(denial.protocol_code(), Some(ErrorCode::ToolNotAuthorized));
1814 }
1815 GuardError::Operation(_) => panic!("operation must not run"),
1816 }
1817 }
1818 }
1819
1820 #[test]
1821 fn diagnostics_explain_denied_call() {
1822 let issuer = SigningKey::generate();
1823 let holder = SigningKey::generate();
1824 let warrant = mint(&issuer, &holder, "read");
1825 let authority = authority(vec![warrant], holder);
1826 let guard = guard_for(&issuer);
1827 let args = HashMap::new();
1828 let call = Call::borrowed("write", &args);
1829 let why = guard
1830 .diagnostics(&authority)
1831 .why_denied(&call)
1832 .expect("denied");
1833 assert!(why.contains("tool-not-authorized"));
1834 assert!(guard
1835 .diagnostics(&authority)
1836 .explain_authority()
1837 .contains("read"));
1838 }
1839
1840 #[test]
1841 fn owned_call_rejects_empty_capability() {
1842 let err = Call::owned("", HashMap::new()).err().expect("empty");
1843 assert_eq!(err, crate::sdk::ArgumentError::EmptyCapability);
1844 }
1845
1846 #[test]
1847 fn received_empty_chain_rejected() {
1848 let issuer = SigningKey::generate();
1849 let holder = SigningKey::generate();
1850 let warrant = mint(&issuer, &holder, "read");
1851 let authority = authority(vec![warrant], holder);
1852 let guard = guard_for(&issuer);
1853 let args = HashMap::new();
1854 let call = Call::borrowed("read", &args);
1855 let pop = guard
1856 .guard(&authority, &call, |authorized| {
1857 Ok::<_, &str>(authorized.pop_signature().clone())
1858 })
1859 .unwrap()
1860 .into_inner();
1861 let err =
1862 ReceivedAuthorization::new(&[], &pop, &[]).expect_err("empty chain must be rejected");
1863 assert_eq!(err, crate::sdk::AuthorityError::EmptyChain);
1864 }
1865
1866 #[test]
1867 fn received_allows_without_signing() {
1868 let issuer = SigningKey::generate();
1869 let holder = SigningKey::generate();
1870 let warrant = mint(&issuer, &holder, "read");
1871 let authority = authority(vec![warrant], holder);
1872 let client = guard_for(&issuer);
1873 let args = HashMap::new();
1874 let call = Call::borrowed("read", &args);
1875 let pop = client
1876 .guard(&authority, &call, |authorized| {
1877 Ok::<_, &str>(authorized.pop_signature().clone())
1878 })
1879 .unwrap()
1880 .into_inner();
1881
1882 let received = ReceivedAuthorization::new(authority.chain(), &pop, &[]).unwrap();
1883 let projection = VerifiedProjection::identical(HashMap::new());
1884 let inbound = Call::from_transport("read", &projection);
1885 let server = guard_for(&issuer);
1886 let runs = AtomicUsize::new(0);
1887 let _ = server
1888 .guard_received(&received, &inbound, |_| {
1889 runs.fetch_add(1, Ordering::SeqCst);
1890 Ok::<_, &str>(())
1891 })
1892 .expect("allow");
1893 assert_eq!(runs.load(Ordering::SeqCst), 1);
1894 }
1895
1896 #[test]
1897 fn received_wrong_signature_denies_without_running() {
1898 let issuer = SigningKey::generate();
1899 let holder = SigningKey::generate();
1900 let other = SigningKey::generate();
1901 let warrant = mint(&issuer, &holder, "read");
1902 let authority = authority(vec![warrant], holder);
1903 let client = guard_for(&issuer);
1904 let args = HashMap::new();
1905 let call = Call::borrowed("read", &args);
1906 client.check(&authority, &call).expect("holder allow");
1907 let junk = other.sign_raw(b"not-the-pop");
1908
1909 let received = ReceivedAuthorization::new(authority.chain(), &junk, &[]).unwrap();
1910 let projection = VerifiedProjection::identical(HashMap::new());
1911 let inbound = Call::from_transport("read", &projection);
1912 let server = guard_for(&issuer);
1913 let runs = AtomicUsize::new(0);
1914 let err = server
1915 .guard_received(&received, &inbound, |_| {
1916 runs.fetch_add(1, Ordering::SeqCst);
1917 Ok::<_, &str>(())
1918 })
1919 .expect_err("deny");
1920 assert_eq!(runs.load(Ordering::SeqCst), 0);
1921 match err {
1922 GuardError::Denied(denial) => {
1923 assert!(
1924 matches!(
1925 denial.protocol_code(),
1926 Some(ErrorCode::PopSignatureInvalid | ErrorCode::SignatureInvalid)
1927 ),
1928 "expected a PoP/signature denial, got {:?}",
1929 denial.protocol_code()
1930 );
1931 }
1932 GuardError::Operation(_) => panic!("operation must not run"),
1933 }
1934 }
1935
1936 #[test]
1937 fn tracker_signed_srl_without_accept_denies() {
1938 let issuer = SigningKey::generate();
1939 let holder = SigningKey::generate();
1940 let warrant = mint(&issuer, &holder, "read");
1941 let tracker = std::sync::Arc::new(
1942 crate::RevocationTracker::with_in_memory_floors(
1943 vec![issuer.public_key()],
1944 Duration::from_secs(60),
1945 Duration::from_secs(5),
1946 )
1947 .unwrap(),
1948 );
1949 let mut authorizer = Authorizer::new();
1950 authorizer.add_trusted_root(issuer.public_key());
1951 let guard = Guard::builder()
1952 .authorizer(authorizer)
1953 .revocation(RevocationMode::SignedSrl)
1954 .revocation_tracker(tracker)
1955 .build()
1956 .unwrap();
1957 let authority = authority(vec![warrant], holder);
1958 let args = HashMap::new();
1959 let call = Call::borrowed("read", &args);
1960 let runs = AtomicUsize::new(0);
1961 let err = guard
1962 .guard(&authority, &call, |_| {
1963 runs.fetch_add(1, Ordering::SeqCst);
1964 Ok::<_, &str>(())
1965 })
1966 .err()
1967 .unwrap();
1968 assert_eq!(runs.load(Ordering::SeqCst), 0);
1969 match err {
1970 GuardError::Denied(denial) => {
1971 assert_eq!(
1972 denial.sdk_kind(),
1973 Some(SdkDenialKind::RevocationStateUnavailable)
1974 );
1975 }
1976 GuardError::Operation(_) => panic!("operation must not run"),
1977 }
1978 }
1979
1980 #[cfg(feature = "receipts")]
1981 #[test]
1982 fn received_invalid_pop_deny_receipt_omits_pop() {
1983 use super::super::evidence::{EvidencePolicy, LocalReceiptSigner, MemoryReceiptSink};
1984
1985 let issuer = SigningKey::generate();
1986 let holder = SigningKey::generate();
1987 let other = SigningKey::generate();
1988 let warrant = mint(&issuer, &holder, "read");
1989 let authority = authority(vec![warrant], holder);
1990 let client = guard_for(&issuer);
1991 let args = HashMap::new();
1992 let call = Call::borrowed("read", &args);
1993 client.check(&authority, &call).expect("holder allow");
1994 let junk = other.sign_raw(b"not-the-pop");
1995 let received = ReceivedAuthorization::new(authority.chain(), &junk, &[]).unwrap();
1996 let projection = VerifiedProjection::identical(HashMap::new());
1997 let inbound = Call::from_transport("read", &projection);
1998 let sink = Arc::new(MemoryReceiptSink::new());
1999 let mut authorizer = Authorizer::new();
2000 authorizer.add_trusted_root(issuer.public_key());
2001 let server = Guard::builder()
2002 .authorizer(authorizer)
2003 .revocation(RevocationMode::TtlOnly {
2004 max_lifetime: Duration::from_secs(3600),
2005 })
2006 .evidence_policy(EvidencePolicy::BestEffort)
2007 .receipt_signer(Arc::new(LocalReceiptSigner::for_development()))
2008 .receipt_sink(sink.clone())
2009 .build()
2010 .unwrap();
2011 assert!(server.check_received(&received, &inbound).is_err());
2012 let stored = sink.stored();
2013 assert_eq!(stored.len(), 1);
2014 let payload = stored[0].verify_signature().unwrap();
2015 assert!(payload.pop_signature.is_none());
2016 assert!(payload.request_hash.is_none());
2017 }
2018
2019 #[cfg(feature = "receipts")]
2020 #[test]
2021 fn received_untrusted_root_deny_receipt_omits_pop() {
2022 use super::super::evidence::{EvidencePolicy, LocalReceiptSigner, MemoryReceiptSink};
2023
2024 let issuer = SigningKey::generate();
2025 let other_root = SigningKey::generate();
2026 let holder = SigningKey::generate();
2027 let warrant = mint(&issuer, &holder, "read");
2028 let presented = authority(vec![warrant], holder.clone());
2029 let client = guard_for(&issuer);
2030 let args = HashMap::new();
2031 let call = Call::borrowed("read", &args);
2032 client.check(&presented, &call).expect("holder allow");
2033 let junk = holder.sign_raw(b"unverified-wire-pop");
2034 let received = ReceivedAuthorization::new(presented.chain(), &junk, &[]).unwrap();
2035 let projection = VerifiedProjection::identical(HashMap::new());
2036 let inbound = Call::from_transport("read", &projection);
2037 let sink = Arc::new(MemoryReceiptSink::new());
2038 let mut stranger = Authorizer::new();
2039 stranger.add_trusted_root(other_root.public_key());
2040 let server = Guard::builder()
2041 .authorizer(stranger)
2042 .revocation(RevocationMode::TtlOnly {
2043 max_lifetime: Duration::from_secs(3600),
2044 })
2045 .evidence_policy(EvidencePolicy::BestEffort)
2046 .receipt_signer(Arc::new(LocalReceiptSigner::for_development()))
2047 .receipt_sink(sink.clone())
2048 .build()
2049 .unwrap();
2050 let denial = server.check_received(&received, &inbound).err().unwrap();
2051 assert!(matches!(
2052 denial.protocol_code(),
2053 Some(ErrorCode::UntrustedRoot | ErrorCode::SignatureInvalid)
2054 ));
2055 let stored = sink.stored();
2056 assert_eq!(stored.len(), 1);
2057 let payload = stored[0].verify_signature().unwrap();
2058 assert!(payload.pop_signature.is_none());
2059 }
2060
2061 #[cfg(feature = "receipts")]
2062 #[test]
2063 fn signer_without_sink_advances_receipt_chain() {
2064 use super::super::evidence::{EvidencePolicy, LocalReceiptSigner};
2065
2066 let issuer = SigningKey::generate();
2067 let holder = SigningKey::generate();
2068 let warrant = mint(&issuer, &holder, "read");
2069 let presented = authority(vec![warrant], holder);
2070 let mut authorizer = Authorizer::new();
2071 authorizer.add_trusted_root(issuer.public_key());
2072 let guard = Guard::builder()
2073 .authorizer(authorizer)
2074 .revocation(RevocationMode::TtlOnly {
2075 max_lifetime: Duration::from_secs(3600),
2076 })
2077 .evidence_policy(EvidencePolicy::BestEffort)
2078 .receipt_signer(Arc::new(LocalReceiptSigner::for_development()))
2079 .build()
2080 .unwrap();
2081 let args = HashMap::new();
2082 let call = Call::borrowed("read", &args);
2083 let first = guard.check(&presented, &call).unwrap();
2084 let a = first.receipt.expect("first receipt");
2085 let second = guard.check(&presented, &call).unwrap();
2086 let b = second.receipt.expect("second receipt");
2087 assert_eq!(
2088 b.verify_signature().unwrap().prev_receipt_hash,
2089 Some(a.digest().unwrap())
2090 );
2091 }
2092
2093 #[cfg(feature = "receipts")]
2094 #[test]
2095 fn poisoned_receipt_link_recovers_under_best_effort() {
2096 use super::super::evidence::{EvidencePolicy, LocalReceiptSigner, MemoryReceiptSink};
2097 use std::sync::Mutex;
2098
2099 let issuer = SigningKey::generate();
2100 let holder = SigningKey::generate();
2101 let warrant = mint(&issuer, &holder, "read");
2102 let presented = authority(vec![warrant], holder);
2103 let link = Arc::new(Mutex::new(None));
2104 let poison = link.clone();
2105 let _ = std::thread::spawn(move || {
2106 let _guard = poison.lock().unwrap();
2107 panic!("poison receipt link");
2108 })
2109 .join();
2110 let mut authorizer = Authorizer::new();
2111 authorizer.add_trusted_root(issuer.public_key());
2112 let guard = Guard::builder()
2113 .authorizer(authorizer)
2114 .revocation(RevocationMode::TtlOnly {
2115 max_lifetime: Duration::from_secs(3600),
2116 })
2117 .evidence_policy(EvidencePolicy::BestEffort)
2118 .receipt_signer(Arc::new(LocalReceiptSigner::for_development()))
2119 .receipt_sink(Arc::new(MemoryReceiptSink::new()))
2120 .receipt_link(link)
2121 .build()
2122 .unwrap();
2123 let args = HashMap::new();
2124 assert!(guard
2125 .check(&presented, &Call::borrowed("read", &args))
2126 .is_ok());
2127 }
2128}