Skip to main content

canic_core/api/auth/admin/
mod.rs

1use super::{DelegationApi, proof_store::AudienceBindingFailureStage};
2use crate::{
3    cdk::types::Principal,
4    dto::{
5        auth::{
6            DelegationAdminCommand, DelegationAdminResponse, DelegationProof,
7            DelegationVerifierProofPushRequest,
8        },
9        error::Error,
10    },
11    ops::{
12        config::ConfigOps,
13        runtime::env::EnvOps,
14        runtime::metrics::auth::{
15            DelegationInstallNormalizationRejectReason, DelegationInstallValidationFailureReason,
16            record_delegation_install_fanout_bucket,
17            record_delegation_install_normalization_rejected,
18            record_delegation_install_normalized_target_count, record_delegation_install_total,
19            record_delegation_install_validation_failed,
20        },
21        storage::{
22            auth::DelegationStateOps, directory::subnet::SubnetDirectoryOps,
23            registry::subnet::SubnetRegistryOps,
24        },
25    },
26    workflow::auth::DelegationWorkflow,
27};
28
29///
30/// PreparedDelegationVerifierPush
31///
32
33struct PreparedDelegationVerifierPush {
34    proof: DelegationProof,
35    verifier_targets: Vec<Principal>,
36    intent: crate::dto::auth::DelegationProofInstallIntent,
37}
38
39impl PreparedDelegationVerifierPush {
40    // Convert a validated admin push plan into the workflow command shape.
41    fn into_command(self) -> DelegationAdminCommand {
42        let request = DelegationVerifierProofPushRequest {
43            proof: self.proof,
44            verifier_targets: self.verifier_targets,
45        };
46        match self.intent {
47            crate::dto::auth::DelegationProofInstallIntent::Prewarm => {
48                DelegationAdminCommand::PrewarmVerifiers(request)
49            }
50            crate::dto::auth::DelegationProofInstallIntent::Repair => {
51                DelegationAdminCommand::RepairVerifiers(request)
52            }
53            crate::dto::auth::DelegationProofInstallIntent::Provisioning => {
54                unreachable!("provisioning does not use explicit admin push")
55            }
56        }
57    }
58}
59
60impl DelegationApi {
61    /// Execute explicit root-controlled delegation repair/prewarm operations.
62    pub async fn admin(cmd: DelegationAdminCommand) -> Result<DelegationAdminResponse, Error> {
63        let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
64        if !cfg.enabled {
65            return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
66        }
67        if !EnvOps::is_root() {
68            return Err(Error::forbidden("delegation admin requires root canister"));
69        }
70
71        let prepared = match cmd {
72            DelegationAdminCommand::PrewarmVerifiers(request) => {
73                record_delegation_install_total(
74                    crate::dto::auth::DelegationProofInstallIntent::Prewarm,
75                );
76                Self::prepare_explicit_verifier_push(
77                    request,
78                    crate::dto::auth::DelegationProofInstallIntent::Prewarm,
79                )
80                .await?
81            }
82            DelegationAdminCommand::RepairVerifiers(request) => {
83                record_delegation_install_total(
84                    crate::dto::auth::DelegationProofInstallIntent::Repair,
85                );
86                Self::prepare_explicit_verifier_push(
87                    request,
88                    crate::dto::auth::DelegationProofInstallIntent::Repair,
89                )
90                .await?
91            }
92        };
93
94        DelegationWorkflow::handle_admin(prepared.into_command())
95            .await
96            .map_err(Self::map_delegation_error)
97    }
98
99    // Normalize and verify an explicit verifier-push request before workflow fanout.
100    async fn prepare_explicit_verifier_push(
101        request: DelegationVerifierProofPushRequest,
102        intent: crate::dto::auth::DelegationProofInstallIntent,
103    ) -> Result<PreparedDelegationVerifierPush, Error> {
104        let request = Self::normalize_explicit_verifier_push_request_with(
105            request,
106            intent,
107            EnvOps::root_pid().map_err(Error::from)?,
108            Self::is_registered_canister,
109        )?;
110        record_delegation_install_normalized_target_count(intent, request.verifier_targets.len());
111        record_delegation_install_fanout_bucket(intent, request.verifier_targets.len());
112        Self::prepare_explicit_verifier_push_proof(&request.proof, intent).await?;
113
114        Ok(PreparedDelegationVerifierPush {
115            proof: request.proof,
116            verifier_targets: request.verifier_targets,
117            intent,
118        })
119    }
120
121    // Normalize explicit verifier push targets with root/signer/registration guards.
122    pub(super) fn normalize_explicit_verifier_push_request_with<F>(
123        request: DelegationVerifierProofPushRequest,
124        intent: crate::dto::auth::DelegationProofInstallIntent,
125        root_pid: Principal,
126        mut is_valid_target: F,
127    ) -> Result<DelegationVerifierProofPushRequest, Error>
128    where
129        F: FnMut(Principal) -> bool,
130    {
131        let signer_pid = request.proof.cert.shard_pid;
132        let mut verifier_targets = Vec::new();
133
134        for principal in request.verifier_targets {
135            if principal == signer_pid {
136                record_delegation_install_normalization_rejected(
137                    intent,
138                    DelegationInstallNormalizationRejectReason::SignerTarget,
139                );
140                return Err(Error::invalid(
141                    "delegation verifier target must not match signer shard",
142                ));
143            }
144            if principal == root_pid {
145                record_delegation_install_normalization_rejected(
146                    intent,
147                    DelegationInstallNormalizationRejectReason::RootTarget,
148                );
149                return Err(Error::invalid(
150                    "delegation verifier target must not match root canister",
151                ));
152            }
153            if !is_valid_target(principal) {
154                record_delegation_install_normalization_rejected(
155                    intent,
156                    DelegationInstallNormalizationRejectReason::UnregisteredTarget,
157                );
158                return Err(Error::invalid(format!(
159                    "delegation verifier target '{principal}' is not registered"
160                )));
161            }
162            if !verifier_targets.contains(&principal) {
163                verifier_targets.push(principal);
164            }
165        }
166
167        for principal in &verifier_targets {
168            Self::ensure_target_in_proof_audience(
169                &request.proof,
170                *principal,
171                intent,
172                AudienceBindingFailureStage::Normalization,
173            )?;
174        }
175
176        Ok(DelegationVerifierProofPushRequest {
177            proof: request.proof,
178            verifier_targets,
179        })
180    }
181
182    // Validate/caches proof dependencies once before explicit fanout.
183    async fn prepare_explicit_verifier_push_proof(
184        proof: &DelegationProof,
185        intent: crate::dto::auth::DelegationProofInstallIntent,
186    ) -> Result<(), Error> {
187        let root_pid = EnvOps::root_pid().map_err(Error::from)?;
188        crate::ops::auth::DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
189            .await
190            .map_err(|err| {
191                record_delegation_install_validation_failed(
192                    intent,
193                    DelegationInstallValidationFailureReason::CacheKeys,
194                );
195                Self::map_delegation_error(err)
196            })?;
197        Self::verify_delegation_proof(proof, root_pid).inspect_err(|_| {
198            record_delegation_install_validation_failed(
199                intent,
200                DelegationInstallValidationFailureReason::VerifyProof,
201            );
202        })?;
203
204        if intent == crate::dto::auth::DelegationProofInstallIntent::Repair {
205            Self::ensure_repair_push_proof_is_locally_available(proof)?;
206        }
207
208        Ok(())
209    }
210
211    // Enforce repair as redistribution of already-installed proof state only.
212    fn ensure_repair_push_proof_is_locally_available(proof: &DelegationProof) -> Result<(), Error> {
213        Self::ensure_repair_push_proof_is_locally_available_with(proof, |candidate| {
214            DelegationStateOps::matching_proof_dto(candidate).map_err(Self::map_delegation_error)
215        })
216    }
217
218    // Check repair preconditions using an injectable lookup for unit tests.
219    pub(super) fn ensure_repair_push_proof_is_locally_available_with<F>(
220        proof: &DelegationProof,
221        lookup: F,
222    ) -> Result<(), Error>
223    where
224        F: FnOnce(&DelegationProof) -> Result<Option<DelegationProof>, Error>,
225    {
226        let Some(stored) = lookup(proof)? else {
227            record_delegation_install_validation_failed(
228                crate::dto::auth::DelegationProofInstallIntent::Repair,
229                DelegationInstallValidationFailureReason::RepairMissingLocal,
230            );
231            return Err(Error::not_found(
232                "delegation repair requires an existing local proof",
233            ));
234        };
235
236        if stored != *proof {
237            record_delegation_install_validation_failed(
238                crate::dto::auth::DelegationProofInstallIntent::Repair,
239                DelegationInstallValidationFailureReason::RepairLocalMismatch,
240            );
241            return Err(Error::invalid(
242                "delegation repair proof must match the existing local proof",
243            ));
244        }
245
246        Ok(())
247    }
248
249    // Return true when a principal is a provisionable verifier canister target.
250    pub(super) fn is_registered_canister(principal: Principal) -> bool {
251        if SubnetRegistryOps::is_registered(principal) {
252            return true;
253        }
254
255        SubnetDirectoryOps::data()
256            .entries
257            .iter()
258            .any(|(_, pid)| *pid == principal)
259    }
260}