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