1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AttestationKeySet, DelegatedToken, DelegatedTokenClaims, DelegationCert,
6 DelegationProof, DelegationProvisionResponse, DelegationProvisionTargetKind,
7 DelegationRequest, RoleAttestationRequest, SignedRoleAttestation,
8 },
9 error::Error,
10 rpc::{
11 Request as RootCapabilityRequest, Response as RootCapabilityResponse,
12 RootRequestMetadata,
13 },
14 },
15 error::InternalErrorClass,
16 log,
17 log::Topic,
18 ops::{
19 auth::{DelegatedTokenOps, DelegatedTokenOpsError},
20 config::ConfigOps,
21 ic::IcOps,
22 rpc::RpcOps,
23 runtime::env::EnvOps,
24 runtime::metrics::auth::{
25 record_attestation_epoch_rejected, record_attestation_refresh_failed,
26 record_attestation_unknown_key_id, record_attestation_verify_failed,
27 record_signer_mint_without_proof,
28 },
29 storage::auth::DelegationStateOps,
30 },
31 protocol,
32 workflow::rpc::request::handler::RootResponseWorkflow,
33};
34use sha2::{Digest, Sha256};
35use std::{
36 future::Future,
37 sync::atomic::{AtomicU64, Ordering},
38};
39
40pub struct DelegationApi;
47
48const DEFAULT_ROOT_REQUEST_TTL_SECONDS: u64 = 300;
49static ROOT_REQUEST_NONCE: AtomicU64 = AtomicU64::new(1);
50
51#[derive(Debug)]
52enum RoleAttestationVerifyFlowError {
53 Initial(DelegatedTokenOpsError),
54 Refresh {
55 trigger: DelegatedTokenOpsError,
56 source: crate::InternalError,
57 },
58 PostRefresh(DelegatedTokenOpsError),
59}
60
61impl DelegationApi {
62 const DELEGATED_TOKENS_DISABLED: &str =
63 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
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 pub async fn sign_token(
87 claims: DelegatedTokenClaims,
88 proof: DelegationProof,
89 ) -> Result<DelegatedToken, Error> {
90 DelegatedTokenOps::sign_token(claims, proof)
91 .await
92 .map_err(Self::map_delegation_error)
93 }
94
95 pub fn verify_token(
100 token: &DelegatedToken,
101 authority_pid: Principal,
102 now_secs: u64,
103 ) -> Result<(), Error> {
104 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
105 .map(|_| ())
106 .map_err(Self::map_delegation_error)
107 }
108
109 pub fn verify_token_verified(
114 token: &DelegatedToken,
115 authority_pid: Principal,
116 now_secs: u64,
117 ) -> Result<(DelegatedTokenClaims, DelegationCert), Error> {
118 DelegatedTokenOps::verify_token(token, authority_pid, now_secs, IcOps::canister_self())
119 .map(|verified| (verified.claims, verified.cert))
120 .map_err(Self::map_delegation_error)
121 }
122
123 pub async fn request_delegation(
127 request: DelegationRequest,
128 ) -> Result<DelegationProvisionResponse, Error> {
129 let request = with_root_request_metadata(request);
130 let response =
131 RootResponseWorkflow::response(RootCapabilityRequest::IssueDelegation(request))
132 .await
133 .map_err(Self::map_delegation_error)?;
134
135 match response {
136 RootCapabilityResponse::DelegationIssued(response) => Ok(response),
137 _ => Err(Error::internal(
138 "invalid root response type for delegation request",
139 )),
140 }
141 }
142
143 pub async fn request_role_attestation(
144 request: RoleAttestationRequest,
145 ) -> Result<SignedRoleAttestation, Error> {
146 let request = with_root_attestation_request_metadata(request);
147 let response =
148 RootResponseWorkflow::response(RootCapabilityRequest::IssueRoleAttestation(request))
149 .await
150 .map_err(Self::map_delegation_error)?;
151
152 match response {
153 RootCapabilityResponse::RoleAttestationIssued(response) => Ok(response),
154 _ => Err(Error::internal(
155 "invalid root response type for role attestation request",
156 )),
157 }
158 }
159
160 pub async fn attestation_key_set() -> Result<AttestationKeySet, Error> {
161 DelegatedTokenOps::attestation_key_set()
162 .await
163 .map_err(Self::map_delegation_error)
164 }
165
166 pub fn replace_attestation_key_set(key_set: AttestationKeySet) {
167 DelegatedTokenOps::replace_attestation_key_set(key_set);
168 }
169
170 pub async fn verify_role_attestation(
171 attestation: &SignedRoleAttestation,
172 min_accepted_epoch: u64,
173 ) -> Result<(), Error> {
174 let configured_min_accepted_epoch = ConfigOps::role_attestation_config()
175 .map_err(Error::from)?
176 .min_accepted_epoch_by_role
177 .get(attestation.payload.role.as_str())
178 .copied();
179 let min_accepted_epoch =
180 resolve_min_accepted_epoch(min_accepted_epoch, configured_min_accepted_epoch);
181
182 let caller = IcOps::msg_caller();
183 let self_pid = IcOps::canister_self();
184 let now_secs = IcOps::now_secs();
185 let verifier_subnet = Some(EnvOps::subnet_pid().map_err(Error::from)?);
186 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
187
188 let verify = || {
189 DelegatedTokenOps::verify_role_attestation_cached(
190 attestation,
191 caller,
192 self_pid,
193 verifier_subnet,
194 now_secs,
195 min_accepted_epoch,
196 )
197 .map(|_| ())
198 };
199 let refresh = || async {
200 let key_set: AttestationKeySet =
201 RpcOps::call_rpc_result(root_pid, protocol::CANIC_ATTESTATION_KEY_SET, ()).await?;
202 DelegatedTokenOps::replace_attestation_key_set(key_set);
203 Ok(())
204 };
205
206 match verify_role_attestation_with_single_refresh(verify, refresh).await {
207 Ok(()) => Ok(()),
208 Err(RoleAttestationVerifyFlowError::Initial(err)) => {
209 record_attestation_verifier_rejection(&err);
210 log_attestation_verifier_rejection(&err, attestation, caller, self_pid, "cached");
211 Err(Self::map_delegation_error(err.into()))
212 }
213 Err(RoleAttestationVerifyFlowError::Refresh { trigger, source }) => {
214 record_attestation_verifier_rejection(&trigger);
215 log_attestation_verifier_rejection(
216 &trigger,
217 attestation,
218 caller,
219 self_pid,
220 "cache_miss_refresh",
221 );
222 record_attestation_refresh_failed();
223 log!(
224 Topic::Auth,
225 Warn,
226 "role attestation refresh failed local={} caller={} key_id={} error={}",
227 self_pid,
228 caller,
229 attestation.key_id,
230 source
231 );
232 Err(Self::map_delegation_error(source))
233 }
234 Err(RoleAttestationVerifyFlowError::PostRefresh(err)) => {
235 record_attestation_verifier_rejection(&err);
236 log_attestation_verifier_rejection(
237 &err,
238 attestation,
239 caller,
240 self_pid,
241 "post_refresh",
242 );
243 Err(Self::map_delegation_error(err.into()))
244 }
245 }
246 }
247
248 pub async fn store_proof(
249 proof: DelegationProof,
250 kind: DelegationProvisionTargetKind,
251 ) -> Result<(), Error> {
252 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
253 if !cfg.enabled {
254 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
255 }
256
257 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
258 let caller = IcOps::msg_caller();
259 if caller != root_pid {
260 return Err(Error::forbidden(
261 "delegation proof store requires root caller",
262 ));
263 }
264
265 DelegatedTokenOps::cache_public_keys_for_cert(&proof.cert)
266 .await
267 .map_err(Self::map_delegation_error)?;
268 if let Err(err) = DelegatedTokenOps::verify_delegation_proof(&proof, root_pid) {
269 let local = IcOps::canister_self();
270 log!(
271 Topic::Auth,
272 Warn,
273 "delegation proof rejected kind={:?} local={} shard={} issued_at={} expires_at={} error={}",
274 kind,
275 local,
276 proof.cert.shard_pid,
277 proof.cert.issued_at,
278 proof.cert.expires_at,
279 err
280 );
281 return Err(Self::map_delegation_error(err));
282 }
283
284 DelegationStateOps::set_proof_from_dto(proof);
285 let local = IcOps::canister_self();
286 let stored = DelegationStateOps::proof_dto()
287 .ok_or_else(|| Error::invariant("delegation proof missing after store"))?;
288 log!(
289 Topic::Auth,
290 Info,
291 "delegation proof stored kind={:?} local={} shard={} issued_at={} expires_at={}",
292 kind,
293 local,
294 stored.cert.shard_pid,
295 stored.cert.issued_at,
296 stored.cert.expires_at
297 );
298
299 Ok(())
300 }
301
302 pub fn require_proof() -> Result<DelegationProof, Error> {
303 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
304 if !cfg.enabled {
305 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
306 }
307
308 DelegationStateOps::proof_dto().ok_or_else(|| {
309 record_signer_mint_without_proof();
310 Error::not_found("delegation proof not set")
311 })
312 }
313}
314
315fn with_root_request_metadata(mut request: DelegationRequest) -> DelegationRequest {
316 if request.metadata.is_none() {
317 request.metadata = Some(new_request_metadata());
318 }
319 request
320}
321
322fn with_root_attestation_request_metadata(
323 mut request: RoleAttestationRequest,
324) -> RoleAttestationRequest {
325 if request.metadata.is_none() {
326 request.metadata = Some(new_request_metadata());
327 }
328 request
329}
330
331fn new_request_metadata() -> RootRequestMetadata {
332 RootRequestMetadata {
333 request_id: generate_request_id(),
334 ttl_seconds: DEFAULT_ROOT_REQUEST_TTL_SECONDS,
335 }
336}
337
338fn generate_request_id() -> [u8; 32] {
339 if let Ok(bytes) = crate::utils::rand::random_bytes(32)
340 && bytes.len() == 32
341 {
342 let mut out = [0u8; 32];
343 out.copy_from_slice(&bytes);
344 return out;
345 }
346
347 let nonce = ROOT_REQUEST_NONCE.fetch_add(1, Ordering::Relaxed);
348 let now = IcOps::now_secs();
349 let caller = IcOps::msg_caller();
350 let canister = IcOps::canister_self();
351
352 let mut hasher = Sha256::new();
353 hasher.update(now.to_be_bytes());
354 hasher.update(nonce.to_be_bytes());
355 hasher.update(caller.as_slice());
356 hasher.update(canister.as_slice());
357 hasher.finalize().into()
358}
359
360async fn verify_role_attestation_with_single_refresh<Verify, Refresh, RefreshFuture>(
361 mut verify: Verify,
362 mut refresh: Refresh,
363) -> Result<(), RoleAttestationVerifyFlowError>
364where
365 Verify: FnMut() -> Result<(), DelegatedTokenOpsError>,
366 Refresh: FnMut() -> RefreshFuture,
367 RefreshFuture: Future<Output = Result<(), crate::InternalError>>,
368{
369 match verify() {
370 Ok(()) => Ok(()),
371 Err(err @ DelegatedTokenOpsError::AttestationUnknownKeyId { .. }) => {
372 refresh()
373 .await
374 .map_err(|source| RoleAttestationVerifyFlowError::Refresh {
375 trigger: err,
376 source,
377 })?;
378 verify().map_err(RoleAttestationVerifyFlowError::PostRefresh)
379 }
380 Err(err) => Err(RoleAttestationVerifyFlowError::Initial(err)),
381 }
382}
383
384fn resolve_min_accepted_epoch(explicit: u64, configured: Option<u64>) -> u64 {
385 if explicit > 0 {
386 explicit
387 } else {
388 configured.unwrap_or(0)
389 }
390}
391
392fn record_attestation_verifier_rejection(err: &DelegatedTokenOpsError) {
393 record_attestation_verify_failed();
394 match err {
395 DelegatedTokenOpsError::AttestationUnknownKeyId { .. } => {
396 record_attestation_unknown_key_id();
397 }
398 DelegatedTokenOpsError::AttestationEpochRejected { .. } => {
399 record_attestation_epoch_rejected();
400 }
401 _ => {}
402 }
403}
404
405fn log_attestation_verifier_rejection(
406 err: &DelegatedTokenOpsError,
407 attestation: &SignedRoleAttestation,
408 caller: Principal,
409 self_pid: Principal,
410 phase: &str,
411) {
412 log!(
413 Topic::Auth,
414 Warn,
415 "role attestation rejected phase={} local={} caller={} subject={} role={} key_id={} audience={:?} subnet={:?} issued_at={} expires_at={} epoch={} error={}",
416 phase,
417 self_pid,
418 caller,
419 attestation.payload.subject,
420 attestation.payload.role,
421 attestation.key_id,
422 attestation.payload.audience,
423 attestation.payload.subnet_id,
424 attestation.payload.issued_at,
425 attestation.payload.expires_at,
426 attestation.payload.epoch,
427 err
428 );
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use crate::InternalErrorOrigin;
435 use futures::executor::block_on;
436 use std::cell::Cell;
437
438 #[test]
439 fn verify_role_attestation_with_single_refresh_accepts_without_refresh() {
440 let verify_calls = Cell::new(0usize);
441 let refresh_calls = Cell::new(0usize);
442
443 let result = block_on(verify_role_attestation_with_single_refresh(
444 || {
445 verify_calls.set(verify_calls.get() + 1);
446 Ok(())
447 },
448 || {
449 refresh_calls.set(refresh_calls.get() + 1);
450 std::future::ready(Ok(()))
451 },
452 ));
453
454 assert!(result.is_ok());
455 assert_eq!(verify_calls.get(), 1, "verify must run exactly once");
456 assert_eq!(refresh_calls.get(), 0, "refresh must not run");
457 }
458
459 #[test]
460 fn verify_role_attestation_with_single_refresh_retries_once_on_unknown_key() {
461 let verify_calls = Cell::new(0usize);
462 let refresh_calls = Cell::new(0usize);
463
464 let result = block_on(verify_role_attestation_with_single_refresh(
465 || {
466 let attempt = verify_calls.get();
467 verify_calls.set(attempt + 1);
468 if attempt == 0 {
469 Err(DelegatedTokenOpsError::AttestationUnknownKeyId { key_id: 7 })
470 } else {
471 Ok(())
472 }
473 },
474 || {
475 refresh_calls.set(refresh_calls.get() + 1);
476 std::future::ready(Ok(()))
477 },
478 ));
479
480 assert!(result.is_ok());
481 assert_eq!(verify_calls.get(), 2, "verify must run exactly twice");
482 assert_eq!(refresh_calls.get(), 1, "refresh must run exactly once");
483 }
484
485 #[test]
486 fn verify_role_attestation_with_single_refresh_fails_closed_on_refresh_error() {
487 let verify_calls = Cell::new(0usize);
488 let refresh_calls = Cell::new(0usize);
489
490 let result = block_on(verify_role_attestation_with_single_refresh(
491 || {
492 verify_calls.set(verify_calls.get() + 1);
493 Err(DelegatedTokenOpsError::AttestationUnknownKeyId { key_id: 9 })
494 },
495 || {
496 refresh_calls.set(refresh_calls.get() + 1);
497 std::future::ready(Err(crate::InternalError::infra(
498 InternalErrorOrigin::Infra,
499 "refresh failed",
500 )))
501 },
502 ));
503
504 match result {
505 Err(RoleAttestationVerifyFlowError::Refresh {
506 trigger: DelegatedTokenOpsError::AttestationUnknownKeyId { key_id },
507 ..
508 }) => assert_eq!(key_id, 9),
509 other => panic!("expected refresh failure for unknown key, got: {other:?}"),
510 }
511
512 assert_eq!(
513 verify_calls.get(),
514 1,
515 "verify must not retry after refresh failure"
516 );
517 assert_eq!(refresh_calls.get(), 1, "refresh must run once");
518 }
519
520 #[test]
521 fn verify_role_attestation_with_single_refresh_does_not_refresh_on_non_unknown_error() {
522 let verify_calls = Cell::new(0usize);
523 let refresh_calls = Cell::new(0usize);
524
525 let result = block_on(verify_role_attestation_with_single_refresh(
526 || {
527 verify_calls.set(verify_calls.get() + 1);
528 Err(DelegatedTokenOpsError::AttestationEpochRejected {
529 epoch: 1,
530 min_accepted_epoch: 2,
531 })
532 },
533 || {
534 refresh_calls.set(refresh_calls.get() + 1);
535 std::future::ready(Ok(()))
536 },
537 ));
538
539 match result {
540 Err(RoleAttestationVerifyFlowError::Initial(
541 DelegatedTokenOpsError::AttestationEpochRejected {
542 epoch,
543 min_accepted_epoch,
544 },
545 )) => {
546 assert_eq!(epoch, 1);
547 assert_eq!(min_accepted_epoch, 2);
548 }
549 other => panic!("expected initial epoch rejection, got: {other:?}"),
550 }
551
552 assert_eq!(verify_calls.get(), 1, "verify must run once");
553 assert_eq!(refresh_calls.get(), 0, "refresh must not run");
554 }
555
556 #[test]
557 fn verify_role_attestation_with_single_refresh_only_attempts_one_refresh() {
558 let verify_calls = Cell::new(0usize);
559 let refresh_calls = Cell::new(0usize);
560
561 let result = block_on(verify_role_attestation_with_single_refresh(
562 || {
563 let attempt = verify_calls.get();
564 verify_calls.set(attempt + 1);
565 if attempt == 0 {
566 Err(DelegatedTokenOpsError::AttestationUnknownKeyId { key_id: 5 })
567 } else {
568 Err(DelegatedTokenOpsError::AttestationUnknownKeyId { key_id: 6 })
569 }
570 },
571 || {
572 refresh_calls.set(refresh_calls.get() + 1);
573 std::future::ready(Ok(()))
574 },
575 ));
576
577 match result {
578 Err(RoleAttestationVerifyFlowError::PostRefresh(
579 DelegatedTokenOpsError::AttestationUnknownKeyId { key_id },
580 )) => assert_eq!(key_id, 6),
581 other => panic!("expected post-refresh unknown-key rejection, got: {other:?}"),
582 }
583
584 assert_eq!(verify_calls.get(), 2, "verify must run exactly twice");
585 assert_eq!(refresh_calls.get(), 1, "refresh must run exactly once");
586 }
587
588 #[test]
589 fn resolve_min_accepted_epoch_prefers_explicit_argument() {
590 assert_eq!(resolve_min_accepted_epoch(7, Some(3)), 7);
591 assert_eq!(resolve_min_accepted_epoch(5, None), 5);
592 }
593
594 #[test]
595 fn resolve_min_accepted_epoch_falls_back_to_config_or_zero() {
596 assert_eq!(resolve_min_accepted_epoch(0, Some(4)), 4);
597 assert_eq!(resolve_min_accepted_epoch(0, None), 0);
598 }
599}