Skip to main content

tenuo/sdk/
guard.rs

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/// Explicit revocation policy. No implicit fallback from SignedSrl to TtlOnly.
36///
37/// There is no default: the builder requires this choice, because the two
38/// modes accept different risk and neither is safe to pick silently.
39#[derive(Clone, Debug)]
40#[non_exhaustive]
41pub enum RevocationMode {
42    /// No revocation list. A warrant stays valid until it expires, so the
43    /// only bound on a leaked or misissued warrant is its lifetime.
44    ///
45    /// Choose this when no signed revocation list is distributed to this
46    /// enforcement point, and keep `max_lifetime` short — minutes, not hours.
47    /// Every warrant in the chain must satisfy the ceiling or the decision
48    /// denies, so the ceiling is enforced rather than advisory.
49    ///
50    /// The residual risk is explicit: a warrant revoked centrally keeps
51    /// working here until it expires.
52    TtlOnly {
53        /// Maximum `expires_at - issued_at` any warrant in the chain may have.
54        max_lifetime: Duration,
55    },
56    /// A current, trusted signed revocation list decides.
57    ///
58    /// Choose this for production. Revocation takes effect as soon as this
59    /// enforcement point holds a fresh list. Missing, stale, untrusted, or
60    /// rolled-back state denies — it never degrades to `TtlOnly`.
61    ///
62    /// Supply the list through [`GuardBuilder::revocation_tracker`] (which
63    /// enforces freshness and a persistent rollback floor) or install one on
64    /// the `Authorizer` before building.
65    SignedSrl,
66    /// TTL ceiling until the tracker accepts an SRL, then signed enforcement.
67    ///
68    /// This is the explicit "until first SRL" mode. [`RuntimeBuilder::ttl_fallback`]
69    /// selects it. After a list is accepted, later checks use that list.
70    TtlUntilSrl {
71        /// Maximum `expires_at - issued_at` while no SRL has been accepted.
72        max_lifetime: Duration,
73    },
74}
75
76/// Enforcement surface. Holds configuration, nothing per-call.
77#[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    /// TTL until the shared tracker accepts its first SRL, then signed-SRL.
100    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    // PoP is verified only after chain/temporal checks and tool lookup.
112    // Embed it only for denials that run after that point.
113    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    /// Start configuring a guard. Every required field must be supplied; there are no defaults for trust or revocation.
131    pub fn builder() -> GuardBuilder {
132        GuardBuilder::default()
133    }
134
135    /// Decide without running anything.
136    ///
137    /// Returns the allow decision, or a [`Denial`] explaining why not. Use this when the
138    /// operation is performed elsewhere, or to test policy.
139    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    /// Decide for an attempt that may carry approvals.
148    ///
149    /// See [`AuthorizationAttempt::with_approvals`] for the approval retry flow.
150    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    /// Authorize, then run `op` exactly once — and only after an allow.
167    ///
168    /// The closure receives the [`AuthorizedCall`] for this invocation, so it can tag an
169    /// outbound request or write an idempotency key. On denial the closure never runs.
170    ///
171    /// ```
172    /// use std::time::Duration;
173    /// use tenuo::sdk::prelude::*;
174    /// use tenuo::{args, constraints};
175    ///
176    /// let root = SigningKey::generate();
177    /// let holder = SigningKey::generate();
178    /// let warrant = Warrant::builder()
179    ///     .capability("read_file", constraints! { "path" => Pattern::new("/data/*")? })
180    ///     .holder(holder.public_key())
181    ///     .ttl(Duration::from_secs(300))
182    ///     .build(&root)?;
183    ///
184    /// let runtime = Runtime::builder()
185    ///     .holder(holder)
186    ///     .trusted_root(root.public_key())
187    ///     .revocation(RevocationMode::TtlOnly { max_lifetime: Duration::from_secs(600) })
188    ///     .build()?;
189    /// let session = runtime.session_from_warrant(warrant)?;
190    ///
191    /// let call = Call::owned("read_file", args! { "path" => "/data/x" })?;
192    /// let out = session.guard(&call, |authorized| {
193    ///     // Tag the downstream write with the decision that permitted it.
194    ///     let _idempotency_key = authorized.dedup_key();
195    ///     Ok::<_, std::io::Error>("ran")
196    /// })?;
197    /// assert_eq!(out.into_inner(), "ran");
198    /// # Ok::<(), Box<dyn std::error::Error>>(())
199    /// ```
200    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    /// Authorize an attempt that may carry approvals, then run `op` once.
210    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    /// Decide over authorization received from a peer.
232    ///
233    /// This is the enforcement-point path: the chain, PoP, and approvals arrived on the
234    /// wire. Nothing is signed here.
235    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    /// Verify received authorization, then run `op` exactly once.
257    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    /// Operator-side explanation. Never for caller-visible text.
282    pub fn diagnostics<'a>(&'a self, authority: &'a PresentedAuthority) -> Diagnostics<'a> {
283        Diagnostics::holder(self, authority)
284    }
285
286    /// Operator-side explanation for received authorization. Never for caller-visible text.
287    pub fn diagnostics_received<'a>(
288        &'a self,
289        received: &'a ReceivedAuthorization<'a>,
290    ) -> Diagnostics<'a> {
291        Diagnostics::received(self, received)
292    }
293
294    /// Mint a local child after confirming the parent is live under this guard.
295    ///
296    /// Trust, expiry, TTL ceiling, and revocation are evaluated at `now()`.
297    /// [`PresentedAuthority::delegate_local`] skips that check.
298    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    /// Mint a remote child after confirming the parent is live under this guard.
308    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    /// Local non-blocking lookup. Never invoked from `guard` / `check`.
380    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    /// Fetch approvals for a denial that requires them, using the configured provider.
393    ///
394    /// Call this after [`Denial::needs_approval`], then retry with
395    /// [`AuthorizationAttempt::with_approvals`].
396    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
1027/// One holder-sign attempt, including approvals carried into a retry.
1028///
1029/// [`Guard::check`] / [`Guard::guard`] construct this with no approvals.
1030/// After an `approval-required` denial, resolve signatures and retry through
1031/// [`Guard::check_attempt`] / [`Guard::guard_attempt`] with
1032/// [`AuthorizationAttempt::with_approvals`]. That is the §7.4 state machine;
1033/// it is not a second way to do the common path.
1034pub struct AuthorizationAttempt<'attempt, 'args> {
1035    call: &'attempt Call<'args>,
1036    approvals: &'attempt [SignedApproval],
1037}
1038
1039impl<'attempt, 'args> AuthorizationAttempt<'attempt, 'args> {
1040    /// First attempt: the call, no approvals yet.
1041    pub fn new(call: &'attempt Call<'args>) -> Self {
1042        Self {
1043            call,
1044            approvals: &[],
1045        }
1046    }
1047
1048    /// Retry after [`Denial::needs_approval`]: the same call plus resolved approvals.
1049    pub fn with_approvals(
1050        call: &'attempt Call<'args>,
1051        approvals: &'attempt [SignedApproval],
1052    ) -> Self {
1053        Self { call, approvals }
1054    }
1055}
1056
1057/// Borrow scoped to one allowed invocation. Not a capability.
1058pub 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    /// Identifies this attempt. Fresh on every retry.
1075    pub fn invocation_id(&self) -> &str {
1076        &self.invocation_id
1077    }
1078
1079    /// Protocol deduplication key over the signed argument view. Stable across retries
1080    /// of the same logical operation.
1081    pub fn dedup_key(&self) -> &str {
1082        &self.dedup_key
1083    }
1084
1085    /// Identifies the decision that permitted this invocation.
1086    pub fn decision_id(&self) -> &str {
1087        &self.decision_id
1088    }
1089
1090    /// Capability that was authorized.
1091    pub fn capability(&self) -> &str {
1092        self.capability
1093    }
1094
1095    /// The single instant this attempt committed to.
1096    pub fn instant(&self) -> VerificationInstant {
1097        self.instant
1098    }
1099
1100    /// Arguments matched against the warrant's constraints.
1101    pub fn execution_args(&self) -> &HashMap<String, ConstraintValue> {
1102        self.execution_args
1103    }
1104
1105    /// Arguments the proof of possession was computed over.
1106    pub fn pop_args(&self) -> &HashMap<String, ConstraintValue> {
1107        self.pop_args
1108    }
1109
1110    /// The proof of possession for this invocation.
1111    pub fn pop_signature(&self) -> &Signature {
1112        &self.pop_signature
1113    }
1114
1115    /// Warrant chain that authorized this invocation, root to leaf.
1116    pub fn chain(&self) -> &[Warrant] {
1117        self.chain
1118    }
1119
1120    /// Approvals that satisfied the gate, if one fired.
1121    pub fn approvals(&self) -> &[SignedApproval] {
1122        self.approvals
1123    }
1124
1125    /// Open the authorized string in `argument` through the guard's workspace.
1126    ///
1127    /// The path is [`Self::execution_args`], not a path the caller supplies.
1128    /// The leaf warrant must bound that argument with [`Subpath`](crate::Subpath),
1129    /// and that root must sit inside the workspace logical ceiling. The returned
1130    /// file has no path.
1131    #[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/// Result of an allowed guarded operation.
1167#[must_use]
1168pub struct Guarded<T> {
1169    /// What the operation returned.
1170    pub value: T,
1171    /// Evidence that the call was authorized.
1172    pub decision: Decision,
1173}
1174
1175impl<T> Guarded<T> {
1176    /// Discard the evidence and take the operation's value.
1177    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/// Build a [`Guard`]. Rejects an empty trust store and a missing revocation mode.
1193#[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    /// The authorizer supplying trusted roots and clearance policy. Required.
1219    pub fn authorizer(mut self, authorizer: Authorizer) -> Self {
1220        self.authorizer = Some(authorizer);
1221        self
1222    }
1223
1224    /// Revocation policy. Required — there is no default; see [`RevocationMode`].
1225    pub fn revocation(mut self, mode: RevocationMode) -> Self {
1226        self.revocation = Some(mode);
1227        self
1228    }
1229
1230    /// Production SignedSrl path: snapshots come from the tracker, not a fallback.
1231    pub fn revocation_tracker(mut self, tracker: std::sync::Arc<RevocationTracker>) -> Self {
1232        self.tracker = Some(tracker);
1233        self
1234    }
1235
1236    /// TTL until `tracker` accepts an SRL, then signed-SRL on later checks.
1237    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    /// Pin the local jail used by [`AuthorizedCall::open`].
1248    ///
1249    /// Absent by default. Without it, `open` fails closed.
1250    #[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    /// Log level for denials. Never changes whether the operation runs.
1258    ///
1259    /// Defaults to [`DenialReporting::Debug`]. `Error` and `Warn` write the
1260    /// denial message, which can quote argument values, to stderr.
1261    pub fn denial_reporting(mut self, reporting: DenialReporting) -> Self {
1262        self.denial_reporting = reporting;
1263        self
1264    }
1265
1266    /// Provider consulted only after a denial requires approval.
1267    pub fn approval_provider(mut self, provider: Arc<dyn ApprovalProvider>) -> Self {
1268        self.approval_provider = Some(provider);
1269        self
1270    }
1271
1272    /// Remote SRL fetch used by the async decision path when the tracker has no
1273    /// fresh snapshot. The sync path never calls this.
1274    #[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    /// Test and forensic clocks only. Production uses [`SystemClock`].
1284    #[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    /// Whether a receipt is best-effort evidence or a gate that denies on persistence failure.
1292    pub fn evidence_policy(mut self, policy: EvidencePolicy) -> Self {
1293        self.evidence = policy;
1294        self
1295    }
1296
1297    #[cfg(feature = "receipts")]
1298    /// Signer for authorization receipts.
1299    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    /// Sink receipts are written to.
1306    pub fn receipt_sink(mut self, sink: std::sync::Arc<dyn ReceiptSink>) -> Self {
1307        self.receipt_sink = Some(sink);
1308        self
1309    }
1310
1311    /// Share previous-receipt hash across sessions of one runtime.
1312    #[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    /// Sync surface cannot enforce a deadline. Calling this makes `build` fail.
1322    #[cfg(feature = "async")]
1323    pub fn deadline(mut self, _deadline: Duration) -> Self {
1324        self.declared_sync_deadline = true;
1325        self
1326    }
1327
1328    /// Build the guard, or fail if a required field is missing or unusable.
1329    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    /// Attach a workspace to a guard that was already built.
1418    ///
1419    /// [`Session::with_filesystem`](crate::sdk::Session::with_filesystem) is the
1420    /// holder-side equivalent. One workspace per job: the logical root is what
1421    /// warrants say, and the local root is this machine's directory for that job.
1422    #[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/// Failure constructing a [`Guard`].
1431#[derive(Debug, Clone, PartialEq, Eq)]
1432#[non_exhaustive]
1433pub enum GuardBuildError {
1434    /// No authorizer was supplied.
1435    MissingAuthorizer,
1436    /// The authorizer trusts no roots, so nothing could ever verify.
1437    EmptyTrustStore,
1438    /// No revocation mode was chosen.
1439    MissingRevocationMode,
1440    /// `SignedSrl` was chosen but no signed list or tracker is available.
1441    SignedSrlUnavailable,
1442    #[cfg(feature = "receipts")]
1443    /// An evidence policy requires receipts but no signer was supplied.
1444    MissingReceiptSigner,
1445    #[cfg(feature = "receipts")]
1446    /// An evidence policy requires receipts but no sink was supplied.
1447    MissingReceiptSink,
1448    #[cfg(feature = "async")]
1449    /// A deadline was configured on the synchronous surface, where it cannot be enforced.
1450    /// Use the async surface for enforceable deadlines.
1451    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}