1use crate::{
2 access::auth::validate_delegated_session_subject,
3 cdk::types::Principal,
4 dto::{
5 auth::{
6 AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationAdminCommand,
7 DelegationAdminResponse, DelegationCert, DelegationProof,
8 DelegationProofInstallRequest, DelegationProvisionResponse, DelegationProvisionStatus,
9 DelegationProvisionTargetKind, DelegationRequest, DelegationVerifierProofPushRequest,
10 RoleAttestationRequest, SignedRoleAttestation,
11 },
12 error::{Error, ErrorCode},
13 rpc::{Request as RootRequest, Response as RootCapabilityResponse},
14 },
15 error::InternalErrorClass,
16 log,
17 log::Topic,
18 ops::{
19 auth::DelegatedTokenOps,
20 config::ConfigOps,
21 ic::IcOps,
22 rpc::RpcOps,
23 runtime::env::EnvOps,
24 runtime::metrics::auth::{
25 DelegationInstallNormalizationRejectReason, DelegationInstallValidationFailureReason,
26 VerifierProofCacheEvictionClass, record_attestation_refresh_failed,
27 record_delegation_install_fanout_bucket,
28 record_delegation_install_normalization_rejected,
29 record_delegation_install_normalized_target_count, record_delegation_install_total,
30 record_delegation_install_validation_failed, record_delegation_provision_complete,
31 record_delegation_verifier_target_count, record_delegation_verifier_target_failed,
32 record_delegation_verifier_target_missing, record_session_bootstrap_rejected_disabled,
33 record_session_bootstrap_rejected_replay_conflict,
34 record_session_bootstrap_rejected_replay_reused,
35 record_session_bootstrap_rejected_subject_mismatch,
36 record_session_bootstrap_rejected_subject_rejected,
37 record_session_bootstrap_rejected_token_invalid,
38 record_session_bootstrap_rejected_ttl_invalid,
39 record_session_bootstrap_rejected_wallet_caller_rejected,
40 record_session_bootstrap_replay_idempotent, record_session_cleared,
41 record_session_created, record_session_pruned, record_session_replaced,
42 record_signer_issue_without_proof, record_verifier_proof_cache_eviction,
43 record_verifier_proof_cache_stats,
44 },
45 storage::{
46 auth::{DelegatedSession, DelegatedSessionBootstrapBinding, DelegationStateOps},
47 directory::subnet::SubnetDirectoryOps,
48 registry::subnet::SubnetRegistryOps,
49 },
50 },
51 protocol,
52 workflow::{auth::DelegationWorkflow, rpc::request::handler::RootResponseWorkflow},
53};
54use sha2::{Digest, Sha256};
55
56mod metadata;
57mod verify_flow;
58
59pub struct DelegationApi;
66
67struct PreparedDelegationVerifierPush {
68 proof: DelegationProof,
69 verifier_targets: Vec<Principal>,
70 intent: crate::dto::auth::DelegationProofInstallIntent,
71}
72
73impl PreparedDelegationVerifierPush {
74 fn into_command(self) -> DelegationAdminCommand {
75 let request = DelegationVerifierProofPushRequest {
76 proof: self.proof,
77 verifier_targets: self.verifier_targets,
78 };
79 match self.intent {
80 crate::dto::auth::DelegationProofInstallIntent::Prewarm => {
81 DelegationAdminCommand::PrewarmVerifiers(request)
82 }
83 crate::dto::auth::DelegationProofInstallIntent::Repair => {
84 DelegationAdminCommand::RepairVerifiers(request)
85 }
86 crate::dto::auth::DelegationProofInstallIntent::Provisioning => {
87 unreachable!("provisioning does not use explicit admin push")
88 }
89 }
90 }
91}
92
93impl DelegationApi {
94 const DELEGATED_TOKENS_DISABLED: &str =
95 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
96 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
97 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
98 b"canic-session-bootstrap-token-fingerprint:v1";
99
100 fn map_delegation_error(err: crate::InternalError) -> Error {
101 match err.class() {
102 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
103 Error::internal(err.to_string())
104 }
105 _ => Error::from(err),
106 }
107 }
108
109 pub fn verify_delegation_proof(
114 proof: &DelegationProof,
115 authority_pid: Principal,
116 ) -> Result<(), Error> {
117 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
118 .map_err(Self::map_delegation_error)
119 }
120
121 #[cfg(canic_test_delegation_material)]
122 #[must_use]
123 pub fn current_signing_proof_for_test() -> Option<DelegationProof> {
124 DelegationStateOps::latest_proof_dto()
125 }
126
127 async fn sign_token(
128 claims: DelegatedTokenClaims,
129 proof: DelegationProof,
130 ) -> Result<DelegatedToken, Error> {
131 DelegatedTokenOps::sign_token(claims, proof)
132 .await
133 .map_err(Self::map_delegation_error)
134 }
135
136 pub async fn issue_token(claims: DelegatedTokenClaims) -> Result<DelegatedToken, Error> {
141 let proof = Self::ensure_signing_proof(&claims).await?;
142 Self::sign_token(claims, proof).await
143 }
144
145 pub fn verify_token(
150 token: &DelegatedToken,
151 authority_pid: Principal,
152 now_secs: u64,
153 ) -> Result<(), Error> {
154 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
155 .map(|_| ())
156 .map_err(Self::map_delegation_error)
157 }
158
159 pub fn verify_token_verified(
164 token: &DelegatedToken,
165 authority_pid: Principal,
166 now_secs: u64,
167 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
168 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
169 .map(|verified| (verified.claims, verified.cert))
170 .map_err(Self::map_delegation_error)
171 }
172
173 pub async fn request_delegation(
177 request: DelegationRequest,
178 ) -> Result<DelegationProvisionResponse, Error> {
179 let request = metadata::with_root_request_metadata(request);
180 if EnvOps::is_root() {
181 let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
182 .await
183 .map_err(Self::map_delegation_error)?;
184
185 return match response {
186 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
187 _ => Err(Error::internal(
188 "invalid root response type for delegation request",
189 )),
190 };
191 }
192
193 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
194 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
195 .await
196 .map_err(Self::map_delegation_error)
197 }
198
199 pub async fn request_role_attestation(
200 request: RoleAttestationRequest,
201 ) -> Result<SignedRoleAttestation, Error> {
202 let request = metadata::with_root_attestation_request_metadata(request);
203 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
204 .await
205 .map_err(Self::map_delegation_error)?;
206
207 match response {
208 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
209 _ => Err(Error::internal(
210 "invalid root response type for role attestation request",
211 )),
212 }
213 }
214
215 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
216 DelegatedTokenOps::attestation_key_set()
217 .await
218 .map_err(Self::map_delegation_error)
219 }
220
221 pub async fn admin(cmd: DelegationAdminCommand) -> Result<DelegationAdminResponse, Error> {
223 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
224 if !cfg.enabled {
225 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
226 }
227 if !EnvOps::is_root() {
228 return Err(Error::forbidden("delegation admin requires root canister"));
229 }
230
231 let prepared = match cmd {
232 DelegationAdminCommand::PrewarmVerifiers(request) => {
233 record_delegation_install_total(
234 crate::dto::auth::DelegationProofInstallIntent::Prewarm,
235 );
236 Self::prepare_explicit_verifier_push(
237 request,
238 crate::dto::auth::DelegationProofInstallIntent::Prewarm,
239 )
240 .await?
241 }
242 DelegationAdminCommand::RepairVerifiers(request) => {
243 record_delegation_install_total(
244 crate::dto::auth::DelegationProofInstallIntent::Repair,
245 );
246 Self::prepare_explicit_verifier_push(
247 request,
248 crate::dto::auth::DelegationProofInstallIntent::Repair,
249 )
250 .await?
251 }
252 };
253
254 DelegationWorkflow::handle_admin(prepared.into_command())
255 .await
256 .map_err(Self::map_delegation_error)
257 }
258
259 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
260 DelegatedTokenOps::replace_attestation_key_set(key_set);
261 }
262
263 pub async fn verify_role_attestation(
264 attestation: &SignedRoleAttestation,
265 min_accepted_epoch: u64,
266 ) -> Result<(), Error> {
267 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
268 .map_err(Error::from)?
269 .min_accepted_epoch_by_role
270 .get(attestation.payload.role.as_str())
271 .copied();
272 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
273 min_accepted_epoch,
274 configured_min_accepted_epoch,
275 );
276
277 let caller = IcOps::msg_caller();
278 let self_pid = IcOps::canister_self();
279 let now_secs = IcOps::now_secs();
280 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
281 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
282
283 let verify = || {
284 DelegatedTokenOps::verify_role_attestation_cached(
285 attestation,
286 caller,
287 self_pid,
288 verifier_subnet,
289 now_secs,
290 min_accepted_epoch,
291 )
292 .map(|_| ())
293 };
294 let refresh = || async {
295 let key_set: AttestationKeySet =
296 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
297 DelegatedTokenOps::replace_attestation_key_set(key_set);
298 Ok(())
299 };
300
301 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
302 Ok(()) => Ok(()),
303 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
304 verify_flow::record_attestation_verifier_rejection(&err);
305 verify_flow::log_attestation_verifier_rejection(
306 &err,
307 attestation,
308 caller,
309 self_pid,
310 "cached",
311 );
312 Err(Self::map_delegation_error(err.into()))
313 }
314 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
315 verify_flow::record_attestation_verifier_rejection(&trigger);
316 verify_flow::log_attestation_verifier_rejection(
317 &trigger,
318 attestation,
319 caller,
320 self_pid,
321 "cache_miss_refresh",
322 );
323 record_attestation_refresh_failed();
324 log!(
325 Topic::Auth,
326 Warn,
327 "role attestation refresh failed local={} caller={} key_id={} error={}",
328 self_pid,
329 caller,
330 attestation.key_id,
331 source
332 );
333 Err(Self::map_delegation_error(source))
334 }
335 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
336 verify_flow::record_attestation_verifier_rejection(&err);
337 verify_flow::log_attestation_verifier_rejection(
338 &err,
339 attestation,
340 caller,
341 self_pid,
342 "post_refresh",
343 );
344 Err(Self::map_delegation_error(err.into()))
345 }
346 }
347 }
348
349 pub fn set_delegated_session_subject(
351 delegated_subject: Principal,
352 bootstrap_token: DelegatedToken,
353 requested_ttl_secs: Option<u64>,
354 ) -> Result<(), Error> {
355 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
356 if !cfg.enabled {
357 record_session_bootstrap_rejected_disabled();
358 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
359 }
360
361 let wallet_caller = IcOps::msg_caller();
362 if let Err(reason) = validate_delegated_session_subject(wallet_caller) {
363 record_session_bootstrap_rejected_wallet_caller_rejected();
364 return Err(Error::forbidden(format!(
365 "delegated session wallet caller rejected: {reason}"
366 )));
367 }
368
369 if let Err(reason) = validate_delegated_session_subject(delegated_subject) {
370 record_session_bootstrap_rejected_subject_rejected();
371 return Err(Error::forbidden(format!(
372 "delegated session subject rejected: {reason}"
373 )));
374 }
375
376 let issued_at = IcOps::now_secs();
377 let authority_pid = EnvOps::root_pid().map_err(Error::from)?;
378 let self_pid = IcOps::canister_self();
379 let verified =
380 DelegatedTokenOps::verify_token(&bootstrap_token, authority_pid, issued_at, self_pid)
381 .map_err(|err| {
382 record_session_bootstrap_rejected_token_invalid();
383 Self::map_delegation_error(err)
384 })?;
385
386 if verified.claims.sub != delegated_subject {
387 record_session_bootstrap_rejected_subject_mismatch();
388 return Err(Error::forbidden(format!(
389 "delegated session subject mismatch: requested={} token_subject={}",
390 delegated_subject, verified.claims.sub
391 )));
392 }
393
394 let configured_max_ttl_secs = cfg
395 .max_ttl_secs
396 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS);
397 let expires_at = Self::clamp_delegated_session_expires_at(
398 issued_at,
399 verified.claims.exp,
400 configured_max_ttl_secs,
401 requested_ttl_secs,
402 )
403 .inspect_err(|_| record_session_bootstrap_rejected_ttl_invalid())?;
404
405 let token_fingerprint =
406 Self::delegated_session_bootstrap_token_fingerprint(&bootstrap_token)
407 .inspect_err(|_| record_session_bootstrap_rejected_token_invalid())?;
408
409 if Self::enforce_bootstrap_replay_policy(
410 wallet_caller,
411 delegated_subject,
412 token_fingerprint,
413 issued_at,
414 )? {
415 return Ok(());
416 }
417
418 let had_active_session =
419 DelegationStateOps::delegated_session(wallet_caller, issued_at).is_some();
420
421 DelegationStateOps::upsert_delegated_session(
422 DelegatedSession {
423 wallet_pid: wallet_caller,
424 delegated_pid: delegated_subject,
425 issued_at,
426 expires_at,
427 bootstrap_token_fingerprint: Some(token_fingerprint),
428 },
429 issued_at,
430 );
431 DelegationStateOps::upsert_delegated_session_bootstrap_binding(
432 DelegatedSessionBootstrapBinding {
433 wallet_pid: wallet_caller,
434 delegated_pid: delegated_subject,
435 token_fingerprint,
436 bound_at: issued_at,
437 expires_at: verified.claims.exp,
438 },
439 issued_at,
440 );
441
442 if had_active_session {
443 record_session_replaced();
444 } else {
445 record_session_created();
446 }
447
448 Ok(())
449 }
450
451 pub fn clear_delegated_session() {
453 let wallet_caller = IcOps::msg_caller();
454 let had_active_session =
455 DelegationStateOps::delegated_session(wallet_caller, IcOps::now_secs()).is_some();
456 DelegationStateOps::clear_delegated_session(wallet_caller);
457 if had_active_session {
458 record_session_cleared();
459 }
460 }
461
462 #[must_use]
464 pub fn delegated_session_subject() -> Option<Principal> {
465 let wallet_caller = IcOps::msg_caller();
466 DelegationStateOps::delegated_session_subject(wallet_caller, IcOps::now_secs())
467 }
468
469 #[must_use]
471 pub fn prune_expired_delegated_sessions() -> usize {
472 let now_secs = IcOps::now_secs();
473 let removed = DelegationStateOps::prune_expired_delegated_sessions(now_secs);
474 let _ = DelegationStateOps::prune_expired_delegated_session_bootstrap_bindings(now_secs);
475 if removed > 0 {
476 record_session_pruned(removed);
477 }
478 removed
479 }
480
481 pub async fn store_proof(
482 request: DelegationProofInstallRequest,
483 kind: DelegationProvisionTargetKind,
484 ) -> Result<(), Error> {
485 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
486 if !cfg.enabled {
487 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
488 }
489
490 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
491 let caller = IcOps::msg_caller();
492 if caller != root_pid {
493 return Err(Error::forbidden(
494 "delegation proof store requires root caller",
495 ));
496 }
497
498 let proof = request.proof;
499 let intent = request.intent;
500
501 DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
502 .await
503 .map_err(Self::map_delegation_error)?;
504 if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
505 let local = IcOps::canister_self();
506 log!(
507 Topic::Auth,
508 Warn,
509 "delegation proof rejected intent={:?} kind={:?} local={} shard={} issued_at={} expires_at={} error={}",
510 intent,
511 kind,
512 local,
513 proof.cert.shard_pid,
514 proof.cert.issued_at,
515 proof.cert.expires_at,
516 err
517 );
518 return Err(Self::map_delegation_error(err));
519 }
520
521 let outcome = DelegationStateOps::upsert_proof_from_dto(proof.clone(), IcOps::now_secs())
522 .map_err(Self::map_delegation_error)?;
523 if kind == DelegationProvisionTargetKind::Verifier {
524 Self::record_verifier_cache_install_outcome(outcome);
525 }
526 let local = IcOps::canister_self();
527 log!(
528 Topic::Auth,
529 Info,
530 "delegation proof stored intent={:?} kind={:?} local={} shard={} issued_at={} expires_at={}",
531 intent,
532 kind,
533 local,
534 proof.cert.shard_pid,
535 proof.cert.issued_at,
536 proof.cert.expires_at
537 );
538
539 Ok(())
540 }
541
542 #[cfg(canic_test_delegation_material)]
548 pub fn install_test_delegation_material(
549 proof: DelegationProof,
550 root_public_key: Vec<u8>,
551 shard_public_key: Vec<u8>,
552 ) -> Result<(), Error> {
553 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
554 let caller = IcOps::msg_caller();
555 if caller != root_pid {
556 return Err(Error::forbidden(
557 "test delegation material install requires root caller",
558 ));
559 }
560
561 if proof.cert.root_pid != root_pid {
562 return Err(Error::invalid(format!(
563 "delegation proof root mismatch: expected={} found={}",
564 root_pid, proof.cert.root_pid
565 )));
566 }
567
568 if root_public_key.is_empty() || shard_public_key.is_empty() {
569 return Err(Error::invalid("delegation public keys must not be empty"));
570 }
571
572 DelegationStateOps::set_root_public_key(root_public_key);
573 DelegationStateOps::set_shard_public_key(proof.cert.shard_pid, shard_public_key);
574 let outcome = DelegationStateOps::upsert_proof_from_dto(proof, IcOps::now_secs())
575 .map_err(Self::map_delegation_error)?;
576 Self::record_verifier_cache_install_outcome(outcome);
577 Ok(())
578 }
579
580 fn require_proof() -> Result<DelegationProof, Error> {
581 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
582 if !cfg.enabled {
583 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
584 }
585
586 DelegationStateOps::latest_proof_dto().ok_or_else(|| {
587 record_signer_issue_without_proof();
588 Error::not_found("delegation proof not installed")
589 })
590 }
591
592 async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
594 let now_secs = IcOps::now_secs();
595
596 match Self::require_proof() {
597 Ok(proof) if !Self::proof_is_reusable_for_claims(&proof, claims, now_secs) => {
598 Self::setup_delegation(claims).await
599 }
600 Ok(proof) => Ok(proof),
601 Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
602 Err(err) => Err(err),
603 }
604 }
605
606 async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
608 let request = Self::delegation_request_from_claims(claims)?;
609 let required_verifier_targets = request.verifier_targets.clone();
610 let response = Self::request_delegation(request).await?;
611 Self::ensure_required_verifier_targets_provisioned(&required_verifier_targets, &response)?;
612 Self::require_proof()
613 }
614
615 fn delegation_request_from_claims(
617 claims: &DelegatedTokenClaims,
618 ) -> Result<DelegationRequest, Error> {
619 let ttl_secs = claims.exp.saturating_sub(claims.iat);
620 if ttl_secs == 0 {
621 return Err(Error::invalid(
622 "delegation ttl_secs must be greater than zero",
623 ));
624 }
625
626 let signer_pid = IcOps::canister_self();
627 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
628 let verifier_targets = Self::derive_required_verifier_targets_from_aud(
629 &claims.aud,
630 signer_pid,
631 root_pid,
632 Self::is_registered_canister,
633 )?;
634
635 Ok(DelegationRequest {
636 shard_pid: signer_pid,
637 scopes: claims.scopes.clone(),
638 aud: claims.aud.clone(),
639 ttl_secs,
640 verifier_targets,
641 include_root_verifier: true,
642 metadata: None,
643 })
644 }
645
646 fn ensure_required_verifier_targets_provisioned(
648 required_targets: &[Principal],
649 response: &DelegationProvisionResponse,
650 ) -> Result<(), Error> {
651 let mut checked = Vec::new();
652 for target in required_targets {
653 if checked.contains(target) {
654 continue;
655 }
656 checked.push(*target);
657 }
658 record_delegation_verifier_target_count(checked.len());
659
660 for target in &checked {
661 let Some(result) = response.results.iter().find(|entry| {
662 entry.kind == DelegationProvisionTargetKind::Verifier && entry.target == *target
663 }) else {
664 record_delegation_verifier_target_missing();
665 return Err(Error::internal(format!(
666 "delegation provisioning missing verifier target result for '{target}'"
667 )));
668 };
669
670 if result.status != DelegationProvisionStatus::Ok {
671 record_delegation_verifier_target_failed();
672 let detail = result
673 .error
674 .as_ref()
675 .map_or_else(|| "unknown error".to_string(), ToString::to_string);
676 return Err(Error::internal(format!(
677 "delegation provisioning failed for required verifier target '{target}': {detail}"
678 )));
679 }
680 }
681
682 record_delegation_provision_complete();
683 Ok(())
684 }
685
686 async fn prepare_explicit_verifier_push(
688 request: DelegationVerifierProofPushRequest,
689 intent: crate::dto::auth::DelegationProofInstallIntent,
690 ) -> Result<PreparedDelegationVerifierPush, Error> {
691 let request = Self::normalize_explicit_verifier_push_request_with(
692 request,
693 intent,
694 EnvOps::root_pid().map_err(Error::from)?,
695 Self::is_registered_canister,
696 )?;
697 record_delegation_install_normalized_target_count(intent, request.verifier_targets.len());
698 record_delegation_install_fanout_bucket(intent, request.verifier_targets.len());
699 Self::prepare_explicit_verifier_push_proof(&request.proof, intent).await?;
700
701 Ok(PreparedDelegationVerifierPush {
702 proof: request.proof,
703 verifier_targets: request.verifier_targets,
704 intent,
705 })
706 }
707
708 fn normalize_explicit_verifier_push_request_with<F>(
710 request: DelegationVerifierProofPushRequest,
711 intent: crate::dto::auth::DelegationProofInstallIntent,
712 root_pid: Principal,
713 mut is_valid_target: F,
714 ) -> Result<DelegationVerifierProofPushRequest, Error>
715 where
716 F: FnMut(Principal) -> bool,
717 {
718 let signer_pid = request.proof.cert.shard_pid;
719 let mut verifier_targets = Vec::new();
720
721 for principal in request.verifier_targets {
722 if principal == signer_pid {
723 record_delegation_install_normalization_rejected(
724 intent,
725 DelegationInstallNormalizationRejectReason::SignerTarget,
726 );
727 return Err(Error::invalid(
728 "delegation verifier target must not match signer shard",
729 ));
730 }
731 if principal == root_pid {
732 record_delegation_install_normalization_rejected(
733 intent,
734 DelegationInstallNormalizationRejectReason::RootTarget,
735 );
736 return Err(Error::invalid(
737 "delegation verifier target must not match root canister",
738 ));
739 }
740 if !is_valid_target(principal) {
741 record_delegation_install_normalization_rejected(
742 intent,
743 DelegationInstallNormalizationRejectReason::UnregisteredTarget,
744 );
745 return Err(Error::invalid(format!(
746 "delegation verifier target '{principal}' is not registered"
747 )));
748 }
749 if !verifier_targets.contains(&principal) {
750 verifier_targets.push(principal);
751 }
752 }
753
754 Ok(DelegationVerifierProofPushRequest {
755 proof: request.proof,
756 verifier_targets,
757 })
758 }
759
760 async fn prepare_explicit_verifier_push_proof(
762 proof: &DelegationProof,
763 intent: crate::dto::auth::DelegationProofInstallIntent,
764 ) -> Result<(), Error> {
765 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
766 DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
767 .await
768 .map_err(|err| {
769 record_delegation_install_validation_failed(
770 intent,
771 DelegationInstallValidationFailureReason::CacheKeys,
772 );
773 Self::map_delegation_error(err)
774 })?;
775 Self::verify_delegation_proof(proof, root_pid).inspect_err(|_| {
776 record_delegation_install_validation_failed(
777 intent,
778 DelegationInstallValidationFailureReason::VerifyProof,
779 );
780 })?;
781
782 if intent == crate::dto::auth::DelegationProofInstallIntent::Repair {
783 Self::ensure_repair_push_proof_is_locally_available(proof)?;
784 }
785
786 Ok(())
787 }
788
789 fn ensure_repair_push_proof_is_locally_available(proof: &DelegationProof) -> Result<(), Error> {
791 Self::ensure_repair_push_proof_is_locally_available_with(proof, |candidate| {
792 DelegationStateOps::matching_proof_dto(candidate).map_err(Self::map_delegation_error)
793 })
794 }
795
796 fn ensure_repair_push_proof_is_locally_available_with<F>(
798 proof: &DelegationProof,
799 lookup: F,
800 ) -> Result<(), Error>
801 where
802 F: FnOnce(&DelegationProof) -> Result<Option<DelegationProof>, Error>,
803 {
804 let Some(stored) = lookup(proof)? else {
805 record_delegation_install_validation_failed(
806 crate::dto::auth::DelegationProofInstallIntent::Repair,
807 DelegationInstallValidationFailureReason::RepairMissingLocal,
808 );
809 return Err(Error::not_found(
810 "delegation repair requires an existing local proof",
811 ));
812 };
813
814 if stored != *proof {
815 record_delegation_install_validation_failed(
816 crate::dto::auth::DelegationProofInstallIntent::Repair,
817 DelegationInstallValidationFailureReason::RepairLocalMismatch,
818 );
819 return Err(Error::invalid(
820 "delegation repair proof must match the existing local proof",
821 ));
822 }
823
824 Ok(())
825 }
826
827 fn record_verifier_cache_install_outcome(
829 outcome: crate::ops::storage::auth::DelegationProofUpsertOutcome,
830 ) {
831 record_verifier_proof_cache_stats(
832 outcome.stats.size,
833 outcome.stats.active_count,
834 outcome.stats.capacity,
835 outcome.stats.profile,
836 outcome.stats.active_window_secs,
837 );
838
839 if let Some(class) = outcome.evicted {
840 let class = match class {
841 crate::ops::storage::auth::DelegationProofEvictionClass::Cold => {
842 VerifierProofCacheEvictionClass::Cold
843 }
844 crate::ops::storage::auth::DelegationProofEvictionClass::Active => {
845 VerifierProofCacheEvictionClass::Active
846 }
847 };
848 record_verifier_proof_cache_eviction(class);
849 }
850 }
851
852 fn derive_required_verifier_targets_from_aud<F>(
854 audience: &[Principal],
855 signer_pid: Principal,
856 root_pid: Principal,
857 mut is_valid_target: F,
858 ) -> Result<Vec<Principal>, Error>
859 where
860 F: FnMut(Principal) -> bool,
861 {
862 let mut verifier_targets = Vec::new();
863 for principal in audience {
864 if *principal == signer_pid || *principal == root_pid {
865 continue;
866 }
867
868 if !is_valid_target(*principal) {
869 return Err(Error::invalid(format!(
870 "delegation audience principal '{principal}' is invalid for canonical verifier provisioning"
871 )));
872 }
873
874 if !verifier_targets.contains(principal) {
875 verifier_targets.push(*principal);
876 }
877 }
878
879 Ok(verifier_targets)
880 }
881
882 fn is_registered_canister(principal: Principal) -> bool {
884 if SubnetRegistryOps::is_registered(principal) {
885 return true;
886 }
887
888 SubnetDirectoryOps::data()
889 .entries
890 .iter()
891 .any(|(_, pid)| *pid == principal)
892 }
893
894 fn proof_is_reusable_for_claims(
896 proof: &DelegationProof,
897 claims: &DelegatedTokenClaims,
898 now_secs: u64,
899 ) -> bool {
900 if now_secs > proof.cert.expires_at {
901 return false;
902 }
903
904 if claims.shard_pid != proof.cert.shard_pid {
905 return false;
906 }
907
908 if claims.iat < proof.cert.issued_at || claims.exp > proof.cert.expires_at {
909 return false;
910 }
911
912 Self::is_principal_subset(&claims.aud, &proof.cert.aud)
913 && Self::is_string_subset(&claims.scopes, &proof.cert.scopes)
914 }
915
916 fn is_principal_subset(
918 subset: &[crate::cdk::types::Principal],
919 superset: &[crate::cdk::types::Principal],
920 ) -> bool {
921 subset.iter().all(|item| superset.contains(item))
922 }
923
924 fn is_string_subset(subset: &[String], superset: &[String]) -> bool {
926 subset.iter().all(|item| superset.contains(item))
927 }
928
929 fn delegated_session_bootstrap_token_fingerprint(
930 token: &DelegatedToken,
931 ) -> Result<[u8; 32], Error> {
932 let token_bytes = crate::cdk::candid::encode_one(token).map_err(|err| {
933 Error::internal(format!("bootstrap token fingerprint encode failed: {err}"))
934 })?;
935 let mut hasher = Sha256::new();
936 hasher.update(Self::SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN);
937 hasher.update(token_bytes);
938 Ok(hasher.finalize().into())
939 }
940
941 fn enforce_bootstrap_replay_policy(
943 wallet_caller: Principal,
944 delegated_subject: Principal,
945 token_fingerprint: [u8; 32],
946 issued_at: u64,
947 ) -> Result<bool, Error> {
948 let Some(binding) =
949 DelegationStateOps::delegated_session_bootstrap_binding(token_fingerprint, issued_at)
950 else {
951 return Ok(false);
952 };
953
954 if binding.wallet_pid == wallet_caller && binding.delegated_pid == delegated_subject {
955 let active_same_session =
956 DelegationStateOps::delegated_session(wallet_caller, issued_at).is_some_and(
957 |session| {
958 session.delegated_pid == delegated_subject
959 && session.bootstrap_token_fingerprint == Some(token_fingerprint)
960 },
961 );
962
963 if active_same_session {
964 record_session_bootstrap_replay_idempotent();
965 return Ok(true);
966 }
967
968 record_session_bootstrap_rejected_replay_reused();
969 return Err(Error::forbidden(
970 "delegated session bootstrap token replay rejected; use a fresh token",
971 ));
972 }
973
974 record_session_bootstrap_rejected_replay_conflict();
975 Err(Error::forbidden(format!(
976 "delegated session bootstrap token already bound (wallet={} delegated_subject={})",
977 binding.wallet_pid, binding.delegated_pid
978 )))
979 }
980
981 fn clamp_delegated_session_expires_at(
982 now_secs: u64,
983 token_expires_at: u64,
984 configured_max_ttl_secs: u64,
985 requested_ttl_secs: Option<u64>,
986 ) -> Result<u64, Error> {
987 if configured_max_ttl_secs == 0 {
988 return Err(Error::invariant(
989 "delegated session configured max ttl_secs must be greater than zero",
990 ));
991 }
992
993 if let Some(ttl_secs) = requested_ttl_secs
994 && ttl_secs == 0
995 {
996 return Err(Error::invalid(
997 "delegated session requested ttl_secs must be greater than zero",
998 ));
999 }
1000
1001 let mut expires_at = token_expires_at;
1002 expires_at = expires_at.min(now_secs.saturating_add(configured_max_ttl_secs));
1003 if let Some(ttl_secs) = requested_ttl_secs {
1004 expires_at = expires_at.min(now_secs.saturating_add(ttl_secs));
1005 }
1006
1007 if expires_at <= now_secs {
1008 return Err(Error::forbidden(
1009 "delegated session bootstrap token is expired",
1010 ));
1011 }
1012
1013 Ok(expires_at)
1014 }
1015}
1016
1017#[cfg(test)]
1018mod tests;