Skip to main content

canic_core/api/auth/
mod.rs

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
59///
60/// DelegationApi
61///
62/// Requires auth.delegated_tokens.enabled = true in config.
63///
64
65pub 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    /// Full delegation proof verification (structure + signature).
110    ///
111    /// Purely local verification; does not read certified data or require a
112    /// query context.
113    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    /// Issue a delegated token using a reusable local proof when possible.
137    ///
138    /// If the proof is missing or no longer valid for the requested claims, this
139    /// performs canonical shard-initiated setup and retries with the refreshed proof.
140    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    /// Full delegated token verification (structure + signature).
146    ///
147    /// Purely local verification; does not read certified data or require a
148    /// query context.
149    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    /// Verify a delegated token and return verified contents.
160    ///
161    /// This is intended for application-layer session construction.
162    /// It performs full verification and returns verified claims and cert.
163    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    /// Canonical shard-initiated delegation request (user_shard -> root).
174    ///
175    /// Caller must match shard_pid and be registered to the subnet.
176    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    /// Execute explicit root-controlled delegation repair/prewarm operations.
222    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    /// Persist a temporary delegated session subject for the caller wallet.
350    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    /// Remove the caller's delegated session subject.
452    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    /// Read the caller's active delegated session subject, if configured.
463    #[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    /// Prune all currently expired delegated sessions.
470    #[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    /// Install delegation proof and key material directly, bypassing management-key lookups.
543    ///
544    /// This is intended for controlled root-driven test flows where deterministic
545    /// key material is used instead of chain-key ECDSA.
546    // Compiled only for controlled test canister builds.
547    #[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    // Resolve a proof that is currently usable for token issuance.
593    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    // Provision a fresh delegation from root, then resolve the latest locally stored proof.
607    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    // Build a canonical delegation request from token claims.
616    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    // Validate required verifier fanout and fail closed when any required target is missing/failing.
647    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    // Normalize and verify an explicit verifier-push request before workflow fanout.
687    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    // Normalize explicit verifier push targets with root/signer/registration guards.
709    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    // Validate/caches proof dependencies once before explicit fanout.
761    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    // Enforce repair as redistribution of already-installed proof state only.
790    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    // Check repair preconditions using an injectable lookup for unit tests.
797    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    // Record verifier-cache occupancy/utilization and any eviction caused by install.
828    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    // Derive required verifier targets from audience with strict filtering/validation.
853    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    // Return true when a principal is a provisionable verifier canister target.
883    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    // Check whether a proof can be reused safely for the requested claims.
895    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    // Return true when every principal in `subset` is present in `superset`.
917    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    // Return true when every scope in `subset` is present in `superset`.
925    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    // Enforce replay policy for delegated-session bootstrap by token fingerprint.
942    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;