1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationAudience,
6 DelegationCert, DelegationProof, DelegationProvisionResponse,
7 DelegationProvisionStatus, DelegationProvisionTargetKind, DelegationRequest,
8 RoleAttestationRequest, SignedRoleAttestation,
9 },
10 error::{Error, ErrorCode},
11 rpc::{Request as RootRequest, Response as RootCapabilityResponse},
12 },
13 error::InternalErrorClass,
14 ids::cap,
15 log,
16 log::Topic,
17 ops::{
18 auth::{DelegatedTokenOps, audience},
19 config::ConfigOps,
20 ic::IcOps,
21 rpc::RpcOps,
22 runtime::env::EnvOps,
23 runtime::metrics::auth::{
24 record_attestation_refresh_failed, record_delegation_provision_complete,
25 record_delegation_verifier_target_count, record_delegation_verifier_target_failed,
26 record_delegation_verifier_target_missing, record_signer_issue_without_proof,
27 },
28 storage::auth::DelegationStateOps,
29 },
30 protocol,
31 workflow::rpc::request::handler::RootResponseWorkflow,
32};
33
34#[cfg(test)]
35use crate::ids::CanisterRole;
36
37mod admin;
45mod metadata;
46mod proof_store;
47mod session;
48mod verify_flow;
49
50pub struct DelegationApi;
57
58impl DelegationApi {
59 const DELEGATED_TOKENS_DISABLED: &str =
60 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
61 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
62 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
63 b"canic-session-bootstrap-token-fingerprint:v1";
64
65 fn map_delegation_error(err: crate::InternalError) -> Error {
66 match err.class() {
67 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
68 Error::internal(err.to_string())
69 }
70 _ => Error::from(err),
71 }
72 }
73
74 pub fn verify_delegation_proof(
79 proof: &DelegationProof,
80 authority_pid: Principal,
81 ) -> Result<(), Error> {
82 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
83 .map_err(Self::map_delegation_error)
84 }
85
86 #[cfg(canic_test_delegation_material)]
87 #[must_use]
88 pub fn current_signing_proof_for_test() -> Option<DelegationProof> {
89 DelegationStateOps::latest_proof_dto()
90 }
91
92 #[must_use]
94 pub fn has_signing_proof() -> bool {
95 DelegationStateOps::latest_proof_dto().is_some()
96 }
97
98 async fn sign_token(
99 claims: DelegatedTokenClaims,
100 proof: DelegationProof,
101 ) -> Result<DelegatedToken, Error> {
102 DelegatedTokenOps::sign_token(claims, proof)
103 .await
104 .map_err(Self::map_delegation_error)
105 }
106
107 pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
109 DelegatedTokenOps::local_shard_public_key_sec1(IcOps::canister_self())
110 .await
111 .map_err(Self::map_delegation_error)
112 }
113
114 pub async fn issue_token(claims: DelegatedTokenClaims) -> Result<DelegatedToken, Error> {
119 let proof = Self::ensure_signing_proof(&claims).await?;
120 let claims = Self::canonicalize_claims_for_proof(claims, &proof);
121 Self::sign_token(claims, proof).await
122 }
123
124 pub fn verify_token(
129 token: &DelegatedToken,
130 authority_pid: Principal,
131 now_secs: u64,
132 ) -> Result<(), Error> {
133 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
134 .map(|_| ())
135 .map_err(Self::map_delegation_error)
136 }
137
138 pub fn verify_token_verified(
143 token: &DelegatedToken,
144 authority_pid: Principal,
145 now_secs: u64,
146 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
147 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
148 .map(crate::ops::auth::VerifiedDelegatedToken::into_parts)
149 .map_err(Self::map_delegation_error)
150 }
151
152 pub fn verify_token_for_caller(
157 token: &DelegatedToken,
158 authority_pid: Principal,
159 now_secs: u64,
160 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
161 let verified = DelegatedTokenOps::verify_token_for_reissue(token, authority_pid, now_secs)
162 .map_err(Self::map_delegation_error)?;
163 Self::ensure_claims_bound_to_caller(&verified.claims.to_dto(), IcOps::msg_caller())?;
164 Ok(verified.into_parts())
165 }
166
167 pub async fn reissue_token(
173 token: DelegatedToken,
174 aud: DelegationAudience,
175 ) -> Result<DelegatedToken, Error> {
176 let aud = Self::normalize_audience(aud)?;
177 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
178 let now_secs = IcOps::now_secs();
179 let (old_claims, _) = Self::verify_token_for_caller(&token, root_pid, now_secs)?;
180 let replacement_claims = DelegatedTokenClaims {
181 aud,
182 iat: now_secs,
183 ..old_claims.clone()
184 };
185
186 Self::reissue_token_from_verified(old_claims, replacement_claims).await
187 }
188
189 pub async fn ensure_token(
195 token: Option<DelegatedToken>,
196 aud: DelegationAudience,
197 ) -> Result<DelegatedToken, Error> {
198 let requested_aud = Self::normalize_audience(aud)?;
199 match token {
200 Some(token) => Self::ensure_existing_token_for_audience(token, requested_aud).await,
201 None => Self::issue_token_for_caller_audience(requested_aud).await,
202 }
203 }
204
205 pub async fn reissue_token_from_verified(
210 old_claims: DelegatedTokenClaims,
211 replacement_claims: DelegatedTokenClaims,
212 ) -> Result<DelegatedToken, Error> {
213 Self::ensure_reissue_claims_allowed(&old_claims, &replacement_claims)?;
214 let proof = Self::ensure_signing_proof(&replacement_claims).await?;
215 let replacement_claims = Self::canonicalize_reissue_claims_for_proof(
216 replacement_claims,
217 &proof,
218 old_claims.exp,
219 )?;
220 Self::sign_token(replacement_claims, proof).await
221 }
222
223 async fn ensure_existing_token_for_audience(
225 token: DelegatedToken,
226 requested_aud: DelegationAudience,
227 ) -> Result<DelegatedToken, Error> {
228 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
229 let now_secs = IcOps::now_secs();
230 let (old_claims, _) = Self::verify_token_for_caller(&token, root_pid, now_secs)?;
231 if audience::roles_subset(&requested_aud, &old_claims.aud) {
232 return Ok(token);
233 }
234
235 let aud = Self::merge_audience_for_reissue(old_claims.aud.clone(), requested_aud);
236 let replacement_claims = DelegatedTokenClaims {
237 aud,
238 iat: now_secs,
239 ..old_claims.clone()
240 };
241
242 Self::reissue_token_from_verified(old_claims, replacement_claims).await
243 }
244
245 async fn issue_token_for_caller_audience(
247 aud: DelegationAudience,
248 ) -> Result<DelegatedToken, Error> {
249 let caller = IcOps::msg_caller();
250 if let Err(reason) = crate::access::auth::validate_delegated_session_subject(caller) {
251 return Err(Error::forbidden(format!(
252 "delegated token caller rejected: {reason}"
253 )));
254 }
255
256 let now_secs = IcOps::now_secs();
257 let ttl_secs = ConfigOps::delegated_tokens_config()
258 .map_err(Error::from)?
259 .max_ttl_secs
260 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS);
261 let claims = DelegatedTokenClaims {
262 sub: caller,
263 shard_pid: IcOps::canister_self(),
264 scopes: vec![cap::VERIFY.to_string()],
265 aud,
266 iat: now_secs,
267 exp: now_secs.saturating_add(ttl_secs),
268 ext: None,
269 };
270
271 Self::issue_token(claims).await
272 }
273
274 pub async fn request_delegation(
278 request: DelegationRequest,
279 ) -> Result<DelegationProvisionResponse, Error> {
280 let request = metadata::with_root_request_metadata(request);
281 Self::request_delegation_remote(request).await
282 }
283
284 pub async fn request_role_attestation(
285 request: RoleAttestationRequest,
286 ) -> Result<SignedRoleAttestation, Error> {
287 let request = metadata::with_root_attestation_request_metadata(request);
288 let response = Self::request_role_attestation_remote(request).await?;
289
290 match response {
291 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
292 _ => Err(Error::internal(
293 "invalid root response type for role attestation request",
294 )),
295 }
296 }
297
298 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
299 DelegatedTokenOps::attestation_key_set()
300 .await
301 .map_err(Self::map_delegation_error)
302 }
303
304 pub async fn prewarm_root_key_material() -> Result<(), Error> {
306 EnvOps::require_root().map_err(Error::from)?;
307 DelegatedTokenOps::prewarm_root_key_material()
308 .await
309 .map_err(|err| {
310 log!(Topic::Auth, Warn, "root auth key prewarm failed: {err}");
311 Self::map_delegation_error(err)
312 })
313 }
314
315 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
316 DelegatedTokenOps::replace_attestation_key_set(key_set);
317 }
318
319 pub async fn verify_role_attestation(
320 attestation: &SignedRoleAttestation,
321 min_accepted_epoch: u64,
322 ) -> Result<(), Error> {
323 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
324 .map_err(Error::from)?
325 .min_accepted_epoch_by_role
326 .get(attestation.payload.role.as_str())
327 .copied();
328 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
329 min_accepted_epoch,
330 configured_min_accepted_epoch,
331 );
332
333 let caller = IcOps::msg_caller();
334 let self_pid = IcOps::canister_self();
335 let now_secs = IcOps::now_secs();
336 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
337 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
338
339 let verify = || {
340 DelegatedTokenOps::verify_role_attestation_cached(
341 attestation,
342 caller,
343 self_pid,
344 verifier_subnet,
345 now_secs,
346 min_accepted_epoch,
347 )
348 .map(|_| ())
349 };
350 let refresh = || async {
351 let key_set: AttestationKeySet =
352 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
353 DelegatedTokenOps::replace_attestation_key_set(key_set);
354 Ok(())
355 };
356
357 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
358 Ok(()) => Ok(()),
359 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
360 verify_flow::record_attestation_verifier_rejection(&err);
361 verify_flow::log_attestation_verifier_rejection(
362 &err,
363 attestation,
364 caller,
365 self_pid,
366 "cached",
367 );
368 Err(Self::map_delegation_error(err.into()))
369 }
370 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
371 verify_flow::record_attestation_verifier_rejection(&trigger);
372 verify_flow::log_attestation_verifier_rejection(
373 &trigger,
374 attestation,
375 caller,
376 self_pid,
377 "cache_miss_refresh",
378 );
379 record_attestation_refresh_failed();
380 log!(
381 Topic::Auth,
382 Warn,
383 "role attestation refresh failed local={} caller={} key_id={} error={}",
384 self_pid,
385 caller,
386 attestation.key_id,
387 source
388 );
389 Err(Self::map_delegation_error(source))
390 }
391 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
392 verify_flow::record_attestation_verifier_rejection(&err);
393 verify_flow::log_attestation_verifier_rejection(
394 &err,
395 attestation,
396 caller,
397 self_pid,
398 "post_refresh",
399 );
400 Err(Self::map_delegation_error(err.into()))
401 }
402 }
403 }
404
405 fn require_proof() -> Result<DelegationProof, Error> {
406 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
407 if !cfg.enabled {
408 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
409 }
410
411 DelegationStateOps::latest_proof_dto().ok_or_else(|| {
412 record_signer_issue_without_proof();
413 Error::not_found("delegation proof not installed")
414 })
415 }
416
417 async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
419 let now_secs = IcOps::now_secs();
420
421 match Self::require_proof() {
422 Ok(proof)
423 if !DelegatedTokenOps::proof_reusable_for_claims(&proof, claims, now_secs) =>
424 {
425 Self::setup_delegation(claims).await
426 }
427 Ok(proof) => Ok(proof),
428 Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
429 Err(err) => Err(err),
430 }
431 }
432
433 async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
435 let shard_public_key_sec1 =
436 DelegatedTokenOps::local_shard_public_key_sec1(claims.shard_pid)
437 .await
438 .map_err(Self::map_delegation_error)?;
439 let request = Self::delegation_request_from_claims(claims, shard_public_key_sec1)?;
440 let required_verifier_targets = request.verifier_targets.clone();
441 let response = Self::request_delegation_remote(request).await?;
442 Self::ensure_required_verifier_targets_provisioned(&required_verifier_targets, &response)?;
443 let proof = response.proof;
444 Self::store_local_signer_proof(proof.clone()).await?;
445 Ok(proof)
446 }
447
448 fn canonicalize_claims_for_proof(
451 claims: DelegatedTokenClaims,
452 proof: &DelegationProof,
453 ) -> DelegatedTokenClaims {
454 if claims.iat >= proof.cert.issued_at && claims.exp <= proof.cert.expires_at {
455 return claims;
456 }
457
458 DelegatedTokenClaims {
459 iat: proof.cert.issued_at,
460 exp: proof.cert.expires_at,
461 ..claims
462 }
463 }
464
465 fn ensure_claims_bound_to_caller(
467 claims: &DelegatedTokenClaims,
468 caller: Principal,
469 ) -> Result<(), Error> {
470 if claims.sub == caller {
471 Ok(())
472 } else {
473 Err(Error::forbidden(format!(
474 "delegated token subject '{}' does not match caller '{}'",
475 claims.sub, caller
476 )))
477 }
478 }
479
480 fn normalize_audience(audience: DelegationAudience) -> Result<DelegationAudience, Error> {
482 let DelegationAudience::Roles(roles) = audience else {
483 return Ok(DelegationAudience::Any);
484 };
485
486 let mut out = Vec::new();
487 for role in roles {
488 if !out.contains(&role) {
489 out.push(role);
490 }
491 }
492
493 if out.is_empty() {
494 return Err(Error::invalid("token audience role list must not be empty"));
495 }
496
497 Ok(DelegationAudience::Roles(out))
498 }
499
500 fn merge_audience_for_reissue(
502 current: DelegationAudience,
503 requested: DelegationAudience,
504 ) -> DelegationAudience {
505 match (current, requested) {
506 (DelegationAudience::Any, _) | (_, DelegationAudience::Any) => DelegationAudience::Any,
507 (DelegationAudience::Roles(mut current), DelegationAudience::Roles(requested)) => {
508 for role in requested {
509 if !current.contains(&role) {
510 current.push(role);
511 }
512 }
513 DelegationAudience::Roles(current)
514 }
515 }
516 }
517
518 fn ensure_reissue_claims_allowed(
520 old_claims: &DelegatedTokenClaims,
521 replacement_claims: &DelegatedTokenClaims,
522 ) -> Result<(), Error> {
523 if audience::has_empty_roles(&replacement_claims.aud) {
524 return Err(Error::invalid(
525 "replacement token audience role list must not be empty",
526 ));
527 }
528
529 if replacement_claims.sub != old_claims.sub {
530 return Err(Error::forbidden(format!(
531 "replacement token subject '{}' must match old subject '{}'",
532 replacement_claims.sub, old_claims.sub
533 )));
534 }
535
536 if replacement_claims.shard_pid != old_claims.shard_pid {
537 return Err(Error::forbidden(format!(
538 "replacement token shard '{}' must match old shard '{}'",
539 replacement_claims.shard_pid, old_claims.shard_pid
540 )));
541 }
542
543 if replacement_claims.exp > old_claims.exp {
544 return Err(Error::forbidden(
545 "replacement token expiry must not exceed old token expiry",
546 ));
547 }
548
549 if replacement_claims.exp < replacement_claims.iat {
550 return Err(Error::invalid(
551 "replacement token expiry must not precede issued_at",
552 ));
553 }
554
555 if !audience::strings_subset(&replacement_claims.scopes, &old_claims.scopes) {
556 return Err(Error::forbidden(
557 "replacement token scopes must be a subset of old token scopes",
558 ));
559 }
560
561 Ok(())
562 }
563
564 fn canonicalize_reissue_claims_for_proof(
566 claims: DelegatedTokenClaims,
567 proof: &DelegationProof,
568 old_exp: u64,
569 ) -> Result<DelegatedTokenClaims, Error> {
570 let iat = claims.iat.max(proof.cert.issued_at);
571 let exp = claims.exp.min(old_exp).min(proof.cert.expires_at);
572
573 if exp < iat {
574 return Err(Error::invalid(
575 "replacement token expiry is outside the current signing proof window",
576 ));
577 }
578
579 Ok(DelegatedTokenClaims { iat, exp, ..claims })
580 }
581
582 fn delegation_request_from_claims(
584 claims: &DelegatedTokenClaims,
585 shard_public_key_sec1: Vec<u8>,
586 ) -> Result<DelegationRequest, Error> {
587 let ttl_secs = claims.exp.saturating_sub(claims.iat);
588 if ttl_secs == 0 {
589 return Err(Error::invalid(
590 "delegation ttl_secs must be greater than zero",
591 ));
592 }
593
594 let signer_pid = IcOps::canister_self();
595 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
596 let verifier_targets = DelegatedTokenOps::required_verifier_targets_from_audience(
597 &claims.aud,
598 signer_pid,
599 root_pid,
600 )
601 .map_err(|role| {
602 Error::invalid(format!(
603 "delegation audience role '{role}' is invalid for canonical verifier provisioning"
604 ))
605 })?;
606
607 Ok(DelegationRequest {
608 shard_pid: signer_pid,
609 scopes: claims.scopes.clone(),
610 aud: claims.aud.clone(),
611 ttl_secs,
612 verifier_targets,
613 include_root_verifier: true,
614 shard_public_key_sec1,
615 metadata: None,
616 })
617 }
618
619 fn ensure_required_verifier_targets_provisioned(
621 required_targets: &[Principal],
622 response: &DelegationProvisionResponse,
623 ) -> Result<(), Error> {
624 let mut checked = Vec::new();
625 for target in required_targets {
626 if checked.contains(target) {
627 continue;
628 }
629 checked.push(*target);
630 }
631 record_delegation_verifier_target_count(checked.len());
632
633 for target in &checked {
634 let Some(result) = response.results.iter().find(|entry| {
635 entry.kind == DelegationProvisionTargetKind::Verifier && entry.target == *target
636 }) else {
637 record_delegation_verifier_target_missing();
638 return Err(Error::internal(format!(
639 "delegation provisioning missing verifier target result for '{target}'"
640 )));
641 };
642
643 if result.status != DelegationProvisionStatus::Ok {
644 record_delegation_verifier_target_failed();
645 let detail = result
646 .error
647 .as_ref()
648 .map_or_else(|| "unknown error".to_string(), ToString::to_string);
649 return Err(Error::internal(format!(
650 "delegation provisioning failed for required verifier target '{target}': {detail}"
651 )));
652 }
653 }
654
655 record_delegation_provision_complete();
656 Ok(())
657 }
658
659 #[cfg(test)]
661 fn derive_required_verifier_targets_from_aud(
662 audience: &DelegationAudience,
663 signer_pid: Principal,
664 root_pid: Principal,
665 mut resolve_role: impl FnMut(&CanisterRole) -> Result<Vec<Principal>, ()>,
666 ) -> Result<Vec<Principal>, Error> {
667 let mut verifier_targets = Vec::new();
668 let DelegationAudience::Roles(roles) = audience else {
669 return Ok(verifier_targets);
670 };
671 if roles.is_empty() {
672 return Err(Error::invalid(
673 "delegation audience role list must not be empty",
674 ));
675 }
676
677 for role in roles {
678 let pids = resolve_role(role).map_err(|()| {
679 Error::invalid(format!(
680 "delegation audience role '{role}' is invalid for canonical verifier provisioning"
681 ))
682 })?;
683 for pid in pids {
684 if pid == signer_pid || pid == root_pid || verifier_targets.contains(&pid) {
685 continue;
686 }
687 verifier_targets.push(pid);
688 }
689 }
690 Ok(verifier_targets)
691 }
692
693 }
701
702impl DelegationApi {
703 pub async fn request_delegation_root(
705 request: DelegationRequest,
706 ) -> Result<DelegationProvisionResponse, Error> {
707 let request = metadata::with_root_request_metadata(request);
708 let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
709 .await
710 .map_err(Self::map_delegation_error)?;
711
712 match response {
713 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
714 _ => Err(Error::internal(
715 "invalid root response type for delegation request",
716 )),
717 }
718 }
719
720 async fn request_delegation_remote(
722 request: DelegationRequest,
723 ) -> Result<DelegationProvisionResponse, Error> {
724 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
725 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
726 .await
727 .map_err(Self::map_delegation_error)
728 }
729
730 pub async fn request_role_attestation_root(
732 request: RoleAttestationRequest,
733 ) -> Result<SignedRoleAttestation, Error> {
734 let request = metadata::with_root_attestation_request_metadata(request);
735 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
736 .await
737 .map_err(Self::map_delegation_error)?;
738
739 match response {
740 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
741 _ => Err(Error::internal(
742 "invalid root response type for role attestation request",
743 )),
744 }
745 }
746
747 async fn request_role_attestation_remote(
749 request: RoleAttestationRequest,
750 ) -> Result<RootCapabilityResponse, Error> {
751 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
752 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
753 .await
754 .map_err(Self::map_delegation_error)
755 }
756}
757
758#[cfg(test)]
759mod tests;