1use crate::{
2 InternalError, InternalErrorOrigin,
3 cdk::types::Principal,
4 dto::{
5 auth::{
6 DelegatedToken, DelegatedTokenClaims, DelegationAdminCommand, DelegationAdminResponse,
7 DelegationCert, DelegationProof, DelegationProofStatus, DelegationProvisionRequest,
8 DelegationProvisionResponse, DelegationProvisionTargetKind, DelegationRotationStatus,
9 DelegationStatusResponse,
10 },
11 error::Error,
12 },
13 error::InternalErrorClass,
14 log,
15 log::Topic,
16 ops::{
17 auth::DelegatedTokenOps,
18 config::ConfigOps,
19 ic::IcOps,
20 runtime::delegation::DelegationRuntimeOps,
21 runtime::env::EnvOps,
22 runtime::metrics::auth::record_signer_mint_without_proof,
23 storage::{
24 auth::DelegationStateOps, placement::sharding_lifecycle::ShardingLifecycleOps,
25 registry::subnet::SubnetRegistryOps,
26 },
27 },
28 workflow::auth::{DelegationPushOrigin, DelegationWorkflow},
29};
30use std::{sync::Arc, time::Duration};
31
32pub struct DelegationApi;
39
40impl DelegationApi {
41 const DELEGATED_TOKENS_DISABLED: &str =
42 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
43
44 fn map_delegation_error(err: crate::InternalError) -> Error {
45 match err.class() {
46 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
47 Error::internal(err.to_string())
48 }
49 _ => Error::from(err),
50 }
51 }
52
53 pub fn verify_delegation_proof(
58 proof: &DelegationProof,
59 authority_pid: Principal,
60 ) -> Result<(), Error> {
61 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
62 .map_err(Self::map_delegation_error)
63 }
64
65 pub fn sign_token(
66 token_version: u16,
67 claims: DelegatedTokenClaims,
68 proof: DelegationProof,
69 ) -> Result<DelegatedToken, Error> {
70 DelegatedTokenOps::sign_token(token_version, claims, proof)
71 .map_err(Self::map_delegation_error)
72 }
73
74 pub fn verify_token(
79 token: &DelegatedToken,
80 authority_pid: Principal,
81 now_secs: u64,
82 ) -> Result<(), Error> {
83 DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
84 .map(|_| ())
85 .map_err(Self::map_delegation_error)
86 }
87
88 pub fn verify_token_verified(
93 token: &DelegatedToken,
94 authority_pid: Principal,
95 now_secs: u64,
96 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
97 DelegatedTokenOps::verify_token(token, authority_pid, now_secs)
98 .map(|verified| (verified.claims, verified.cert))
99 .map_err(Self::map_delegation_error)
100 }
101
102 pub async fn provision(
105 request: DelegationProvisionRequest,
106 ) -> Result<DelegationProvisionResponse, Error> {
107 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
108 if !cfg.enabled {
109 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
110 }
111
112 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
113 let caller = IcOps::msg_caller();
114 if caller != root_pid {
115 return Err(Error::forbidden(
116 "delegation provision requires root caller",
117 ));
118 }
119
120 validate_issuance_policy(&request.cert)?;
121 log!(
122 Topic::Auth,
123 Info,
124 "delegation provision start signer={} signer_targets={:?} verifier_targets={:?}",
125 request.cert.signer_pid,
126 request.signer_targets,
127 request.verifier_targets
128 );
129 DelegationWorkflow::provision(request)
130 .await
131 .map_err(Self::map_delegation_error)
132 }
133
134 pub fn store_proof(
135 proof: DelegationProof,
136 kind: DelegationProvisionTargetKind,
137 ) -> Result<(), Error> {
138 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
139 if !cfg.enabled {
140 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
141 }
142
143 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
144 let caller = IcOps::msg_caller();
145 if caller != root_pid {
146 return Err(Error::forbidden(
147 "delegation proof store requires root caller",
148 ));
149 }
150
151 if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
152 let local = IcOps::canister_self();
153 log!(
154 Topic::Auth,
155 Warn,
156 "delegation proof rejected kind={:?} local={} signer={} issued_at={} expires_at={} error={}",
157 kind,
158 local,
159 proof.cert.signer_pid,
160 proof.cert.issued_at,
161 proof.cert.expires_at,
162 err
163 );
164 return Err(Self::map_delegation_error(err));
165 }
166
167 DelegationStateOps::set_proof_from_dto(proof);
168 let local = IcOps::canister_self();
169 let stored = DelegationStateOps::proof_dto()
170 .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
171 log!(
172 Topic::Auth,
173 Info,
174 "delegation proof stored kind={:?} local={} signer={} issued_at={} expires_at={}",
175 kind,
176 local,
177 stored.cert.signer_pid,
178 stored.cert.issued_at,
179 stored.cert.expires_at
180 );
181
182 Ok(())
183 }
184
185 pub fn require_proof() -> Result<DelegationProof, Error> {
186 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
187 if !cfg.enabled {
188 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
189 }
190
191 DelegationStateOps::proof_dto().ok_or_else(|| {
192 record_signer_mint_without_proof();
193 Error::not_found("delegation proof not set")
194 })
195 }
196
197 pub fn status() -> Result<DelegationStatusResponse, Error> {
203 let proof = DelegationStateOps::proof_dto();
204 let rotation_state = DelegationRuntimeOps::rotation_state();
205 let rotation_targets = ShardingLifecycleOps::rotation_targets();
206
207 Ok(DelegationStatusResponse {
208 has_proof: proof.is_some(),
209 proof: proof.map(|proof| DelegationProofStatus {
210 signer_pid: proof.cert.signer_pid,
211 issued_at: proof.cert.issued_at,
212 expires_at: proof.cert.expires_at,
213 }),
214 rotation: DelegationRotationStatus {
215 active: rotation_state.active,
216 interval_secs: rotation_state.interval_secs,
217 last_rotation_at: rotation_state.last_rotation_at,
218 },
219 rotation_targets,
220 })
221 }
222}
223
224pub struct DelegationAdminApi;
231
232impl DelegationAdminApi {
233 pub async fn admin(cmd: DelegationAdminCommand) -> Result<DelegationAdminResponse, Error> {
234 match cmd {
235 DelegationAdminCommand::StartRotation { interval_secs } => {
236 let started = Self::start_rotation(interval_secs).await?;
237 Ok(if started {
238 DelegationAdminResponse::RotationStarted
239 } else {
240 DelegationAdminResponse::RotationAlreadyRunning
241 })
242 }
243 DelegationAdminCommand::StopRotation => {
244 let stopped = Self::stop_rotation().await?;
245 Ok(if stopped {
246 DelegationAdminResponse::RotationStopped
247 } else {
248 DelegationAdminResponse::RotationNotRunning
249 })
250 }
251 }
252 }
253
254 #[allow(clippy::unused_async)]
255 pub async fn start_rotation(interval_secs: u64) -> Result<bool, Error> {
256 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
260 if !cfg.enabled {
261 return Err(Error::forbidden(DelegationApi::DELEGATED_TOKENS_DISABLED));
262 }
263
264 if interval_secs == 0 {
265 return Err(Error::invalid(
266 "rotation interval must be greater than zero",
267 ));
268 }
269
270 let template = rotation_template()?;
271 let template = Arc::new(template);
272 let interval = Duration::from_secs(interval_secs);
273
274 let started = DelegationWorkflow::start_rotation(
275 interval,
276 Arc::new({
277 let template = Arc::clone(&template);
278 move || {
279 let now_secs = IcOps::now_secs();
280 let cert = build_rotation_cert(template.as_ref(), now_secs);
281 validate_issuance_policy_internal(&cert)?;
282 Ok(cert)
283 }
284 }),
285 Arc::new(|proof| {
286 DelegationStateOps::set_proof_from_dto(proof.clone());
287
288 let targets = ShardingLifecycleOps::rotation_targets();
289 log!(
290 Topic::Auth,
291 Info,
292 "delegation rotation targets={:?} signer={} issued_at={} expires_at={}",
293 targets,
294 proof.cert.signer_pid,
295 proof.cert.issued_at,
296 proof.cert.expires_at
297 );
298 if !targets.is_empty() {
299 IcOps::spawn(async move {
300 for target in targets {
301 let _ = DelegationWorkflow::push_proof(
302 target,
303 &proof,
304 DelegationProvisionTargetKind::Signer,
305 DelegationPushOrigin::Rotation,
306 )
307 .await;
308 }
309 });
310 }
311
312 Ok(())
313 }),
314 );
315
316 if started {
317 log!(
318 Topic::Auth,
319 Info,
320 "delegation rotation started interval_secs={interval_secs}"
321 );
322 }
323
324 Ok(started)
325 }
326
327 #[allow(clippy::unused_async)]
328 pub async fn stop_rotation() -> Result<bool, Error> {
329 Ok(DelegationWorkflow::stop_rotation())
330 }
331}
332
333fn validate_issuance_policy(cert: &DelegationCert) -> Result<(), Error> {
334 if cert.expires_at <= cert.issued_at {
335 return Err(Error::invalid(
336 "delegation expires_at must be greater than issued_at",
337 ));
338 }
339
340 if cert.audiences.is_empty() {
341 return Err(Error::invalid("delegation audiences must not be empty"));
342 }
343
344 if cert.scopes.is_empty() {
345 return Err(Error::invalid("delegation scopes must not be empty"));
346 }
347
348 if cert.audiences.iter().any(String::is_empty) {
349 return Err(Error::invalid("delegation audience must not be empty"));
350 }
351
352 if cert.scopes.iter().any(String::is_empty) {
353 return Err(Error::invalid("delegation scope must not be empty"));
354 }
355
356 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
357 if cert.signer_pid == root_pid {
358 return Err(Error::invalid("delegation signer must not be root"));
359 }
360
361 let record = SubnetRegistryOps::get(cert.signer_pid)
362 .ok_or_else(|| Error::invalid("delegation signer must be registered to subnet"))?;
363 if record.role.is_root() {
364 return Err(Error::invalid("delegation signer role must not be root"));
365 }
366
367 Ok(())
368}
369
370fn validate_issuance_policy_internal(cert: &DelegationCert) -> Result<(), InternalError> {
371 validate_issuance_policy(cert)
372 .map_err(|err| InternalError::domain(InternalErrorOrigin::Domain, err.message))
373}
374
375struct DelegationRotationTemplate {
380 v: u16,
381 signer_pid: Principal,
382 audiences: Vec<String>,
383 scopes: Vec<String>,
384 ttl_secs: u64,
385}
386
387fn rotation_template() -> Result<DelegationRotationTemplate, Error> {
388 let proof = DelegationStateOps::proof_dto()
389 .ok_or_else(|| Error::not_found("delegation proof not set"))?;
390 let cert = proof.cert;
391
392 if cert.expires_at <= cert.issued_at {
393 return Err(Error::invalid(
394 "delegation cert expires_at must be greater than issued_at",
395 ));
396 }
397
398 let ttl_secs = cert.expires_at - cert.issued_at;
399
400 Ok(DelegationRotationTemplate {
401 v: cert.v,
402 signer_pid: cert.signer_pid,
403 audiences: cert.audiences,
404 scopes: cert.scopes,
405 ttl_secs,
406 })
407}
408
409fn build_rotation_cert(template: &DelegationRotationTemplate, now_secs: u64) -> DelegationCert {
410 DelegationCert {
411 v: template.v,
412 signer_pid: template.signer_pid,
413 audiences: template.audiences.clone(),
414 scopes: template.scopes.clone(),
415 issued_at: now_secs,
416 expires_at: now_secs.saturating_add(template.ttl_secs),
417 }
418}