1use crate::{
2 access::auth::validate_delegated_session_subject,
3 cdk::types::Principal,
4 dto::{
5 auth::{
6 AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationCert,
7 DelegationProof, DelegationProvisionResponse, DelegationProvisionStatus,
8 DelegationProvisionTargetKind, DelegationRequest, RoleAttestationRequest,
9 SignedRoleAttestation,
10 },
11 error::{Error, ErrorCode},
12 rpc::{Request as RootRequest, Response as RootCapabilityResponse},
13 },
14 error::InternalErrorClass,
15 log,
16 log::Topic,
17 ops::{
18 auth::DelegatedTokenOps,
19 config::ConfigOps,
20 ic::IcOps,
21 rpc::RpcOps,
22 runtime::env::EnvOps,
23 runtime::metrics::auth::{
24 record_attestation_refresh_failed, record_delegation_provision_complete,
25 record_delegation_verifier_target_count, record_delegation_verifier_target_failed,
26 record_delegation_verifier_target_missing, record_session_bootstrap_rejected_disabled,
27 record_session_bootstrap_rejected_replay_conflict,
28 record_session_bootstrap_rejected_replay_reused,
29 record_session_bootstrap_rejected_subject_mismatch,
30 record_session_bootstrap_rejected_subject_rejected,
31 record_session_bootstrap_rejected_token_invalid,
32 record_session_bootstrap_rejected_ttl_invalid,
33 record_session_bootstrap_rejected_wallet_caller_rejected,
34 record_session_bootstrap_replay_idempotent, record_session_cleared,
35 record_session_created, record_session_pruned, record_session_replaced,
36 record_signer_issue_without_proof,
37 },
38 storage::{
39 auth::{DelegatedSession, DelegatedSessionBootstrapBinding, DelegationStateOps},
40 directory::subnet::SubnetDirectoryOps,
41 registry::subnet::SubnetRegistryOps,
42 },
43 },
44 protocol,
45 workflow::rpc::request::handler::RootResponseWorkflow,
46};
47use sha2::{Digest, Sha256};
48
49mod metadata;
50mod verify_flow;
51
52pub struct DelegationApi;
59
60impl DelegationApi {
61 const DELEGATED_TOKENS_DISABLED: &str =
62 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
63 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
64 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
65 b"canic-session-bootstrap-token-fingerprint:v1";
66
67 fn map_delegation_error(err: crate::InternalError) -> Error {
68 match err.class() {
69 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
70 Error::internal(err.to_string())
71 }
72 _ => Error::from(err),
73 }
74 }
75
76 pub fn verify_delegation_proof(
81 proof: &DelegationProof,
82 authority_pid: Principal,
83 ) -> Result<(), Error> {
84 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
85 .map_err(Self::map_delegation_error)
86 }
87
88 async fn sign_token(
89 claims: DelegatedTokenClaims,
90 proof: DelegationProof,
91 ) -> Result<DelegatedToken, Error> {
92 DelegatedTokenOps::sign_token(claims, proof)
93 .await
94 .map_err(Self::map_delegation_error)
95 }
96
97 pub async fn issue_token(claims: DelegatedTokenClaims) -> Result<DelegatedToken, Error> {
102 let proof = Self::ensure_signing_proof(&claims).await?;
103 Self::sign_token(claims, proof).await
104 }
105
106 pub fn verify_token(
111 token: &DelegatedToken,
112 authority_pid: Principal,
113 now_secs: u64,
114 ) -> Result<(), Error> {
115 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
116 .map(|_| ())
117 .map_err(Self::map_delegation_error)
118 }
119
120 pub fn verify_token_verified(
125 token: &DelegatedToken,
126 authority_pid: Principal,
127 now_secs: u64,
128 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
129 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
130 .map(|verified| (verified.claims, verified.cert))
131 .map_err(Self::map_delegation_error)
132 }
133
134 pub async fn request_delegation(
138 request: DelegationRequest,
139 ) -> Result<DelegationProvisionResponse, Error> {
140 let request = metadata::with_root_request_metadata(request);
141 if EnvOps::is_root() {
142 let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
143 .await
144 .map_err(Self::map_delegation_error)?;
145
146 return match response {
147 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
148 _ => Err(Error::internal(
149 "invalid root response type for delegation request",
150 )),
151 };
152 }
153
154 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
155 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
156 .await
157 .map_err(Self::map_delegation_error)
158 }
159
160 pub async fn request_role_attestation(
161 request: RoleAttestationRequest,
162 ) -> Result<SignedRoleAttestation, Error> {
163 let request = metadata::with_root_attestation_request_metadata(request);
164 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
165 .await
166 .map_err(Self::map_delegation_error)?;
167
168 match response {
169 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
170 _ => Err(Error::internal(
171 "invalid root response type for role attestation request",
172 )),
173 }
174 }
175
176 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
177 DelegatedTokenOps::attestation_key_set()
178 .await
179 .map_err(Self::map_delegation_error)
180 }
181
182 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
183 DelegatedTokenOps::replace_attestation_key_set(key_set);
184 }
185
186 pub async fn verify_role_attestation(
187 attestation: &SignedRoleAttestation,
188 min_accepted_epoch: u64,
189 ) -> Result<(), Error> {
190 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
191 .map_err(Error::from)?
192 .min_accepted_epoch_by_role
193 .get(attestation.payload.role.as_str())
194 .copied();
195 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
196 min_accepted_epoch,
197 configured_min_accepted_epoch,
198 );
199
200 let caller = IcOps::msg_caller();
201 let self_pid = IcOps::canister_self();
202 let now_secs = IcOps::now_secs();
203 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
204 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
205
206 let verify = || {
207 DelegatedTokenOps::verify_role_attestation_cached(
208 attestation,
209 caller,
210 self_pid,
211 verifier_subnet,
212 now_secs,
213 min_accepted_epoch,
214 )
215 .map(|_| ())
216 };
217 let refresh = || async {
218 let key_set: AttestationKeySet =
219 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
220 DelegatedTokenOps::replace_attestation_key_set(key_set);
221 Ok(())
222 };
223
224 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
225 Ok(()) => Ok(()),
226 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
227 verify_flow::record_attestation_verifier_rejection(&err);
228 verify_flow::log_attestation_verifier_rejection(
229 &err,
230 attestation,
231 caller,
232 self_pid,
233 "cached",
234 );
235 Err(Self::map_delegation_error(err.into()))
236 }
237 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
238 verify_flow::record_attestation_verifier_rejection(&trigger);
239 verify_flow::log_attestation_verifier_rejection(
240 &trigger,
241 attestation,
242 caller,
243 self_pid,
244 "cache_miss_refresh",
245 );
246 record_attestation_refresh_failed();
247 log!(
248 Topic::Auth,
249 Warn,
250 "role attestation refresh failed local={} caller={} key_id={} error={}",
251 self_pid,
252 caller,
253 attestation.key_id,
254 source
255 );
256 Err(Self::map_delegation_error(source))
257 }
258 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
259 verify_flow::record_attestation_verifier_rejection(&err);
260 verify_flow::log_attestation_verifier_rejection(
261 &err,
262 attestation,
263 caller,
264 self_pid,
265 "post_refresh",
266 );
267 Err(Self::map_delegation_error(err.into()))
268 }
269 }
270 }
271
272 pub fn set_delegated_session_subject(
274 delegated_subject: Principal,
275 bootstrap_token: DelegatedToken,
276 requested_ttl_secs: Option<u64>,
277 ) -> Result<(), Error> {
278 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
279 if !cfg.enabled {
280 record_session_bootstrap_rejected_disabled();
281 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
282 }
283
284 let wallet_caller = IcOps::msg_caller();
285 if let Err(reason) = validate_delegated_session_subject(wallet_caller) {
286 record_session_bootstrap_rejected_wallet_caller_rejected();
287 return Err(Error::forbidden(format!(
288 "delegated session wallet caller rejected: {reason}"
289 )));
290 }
291
292 if let Err(reason) = validate_delegated_session_subject(delegated_subject) {
293 record_session_bootstrap_rejected_subject_rejected();
294 return Err(Error::forbidden(format!(
295 "delegated session subject rejected: {reason}"
296 )));
297 }
298
299 let issued_at = IcOps::now_secs();
300 let authority_pid = EnvOps::root_pid().map_err(Error::from)?;
301 let self_pid = IcOps::canister_self();
302 let verified =
303 DelegatedTokenOps::verify_token(&bootstrap_token, authority_pid, issued_at, self_pid)
304 .map_err(|err| {
305 record_session_bootstrap_rejected_token_invalid();
306 Self::map_delegation_error(err)
307 })?;
308
309 if verified.claims.sub != delegated_subject {
310 record_session_bootstrap_rejected_subject_mismatch();
311 return Err(Error::forbidden(format!(
312 "delegated session subject mismatch: requested={} token_subject={}",
313 delegated_subject, verified.claims.sub
314 )));
315 }
316
317 let configured_max_ttl_secs = cfg
318 .max_ttl_secs
319 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS);
320 let expires_at = Self::clamp_delegated_session_expires_at(
321 issued_at,
322 verified.claims.exp,
323 configured_max_ttl_secs,
324 requested_ttl_secs,
325 )
326 .inspect_err(|_| record_session_bootstrap_rejected_ttl_invalid())?;
327
328 let token_fingerprint =
329 Self::delegated_session_bootstrap_token_fingerprint(&bootstrap_token)
330 .inspect_err(|_| record_session_bootstrap_rejected_token_invalid())?;
331
332 if Self::enforce_bootstrap_replay_policy(
333 wallet_caller,
334 delegated_subject,
335 token_fingerprint,
336 issued_at,
337 )? {
338 return Ok(());
339 }
340
341 let had_active_session =
342 DelegationStateOps::delegated_session(wallet_caller, issued_at).is_some();
343
344 DelegationStateOps::upsert_delegated_session(
345 DelegatedSession {
346 wallet_pid: wallet_caller,
347 delegated_pid: delegated_subject,
348 issued_at,
349 expires_at,
350 bootstrap_token_fingerprint: Some(token_fingerprint),
351 },
352 issued_at,
353 );
354 DelegationStateOps::upsert_delegated_session_bootstrap_binding(
355 DelegatedSessionBootstrapBinding {
356 wallet_pid: wallet_caller,
357 delegated_pid: delegated_subject,
358 token_fingerprint,
359 bound_at: issued_at,
360 expires_at: verified.claims.exp,
361 },
362 issued_at,
363 );
364
365 if had_active_session {
366 record_session_replaced();
367 } else {
368 record_session_created();
369 }
370
371 Ok(())
372 }
373
374 pub fn clear_delegated_session() {
376 let wallet_caller = IcOps::msg_caller();
377 let had_active_session =
378 DelegationStateOps::delegated_session(wallet_caller, IcOps::now_secs()).is_some();
379 DelegationStateOps::clear_delegated_session(wallet_caller);
380 if had_active_session {
381 record_session_cleared();
382 }
383 }
384
385 #[must_use]
387 pub fn delegated_session_subject() -> Option<Principal> {
388 let wallet_caller = IcOps::msg_caller();
389 DelegationStateOps::delegated_session_subject(wallet_caller, IcOps::now_secs())
390 }
391
392 #[must_use]
394 pub fn prune_expired_delegated_sessions() -> usize {
395 let now_secs = IcOps::now_secs();
396 let removed = DelegationStateOps::prune_expired_delegated_sessions(now_secs);
397 let _ = DelegationStateOps::prune_expired_delegated_session_bootstrap_bindings(now_secs);
398 if removed > 0 {
399 record_session_pruned(removed);
400 }
401 removed
402 }
403
404 pub async fn store_proof(
405 proof: DelegationProof,
406 kind: DelegationProvisionTargetKind,
407 ) -> Result<(), Error> {
408 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
409 if !cfg.enabled {
410 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
411 }
412
413 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
414 let caller = IcOps::msg_caller();
415 if caller != root_pid {
416 return Err(Error::forbidden(
417 "delegation proof store requires root caller",
418 ));
419 }
420
421 DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
422 .await
423 .map_err(Self::map_delegation_error)?;
424 if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
425 let local = IcOps::canister_self();
426 log!(
427 Topic::Auth,
428 Warn,
429 "delegation proof rejected kind={:?} local={} shard={} issued_at={} expires_at={} error={}",
430 kind,
431 local,
432 proof.cert.shard_pid,
433 proof.cert.issued_at,
434 proof.cert.expires_at,
435 err
436 );
437 return Err(Self::map_delegation_error(err));
438 }
439
440 DelegationStateOps::set_proof_from_dto(proof);
441 let local = IcOps::canister_self();
442 let stored = DelegationStateOps::proof_dto()
443 .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
444 log!(
445 Topic::Auth,
446 Info,
447 "delegation proof stored kind={:?} local={} shard={} issued_at={} expires_at={}",
448 kind,
449 local,
450 stored.cert.shard_pid,
451 stored.cert.issued_at,
452 stored.cert.expires_at
453 );
454
455 Ok(())
456 }
457
458 #[cfg(canic_test_delegation_material)]
464 pub fn install_test_delegation_material(
465 proof: DelegationProof,
466 root_public_key: Vec<u8>,
467 shard_public_key: Vec<u8>,
468 ) -> Result<(), Error> {
469 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
470 let caller = IcOps::msg_caller();
471 if caller != root_pid {
472 return Err(Error::forbidden(
473 "test delegation material install requires root caller",
474 ));
475 }
476
477 if proof.cert.root_pid != root_pid {
478 return Err(Error::invalid(format!(
479 "delegation proof root mismatch: expected={} found={}",
480 root_pid, proof.cert.root_pid
481 )));
482 }
483
484 if root_public_key.is_empty() || shard_public_key.is_empty() {
485 return Err(Error::invalid("delegation public keys must not be empty"));
486 }
487
488 DelegationStateOps::set_root_public_key(root_public_key);
489 DelegationStateOps::set_shard_public_key(proof.cert.shard_pid, shard_public_key);
490 DelegationStateOps::set_proof_from_dto(proof);
491 Ok(())
492 }
493
494 fn require_proof() -> Result<DelegationProof, Error> {
495 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
496 if !cfg.enabled {
497 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
498 }
499
500 DelegationStateOps::proof_dto().ok_or_else(|| {
501 record_signer_issue_without_proof();
502 Error::not_found("delegation proof not set")
503 })
504 }
505
506 async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
508 let now_secs = IcOps::now_secs();
509
510 match Self::require_proof() {
511 Ok(proof) if !Self::proof_is_reusable_for_claims(&proof, claims, now_secs) => {
512 Self::setup_delegation(claims).await
513 }
514 Ok(proof) => Ok(proof),
515 Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
516 Err(err) => Err(err),
517 }
518 }
519
520 async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
522 let request = Self::delegation_request_from_claims(claims)?;
523 let required_verifier_targets = request.verifier_targets.clone();
524 let response = Self::request_delegation(request).await?;
525 Self::ensure_required_verifier_targets_provisioned(&required_verifier_targets, &response)?;
526 Self::require_proof()
527 }
528
529 fn delegation_request_from_claims(
531 claims: &DelegatedTokenClaims,
532 ) -> Result<DelegationRequest, Error> {
533 let ttl_secs = claims.exp.saturating_sub(claims.iat);
534 if ttl_secs == 0 {
535 return Err(Error::invalid(
536 "delegation ttl_secs must be greater than zero",
537 ));
538 }
539
540 let signer_pid = IcOps::canister_self();
541 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
542 let verifier_targets = Self::derive_required_verifier_targets_from_aud(
543 &claims.aud,
544 signer_pid,
545 root_pid,
546 Self::is_registered_canister,
547 )?;
548
549 Ok(DelegationRequest {
550 shard_pid: signer_pid,
551 scopes: claims.scopes.clone(),
552 aud: claims.aud.clone(),
553 ttl_secs,
554 verifier_targets,
555 include_root_verifier: true,
556 metadata: None,
557 })
558 }
559
560 fn ensure_required_verifier_targets_provisioned(
562 required_targets: &[Principal],
563 response: &DelegationProvisionResponse,
564 ) -> Result<(), Error> {
565 let mut checked = Vec::new();
566 for target in required_targets {
567 if checked.contains(target) {
568 continue;
569 }
570 checked.push(*target);
571 }
572 record_delegation_verifier_target_count(checked.len());
573
574 for target in &checked {
575 let Some(result) = response.results.iter().find(|entry| {
576 entry.kind == DelegationProvisionTargetKind::Verifier && entry.target == *target
577 }) else {
578 record_delegation_verifier_target_missing();
579 return Err(Error::internal(format!(
580 "delegation provisioning missing verifier target result for '{target}'"
581 )));
582 };
583
584 if result.status != DelegationProvisionStatus::Ok {
585 record_delegation_verifier_target_failed();
586 let detail = result
587 .error
588 .as_ref()
589 .map_or_else(|| "unknown error".to_string(), ToString::to_string);
590 return Err(Error::internal(format!(
591 "delegation provisioning failed for required verifier target '{target}': {detail}"
592 )));
593 }
594 }
595
596 record_delegation_provision_complete();
597 Ok(())
598 }
599
600 fn derive_required_verifier_targets_from_aud<F>(
602 audience: &[Principal],
603 signer_pid: Principal,
604 root_pid: Principal,
605 mut is_valid_target: F,
606 ) -> Result<Vec<Principal>, Error>
607 where
608 F: FnMut(Principal) -> bool,
609 {
610 let mut verifier_targets = Vec::new();
611 for principal in audience {
612 if *principal == signer_pid || *principal == root_pid {
613 continue;
614 }
615
616 if !is_valid_target(*principal) {
617 return Err(Error::invalid(format!(
618 "delegation audience principal '{principal}' is invalid for canonical verifier provisioning"
619 )));
620 }
621
622 if !verifier_targets.contains(principal) {
623 verifier_targets.push(*principal);
624 }
625 }
626
627 Ok(verifier_targets)
628 }
629
630 fn is_registered_canister(principal: Principal) -> bool {
632 if SubnetRegistryOps::is_registered(principal) {
633 return true;
634 }
635
636 SubnetDirectoryOps::data()
637 .entries
638 .iter()
639 .any(|(_, pid)| *pid == principal)
640 }
641
642 fn proof_is_reusable_for_claims(
644 proof: &DelegationProof,
645 claims: &DelegatedTokenClaims,
646 now_secs: u64,
647 ) -> bool {
648 if now_secs > proof.cert.expires_at {
649 return false;
650 }
651
652 if claims.shard_pid != proof.cert.shard_pid {
653 return false;
654 }
655
656 if claims.iat < proof.cert.issued_at || claims.exp > proof.cert.expires_at {
657 return false;
658 }
659
660 Self::is_principal_subset(&claims.aud, &proof.cert.aud)
661 && Self::is_string_subset(&claims.scopes, &proof.cert.scopes)
662 }
663
664 fn is_principal_subset(
666 subset: &[crate::cdk::types::Principal],
667 superset: &[crate::cdk::types::Principal],
668 ) -> bool {
669 subset.iter().all(|item| superset.contains(item))
670 }
671
672 fn is_string_subset(subset: &[String], superset: &[String]) -> bool {
674 subset.iter().all(|item| superset.contains(item))
675 }
676
677 fn delegated_session_bootstrap_token_fingerprint(
678 token: &DelegatedToken,
679 ) -> Result<[u8; 32], Error> {
680 let token_bytes = crate::cdk::candid::encode_one(token).map_err(|err| {
681 Error::internal(format!("bootstrap token fingerprint encode failed: {err}"))
682 })?;
683 let mut hasher = Sha256::new();
684 hasher.update(Self::SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN);
685 hasher.update(token_bytes);
686 Ok(hasher.finalize().into())
687 }
688
689 fn enforce_bootstrap_replay_policy(
691 wallet_caller: Principal,
692 delegated_subject: Principal,
693 token_fingerprint: [u8; 32],
694 issued_at: u64,
695 ) -> Result<bool, Error> {
696 let Some(binding) =
697 DelegationStateOps::delegated_session_bootstrap_binding(token_fingerprint, issued_at)
698 else {
699 return Ok(false);
700 };
701
702 if binding.wallet_pid == wallet_caller && binding.delegated_pid == delegated_subject {
703 let active_same_session =
704 DelegationStateOps::delegated_session(wallet_caller, issued_at).is_some_and(
705 |session| {
706 session.delegated_pid == delegated_subject
707 && session.bootstrap_token_fingerprint == Some(token_fingerprint)
708 },
709 );
710
711 if active_same_session {
712 record_session_bootstrap_replay_idempotent();
713 return Ok(true);
714 }
715
716 record_session_bootstrap_rejected_replay_reused();
717 return Err(Error::forbidden(
718 "delegated session bootstrap token replay rejected; use a fresh token",
719 ));
720 }
721
722 record_session_bootstrap_rejected_replay_conflict();
723 Err(Error::forbidden(format!(
724 "delegated session bootstrap token already bound (wallet={} delegated_subject={})",
725 binding.wallet_pid, binding.delegated_pid
726 )))
727 }
728
729 fn clamp_delegated_session_expires_at(
730 now_secs: u64,
731 token_expires_at: u64,
732 configured_max_ttl_secs: u64,
733 requested_ttl_secs: Option<u64>,
734 ) -> Result<u64, Error> {
735 if configured_max_ttl_secs == 0 {
736 return Err(Error::invariant(
737 "delegated session configured max ttl_secs must be greater than zero",
738 ));
739 }
740
741 if let Some(ttl_secs) = requested_ttl_secs
742 && ttl_secs == 0
743 {
744 return Err(Error::invalid(
745 "delegated session requested ttl_secs must be greater than zero",
746 ));
747 }
748
749 let mut expires_at = token_expires_at;
750 expires_at = expires_at.min(now_secs.saturating_add(configured_max_ttl_secs));
751 if let Some(ttl_secs) = requested_ttl_secs {
752 expires_at = expires_at.min(now_secs.saturating_add(ttl_secs));
753 }
754
755 if expires_at <= now_secs {
756 return Err(Error::forbidden(
757 "delegated session bootstrap token is expired",
758 ));
759 }
760
761 Ok(expires_at)
762 }
763}
764
765#[cfg(test)]
766mod tests;