1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationAudience,
6 DelegationCert, DelegationProof, DelegationProvisionResponse, DelegationRequest,
7 RoleAttestationRequest, SignedRoleAttestation,
8 },
9 error::{Error, ErrorCode},
10 rpc::{Request as RootRequest, Response as RootCapabilityResponse},
11 },
12 error::InternalErrorClass,
13 ids::cap,
14 log,
15 log::Topic,
16 ops::{
17 auth::{DelegatedTokenOps, audience},
18 config::ConfigOps,
19 ic::IcOps,
20 rpc::RpcOps,
21 runtime::env::EnvOps,
22 runtime::metrics::auth::{
23 record_attestation_refresh_failed, record_signer_issue_without_proof,
24 },
25 storage::auth::DelegationStateOps,
26 },
27 protocol,
28 workflow::rpc::request::handler::RootResponseWorkflow,
29};
30
31#[cfg(test)]
32use crate::ids::CanisterRole;
33
34mod admin;
42mod metadata;
43mod proof_store;
44mod session;
45mod verify_flow;
46
47pub struct DelegationApi;
54
55impl DelegationApi {
56 const DELEGATED_TOKENS_DISABLED: &str =
57 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
58 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
59 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
60 b"canic-session-bootstrap-token-fingerprint:v1";
61
62 fn map_delegation_error(err: crate::InternalError) -> Error {
63 match err.class() {
64 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
65 Error::internal(err.to_string())
66 }
67 _ => Error::from(err),
68 }
69 }
70
71 pub fn verify_delegation_proof(
76 proof: &DelegationProof,
77 authority_pid: Principal,
78 ) -> Result<(), Error> {
79 DelegatedTokenOps::verify_delegation_proof(proof, authority_pid)
80 .map_err(Self::map_delegation_error)
81 }
82
83 #[cfg(canic_test_delegation_material)]
84 #[must_use]
85 pub fn current_signing_proof_for_test() -> Option<DelegationProof> {
86 DelegationStateOps::latest_proof_dto()
87 }
88
89 #[must_use]
91 pub fn has_signing_proof() -> bool {
92 DelegationStateOps::latest_proof_dto().is_some()
93 }
94
95 async fn sign_token(
96 claims: DelegatedTokenClaims,
97 proof: DelegationProof,
98 ) -> Result<DelegatedToken, Error> {
99 DelegatedTokenOps::sign_token(claims, proof)
100 .await
101 .map_err(Self::map_delegation_error)
102 }
103
104 pub async fn local_shard_public_key_sec1() -> Result<Vec<u8>, Error> {
106 DelegatedTokenOps::local_shard_public_key_sec1(IcOps::canister_self())
107 .await
108 .map_err(Self::map_delegation_error)
109 }
110
111 pub async fn issue_token(claims: DelegatedTokenClaims) -> Result<DelegatedToken, Error> {
116 let proof = Self::ensure_signing_proof(&claims).await?;
117 let claims = Self::canonicalize_claims_for_proof(claims, &proof);
118 Self::sign_token(claims, proof).await
119 }
120
121 pub fn verify_token(
126 token: &DelegatedToken,
127 authority_pid: Principal,
128 now_secs: u64,
129 ) -> Result<(), Error> {
130 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
131 .map(|_| ())
132 .map_err(Self::map_delegation_error)
133 }
134
135 pub fn verify_token_verified(
140 token: &DelegatedToken,
141 authority_pid: Principal,
142 now_secs: u64,
143 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
144 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
145 .map(crate::ops::auth::VerifiedDelegatedToken::into_parts)
146 .map_err(Self::map_delegation_error)
147 }
148
149 pub fn verify_token_for_caller(
154 token: &DelegatedToken,
155 authority_pid: Principal,
156 now_secs: u64,
157 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
158 let verified = DelegatedTokenOps::verify_token_for_reissue(token, authority_pid, now_secs)
159 .map_err(Self::map_delegation_error)?;
160 Self::ensure_claims_bound_to_caller(&verified.claims.to_dto(), IcOps::msg_caller())?;
161 Ok(verified.into_parts())
162 }
163
164 pub async fn reissue_token(
170 token: DelegatedToken,
171 aud: DelegationAudience,
172 ) -> Result<DelegatedToken, Error> {
173 let aud = Self::normalize_audience(aud)?;
174 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
175 let now_secs = IcOps::now_secs();
176 let (old_claims, _) = Self::verify_token_for_caller(&token, root_pid, now_secs)?;
177 let replacement_claims = DelegatedTokenClaims {
178 aud,
179 iat: now_secs,
180 ..old_claims.clone()
181 };
182
183 Self::reissue_token_from_verified(old_claims, replacement_claims).await
184 }
185
186 pub async fn ensure_token(
192 token: Option<DelegatedToken>,
193 aud: DelegationAudience,
194 ) -> Result<DelegatedToken, Error> {
195 let requested_aud = Self::normalize_audience(aud)?;
196 match token {
197 Some(token) => Self::ensure_existing_token_for_audience(token, requested_aud).await,
198 None => Self::issue_token_for_caller_audience(requested_aud).await,
199 }
200 }
201
202 pub async fn reissue_token_from_verified(
207 old_claims: DelegatedTokenClaims,
208 replacement_claims: DelegatedTokenClaims,
209 ) -> Result<DelegatedToken, Error> {
210 Self::ensure_reissue_claims_allowed(&old_claims, &replacement_claims)?;
211 let proof = Self::ensure_signing_proof(&replacement_claims).await?;
212 let replacement_claims = Self::canonicalize_reissue_claims_for_proof(
213 replacement_claims,
214 &proof,
215 old_claims.exp,
216 )?;
217 Self::sign_token(replacement_claims, proof).await
218 }
219
220 async fn ensure_existing_token_for_audience(
222 token: DelegatedToken,
223 requested_aud: DelegationAudience,
224 ) -> Result<DelegatedToken, Error> {
225 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
226 let now_secs = IcOps::now_secs();
227 let (old_claims, _) = Self::verify_token_for_caller(&token, root_pid, now_secs)?;
228 if audience::roles_subset(&requested_aud, &old_claims.aud) {
229 return Ok(token);
230 }
231
232 let aud = Self::merge_audience_for_reissue(old_claims.aud.clone(), requested_aud);
233 let replacement_claims = DelegatedTokenClaims {
234 aud,
235 iat: now_secs,
236 ..old_claims.clone()
237 };
238
239 Self::reissue_token_from_verified(old_claims, replacement_claims).await
240 }
241
242 async fn issue_token_for_caller_audience(
244 aud: DelegationAudience,
245 ) -> Result<DelegatedToken, Error> {
246 let caller = IcOps::msg_caller();
247 if let Err(reason) = crate::access::auth::validate_delegated_session_subject(caller) {
248 return Err(Error::forbidden(format!(
249 "delegated token caller rejected: {reason}"
250 )));
251 }
252
253 let now_secs = IcOps::now_secs();
254 let ttl_secs = ConfigOps::delegated_tokens_config()
255 .map_err(Error::from)?
256 .max_ttl_secs
257 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS);
258 let claims = DelegatedTokenClaims {
259 sub: caller,
260 shard_pid: IcOps::canister_self(),
261 scopes: vec![cap::VERIFY.to_string()],
262 aud,
263 iat: now_secs,
264 exp: now_secs.saturating_add(ttl_secs),
265 ext: None,
266 };
267
268 Self::issue_token(claims).await
269 }
270
271 pub async fn request_delegation(
275 request: DelegationRequest,
276 ) -> Result<DelegationProvisionResponse, Error> {
277 let request = metadata::with_root_request_metadata(request);
278 Self::request_delegation_remote(request).await
279 }
280
281 pub async fn request_role_attestation(
282 request: RoleAttestationRequest,
283 ) -> Result<SignedRoleAttestation, Error> {
284 let request = metadata::with_root_attestation_request_metadata(request);
285 let response = Self::request_role_attestation_remote(request).await?;
286
287 match response {
288 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
289 _ => Err(Error::internal(
290 "invalid root response type for role attestation request",
291 )),
292 }
293 }
294
295 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
296 DelegatedTokenOps::attestation_key_set()
297 .await
298 .map_err(Self::map_delegation_error)
299 }
300
301 pub async fn prewarm_root_key_material() -> Result<(), Error> {
303 EnvOps::require_root().map_err(Error::from)?;
304 DelegatedTokenOps::prewarm_root_key_material()
305 .await
306 .map_err(|err| {
307 log!(Topic::Auth, Warn, "root auth key prewarm failed: {err}");
308 Self::map_delegation_error(err)
309 })
310 }
311
312 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
313 DelegatedTokenOps::replace_attestation_key_set(key_set);
314 }
315
316 pub async fn verify_role_attestation(
317 attestation: &SignedRoleAttestation,
318 min_accepted_epoch: u64,
319 ) -> Result<(), Error> {
320 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
321 .map_err(Error::from)?
322 .min_accepted_epoch_by_role
323 .get(attestation.payload.role.as_str())
324 .copied();
325 let min_accepted_epoch = verify_flow::resolve_min_accepted_epoch(
326 min_accepted_epoch,
327 configured_min_accepted_epoch,
328 );
329
330 let caller = IcOps::msg_caller();
331 let self_pid = IcOps::canister_self();
332 let now_secs = IcOps::now_secs();
333 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
334 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
335
336 let verify = || {
337 DelegatedTokenOps::verify_role_attestation_cached(
338 attestation,
339 caller,
340 self_pid,
341 verifier_subnet,
342 now_secs,
343 min_accepted_epoch,
344 )
345 .map(|_| ())
346 };
347 let refresh = || async {
348 let key_set: AttestationKeySet =
349 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
350 DelegatedTokenOps::replace_attestation_key_set(key_set);
351 Ok(())
352 };
353
354 match verify_flow::verify_role_attestation_with_single_refresh(verify, refresh).await {
355 Ok(()) => Ok(()),
356 Err(verify_flow::RoleAttestationVerifyFlowError::Initial(err)) => {
357 verify_flow::record_attestation_verifier_rejection(&err);
358 verify_flow::log_attestation_verifier_rejection(
359 &err,
360 attestation,
361 caller,
362 self_pid,
363 "cached",
364 );
365 Err(Self::map_delegation_error(err.into()))
366 }
367 Err(verify_flow::RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
368 verify_flow::record_attestation_verifier_rejection(&trigger);
369 verify_flow::log_attestation_verifier_rejection(
370 &trigger,
371 attestation,
372 caller,
373 self_pid,
374 "cache_miss_refresh",
375 );
376 record_attestation_refresh_failed();
377 log!(
378 Topic::Auth,
379 Warn,
380 "role attestation refresh failed local={} caller={} key_id={} error={}",
381 self_pid,
382 caller,
383 attestation.key_id,
384 source
385 );
386 Err(Self::map_delegation_error(source))
387 }
388 Err(verify_flow::RoleAttestationVerifyFlowError::PostRefresh(err)) => {
389 verify_flow::record_attestation_verifier_rejection(&err);
390 verify_flow::log_attestation_verifier_rejection(
391 &err,
392 attestation,
393 caller,
394 self_pid,
395 "post_refresh",
396 );
397 Err(Self::map_delegation_error(err.into()))
398 }
399 }
400 }
401
402 fn require_proof() -> Result<DelegationProof, Error> {
403 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
404 if !cfg.enabled {
405 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
406 }
407
408 DelegationStateOps::latest_proof_dto().ok_or_else(|| {
409 record_signer_issue_without_proof();
410 Error::not_found("delegation proof not installed")
411 })
412 }
413
414 async fn ensure_signing_proof(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
416 let now_secs = IcOps::now_secs();
417
418 match Self::require_proof() {
419 Ok(proof)
420 if !DelegatedTokenOps::proof_reusable_for_claims(&proof, claims, now_secs) =>
421 {
422 Self::setup_delegation(claims).await
423 }
424 Ok(proof) => Ok(proof),
425 Err(err) if err.code == ErrorCode::NotFound => Self::setup_delegation(claims).await,
426 Err(err) => Err(err),
427 }
428 }
429
430 async fn setup_delegation(claims: &DelegatedTokenClaims) -> Result<DelegationProof, Error> {
432 let shard_public_key_sec1 =
433 DelegatedTokenOps::local_shard_public_key_sec1(claims.shard_pid)
434 .await
435 .map_err(Self::map_delegation_error)?;
436 let request = Self::delegation_request_from_claims(claims, shard_public_key_sec1)?;
437 let response = Self::request_delegation_remote(request).await?;
438 let proof = response.proof;
439 Self::store_local_signer_proof(proof.clone()).await?;
440 Ok(proof)
441 }
442
443 fn canonicalize_claims_for_proof(
446 claims: DelegatedTokenClaims,
447 proof: &DelegationProof,
448 ) -> DelegatedTokenClaims {
449 if claims.iat >= proof.cert.issued_at && claims.exp <= proof.cert.expires_at {
450 return claims;
451 }
452
453 DelegatedTokenClaims {
454 iat: proof.cert.issued_at,
455 exp: proof.cert.expires_at,
456 ..claims
457 }
458 }
459
460 fn ensure_claims_bound_to_caller(
462 claims: &DelegatedTokenClaims,
463 caller: Principal,
464 ) -> Result<(), Error> {
465 if claims.sub == caller {
466 Ok(())
467 } else {
468 Err(Error::forbidden(format!(
469 "delegated token subject '{}' does not match caller '{}'",
470 claims.sub, caller
471 )))
472 }
473 }
474
475 fn normalize_audience(audience: DelegationAudience) -> Result<DelegationAudience, Error> {
477 let DelegationAudience::Roles(roles) = audience else {
478 return Ok(DelegationAudience::Any);
479 };
480
481 let mut out = Vec::new();
482 for role in roles {
483 if !out.contains(&role) {
484 out.push(role);
485 }
486 }
487
488 if out.is_empty() {
489 return Err(Error::invalid("token audience role list must not be empty"));
490 }
491
492 Ok(DelegationAudience::Roles(out))
493 }
494
495 fn merge_audience_for_reissue(
497 current: DelegationAudience,
498 requested: DelegationAudience,
499 ) -> DelegationAudience {
500 match (current, requested) {
501 (DelegationAudience::Any, _) | (_, DelegationAudience::Any) => DelegationAudience::Any,
502 (DelegationAudience::Roles(mut current), DelegationAudience::Roles(requested)) => {
503 for role in requested {
504 if !current.contains(&role) {
505 current.push(role);
506 }
507 }
508 DelegationAudience::Roles(current)
509 }
510 }
511 }
512
513 fn ensure_reissue_claims_allowed(
515 old_claims: &DelegatedTokenClaims,
516 replacement_claims: &DelegatedTokenClaims,
517 ) -> Result<(), Error> {
518 if audience::has_empty_roles(&replacement_claims.aud) {
519 return Err(Error::invalid(
520 "replacement token audience role list 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 shard_public_key_sec1: Vec<u8>,
581 ) -> Result<DelegationRequest, Error> {
582 let ttl_secs = claims.exp.saturating_sub(claims.iat);
583 if ttl_secs == 0 {
584 return Err(Error::invalid(
585 "delegation ttl_secs must be greater than zero",
586 ));
587 }
588
589 let signer_pid = IcOps::canister_self();
590
591 Ok(DelegationRequest {
592 shard_pid: signer_pid,
593 scopes: claims.scopes.clone(),
594 aud: claims.aud.clone(),
595 ttl_secs,
596 shard_public_key_sec1,
597 metadata: None,
598 })
599 }
600
601 }
609
610impl DelegationApi {
611 pub async fn request_delegation_root(
613 request: DelegationRequest,
614 ) -> Result<DelegationProvisionResponse, Error> {
615 let request = metadata::with_root_request_metadata(request);
616 let response = RootResponseWorkflow::response(RootRequest::issue_delegation(request))
617 .await
618 .map_err(Self::map_delegation_error)?;
619
620 match response {
621 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
622 _ => Err(Error::internal(
623 "invalid root response type for delegation request",
624 )),
625 }
626 }
627
628 async fn request_delegation_remote(
630 request: DelegationRequest,
631 ) -> Result<DelegationProvisionResponse, Error> {
632 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
633 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_DELEGATION, request)
634 .await
635 .map_err(Self::map_delegation_error)
636 }
637
638 pub async fn request_role_attestation_root(
640 request: RoleAttestationRequest,
641 ) -> Result<SignedRoleAttestation, Error> {
642 let request = metadata::with_root_attestation_request_metadata(request);
643 let response = RootResponseWorkflow::response(RootRequest::issue_role_attestation(request))
644 .await
645 .map_err(Self::map_delegation_error)?;
646
647 match response {
648 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
649 _ => Err(Error::internal(
650 "invalid root response type for role attestation request",
651 )),
652 }
653 }
654
655 async fn request_role_attestation_remote(
657 request: RoleAttestationRequest,
658 ) -> Result<RootCapabilityResponse, Error> {
659 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
660 RpcOps::call_rpc_result(root_pid, protocol::CANIC_REQUEST_ROLE_ATTESTATION, request)
661 .await
662 .map_err(Self::map_delegation_error)
663 }
664}
665
666#[cfg(test)]
667mod tests;