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