1use crate::{
2 cdk::types::Principal,
3 dto::{
4 auth::{
5 AuthRequestMetadata, DelegatedRoleGrant, DelegatedToken, DelegatedTokenGetRequest,
6 DelegatedTokenPrepareRequest, DelegatedTokenPrepareResponse, DelegationAudience,
7 DelegationProof, DelegationProofGetRequest, DelegationProofIssueRequest,
8 DelegationProofPrepareResponse, InstallActiveDelegationProofRequest,
9 InstallActiveDelegationProofResponse, RoleAttestationGetRequest,
10 RoleAttestationPrepareResponse, RoleAttestationRequest, SignedRoleAttestation,
11 },
12 error::Error,
13 },
14 error::InternalErrorClass,
15 ops::{
16 auth::{
17 AuthOps, SignDelegatedTokenInput, SignDelegationProofInput, SignRoleAttestationInput,
18 VerifyDelegatedTokenRuntimeInput,
19 },
20 config::ConfigOps,
21 ic::IcOps,
22 replay::{
23 guard::secs_to_ns,
24 model::{CommandKind, OperationId, RecoveryReason, ReplayActor, ReplayPayloadHasher},
25 receipt::{
26 ReplayReceiptDecision, ReplayReceiptReserveInput, ReplayReceiptStoreError,
27 ReplayReceiptToken, abort_reserved_receipt, commit_receipt_response,
28 mark_recovery_required, reserve_or_replay_receipt,
29 },
30 },
31 runtime::env::EnvOps,
32 storage::registry::subnet::SubnetRegistryOps,
33 },
34};
35use candid::{decode_one, encode_one};
36use root_client::RootAuthMaterialClient;
37
38mod metadata;
42mod root_client;
43mod session;
44
45pub struct AuthApi;
52
53impl AuthApi {
54 const DELEGATED_TOKENS_DISABLED: &str =
55 "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml";
56 const MAX_DELEGATED_SESSION_TTL_SECS: u64 = 24 * 60 * 60;
57 const DELEGATION_REPLAY_COMMAND_KIND: &str = "auth.prepare_delegation_proof.v1";
58 const DELEGATION_REPLAY_RESPONSE_SCHEMA_VERSION: u32 = 1;
59 const MAX_DELEGATION_REPLAY_TTL_NS: u64 = 300_000_000_000;
60 const ROLE_ATTESTATION_REPLAY_COMMAND_KIND: &str = "auth.prepare_role_attestation.v1";
61 const ROLE_ATTESTATION_REPLAY_RESPONSE_SCHEMA_VERSION: u32 = 1;
62 const MAX_ROLE_ATTESTATION_REPLAY_TTL_NS: u64 = 300_000_000_000;
63 const TOKEN_PREPARE_REPLAY_COMMAND_KIND: &str = "auth.prepare_delegated_token.v1";
64 const TOKEN_PREPARE_REPLAY_RESPONSE_SCHEMA_VERSION: u32 = 1;
65 const MAX_TOKEN_REPLAY_TTL_NS: u64 = 300_000_000_000;
66 const SESSION_BOOTSTRAP_TOKEN_FINGERPRINT_DOMAIN: &[u8] =
67 b"canic-session-bootstrap-token-fingerprint";
68
69 fn map_auth_error(err: crate::InternalError) -> Error {
71 match err.class() {
72 InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
73 Error::internal(err.to_string())
74 }
75 _ => Error::from(err),
76 }
77 }
78
79 fn verify_token_material(
84 token: &DelegatedToken,
85 max_cert_ttl_ns: u64,
86 max_token_ttl_ns: u64,
87 required_scopes: &[String],
88 now_ns: u64,
89 ) -> Result<Principal, Error> {
90 AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
91 token,
92 caller: IcOps::msg_caller(),
93 max_cert_ttl_ns,
94 max_token_ttl_ns,
95 required_scopes,
96 now_ns,
97 })
98 .map(|verified| verified.subject)
99 .map_err(Self::map_auth_error)
100 }
101
102 pub fn prepare_delegated_token(
104 request: DelegatedTokenPrepareRequest,
105 ) -> Result<DelegatedTokenPrepareResponse, Error> {
106 let label = "delegated token prepare";
107 let metadata = Self::token_replay_metadata(request.metadata, label)?;
108 let caller = IcOps::msg_caller();
109 let command_kind = Self::token_prepare_replay_command_kind();
110 let actor = ReplayActor::direct_caller(caller);
111 let payload_hash = Self::token_prepare_replay_payload_hash(&command_kind, &actor, &request);
112 let now_ns = IcOps::now_nanos();
113 let expires_at_ns = now_ns.checked_add(metadata.ttl_ns).ok_or_else(|| {
114 Error::invalid("delegated token prepare replay metadata ttl_ns overflows nanoseconds")
115 })?;
116 let replay_input = ReplayReceiptReserveInput::new(
117 command_kind,
118 OperationId::from_bytes(metadata.request_id),
119 actor,
120 payload_hash,
121 now_ns,
122 )
123 .with_expires_at_ns(expires_at_ns);
124
125 let token = match reserve_or_replay_receipt(replay_input)
126 .map_err(Self::map_token_prepare_replay_store_error)?
127 {
128 ReplayReceiptDecision::Fresh(token) => token,
129 decision => return Self::map_token_prepare_replay_decision(decision),
130 };
131
132 let prepared = AuthOps::prepare_delegated_token_issuer_proof(
133 SignDelegatedTokenInput {
134 subject: request.subject,
135 audience: request.aud,
136 grants: request.grants,
137 ttl_ns: request.ttl_ns,
138 nonce: request.nonce,
139 ext: request.ext,
140 },
141 metadata.request_id,
142 caller,
143 )
144 .map_err(|err| {
145 abort_reserved_receipt(&token);
146 Self::map_auth_error(err)
147 })?;
148
149 let response = DelegatedTokenPrepareResponse {
150 claims: prepared.prepared.claims,
151 claims_hash: prepared.claims_hash,
152 retrieval_expires_at_ns: prepared.retrieval_expires_at_ns,
153 };
154
155 let response_bytes = match Self::encode_token_prepare_response(&response) {
156 Ok(response_bytes) => response_bytes,
157 Err(err) => {
158 mark_recovery_required(
159 &token,
160 RecoveryReason::ResponseCommitFailed,
161 secs_to_ns(IcOps::now_secs()),
162 );
163 return Err(err);
164 }
165 };
166
167 commit_receipt_response(
168 &token,
169 Self::TOKEN_PREPARE_REPLAY_RESPONSE_SCHEMA_VERSION,
170 response_bytes,
171 secs_to_ns(IcOps::now_secs()),
172 );
173 Ok(response)
174 }
175
176 pub fn get_delegated_token(request: DelegatedTokenGetRequest) -> Result<DelegatedToken, Error> {
178 AuthOps::get_delegated_token_issuer_proof(request.claims_hash, IcOps::msg_caller())
179 .map_err(Self::map_auth_error)
180 }
181
182 pub fn install_active_delegation_proof(
184 request: InstallActiveDelegationProofRequest,
185 ) -> Result<InstallActiveDelegationProofResponse, Error> {
186 let active_proof =
187 AuthOps::install_active_delegation_proof(request.proof, IcOps::msg_caller())
188 .map_err(Self::map_auth_error)?;
189
190 Ok(InstallActiveDelegationProofResponse { active_proof })
191 }
192
193 pub async fn prepare_delegation_proof(
195 request: DelegationProofIssueRequest,
196 ) -> Result<DelegationProofPrepareResponse, Error> {
197 let request = metadata::with_delegation_request_metadata(request);
198 Self::prepare_delegation_proof_remote(request).await
199 }
200
201 pub fn prepare_delegation_proof_root(
203 request: DelegationProofIssueRequest,
204 ) -> Result<DelegationProofPrepareResponse, Error> {
205 EnvOps::require_root().map_err(Error::from)?;
206 let caller = IcOps::msg_caller();
207 Self::validate_delegation_request_caller(caller, request.issuer_pid)?;
208 let max_cert_ttl_ns = Self::delegated_token_max_ttl_ns()?;
209 let metadata = Self::delegation_replay_metadata(request.metadata)?;
210 let command_kind = Self::delegation_replay_command_kind();
211 let actor = ReplayActor::direct_caller(caller);
212 let payload_hash = Self::delegation_replay_payload_hash(&command_kind, &actor, &request);
213 let now_ns = IcOps::now_nanos();
214 let expires_at_ns = now_ns.checked_add(metadata.ttl_ns).ok_or_else(|| {
215 Error::invalid("delegation proof replay metadata ttl_ns overflows nanoseconds")
216 })?;
217 let replay_input = ReplayReceiptReserveInput::new(
218 command_kind,
219 OperationId::from_bytes(metadata.request_id),
220 actor,
221 payload_hash,
222 now_ns,
223 )
224 .with_expires_at_ns(expires_at_ns);
225
226 let token = match reserve_or_replay_receipt(replay_input)
227 .map_err(Self::map_delegation_replay_store_error)?
228 {
229 ReplayReceiptDecision::Fresh(token) => token,
230 decision => return Self::map_delegation_replay_decision(decision),
231 };
232
233 Self::prepare_fresh_delegation_proof(token, caller, request, max_cert_ttl_ns)
234 }
235
236 pub fn get_delegation_proof_root(
238 request: DelegationProofGetRequest,
239 ) -> Result<DelegationProof, Error> {
240 EnvOps::require_root().map_err(Error::from)?;
241 let caller = IcOps::msg_caller();
242 AuthOps::get_delegation_proof(caller, request.cert_hash).map_err(Self::map_auth_error)
243 }
244
245 pub fn prepare_role_attestation_root(
247 request: RoleAttestationRequest,
248 ) -> Result<RoleAttestationPrepareResponse, Error> {
249 EnvOps::require_root().map_err(Error::from)?;
250 let caller = IcOps::msg_caller();
251 Self::validate_role_attestation_request(caller, &request)?;
252 let metadata = Self::role_attestation_replay_metadata(request.metadata)?;
253 let command_kind = Self::role_attestation_replay_command_kind();
254 let actor = ReplayActor::direct_caller(caller);
255 let payload_hash =
256 Self::role_attestation_replay_payload_hash(&command_kind, &actor, &request);
257 let now_ns = IcOps::now_nanos();
258 let expires_at_ns = now_ns.checked_add(metadata.ttl_ns).ok_or_else(|| {
259 Error::invalid("role attestation replay metadata ttl_ns overflows nanoseconds")
260 })?;
261 let replay_input = ReplayReceiptReserveInput::new(
262 command_kind,
263 OperationId::from_bytes(metadata.request_id),
264 actor,
265 payload_hash,
266 now_ns,
267 )
268 .with_expires_at_ns(expires_at_ns);
269
270 let token = match reserve_or_replay_receipt(replay_input)
271 .map_err(Self::map_role_attestation_replay_store_error)?
272 {
273 ReplayReceiptDecision::Fresh(token) => token,
274 decision => return Self::map_role_attestation_replay_decision(decision),
275 };
276
277 let prepared = match AuthOps::prepare_role_attestation(SignRoleAttestationInput {
278 operation_id: token.receipt().operation_id.into_bytes(),
279 subject: request.subject,
280 role: request.role,
281 subnet_id: request.subnet_id,
282 audience: request.audience,
283 ttl_ns: request.ttl_ns,
284 epoch: request.epoch,
285 issued_at_ns: now_ns,
286 }) {
287 Ok(prepared) => prepared,
288 Err(err) => {
289 abort_reserved_receipt(&token);
290 return Err(Self::map_auth_error(err));
291 }
292 };
293
294 let response = RoleAttestationPrepareResponse {
295 payload: prepared.payload,
296 payload_hash: prepared.payload_hash,
297 retrieval_expires_at_ns: prepared.retrieval_expires_at_ns,
298 };
299
300 let response_bytes = match Self::encode_role_attestation_prepare_response(&response) {
301 Ok(response_bytes) => response_bytes,
302 Err(err) => {
303 mark_recovery_required(
304 &token,
305 RecoveryReason::ResponseCommitFailed,
306 secs_to_ns(IcOps::now_secs()),
307 );
308 return Err(err);
309 }
310 };
311
312 commit_receipt_response(
313 &token,
314 Self::ROLE_ATTESTATION_REPLAY_RESPONSE_SCHEMA_VERSION,
315 response_bytes,
316 secs_to_ns(IcOps::now_secs()),
317 );
318 Ok(response)
319 }
320
321 pub fn get_role_attestation_root(
323 request: RoleAttestationGetRequest,
324 ) -> Result<SignedRoleAttestation, Error> {
325 EnvOps::require_root().map_err(Error::from)?;
326 AuthOps::get_role_attestation(IcOps::msg_caller(), request.payload_hash)
327 .map_err(Self::map_auth_error)
328 }
329
330 fn prepare_fresh_delegation_proof(
331 token: ReplayReceiptToken,
332 _caller: Principal,
333 request: DelegationProofIssueRequest,
334 max_cert_ttl_ns: u64,
335 ) -> Result<DelegationProofPrepareResponse, Error> {
336 let max_token_ttl_ns = request.cert_ttl_ns.min(max_cert_ttl_ns);
337 let prepared = match AuthOps::prepare_delegation_proof(SignDelegationProofInput {
338 operation_id: token.receipt().operation_id.into_bytes(),
339 audience: request.aud,
340 grants: request.grants,
341 issuer_pid: request.issuer_pid,
342 cert_ttl_ns: request.cert_ttl_ns,
343 max_token_ttl_ns,
344 max_cert_ttl_ns,
345 issued_at_ns: IcOps::now_nanos(),
346 }) {
347 Ok(prepared) => prepared,
348 Err(err) => {
349 abort_reserved_receipt(&token);
350 return Err(Self::map_auth_error(err));
351 }
352 };
353
354 let response = DelegationProofPrepareResponse {
355 cert: prepared.cert,
356 cert_hash: prepared.cert_hash,
357 retrieval_expires_at_ns: prepared.retrieval_expires_at_ns,
358 };
359
360 let response_bytes = match Self::encode_delegation_prepare_response(&response) {
361 Ok(response_bytes) => response_bytes,
362 Err(err) => {
363 mark_recovery_required(
364 &token,
365 RecoveryReason::ResponseCommitFailed,
366 secs_to_ns(IcOps::now_secs()),
367 );
368 return Err(err);
369 }
370 };
371
372 commit_receipt_response(
373 &token,
374 Self::DELEGATION_REPLAY_RESPONSE_SCHEMA_VERSION,
375 response_bytes,
376 secs_to_ns(IcOps::now_secs()),
377 );
378 Ok(response)
379 }
380
381 pub async fn verify_role_attestation(
383 attestation: &SignedRoleAttestation,
384 min_accepted_epoch: u64,
385 ) -> Result<(), Error> {
386 crate::workflow::runtime::auth::RuntimeAuthWorkflow::verify_role_attestation(
387 attestation,
388 min_accepted_epoch,
389 )
390 .await
391 .map_err(Self::map_auth_error)
392 }
393
394 fn delegated_token_max_ttl_ns() -> Result<u64, Error> {
396 let cfg = ConfigOps::delegated_tokens_config().map_err(Error::from)?;
397 if !cfg.enabled {
398 return Err(Error::forbidden(Self::DELEGATED_TOKENS_DISABLED));
399 }
400
401 let max_ttl_secs = cfg
402 .max_ttl_secs
403 .unwrap_or(Self::MAX_DELEGATED_SESSION_TTL_SECS);
404 max_ttl_secs.checked_mul(1_000_000_000).ok_or_else(|| {
405 Error::invalid("auth.delegated_tokens.max_ttl_secs overflows nanoseconds")
406 })
407 }
408
409 fn validate_delegation_request_caller(
410 caller: Principal,
411 issuer_pid: Principal,
412 ) -> Result<(), Error> {
413 if caller == issuer_pid {
414 return Ok(());
415 }
416
417 Err(Error::forbidden(format!(
418 "delegation request caller {caller} must match issuer_pid {issuer_pid}"
419 )))
420 }
421
422 fn validate_role_attestation_request(
423 caller: Principal,
424 request: &RoleAttestationRequest,
425 ) -> Result<(), Error> {
426 if request.subject != caller {
427 return Err(Error::forbidden(format!(
428 "role attestation subject {} must match caller {}",
429 request.subject, caller
430 )));
431 }
432
433 let registered = SubnetRegistryOps::get(request.subject).ok_or_else(|| {
434 Error::forbidden(format!(
435 "role attestation subject {} is not registered",
436 request.subject
437 ))
438 })?;
439 if registered.role != request.role {
440 return Err(Error::forbidden(format!(
441 "role attestation role mismatch for subject {}: requested {}, registered {}",
442 request.subject, request.role, registered.role
443 )));
444 }
445
446 if let Some(requested_subnet) = request.subnet_id {
447 let local_subnet = EnvOps::subnet_pid().map_err(Error::from)?;
448 if requested_subnet != local_subnet {
449 return Err(Error::forbidden(format!(
450 "role attestation subnet mismatch for subject {}: requested {}, local {}",
451 request.subject, requested_subnet, local_subnet
452 )));
453 }
454 }
455
456 let max_ttl_ns = Self::role_attestation_max_ttl_ns()?;
457 if request.ttl_ns == 0 || request.ttl_ns > max_ttl_ns {
458 return Err(Error::invalid(format!(
459 "role attestation ttl_ns must satisfy 0 < ttl_ns <= {max_ttl_ns} (got {})",
460 request.ttl_ns
461 )));
462 }
463
464 Ok(())
465 }
466
467 fn role_attestation_max_ttl_ns() -> Result<u64, Error> {
468 let cfg = ConfigOps::role_attestation_config().map_err(Error::from)?;
469 cfg.max_ttl_secs.checked_mul(1_000_000_000).ok_or_else(|| {
470 Error::invalid("auth.role_attestation.max_ttl_secs overflows nanoseconds")
471 })
472 }
473
474 fn delegation_replay_metadata(
475 metadata: Option<AuthRequestMetadata>,
476 ) -> Result<AuthRequestMetadata, Error> {
477 let metadata = metadata.ok_or_else(Error::operation_id_required)?;
478 if metadata.ttl_ns == 0 {
479 return Err(Error::invalid(
480 "delegation proof replay metadata ttl_ns must be greater than zero",
481 ));
482 }
483 if metadata.ttl_ns > Self::MAX_DELEGATION_REPLAY_TTL_NS {
484 return Err(Error::invalid(format!(
485 "delegation proof replay metadata ttl_ns={} exceeds max {}",
486 metadata.ttl_ns,
487 Self::MAX_DELEGATION_REPLAY_TTL_NS
488 )));
489 }
490 Ok(metadata)
491 }
492
493 fn role_attestation_replay_metadata(
494 metadata: Option<crate::dto::rpc::RootRequestMetadata>,
495 ) -> Result<crate::dto::rpc::RootRequestMetadata, Error> {
496 let metadata = metadata.ok_or_else(Error::operation_id_required)?;
497 if metadata.ttl_ns == 0 {
498 return Err(Error::invalid(
499 "role attestation replay metadata ttl_ns must be greater than zero",
500 ));
501 }
502 if metadata.ttl_ns > Self::MAX_ROLE_ATTESTATION_REPLAY_TTL_NS {
503 return Err(Error::invalid(format!(
504 "role attestation replay metadata ttl_ns={} exceeds max {}",
505 metadata.ttl_ns,
506 Self::MAX_ROLE_ATTESTATION_REPLAY_TTL_NS
507 )));
508 }
509 Ok(metadata)
510 }
511
512 fn token_replay_metadata(
513 metadata: Option<AuthRequestMetadata>,
514 label: &str,
515 ) -> Result<AuthRequestMetadata, Error> {
516 let metadata = metadata.ok_or_else(Error::operation_id_required)?;
517 if metadata.ttl_ns == 0 {
518 return Err(Error::invalid(format!(
519 "{label} replay metadata ttl_ns must be greater than zero"
520 )));
521 }
522 if metadata.ttl_ns > Self::MAX_TOKEN_REPLAY_TTL_NS {
523 return Err(Error::invalid(format!(
524 "{label} replay metadata ttl_ns={} exceeds max {}",
525 metadata.ttl_ns,
526 Self::MAX_TOKEN_REPLAY_TTL_NS
527 )));
528 }
529 Ok(metadata)
530 }
531
532 fn delegation_replay_command_kind() -> CommandKind {
533 CommandKind::new(Self::DELEGATION_REPLAY_COMMAND_KIND)
534 .expect("delegation replay command kind is a valid static label")
535 }
536
537 fn role_attestation_replay_command_kind() -> CommandKind {
538 CommandKind::new(Self::ROLE_ATTESTATION_REPLAY_COMMAND_KIND)
539 .expect("role attestation replay command kind is a valid static label")
540 }
541
542 fn token_prepare_replay_command_kind() -> CommandKind {
543 CommandKind::new(Self::TOKEN_PREPARE_REPLAY_COMMAND_KIND)
544 .expect("delegated-token prepare replay command kind is a valid static label")
545 }
546
547 fn delegation_replay_payload_hash(
548 command_kind: &CommandKind,
549 actor: &ReplayActor,
550 request: &DelegationProofIssueRequest,
551 ) -> [u8; 32] {
552 let mut hasher = ReplayPayloadHasher::new(command_kind, actor);
553 hasher.hash_principal(&request.issuer_pid);
554 Self::hash_delegation_audience(&mut hasher, &request.aud);
555 Self::hash_delegated_role_grants(&mut hasher, &request.grants);
556 hasher.hash_u64(request.cert_ttl_ns);
557 hasher.finish()
558 }
559
560 fn role_attestation_replay_payload_hash(
561 command_kind: &CommandKind,
562 actor: &ReplayActor,
563 request: &RoleAttestationRequest,
564 ) -> [u8; 32] {
565 let mut hasher = ReplayPayloadHasher::new(command_kind, actor);
566 hasher.hash_principal(&request.subject);
567 hasher.hash_role(&request.role);
568 hasher.hash_bool(request.subnet_id.is_some());
569 if let Some(subnet_id) = request.subnet_id {
570 hasher.hash_principal(&subnet_id);
571 }
572 hasher.hash_principal(&request.audience);
573 hasher.hash_u64(request.ttl_ns);
574 hasher.hash_u64(request.epoch);
575 hasher.finish()
576 }
577
578 fn token_prepare_replay_payload_hash(
579 command_kind: &CommandKind,
580 actor: &ReplayActor,
581 request: &DelegatedTokenPrepareRequest,
582 ) -> [u8; 32] {
583 let mut hasher = ReplayPayloadHasher::new(command_kind, actor);
584 hasher.hash_principal(&request.subject);
585 Self::hash_delegation_audience(&mut hasher, &request.aud);
586 Self::hash_delegated_role_grants(&mut hasher, &request.grants);
587 hasher.hash_u64(request.ttl_ns);
588 hasher.hash_bytes(&request.nonce);
589 Self::hash_optional_bytes(&mut hasher, request.ext.as_deref());
590 hasher.finish()
591 }
592
593 fn hash_delegation_audience(hasher: &mut ReplayPayloadHasher, aud: &DelegationAudience) {
594 match aud {
595 DelegationAudience::Canister(canister) => {
596 hasher.hash_str("canister");
597 hasher.hash_principal(canister);
598 }
599 DelegationAudience::CanicSubnet(subnet) => {
600 hasher.hash_str("canic_subnet");
601 hasher.hash_principal(subnet);
602 }
603 DelegationAudience::Project(project) => {
604 hasher.hash_str("project");
605 hasher.hash_str(project);
606 }
607 }
608 }
609
610 fn hash_optional_bytes(hasher: &mut ReplayPayloadHasher, bytes: Option<&[u8]>) {
611 hasher.hash_bool(bytes.is_some());
612 if let Some(bytes) = bytes {
613 hasher.hash_bytes(bytes);
614 }
615 }
616
617 fn hash_delegated_role_grants(hasher: &mut ReplayPayloadHasher, grants: &[DelegatedRoleGrant]) {
618 hasher.hash_u64(grants.len() as u64);
619 for grant in grants {
620 hasher.hash_role(&grant.target);
621 Self::hash_string_vec(hasher, &grant.scopes);
622 }
623 }
624
625 fn hash_string_vec(hasher: &mut ReplayPayloadHasher, values: &[String]) {
626 hasher.hash_u64(values.len() as u64);
627 for value in values {
628 hasher.hash_str(value);
629 }
630 }
631
632 fn map_token_prepare_replay_decision(
633 decision: ReplayReceiptDecision,
634 ) -> Result<DelegatedTokenPrepareResponse, Error> {
635 match decision {
636 ReplayReceiptDecision::Fresh(_) => Err(Error::invariant(
637 "fresh delegated token replay decision escaped",
638 )),
639 ReplayReceiptDecision::ReturnCommitted(receipt) => {
640 Self::decode_token_prepare_response(&receipt)
641 }
642 ReplayReceiptDecision::OperationInProgress => Err(Error::conflict(
643 "delegated token prepare request is already in progress; retry later with the same request id",
644 )),
645 ReplayReceiptDecision::ActorMismatch => Err(Error::conflict(
646 "delegated token prepare request id was reused by a different caller",
647 )),
648 ReplayReceiptDecision::PayloadMismatch => Err(Error::conflict(
649 "delegated token prepare request id was reused with a different payload",
650 )),
651 ReplayReceiptDecision::Expired => Err(Error::conflict(
652 "delegated token prepare replay receipt expired; retry with a new request id",
653 )),
654 ReplayReceiptDecision::RecoveryRequired(reason) => Err(Error::conflict(format!(
655 "delegated token prepare request requires recovery before replay: {reason:?}"
656 ))),
657 ReplayReceiptDecision::TerminalFailed {
658 error_code,
659 error_bytes,
660 error_bytes_truncated,
661 } => Err(Error::conflict(format!(
662 "delegated token prepare request previously failed: {error_code:?}; error_bytes_len={}; truncated={error_bytes_truncated}",
663 error_bytes.len()
664 ))),
665 ReplayReceiptDecision::PendingActorQuotaExceeded { max_pending, .. } => {
666 Err(Error::exhausted(format!(
667 "delegated token prepare pending replay receipt quota exceeded for caller; max_pending={max_pending}"
668 )))
669 }
670 ReplayReceiptDecision::PendingCommandQuotaExceeded { max_pending, .. } => {
671 Err(Error::exhausted(format!(
672 "delegated token prepare pending replay receipt quota exceeded for command kind; max_pending={max_pending}"
673 )))
674 }
675 }
676 }
677
678 fn map_token_prepare_replay_store_error(err: ReplayReceiptStoreError) -> Error {
679 match err {
680 ReplayReceiptStoreError::ReceiptDecodeFailed(message) => Error::internal(format!(
681 "failed to decode delegated token prepare replay receipt: {message}"
682 )),
683 }
684 }
685
686 fn encode_token_prepare_response(
687 response: &DelegatedTokenPrepareResponse,
688 ) -> Result<Vec<u8>, Error> {
689 encode_one(response).map_err(|err| {
690 Error::internal(format!(
691 "failed to encode delegated token prepare replay response: {err}"
692 ))
693 })
694 }
695
696 fn decode_token_prepare_response(
697 receipt: &crate::ops::replay::model::ReplayReceipt,
698 ) -> Result<DelegatedTokenPrepareResponse, Error> {
699 let response_schema_version = receipt.response_schema_version.ok_or_else(|| {
700 Error::internal(
701 "delegated token prepare replay receipt is missing response schema version",
702 )
703 })?;
704 if response_schema_version != Self::TOKEN_PREPARE_REPLAY_RESPONSE_SCHEMA_VERSION {
705 return Err(Error::internal(format!(
706 "unsupported delegated token prepare replay response schema version {response_schema_version}"
707 )));
708 }
709 let response_bytes = receipt.response_bytes.as_deref().ok_or_else(|| {
710 Error::internal("delegated token prepare replay receipt is missing response bytes")
711 })?;
712 decode_one(response_bytes).map_err(|err| {
713 Error::internal(format!(
714 "failed to decode delegated token prepare replay response: {err}"
715 ))
716 })
717 }
718
719 fn map_role_attestation_replay_decision(
720 decision: ReplayReceiptDecision,
721 ) -> Result<RoleAttestationPrepareResponse, Error> {
722 match decision {
723 ReplayReceiptDecision::Fresh(_) => Err(Error::invariant(
724 "fresh role attestation replay decision escaped",
725 )),
726 ReplayReceiptDecision::ReturnCommitted(receipt) => {
727 Self::decode_role_attestation_prepare_response(&receipt)
728 }
729 ReplayReceiptDecision::OperationInProgress => Err(Error::conflict(
730 "role attestation prepare request is already in progress; retry later with the same request id",
731 )),
732 ReplayReceiptDecision::ActorMismatch => Err(Error::conflict(
733 "role attestation prepare request id was reused by a different caller",
734 )),
735 ReplayReceiptDecision::PayloadMismatch => Err(Error::conflict(
736 "role attestation prepare request id was reused with a different payload",
737 )),
738 ReplayReceiptDecision::Expired => Err(Error::conflict(
739 "role attestation prepare replay receipt expired; retry with a new request id",
740 )),
741 ReplayReceiptDecision::RecoveryRequired(reason) => Err(Error::conflict(format!(
742 "role attestation prepare request requires recovery before replay: {reason:?}"
743 ))),
744 ReplayReceiptDecision::TerminalFailed {
745 error_code,
746 error_bytes,
747 error_bytes_truncated,
748 } => Err(Error::conflict(format!(
749 "role attestation prepare request previously failed: {error_code:?}; error_bytes_len={}; truncated={error_bytes_truncated}",
750 error_bytes.len()
751 ))),
752 ReplayReceiptDecision::PendingActorQuotaExceeded { max_pending, .. } => {
753 Err(Error::exhausted(format!(
754 "role attestation prepare pending replay receipt quota exceeded for caller; max_pending={max_pending}"
755 )))
756 }
757 ReplayReceiptDecision::PendingCommandQuotaExceeded { max_pending, .. } => {
758 Err(Error::exhausted(format!(
759 "role attestation prepare pending replay receipt quota exceeded for command kind; max_pending={max_pending}"
760 )))
761 }
762 }
763 }
764
765 fn map_role_attestation_replay_store_error(err: ReplayReceiptStoreError) -> Error {
766 match err {
767 ReplayReceiptStoreError::ReceiptDecodeFailed(message) => Error::internal(format!(
768 "failed to decode role attestation replay receipt: {message}"
769 )),
770 }
771 }
772
773 fn encode_role_attestation_prepare_response(
774 response: &RoleAttestationPrepareResponse,
775 ) -> Result<Vec<u8>, Error> {
776 encode_one(response).map_err(|err| {
777 Error::internal(format!(
778 "failed to encode role attestation prepare replay response: {err}"
779 ))
780 })
781 }
782
783 fn decode_role_attestation_prepare_response(
784 receipt: &crate::ops::replay::model::ReplayReceipt,
785 ) -> Result<RoleAttestationPrepareResponse, Error> {
786 let response_schema_version = receipt.response_schema_version.ok_or_else(|| {
787 Error::internal(
788 "role attestation prepare replay receipt is missing response schema version",
789 )
790 })?;
791 if response_schema_version != Self::ROLE_ATTESTATION_REPLAY_RESPONSE_SCHEMA_VERSION {
792 return Err(Error::internal(format!(
793 "unsupported role attestation prepare replay response schema version {response_schema_version}"
794 )));
795 }
796 let response_bytes = receipt.response_bytes.as_deref().ok_or_else(|| {
797 Error::internal("role attestation prepare replay receipt is missing response bytes")
798 })?;
799 decode_one(response_bytes).map_err(|err| {
800 Error::internal(format!(
801 "failed to decode role attestation prepare replay response: {err}"
802 ))
803 })
804 }
805
806 fn map_delegation_replay_decision(
807 decision: ReplayReceiptDecision,
808 ) -> Result<DelegationProofPrepareResponse, Error> {
809 match decision {
810 ReplayReceiptDecision::Fresh(_) => {
811 Err(Error::invariant("fresh delegation replay decision escaped"))
812 }
813 ReplayReceiptDecision::ReturnCommitted(receipt) => {
814 Self::decode_delegation_prepare_response(&receipt)
815 }
816 ReplayReceiptDecision::OperationInProgress => Err(Error::conflict(
817 "delegation proof request is already in progress; retry later with the same request id",
818 )),
819 ReplayReceiptDecision::ActorMismatch => Err(Error::conflict(
820 "delegation proof request id was reused by a different caller",
821 )),
822 ReplayReceiptDecision::PayloadMismatch => Err(Error::conflict(
823 "delegation proof request id was reused with a different payload",
824 )),
825 ReplayReceiptDecision::Expired => Err(Error::conflict(
826 "delegation proof replay receipt expired; retry with a new request id",
827 )),
828 ReplayReceiptDecision::RecoveryRequired(reason) => Err(Error::conflict(format!(
829 "delegation proof request requires recovery before replay: {reason:?}"
830 ))),
831 ReplayReceiptDecision::TerminalFailed {
832 error_code,
833 error_bytes,
834 error_bytes_truncated,
835 } => Err(Error::conflict(format!(
836 "delegation proof request previously failed: {error_code:?}; error_bytes_len={}; truncated={error_bytes_truncated}",
837 error_bytes.len()
838 ))),
839 ReplayReceiptDecision::PendingActorQuotaExceeded { max_pending, .. } => {
840 Err(Error::exhausted(format!(
841 "delegation proof pending replay receipt quota exceeded for caller; max_pending={max_pending}"
842 )))
843 }
844 ReplayReceiptDecision::PendingCommandQuotaExceeded { max_pending, .. } => {
845 Err(Error::exhausted(format!(
846 "delegation proof pending replay receipt quota exceeded for command kind; max_pending={max_pending}"
847 )))
848 }
849 }
850 }
851
852 fn map_delegation_replay_store_error(err: ReplayReceiptStoreError) -> Error {
853 match err {
854 ReplayReceiptStoreError::ReceiptDecodeFailed(message) => Error::internal(format!(
855 "failed to decode delegation replay receipt: {message}"
856 )),
857 }
858 }
859
860 fn encode_delegation_prepare_response(
861 response: &DelegationProofPrepareResponse,
862 ) -> Result<Vec<u8>, Error> {
863 encode_one(response).map_err(|err| {
864 Error::internal(format!(
865 "failed to encode delegation proof prepare replay response: {err}"
866 ))
867 })
868 }
869
870 fn decode_delegation_prepare_response(
871 receipt: &crate::ops::replay::model::ReplayReceipt,
872 ) -> Result<DelegationProofPrepareResponse, Error> {
873 let response_schema_version = receipt.response_schema_version.ok_or_else(|| {
874 Error::internal("delegation replay receipt is missing response schema version")
875 })?;
876 if response_schema_version != Self::DELEGATION_REPLAY_RESPONSE_SCHEMA_VERSION {
877 return Err(Error::internal(format!(
878 "unsupported delegation replay response schema version {response_schema_version}"
879 )));
880 }
881 let response_bytes = receipt.response_bytes.as_deref().ok_or_else(|| {
882 Error::internal("delegation replay receipt is missing response bytes")
883 })?;
884 decode_one(response_bytes).map_err(|err| {
885 Error::internal(format!(
886 "failed to decode delegation proof prepare replay response: {err}"
887 ))
888 })
889 }
890}
891
892impl AuthApi {
893 async fn prepare_delegation_proof_remote(
895 request: DelegationProofIssueRequest,
896 ) -> Result<DelegationProofPrepareResponse, Error> {
897 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
898 RootAuthMaterialClient::new(root_pid)
899 .prepare_delegation_proof(request)
900 .await
901 .map_err(Self::map_auth_error)
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::AuthApi;
908 use crate::{
909 cdk::types::Principal,
910 dto::{
911 auth::{
912 AuthRequestMetadata, DelegatedRoleGrant, DelegatedTokenPrepareRequest,
913 DelegationAudience, DelegationProofIssueRequest,
914 },
915 error::ErrorCode,
916 },
917 };
918
919 fn p(id: u8) -> Principal {
920 Principal::from_slice(&[id; 29])
921 }
922
923 fn delegation_request(metadata_id: u8) -> DelegationProofIssueRequest {
924 DelegationProofIssueRequest {
925 metadata: Some(meta(metadata_id, 60_000_000_000)),
926 issuer_pid: p(2),
927 aud: DelegationAudience::Project("test".to_string()),
928 grants: vec![grant("project_instance", &["canic.verify"])],
929 cert_ttl_ns: 60_000_000_000,
930 }
931 }
932
933 fn grant(role: &str, scopes: &[&str]) -> DelegatedRoleGrant {
934 DelegatedRoleGrant {
935 target: crate::ids::CanisterRole::owned(role.to_string()),
936 scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
937 }
938 }
939
940 fn meta(id: u8, ttl_ns: u64) -> AuthRequestMetadata {
941 AuthRequestMetadata {
942 request_id: [id; 32],
943 ttl_ns,
944 }
945 }
946
947 fn token_prepare_request(metadata_id: u8) -> DelegatedTokenPrepareRequest {
948 DelegatedTokenPrepareRequest {
949 metadata: Some(meta(metadata_id, 60_000_000_000)),
950 subject: p(8),
951 aud: DelegationAudience::Project("test".to_string()),
952 grants: vec![grant("project_instance", &["canic.verify"])],
953 ttl_ns: 30_000_000_000,
954 nonce: [9; 16],
955 ext: None,
956 }
957 }
958
959 #[test]
960 fn delegation_request_caller_must_match_requested_issuer() {
961 AuthApi::validate_delegation_request_caller(p(2), p(2)).expect("matching shard");
962
963 let err = AuthApi::validate_delegation_request_caller(p(1), p(2))
964 .expect_err("mismatched caller must fail");
965
966 assert_eq!(err.code, ErrorCode::Forbidden);
967 }
968
969 #[test]
970 fn delegation_replay_metadata_rejects_missing_or_invalid_ttl() {
971 let missing = AuthApi::delegation_replay_metadata(None).expect_err("metadata is required");
972 assert_eq!(missing.code, ErrorCode::OperationIdRequired);
973
974 let zero = AuthApi::delegation_replay_metadata(Some(AuthRequestMetadata {
975 request_id: [1; 32],
976 ttl_ns: 0,
977 }))
978 .expect_err("zero ttl is invalid");
979 assert_eq!(zero.code, ErrorCode::InvalidInput);
980
981 let too_large = AuthApi::delegation_replay_metadata(Some(AuthRequestMetadata {
982 request_id: [1; 32],
983 ttl_ns: AuthApi::MAX_DELEGATION_REPLAY_TTL_NS + 1,
984 }))
985 .expect_err("oversized ttl is invalid");
986 assert_eq!(too_large.code, ErrorCode::InvalidInput);
987 }
988
989 #[test]
990 fn delegation_replay_payload_hash_ignores_metadata() {
991 let command_kind = AuthApi::delegation_replay_command_kind();
992 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
993 let a = delegation_request(1);
994 let b = delegation_request(9);
995
996 assert_eq!(
997 AuthApi::delegation_replay_payload_hash(&command_kind, &actor, &a),
998 AuthApi::delegation_replay_payload_hash(&command_kind, &actor, &b)
999 );
1000 }
1001
1002 #[test]
1003 fn delegated_token_replay_metadata_rejects_missing_or_invalid_ttl() {
1004 let missing =
1005 AuthApi::token_replay_metadata(None, "delegated token mint").expect_err("required");
1006 assert_eq!(missing.code, ErrorCode::OperationIdRequired);
1007
1008 let zero = AuthApi::token_replay_metadata(Some(meta(1, 0)), "delegated token mint")
1009 .expect_err("zero ttl is invalid");
1010 assert_eq!(zero.code, ErrorCode::InvalidInput);
1011
1012 let too_large = AuthApi::token_replay_metadata(
1013 Some(meta(1, AuthApi::MAX_TOKEN_REPLAY_TTL_NS + 1)),
1014 "delegated token mint",
1015 )
1016 .expect_err("oversized ttl is invalid");
1017 assert_eq!(too_large.code, ErrorCode::InvalidInput);
1018 }
1019
1020 #[test]
1021 fn delegation_replay_payload_hash_binds_authoritative_payload() {
1022 let command_kind = AuthApi::delegation_replay_command_kind();
1023 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1024 let a = delegation_request(1);
1025 let mut b = a.clone();
1026 b.cert_ttl_ns += 1;
1027
1028 assert_ne!(
1029 AuthApi::delegation_replay_payload_hash(&command_kind, &actor, &a),
1030 AuthApi::delegation_replay_payload_hash(&command_kind, &actor, &b)
1031 );
1032 }
1033
1034 #[test]
1035 fn delegated_token_prepare_payload_hash_ignores_metadata() {
1036 let command_kind = AuthApi::token_prepare_replay_command_kind();
1037 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1038 let a = token_prepare_request(1);
1039 let b = token_prepare_request(9);
1040
1041 assert_eq!(
1042 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &a),
1043 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &b)
1044 );
1045 }
1046
1047 #[test]
1048 fn delegated_token_prepare_payload_hash_binds_authoritative_payload() {
1049 let command_kind = AuthApi::token_prepare_replay_command_kind();
1050 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1051 let a = token_prepare_request(1);
1052 let mut b = a.clone();
1053 b.nonce = [10; 16];
1054
1055 assert_ne!(
1056 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &a),
1057 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &b)
1058 );
1059 }
1060
1061 #[test]
1062 fn delegated_token_prepare_payload_hash_binds_ext() {
1063 let command_kind = AuthApi::token_prepare_replay_command_kind();
1064 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1065 let a = token_prepare_request(1);
1066 let mut b = a.clone();
1067 b.ext = Some(b"app-context".to_vec());
1068
1069 assert_ne!(
1070 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &a),
1071 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &b)
1072 );
1073 }
1074}