1use super::authority::{AuthorityError, PresentedAuthority};
11use super::call::Call;
12use super::decision::{Denial, DenialReporting, GuardError};
13use super::delegation::{DelegationError, DelegationProfile};
14use super::diagnostics::Diagnostics;
15use super::guard::{AuthorizedCall, Guard, GuardBuildError, Guarded, RevocationMode};
16use super::identity::{IdentityError, PersistentIdentity};
17use super::signer::LocalSigner;
18use crate::crypto::{PublicKey, SigningKey};
19use crate::planes::Authorizer;
20use crate::revocation::SignedRevocationList;
21use crate::revocation_tracker::{
22 FileFloorStore, InMemoryFloorStore, RevocationError, RevocationFloorStore, RevocationTracker,
23 RevocationUpdate,
24};
25use crate::warrant::Warrant;
26use crate::Error;
27use chrono::{DateTime, Utc};
28use std::fmt;
29use std::path::PathBuf;
30use std::sync::Arc;
31use std::time::Duration;
32
33#[cfg(feature = "receipts")]
34use super::evidence::{EvidencePolicy, LocalReceiptSigner, MemoryReceiptSink};
35#[cfg(feature = "receipts")]
36use crate::receipt::Receipt;
37
38const DEFAULT_SRL_MAX_AGE: Duration = Duration::from_secs(300);
39const DEFAULT_SRL_CLOCK_TOLERANCE: Duration = Duration::from_secs(30);
40
41#[derive(Clone)]
43pub struct Runtime {
44 identity: PersistentIdentity,
45 roots: Vec<PublicKey>,
46 ttl_fallback: Duration,
47 revocation: Option<RevocationMode>,
48 tracker: Arc<RevocationTracker>,
49 denial_reporting: DenialReporting,
50 #[cfg(feature = "receipts")]
51 evidence: EvidencePolicy,
52 #[cfg(feature = "receipts")]
53 receipt_capacity: usize,
54 #[cfg(feature = "receipts")]
55 receipt_link: Arc<std::sync::Mutex<Option<[u8; 32]>>>,
56}
57
58pub struct Session {
60 enforcer: Guard,
61 authority: PresentedAuthority,
62 #[cfg(feature = "receipts")]
63 receipts: Option<Arc<MemoryReceiptSink>>,
64}
65
66pub struct RuntimeBuilder {
70 identity: Option<PersistentIdentity>,
71 roots: Vec<PublicKey>,
72 ttl_fallback: Option<Duration>,
73 revocation: Option<RevocationMode>,
74 srl_max_age: Duration,
75 srl_clock_tolerance: Duration,
76 floor_path: Option<PathBuf>,
77 denial_reporting: DenialReporting,
78 #[cfg(feature = "receipts")]
79 evidence: EvidencePolicy,
80 #[cfg(feature = "receipts")]
81 receipt_capacity: usize,
82}
83
84impl Runtime {
85 pub fn builder() -> RuntimeBuilder {
87 RuntimeBuilder {
88 identity: None,
89 roots: Vec::new(),
90 ttl_fallback: None,
91 revocation: None,
92 srl_max_age: DEFAULT_SRL_MAX_AGE,
93 srl_clock_tolerance: DEFAULT_SRL_CLOCK_TOLERANCE,
94 floor_path: None,
95 denial_reporting: DenialReporting::default(),
96 #[cfg(feature = "receipts")]
97 evidence: EvidencePolicy::Disabled,
98 #[cfg(feature = "receipts")]
99 receipt_capacity: 10_000,
100 }
101 }
102
103 pub fn identity(&self) -> &PersistentIdentity {
105 &self.identity
106 }
107
108 pub fn trusted_roots(&self) -> &[PublicKey] {
110 &self.roots
111 }
112
113 pub fn apply_signed_revocation_list_now(
115 &self,
116 srl: SignedRevocationList,
117 ) -> Result<u64, RuntimeError> {
118 self.apply_signed_revocation_list(srl, Utc::now())
119 }
120
121 pub fn apply_signed_revocation_list(
127 &self,
128 srl: SignedRevocationList,
129 fetched_at: DateTime<Utc>,
130 ) -> Result<u64, RuntimeError> {
131 if matches!(self.revocation, Some(RevocationMode::TtlOnly { .. })) {
132 return Err(RuntimeError::TtlOnlyIgnoresSrl);
133 }
134 let version = srl.version();
135 self.tracker
136 .accept(RevocationUpdate { srl, fetched_at }, Utc::now())?;
137 Ok(version)
138 }
139
140 pub fn apply_encoded_revocation_list(
142 &self,
143 encoded: &str,
144 fetched_at: DateTime<Utc>,
145 ) -> Result<u64, RuntimeError> {
146 let srl = SignedRevocationList::from_base64(encoded).map_err(RuntimeError::Warrant)?;
147 self.apply_signed_revocation_list(srl, fetched_at)
148 }
149
150 pub fn session_from_warrant(
152 &self,
153 warrant: impl SessionWarrant,
154 ) -> Result<Session, RuntimeError> {
155 let warrant = warrant.into_warrant()?;
156 let mut authorizer = Authorizer::new();
157 for root in &self.roots {
158 authorizer.add_trusted_root(root.clone());
159 }
160
161 let mut builder = Guard::builder()
162 .authorizer(authorizer)
163 .denial_reporting(self.denial_reporting);
164 builder = match &self.revocation {
165 None | Some(RevocationMode::TtlUntilSrl { .. }) => {
166 builder.ttl_until_signed_srl(self.ttl_fallback, self.tracker.clone())
167 }
168 Some(RevocationMode::TtlOnly { max_lifetime }) => {
169 builder.revocation(RevocationMode::TtlOnly {
170 max_lifetime: *max_lifetime,
171 })
172 }
173 Some(RevocationMode::SignedSrl) => builder
174 .revocation(RevocationMode::SignedSrl)
175 .revocation_tracker(self.tracker.clone()),
176 };
177
178 #[cfg(feature = "receipts")]
179 let receipts = {
180 let receipts = match self.evidence {
181 EvidencePolicy::Disabled => None,
182 EvidencePolicy::BestEffort | EvidencePolicy::RequiredBeforeExecution => Some(
183 Arc::new(MemoryReceiptSink::with_capacity(self.receipt_capacity)),
184 ),
185 };
186 if let Some(sink) = receipts.as_ref() {
187 builder = builder
188 .evidence_policy(self.evidence)
189 .receipt_signer(Arc::new(LocalReceiptSigner::new(
190 self.identity.signing_key().clone(),
191 )))
192 .receipt_sink(sink.clone())
193 .receipt_link(self.receipt_link.clone());
194 }
195 receipts
196 };
197
198 let enforcer = builder.build()?;
199 let authority = PresentedAuthority::new(
200 vec![warrant],
201 Arc::new(LocalSigner::new(self.identity.signing_key().clone())),
202 )?;
203
204 Ok(Session {
205 enforcer,
206 authority,
207 #[cfg(feature = "receipts")]
208 receipts,
209 })
210 }
211}
212
213impl Session {
214 pub fn enforcer(&self) -> &Guard {
216 &self.enforcer
217 }
218
219 pub fn authority(&self) -> &PresentedAuthority {
221 &self.authority
222 }
223
224 #[cfg(feature = "filesystem")]
229 #[cfg_attr(docsrs, doc(cfg(feature = "filesystem")))]
230 pub fn with_filesystem(mut self, workspace: super::filesystem::Workspace) -> Self {
231 self.enforcer = self.enforcer.with_filesystem(workspace);
232 self
233 }
234
235 pub fn guard<T, E>(
237 &self,
238 call: &Call<'_>,
239 op: impl FnOnce(&AuthorizedCall<'_>) -> Result<T, E>,
240 ) -> Result<Guarded<T>, GuardError<E>> {
241 self.enforcer.guard(&self.authority, call, op)
242 }
243
244 pub fn check(&self, call: &Call<'_>) -> Result<crate::sdk::Decision, Denial> {
246 self.enforcer.check(&self.authority, call)
247 }
248
249 #[cfg(feature = "receipts")]
252 #[deprecated(since = "0.3.0", note = "use `peek_receipts`")]
253 pub fn drain_receipts(&self) -> Vec<Receipt> {
254 self.peek_receipts()
255 }
256
257 #[cfg(feature = "receipts")]
259 pub fn peek_receipts(&self) -> Vec<Receipt> {
260 let Some(sink) = self.receipts.as_ref() else {
261 return Vec::new();
262 };
263 sink.pending()
264 }
265
266 #[cfg(feature = "receipts")]
268 pub fn acknowledge_receipts(&self, count: usize) -> usize {
269 let Some(sink) = self.receipts.as_ref() else {
270 return 0;
271 };
272 sink.drop_prefix(count)
273 }
274
275 #[cfg(feature = "receipts")]
277 pub fn receipt_overflows(&self) -> usize {
278 self.receipts
279 .as_ref()
280 .map(|sink| sink.overflowed())
281 .unwrap_or(0)
282 }
283
284 pub fn diagnostics(&self) -> Diagnostics<'_> {
286 self.enforcer.diagnostics(&self.authority)
287 }
288
289 pub fn delegate(
291 &self,
292 profile: &DelegationProfile,
293 ) -> Result<PresentedAuthority, DelegationError> {
294 self.enforcer.delegate(&self.authority, profile)
295 }
296
297 pub fn delegate_to(
299 &self,
300 child_holder: &PublicKey,
301 profile: &DelegationProfile,
302 ) -> Result<Vec<Warrant>, DelegationError> {
303 self.enforcer
304 .delegate_to(&self.authority, child_holder, profile)
305 }
306}
307
308impl RuntimeBuilder {
309 pub fn identity(mut self, identity: PersistentIdentity) -> Self {
311 self.identity = Some(identity);
312 self
313 }
314
315 pub fn holder(self, key: SigningKey) -> Self {
317 self.identity(PersistentIdentity::ephemeral(key))
318 }
319
320 pub fn trusted_roots(mut self, roots: impl Into<Vec<PublicKey>>) -> Self {
322 self.roots = roots.into();
323 self
324 }
325
326 pub fn trusted_root(mut self, root: PublicKey) -> Self {
328 self.roots.push(root);
329 self
330 }
331
332 pub fn ttl_fallback(mut self, max_lifetime: Duration) -> Self {
334 self.ttl_fallback = Some(max_lifetime);
335 if self.revocation.is_none() {
336 self.revocation = Some(RevocationMode::TtlUntilSrl { max_lifetime });
337 }
338 self
339 }
340
341 pub fn revocation(mut self, mode: RevocationMode) -> Self {
344 match &mode {
345 RevocationMode::TtlOnly { max_lifetime }
346 | RevocationMode::TtlUntilSrl { max_lifetime } => {
347 self.ttl_fallback = Some(*max_lifetime);
348 }
349 RevocationMode::SignedSrl => {}
350 }
351 self.revocation = Some(mode);
352 self
353 }
354
355 pub fn srl_max_age(mut self, max_age: Duration) -> Self {
357 self.srl_max_age = max_age;
358 self
359 }
360
361 pub fn srl_clock_tolerance(mut self, tolerance: Duration) -> Self {
363 self.srl_clock_tolerance = tolerance;
364 self
365 }
366
367 pub fn revocation_floor_path(mut self, path: impl Into<PathBuf>) -> Self {
370 self.floor_path = Some(path.into());
371 self
372 }
373
374 pub fn denial_reporting(mut self, reporting: DenialReporting) -> Self {
378 self.denial_reporting = reporting;
379 self
380 }
381
382 #[cfg(feature = "receipts")]
383 pub fn evidence_policy(mut self, policy: EvidencePolicy) -> Self {
385 self.evidence = policy;
386 self
387 }
388
389 #[cfg(feature = "receipts")]
393 pub fn receipt_capacity(mut self, max: usize) -> Self {
394 self.receipt_capacity = max.max(1);
395 self
396 }
397
398 pub fn build(self) -> Result<Runtime, RuntimeError> {
400 let identity = self.identity.ok_or(RuntimeError::MissingIdentity)?;
401 if self.roots.is_empty() {
402 return Err(RuntimeError::MissingRoots);
403 }
404 let ttl_fallback = match (&self.revocation, self.ttl_fallback) {
405 (
406 Some(RevocationMode::TtlOnly { max_lifetime })
407 | Some(RevocationMode::TtlUntilSrl { max_lifetime }),
408 _,
409 ) => *max_lifetime,
410 (_, Some(ttl)) => ttl,
411 (Some(RevocationMode::SignedSrl), None) => Duration::from_secs(60),
412 (None, None) => return Err(RuntimeError::MissingTtlFallback),
413 };
414 if ttl_fallback.is_zero() {
415 return Err(RuntimeError::InvalidTtlFallback);
416 }
417 if self.srl_max_age.is_zero() {
418 return Err(RuntimeError::InvalidSrlMaxAge);
419 }
420 let tracker = Arc::new(build_tracker(
421 &identity,
422 self.roots.clone(),
423 self.srl_max_age,
424 self.srl_clock_tolerance,
425 self.floor_path,
426 )?);
427 Ok(Runtime {
428 identity,
429 roots: self.roots,
430 ttl_fallback,
431 revocation: self.revocation,
432 tracker,
433 denial_reporting: self.denial_reporting,
434 #[cfg(feature = "receipts")]
435 evidence: self.evidence,
436 #[cfg(feature = "receipts")]
437 receipt_capacity: self.receipt_capacity,
438 #[cfg(feature = "receipts")]
439 receipt_link: Arc::new(std::sync::Mutex::new(None)),
440 })
441 }
442}
443
444fn build_tracker(
445 identity: &PersistentIdentity,
446 roots: Vec<PublicKey>,
447 max_age: Duration,
448 clock_tolerance: Duration,
449 floor_path: Option<PathBuf>,
450) -> Result<RevocationTracker, RuntimeError> {
451 let floors: Arc<dyn RevocationFloorStore> = if let Some(path) = floor_path {
452 Arc::new(FileFloorStore::open(path)?)
453 } else if identity.path().as_os_str().is_empty() {
454 Arc::new(InMemoryFloorStore::for_development())
455 } else {
456 let default_path = identity.path().with_extension("srl-floors");
457 Arc::new(FileFloorStore::open(default_path)?)
458 };
459 Ok(RevocationTracker::new(
460 roots,
461 max_age,
462 clock_tolerance,
463 floors,
464 )?)
465}
466
467pub trait SessionWarrant {
469 fn into_warrant(self) -> Result<Warrant, RuntimeError>;
471}
472
473impl SessionWarrant for Warrant {
474 fn into_warrant(self) -> Result<Warrant, RuntimeError> {
475 Ok(self)
476 }
477}
478
479impl SessionWarrant for &Warrant {
480 fn into_warrant(self) -> Result<Warrant, RuntimeError> {
481 Ok(self.clone())
482 }
483}
484
485impl SessionWarrant for &str {
486 fn into_warrant(self) -> Result<Warrant, RuntimeError> {
487 crate::wire::decode_base64(self).map_err(RuntimeError::Warrant)
488 }
489}
490
491impl SessionWarrant for String {
492 fn into_warrant(self) -> Result<Warrant, RuntimeError> {
493 crate::wire::decode_base64(&self).map_err(RuntimeError::Warrant)
494 }
495}
496
497#[derive(Debug)]
499#[non_exhaustive]
500pub enum RuntimeError {
501 MissingIdentity,
503 MissingRoots,
505 MissingTtlFallback,
507 InvalidTtlFallback,
509 InvalidSrlMaxAge,
511 Guard(GuardBuildError),
513 Authority(AuthorityError),
515 Warrant(Error),
517 Revocation(RevocationError),
519 Identity(IdentityError),
521 TtlOnlyIgnoresSrl,
523}
524
525impl From<GuardBuildError> for RuntimeError {
526 fn from(value: GuardBuildError) -> Self {
527 Self::Guard(value)
528 }
529}
530
531impl From<AuthorityError> for RuntimeError {
532 fn from(value: AuthorityError) -> Self {
533 Self::Authority(value)
534 }
535}
536
537impl From<RevocationError> for RuntimeError {
538 fn from(value: RevocationError) -> Self {
539 Self::Revocation(value)
540 }
541}
542
543impl From<IdentityError> for RuntimeError {
544 fn from(value: IdentityError) -> Self {
545 Self::Identity(value)
546 }
547}
548
549impl fmt::Display for RuntimeError {
550 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
551 match self {
552 Self::MissingIdentity => write!(f, "runtime requires a holder identity"),
553 Self::MissingRoots => write!(f, "runtime requires at least one trusted root"),
554 Self::MissingTtlFallback => {
555 write!(
556 f,
557 "runtime requires a TTL fallback before a signed SRL arrives"
558 )
559 }
560 Self::InvalidTtlFallback => write!(f, "TTL fallback must be greater than zero"),
561 Self::InvalidSrlMaxAge => write!(f, "SRL max age must be greater than zero"),
562 Self::Guard(err) => write!(f, "{err}"),
563 Self::Authority(err) => write!(f, "{err}"),
564 Self::Warrant(err) => write!(f, "{err}"),
565 Self::Revocation(err) => write!(f, "{err}"),
566 Self::Identity(err) => write!(f, "{err}"),
567 Self::TtlOnlyIgnoresSrl => {
568 write!(f, "TtlOnly runtime does not apply signed revocation lists")
569 }
570 }
571 }
572}
573
574impl std::error::Error for RuntimeError {
575 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
576 match self {
577 Self::Guard(err) => Some(err),
578 Self::Authority(err) => Some(err),
579 Self::Warrant(err) => Some(err),
580 Self::Revocation(err) => Some(err),
581 Self::Identity(err) => Some(err),
582 Self::MissingIdentity
583 | Self::MissingRoots
584 | Self::MissingTtlFallback
585 | Self::InvalidTtlFallback
586 | Self::InvalidSrlMaxAge
587 | Self::TtlOnlyIgnoresSrl => None,
588 }
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::constraints::ConstraintSet;
596 use crate::sdk::Call;
597 use crate::wire;
598 use std::collections::HashMap;
599
600 fn mint_pair() -> (SigningKey, SigningKey, Warrant) {
601 let issuer = SigningKey::generate();
602 let holder = SigningKey::generate();
603 let warrant = Warrant::builder()
604 .capability("read", ConstraintSet::new())
605 .holder(holder.public_key())
606 .ttl(Duration::from_secs(300))
607 .build(&issuer)
608 .unwrap();
609 (issuer, holder, warrant)
610 }
611
612 fn runtime(issuer: &SigningKey, holder: SigningKey) -> Runtime {
613 Runtime::builder()
614 .holder(holder)
615 .trusted_root(issuer.public_key())
616 .ttl_fallback(Duration::from_secs(600))
617 .build()
618 .unwrap()
619 }
620
621 #[test]
622 fn session_from_decoded_and_encoded_warrant() {
623 let (issuer, holder, warrant) = mint_pair();
624 let encoded = wire::encode_base64(&warrant).unwrap();
625 let runtime = runtime(&issuer, holder);
626 let from_decoded = runtime.session_from_warrant(warrant.clone()).unwrap();
627 let from_encoded = runtime.session_from_warrant(encoded.as_str()).unwrap();
628 assert_eq!(
629 from_decoded.authority().leaf().id(),
630 from_encoded.authority().leaf().id()
631 );
632 let args = HashMap::new();
633 let call = Call::borrowed("read", &args);
634 assert!(from_decoded.check(&call).is_ok());
635 let out = from_decoded
636 .guard(&call, |_| Ok::<_, &str>("ok"))
637 .unwrap()
638 .into_inner();
639 assert_eq!(out, "ok");
640 }
641
642 #[test]
643 fn wrong_holder_is_rejected() {
644 let (issuer, _holder, warrant) = mint_pair();
645 let runtime = runtime(&issuer, SigningKey::generate());
646 let err = runtime.session_from_warrant(warrant).err().unwrap();
647 assert!(matches!(err, RuntimeError::Authority(_)));
648 }
649
650 #[test]
651 fn ttl_fallback_without_srl_still_authorizes() {
652 let (issuer, holder, warrant) = mint_pair();
653 let runtime = runtime(&issuer, holder);
654 let session = runtime.session_from_warrant(warrant).unwrap();
655 let args = HashMap::new();
656 assert!(session.check(&Call::borrowed("read", &args)).is_ok());
657 }
658
659 #[test]
660 fn first_srl_enables_signed_enforcement_and_later_updates() {
661 let issuer = SigningKey::generate();
662 let holder = SigningKey::generate();
663 let warrant = Warrant::builder()
664 .capability("read", ConstraintSet::new())
665 .holder(holder.public_key())
666 .ttl(Duration::from_secs(300))
667 .build(&issuer)
668 .unwrap();
669 let runtime = runtime(&issuer, holder);
670 let now = Utc::now();
671
672 let empty = SignedRevocationList::builder()
673 .version(1)
674 .build(&issuer)
675 .unwrap();
676 assert_eq!(runtime.apply_signed_revocation_list(empty, now).unwrap(), 1);
677
678 let session = runtime.session_from_warrant(warrant.clone()).unwrap();
679 let args = HashMap::new();
680 assert!(session.check(&Call::borrowed("read", &args)).is_ok());
681
682 let revoked = SignedRevocationList::builder()
683 .version(2)
684 .revoke(warrant.id().to_string())
685 .build(&issuer)
686 .unwrap();
687 assert_eq!(
688 runtime.apply_signed_revocation_list(revoked, now).unwrap(),
689 2
690 );
691 let session = runtime.session_from_warrant(warrant).unwrap();
692 let denial = session.check(&Call::borrowed("read", &args)).err().unwrap();
693 assert_eq!(denial.code(), crate::ErrorCode::WarrantRevoked.name());
694 }
695
696 #[test]
697 fn early_session_sees_srl_applied_later() {
698 let issuer = SigningKey::generate();
699 let holder = SigningKey::generate();
700 let warrant = Warrant::builder()
701 .capability("read", ConstraintSet::new())
702 .holder(holder.public_key())
703 .ttl(Duration::from_secs(300))
704 .build(&issuer)
705 .unwrap();
706 let runtime = runtime(&issuer, holder);
707 let session = runtime.session_from_warrant(warrant.clone()).unwrap();
708 let args = HashMap::new();
709 assert!(session.check(&Call::borrowed("read", &args)).is_ok());
710
711 let revoked = SignedRevocationList::builder()
712 .version(1)
713 .revoke(warrant.id().to_string())
714 .build(&issuer)
715 .unwrap();
716 runtime
717 .apply_signed_revocation_list(revoked, Utc::now())
718 .unwrap();
719 let denial = session.check(&Call::borrowed("read", &args)).err().unwrap();
720 assert_eq!(denial.code(), crate::ErrorCode::WarrantRevoked.name());
721 }
722
723 #[test]
724 fn encoded_srl_round_trips() {
725 let issuer = SigningKey::generate();
726 let holder = SigningKey::generate();
727 let runtime = runtime(&issuer, holder);
728 let srl = SignedRevocationList::builder()
729 .version(3)
730 .build(&issuer)
731 .unwrap();
732 let encoded = srl.to_base64().unwrap();
733 assert_eq!(
734 runtime
735 .apply_encoded_revocation_list(&encoded, Utc::now())
736 .unwrap(),
737 3
738 );
739 }
740
741 #[cfg(feature = "receipts")]
742 #[test]
743 fn drain_receipts_does_not_repeat() {
744 let (issuer, holder, warrant) = mint_pair();
745 let runtime = Runtime::builder()
746 .holder(holder)
747 .trusted_root(issuer.public_key())
748 .ttl_fallback(Duration::from_secs(600))
749 .evidence_policy(EvidencePolicy::BestEffort)
750 .build()
751 .unwrap();
752 let session = runtime.session_from_warrant(warrant).unwrap();
753 let args = HashMap::new();
754 let call = Call::borrowed("read", &args);
755 session.check(&call).unwrap();
756 session.check(&call).unwrap();
757 let first = session.peek_receipts();
758 assert_eq!(first.len(), 2);
759 assert_eq!(session.peek_receipts().len(), 2);
760 assert_eq!(session.acknowledge_receipts(2), 2);
761 assert!(session.peek_receipts().is_empty());
762 session.check(&call).unwrap();
763 assert_eq!(session.peek_receipts().len(), 1);
764 assert_eq!(session.acknowledge_receipts(1), 1);
765 assert!(session.peek_receipts().is_empty());
766 }
767
768 #[cfg(feature = "receipts")]
769 #[test]
770 fn overflow_does_not_deny_and_is_counted() {
771 let (issuer, holder, warrant) = mint_pair();
772 let runtime = Runtime::builder()
773 .holder(holder)
774 .trusted_root(issuer.public_key())
775 .ttl_fallback(Duration::from_secs(600))
776 .evidence_policy(EvidencePolicy::BestEffort)
777 .receipt_capacity(1)
778 .build()
779 .unwrap();
780 let session = runtime.session_from_warrant(warrant).unwrap();
781 let args = HashMap::new();
782 let call = Call::borrowed("read", &args);
783 assert!(session.check(&call).is_ok());
784 assert!(session.check(&call).is_ok());
785 assert_eq!(session.peek_receipts().len(), 1);
786 assert_eq!(session.receipt_overflows(), 1);
787 }
788
789 #[cfg(feature = "receipts")]
790 #[test]
791 fn overflow_denies_when_evidence_is_required() {
792 let (issuer, holder, warrant) = mint_pair();
793 let runtime = Runtime::builder()
794 .holder(holder)
795 .trusted_root(issuer.public_key())
796 .ttl_fallback(Duration::from_secs(600))
797 .evidence_policy(EvidencePolicy::RequiredBeforeExecution)
798 .receipt_capacity(1)
799 .build()
800 .unwrap();
801 let session = runtime.session_from_warrant(warrant).unwrap();
802 let args = HashMap::new();
803 let call = Call::borrowed("read", &args);
804 assert!(session.check(&call).is_ok());
805 assert!(session.check(&call).is_err());
806 assert_eq!(session.peek_receipts().len(), 1);
807 assert_eq!(session.receipt_overflows(), 1);
808 }
809
810 #[test]
811 fn revocation_floor_path_does_not_write_beside_the_key() {
812 let dir = tempfile::tempdir().unwrap();
813 let key_path = dir.path().join("holder.key");
814 let floor = dir.path().join("custom.srl-floors");
815 let identity = PersistentIdentity::load_or_generate(&key_path).unwrap();
816 Runtime::builder()
817 .identity(identity)
818 .trusted_root(SigningKey::generate().public_key())
819 .ttl_fallback(Duration::from_secs(600))
820 .revocation_floor_path(&floor)
821 .build()
822 .unwrap();
823 assert!(floor.exists());
824 assert!(!key_path.with_extension("srl-floors").exists());
825 }
826
827 #[test]
828 fn builder_requires_the_three_pieces() {
829 assert!(matches!(
830 Runtime::builder().build().err().unwrap(),
831 RuntimeError::MissingIdentity
832 ));
833 let holder = SigningKey::generate();
834 assert!(matches!(
835 Runtime::builder()
836 .holder(holder.clone())
837 .build()
838 .err()
839 .unwrap(),
840 RuntimeError::MissingRoots
841 ));
842 assert!(matches!(
843 Runtime::builder()
844 .holder(holder)
845 .trusted_root(SigningKey::generate().public_key())
846 .build()
847 .err()
848 .unwrap(),
849 RuntimeError::MissingTtlFallback
850 ));
851 }
852
853 #[test]
854 fn ttl_only_rejects_srl_and_still_allows_revoked_leaf() {
855 let issuer = SigningKey::generate();
856 let holder = SigningKey::generate();
857 let warrant = Warrant::builder()
858 .capability("read", ConstraintSet::new())
859 .holder(holder.public_key())
860 .ttl(Duration::from_secs(300))
861 .build(&issuer)
862 .unwrap();
863 let runtime = Runtime::builder()
864 .holder(holder)
865 .trusted_root(issuer.public_key())
866 .revocation(RevocationMode::TtlOnly {
867 max_lifetime: Duration::from_secs(600),
868 })
869 .build()
870 .unwrap();
871 let revoked = SignedRevocationList::builder()
872 .version(1)
873 .revoke(warrant.id().to_string())
874 .build(&issuer)
875 .unwrap();
876 assert!(matches!(
877 runtime.apply_signed_revocation_list_now(revoked),
878 Err(RuntimeError::TtlOnlyIgnoresSrl)
879 ));
880 let session = runtime.session_from_warrant(warrant).unwrap();
881 let args = HashMap::new();
882 assert!(session.check(&Call::borrowed("read", &args)).is_ok());
883 }
884
885 #[test]
886 fn ttl_until_srl_is_selected_by_ttl_fallback() {
887 let issuer = SigningKey::generate();
888 let holder = SigningKey::generate();
889 let runtime = Runtime::builder()
890 .holder(holder)
891 .trusted_root(issuer.public_key())
892 .ttl_fallback(Duration::from_secs(600))
893 .build()
894 .unwrap();
895 assert!(matches!(
896 runtime.revocation,
897 Some(RevocationMode::TtlUntilSrl { .. })
898 ));
899 }
900
901 #[cfg(feature = "receipts")]
902 #[test]
903 fn guard_deny_emits_receipt_with_roots_and_chain() {
904 let issuer = SigningKey::generate();
905 let holder = SigningKey::generate();
906 let warrant = Warrant::builder()
907 .capability("read", ConstraintSet::new())
908 .holder(holder.public_key())
909 .ttl(Duration::from_secs(300))
910 .build(&issuer)
911 .unwrap();
912 let empty = SignedRevocationList::builder()
913 .version(4)
914 .build(&issuer)
915 .unwrap();
916 let runtime = Runtime::builder()
917 .holder(holder)
918 .trusted_root(issuer.public_key())
919 .ttl_fallback(Duration::from_secs(600))
920 .evidence_policy(EvidencePolicy::BestEffort)
921 .build()
922 .unwrap();
923 runtime.apply_signed_revocation_list_now(empty).unwrap();
924 let first = runtime.session_from_warrant(warrant.clone()).unwrap();
925 let second = runtime.session_from_warrant(warrant).unwrap();
926 let args = HashMap::new();
927 let denied = first
928 .guard(&Call::borrowed("write", &args), |_| Ok::<_, &str>("no"))
929 .err()
930 .unwrap();
931 assert!(matches!(denied, crate::sdk::GuardError::Denied(_)));
932 let receipts = first.peek_receipts();
933 assert_eq!(receipts.len(), 1);
934 let payload = receipts[0].verify_signature().unwrap();
935 assert_eq!(payload.outcome, crate::receipt::Outcome::Deny);
936 assert!(payload.trusted_roots_hash.is_some());
937 assert_eq!(payload.srl_version, Some(4));
938 assert!(payload.srl_hash.is_some());
939 assert!(payload.prev_receipt_hash.is_none());
940 let before = receipts.len();
941 assert!(first
942 .diagnostics()
943 .why_denied(&Call::borrowed("write", &args))
944 .is_some());
945 assert_eq!(first.peek_receipts().len(), before);
946
947 assert!(second.check(&Call::borrowed("read", &args)).is_ok());
948 let chained = second.peek_receipts();
949 assert_eq!(chained.len(), 1);
950 let next = chained[0].verify_signature().unwrap();
951 assert_eq!(next.prev_receipt_hash, Some(receipts[0].digest().unwrap()));
952 }
953
954 #[cfg(feature = "receipts")]
955 #[test]
956 fn deny_flood_does_not_starve_required_allow() {
957 let issuer = SigningKey::generate();
958 let holder = SigningKey::generate();
959 let warrant = Warrant::builder()
960 .capability("read", ConstraintSet::new())
961 .holder(holder.public_key())
962 .ttl(Duration::from_secs(300))
963 .build(&issuer)
964 .unwrap();
965 let runtime = Runtime::builder()
966 .holder(holder)
967 .trusted_root(issuer.public_key())
968 .ttl_fallback(Duration::from_secs(600))
969 .evidence_policy(EvidencePolicy::RequiredBeforeExecution)
970 .receipt_capacity(1)
971 .build()
972 .unwrap();
973 let session = runtime.session_from_warrant(warrant).unwrap();
974 let args = HashMap::new();
975 assert!(session.check(&Call::borrowed("write", &args)).is_err());
976 assert!(session.check(&Call::borrowed("write", &args)).is_err());
977 assert!(session.check(&Call::borrowed("read", &args)).is_ok());
978 }
979
980 #[cfg(feature = "receipts")]
981 #[test]
982 fn persist_failure_does_not_advance_receipt_chain() {
983 let issuer = SigningKey::generate();
984 let holder = SigningKey::generate();
985 let warrant = Warrant::builder()
986 .capability("read", ConstraintSet::new())
987 .holder(holder.public_key())
988 .ttl(Duration::from_secs(300))
989 .build(&issuer)
990 .unwrap();
991 let runtime = Runtime::builder()
992 .holder(holder)
993 .trusted_root(issuer.public_key())
994 .ttl_fallback(Duration::from_secs(600))
995 .evidence_policy(EvidencePolicy::BestEffort)
996 .receipt_capacity(1)
997 .build()
998 .unwrap();
999 let session = runtime.session_from_warrant(warrant).unwrap();
1000 let args = HashMap::new();
1001 let first = session.check(&Call::borrowed("read", &args)).unwrap();
1002 let a = first.receipt.clone().expect("first receipt");
1003 let second = session.check(&Call::borrowed("read", &args)).unwrap();
1004 assert!(second.receipt.is_some());
1005 assert_eq!(session.peek_receipts().len(), 1);
1006 session.acknowledge_receipts(1);
1007 let third = session.check(&Call::borrowed("read", &args)).unwrap();
1008 let c = third.receipt.expect("third receipt");
1009 assert_eq!(
1010 c.verify_signature().unwrap().prev_receipt_hash,
1011 Some(a.digest().unwrap())
1012 );
1013 }
1014
1015 #[cfg(feature = "receipts")]
1016 #[test]
1017 fn concurrent_receipts_share_one_predecessor_chain() {
1018 use std::collections::HashSet;
1019 use std::thread;
1020
1021 let issuer = SigningKey::generate();
1022 let holder = SigningKey::generate();
1023 let warrant = Warrant::builder()
1024 .capability("read", ConstraintSet::new())
1025 .holder(holder.public_key())
1026 .ttl(Duration::from_secs(300))
1027 .build(&issuer)
1028 .unwrap();
1029 let runtime = Runtime::builder()
1030 .holder(holder)
1031 .trusted_root(issuer.public_key())
1032 .ttl_fallback(Duration::from_secs(600))
1033 .evidence_policy(EvidencePolicy::BestEffort)
1034 .receipt_capacity(128)
1035 .build()
1036 .unwrap();
1037 let session = Arc::new(runtime.session_from_warrant(warrant).unwrap());
1038 let threads = 8;
1039 let per_thread = 10;
1040 let handles: Vec<_> = (0..threads)
1041 .map(|_| {
1042 let session = session.clone();
1043 thread::spawn(move || {
1044 let args = HashMap::new();
1045 for _ in 0..per_thread {
1046 session.check(&Call::borrowed("read", &args)).unwrap();
1047 }
1048 })
1049 })
1050 .collect();
1051 for handle in handles {
1052 handle.join().expect("worker");
1053 }
1054 let receipts = session.peek_receipts();
1055 assert_eq!(receipts.len(), threads * per_thread);
1056 let digests: HashSet<_> = receipts.iter().map(|r| r.digest().unwrap()).collect();
1057 assert_eq!(digests.len(), receipts.len());
1058 let mut predecessors = HashSet::new();
1059 let mut roots = 0usize;
1060 for receipt in &receipts {
1061 match receipt.verify_signature().unwrap().prev_receipt_hash {
1062 None => roots += 1,
1063 Some(prev) => assert!(predecessors.insert(prev), "duplicate predecessor"),
1064 }
1065 }
1066 assert_eq!(roots, 1);
1067 assert_eq!(predecessors.len(), receipts.len() - 1);
1068 for prev in &predecessors {
1069 assert!(digests.contains(prev), "link to unpublished receipt");
1070 }
1071 }
1072}