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