canic_core/api/auth/admin/
mod.rs1use 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
29struct PreparedDelegationVerifierPush {
34 proof: DelegationProof,
35 verifier_targets: Vec<Principal>,
36 intent: crate::dto::auth::DelegationProofInstallIntent,
37}
38
39impl PreparedDelegationVerifierPush {
40 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 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 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 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 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 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 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 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}