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 = SubnetRegistryOps::get(request.subject).ok_or_else(|| {
455 Error::forbidden(format!(
456 "role attestation subject {} is not registered",
457 request.subject
458 ))
459 })?;
460 if registered.role != request.role {
461 return Err(Error::forbidden(format!(
462 "role attestation role mismatch for subject {}: requested {}, registered {}",
463 request.subject, request.role, registered.role
464 )));
465 }
466
467 if let Some(requested_subnet) = request.subnet_id {
468 let local_subnet = EnvOps::subnet_pid().map_err(Error::from)?;
469 if requested_subnet != local_subnet {
470 return Err(Error::forbidden(format!(
471 "role attestation subnet mismatch for subject {}: requested {}, local {}",
472 request.subject, requested_subnet, local_subnet
473 )));
474 }
475 }
476
477 let max_ttl_ns = Self::role_attestation_max_ttl_ns()?;
478 if request.ttl_ns == 0 || request.ttl_ns > max_ttl_ns {
479 return Err(Error::invalid(format!(
480 "role attestation ttl_ns must satisfy 0 < ttl_ns <= {max_ttl_ns} (got {})",
481 request.ttl_ns
482 )));
483 }
484
485 Ok(())
486 }
487
488 fn role_attestation_max_ttl_ns() -> Result<u64, Error> {
489 let cfg = ConfigOps::role_attestation_config().map_err(Error::from)?;
490 cfg.max_ttl_secs.checked_mul(1_000_000_000).ok_or_else(|| {
491 Error::invalid("auth.role_attestation.max_ttl_secs overflows nanoseconds")
492 })
493 }
494
495 fn delegation_replay_metadata(
496 metadata: Option<AuthRequestMetadata>,
497 ) -> Result<AuthRequestMetadata, Error> {
498 let metadata = metadata.ok_or_else(Error::operation_id_required)?;
499 if metadata.ttl_ns == 0 {
500 return Err(Error::invalid(
501 "delegation proof replay metadata ttl_ns must be greater than zero",
502 ));
503 }
504 if metadata.ttl_ns > Self::MAX_DELEGATION_REPLAY_TTL_NS {
505 return Err(Error::invalid(format!(
506 "delegation proof replay metadata ttl_ns={} exceeds max {}",
507 metadata.ttl_ns,
508 Self::MAX_DELEGATION_REPLAY_TTL_NS
509 )));
510 }
511 Ok(metadata)
512 }
513
514 fn role_attestation_replay_metadata(
515 metadata: Option<crate::dto::rpc::RootRequestMetadata>,
516 ) -> Result<crate::dto::rpc::RootRequestMetadata, Error> {
517 let metadata = metadata.ok_or_else(Error::operation_id_required)?;
518 if metadata.ttl_ns == 0 {
519 return Err(Error::invalid(
520 "role attestation replay metadata ttl_ns must be greater than zero",
521 ));
522 }
523 if metadata.ttl_ns > Self::MAX_ROLE_ATTESTATION_REPLAY_TTL_NS {
524 return Err(Error::invalid(format!(
525 "role attestation replay metadata ttl_ns={} exceeds max {}",
526 metadata.ttl_ns,
527 Self::MAX_ROLE_ATTESTATION_REPLAY_TTL_NS
528 )));
529 }
530 Ok(metadata)
531 }
532
533 fn token_replay_metadata(
534 metadata: Option<AuthRequestMetadata>,
535 label: &str,
536 ) -> Result<AuthRequestMetadata, Error> {
537 let metadata = metadata.ok_or_else(Error::operation_id_required)?;
538 if metadata.ttl_ns == 0 {
539 return Err(Error::invalid(format!(
540 "{label} replay metadata ttl_ns must be greater than zero"
541 )));
542 }
543 if metadata.ttl_ns > Self::MAX_TOKEN_REPLAY_TTL_NS {
544 return Err(Error::invalid(format!(
545 "{label} replay metadata ttl_ns={} exceeds max {}",
546 metadata.ttl_ns,
547 Self::MAX_TOKEN_REPLAY_TTL_NS
548 )));
549 }
550 Ok(metadata)
551 }
552
553 fn delegation_replay_command_kind() -> CommandKind {
554 CommandKind::new(Self::DELEGATION_REPLAY_COMMAND_KIND)
555 .expect("delegation replay command kind is a valid static label")
556 }
557
558 fn role_attestation_replay_command_kind() -> CommandKind {
559 CommandKind::new(Self::ROLE_ATTESTATION_REPLAY_COMMAND_KIND)
560 .expect("role attestation replay command kind is a valid static label")
561 }
562
563 fn token_prepare_replay_command_kind() -> CommandKind {
564 CommandKind::new(Self::TOKEN_PREPARE_REPLAY_COMMAND_KIND)
565 .expect("delegated-token prepare replay command kind is a valid static label")
566 }
567
568 fn delegation_replay_payload_hash(
569 command_kind: &CommandKind,
570 actor: &ReplayActor,
571 request: &DelegationProofIssueRequest,
572 ) -> [u8; 32] {
573 let mut hasher = ReplayPayloadHasher::new(command_kind, actor);
574 hasher.hash_principal(&request.issuer_pid);
575 Self::hash_delegation_audience(&mut hasher, &request.aud);
576 Self::hash_delegated_role_grants(&mut hasher, &request.grants);
577 hasher.hash_u64(request.cert_ttl_ns);
578 hasher.finish()
579 }
580
581 fn role_attestation_replay_payload_hash(
582 command_kind: &CommandKind,
583 actor: &ReplayActor,
584 request: &RoleAttestationRequest,
585 ) -> [u8; 32] {
586 let mut hasher = ReplayPayloadHasher::new(command_kind, actor);
587 hasher.hash_principal(&request.subject);
588 hasher.hash_role(&request.role);
589 hasher.hash_bool(request.subnet_id.is_some());
590 if let Some(subnet_id) = request.subnet_id {
591 hasher.hash_principal(&subnet_id);
592 }
593 hasher.hash_principal(&request.audience);
594 hasher.hash_u64(request.ttl_ns);
595 hasher.hash_u64(request.epoch);
596 hasher.finish()
597 }
598
599 fn token_prepare_replay_payload_hash(
600 command_kind: &CommandKind,
601 actor: &ReplayActor,
602 request: &DelegatedTokenPrepareRequest,
603 ) -> [u8; 32] {
604 let mut hasher = ReplayPayloadHasher::new(command_kind, actor);
605 hasher.hash_principal(&request.subject);
606 Self::hash_delegation_audience(&mut hasher, &request.aud);
607 Self::hash_delegated_role_grants(&mut hasher, &request.grants);
608 hasher.hash_u64(request.ttl_ns);
609 Self::hash_optional_bytes(&mut hasher, request.ext.as_deref());
610 hasher.finish()
611 }
612
613 fn hash_delegation_audience(hasher: &mut ReplayPayloadHasher, aud: &DelegationAudience) {
614 match aud {
615 DelegationAudience::Canister(canister) => {
616 hasher.hash_str("canister");
617 hasher.hash_principal(canister);
618 }
619 DelegationAudience::CanicSubnet(subnet) => {
620 hasher.hash_str("canic_subnet");
621 hasher.hash_principal(subnet);
622 }
623 DelegationAudience::Project(project) => {
624 hasher.hash_str("project");
625 hasher.hash_str(project);
626 }
627 }
628 }
629
630 fn hash_optional_bytes(hasher: &mut ReplayPayloadHasher, bytes: Option<&[u8]>) {
631 hasher.hash_bool(bytes.is_some());
632 if let Some(bytes) = bytes {
633 hasher.hash_bytes(bytes);
634 }
635 }
636
637 fn hash_delegated_role_grants(hasher: &mut ReplayPayloadHasher, grants: &[DelegatedRoleGrant]) {
638 hasher.hash_u64(grants.len() as u64);
639 for grant in grants {
640 hasher.hash_role(&grant.target);
641 Self::hash_string_vec(hasher, &grant.scopes);
642 }
643 }
644
645 fn hash_string_vec(hasher: &mut ReplayPayloadHasher, values: &[String]) {
646 hasher.hash_u64(values.len() as u64);
647 for value in values {
648 hasher.hash_str(value);
649 }
650 }
651
652 fn map_token_prepare_replay_decision(
653 decision: ReplayReceiptDecision,
654 ) -> Result<DelegatedTokenPrepareResponse, Error> {
655 match decision {
656 ReplayReceiptDecision::Fresh(_) => Err(Error::invariant(
657 "fresh delegated token replay decision escaped",
658 )),
659 ReplayReceiptDecision::ReturnCommitted(receipt) => {
660 Self::decode_token_prepare_response(&receipt)
661 }
662 ReplayReceiptDecision::OperationInProgress => Err(Error::conflict(
663 "delegated token prepare request is already in progress; retry later with the same request id",
664 )),
665 ReplayReceiptDecision::ActorMismatch => Err(Error::conflict(
666 "delegated token prepare request id was reused by a different caller",
667 )),
668 ReplayReceiptDecision::PayloadMismatch => Err(Error::conflict(
669 "delegated token prepare request id was reused with a different payload",
670 )),
671 ReplayReceiptDecision::Expired => Err(Error::conflict(
672 "delegated token prepare replay receipt expired; retry with a new request id",
673 )),
674 ReplayReceiptDecision::RecoveryRequired(reason) => Err(Error::conflict(format!(
675 "delegated token prepare request requires recovery before replay: {reason:?}"
676 ))),
677 ReplayReceiptDecision::TerminalFailed {
678 error_code,
679 error_bytes,
680 error_bytes_truncated,
681 } => Err(Error::conflict(format!(
682 "delegated token prepare request previously failed: {error_code:?}; error_bytes_len={}; truncated={error_bytes_truncated}",
683 error_bytes.len()
684 ))),
685 ReplayReceiptDecision::PendingActorQuotaExceeded { max_pending, .. } => {
686 Err(Error::exhausted(format!(
687 "delegated token prepare pending replay receipt quota exceeded for caller; max_pending={max_pending}"
688 )))
689 }
690 ReplayReceiptDecision::PendingCommandQuotaExceeded { max_pending, .. } => {
691 Err(Error::exhausted(format!(
692 "delegated token prepare pending replay receipt quota exceeded for command kind; max_pending={max_pending}"
693 )))
694 }
695 }
696 }
697
698 fn map_token_prepare_replay_store_error(err: ReplayReceiptStoreError) -> Error {
699 match err {
700 ReplayReceiptStoreError::ReceiptDecodeFailed(message) => Error::internal(format!(
701 "failed to decode delegated token prepare replay receipt: {message}"
702 )),
703 }
704 }
705
706 fn encode_token_prepare_response(
707 response: &DelegatedTokenPrepareResponse,
708 ) -> Result<Vec<u8>, Error> {
709 encode_one(response).map_err(|err| {
710 Error::internal(format!(
711 "failed to encode delegated token prepare replay response: {err}"
712 ))
713 })
714 }
715
716 fn decode_token_prepare_response(
717 receipt: &crate::ops::replay::model::ReplayReceipt,
718 ) -> Result<DelegatedTokenPrepareResponse, Error> {
719 let response_schema_version = receipt.response_schema_version.ok_or_else(|| {
720 Error::internal(
721 "delegated token prepare replay receipt is missing response schema version",
722 )
723 })?;
724 if response_schema_version != Self::TOKEN_PREPARE_REPLAY_RESPONSE_SCHEMA_VERSION {
725 return Err(Error::internal(format!(
726 "unsupported delegated token prepare replay response schema version {response_schema_version}"
727 )));
728 }
729 let response_bytes = receipt.response_bytes.as_deref().ok_or_else(|| {
730 Error::internal("delegated token prepare replay receipt is missing response bytes")
731 })?;
732 decode_one(response_bytes).map_err(|err| {
733 Error::internal(format!(
734 "failed to decode delegated token prepare replay response: {err}"
735 ))
736 })
737 }
738
739 fn map_role_attestation_replay_decision(
740 decision: ReplayReceiptDecision,
741 ) -> Result<RoleAttestationPrepareResponse, Error> {
742 match decision {
743 ReplayReceiptDecision::Fresh(_) => Err(Error::invariant(
744 "fresh role attestation replay decision escaped",
745 )),
746 ReplayReceiptDecision::ReturnCommitted(receipt) => {
747 Self::decode_role_attestation_prepare_response(&receipt)
748 }
749 ReplayReceiptDecision::OperationInProgress => Err(Error::conflict(
750 "role attestation prepare request is already in progress; retry later with the same request id",
751 )),
752 ReplayReceiptDecision::ActorMismatch => Err(Error::conflict(
753 "role attestation prepare request id was reused by a different caller",
754 )),
755 ReplayReceiptDecision::PayloadMismatch => Err(Error::conflict(
756 "role attestation prepare request id was reused with a different payload",
757 )),
758 ReplayReceiptDecision::Expired => Err(Error::conflict(
759 "role attestation prepare replay receipt expired; retry with a new request id",
760 )),
761 ReplayReceiptDecision::RecoveryRequired(reason) => Err(Error::conflict(format!(
762 "role attestation prepare request requires recovery before replay: {reason:?}"
763 ))),
764 ReplayReceiptDecision::TerminalFailed {
765 error_code,
766 error_bytes,
767 error_bytes_truncated,
768 } => Err(Error::conflict(format!(
769 "role attestation prepare request previously failed: {error_code:?}; error_bytes_len={}; truncated={error_bytes_truncated}",
770 error_bytes.len()
771 ))),
772 ReplayReceiptDecision::PendingActorQuotaExceeded { max_pending, .. } => {
773 Err(Error::exhausted(format!(
774 "role attestation prepare pending replay receipt quota exceeded for caller; max_pending={max_pending}"
775 )))
776 }
777 ReplayReceiptDecision::PendingCommandQuotaExceeded { max_pending, .. } => {
778 Err(Error::exhausted(format!(
779 "role attestation prepare pending replay receipt quota exceeded for command kind; max_pending={max_pending}"
780 )))
781 }
782 }
783 }
784
785 fn map_role_attestation_replay_store_error(err: ReplayReceiptStoreError) -> Error {
786 match err {
787 ReplayReceiptStoreError::ReceiptDecodeFailed(message) => Error::internal(format!(
788 "failed to decode role attestation replay receipt: {message}"
789 )),
790 }
791 }
792
793 fn encode_role_attestation_prepare_response(
794 response: &RoleAttestationPrepareResponse,
795 ) -> Result<Vec<u8>, Error> {
796 encode_one(response).map_err(|err| {
797 Error::internal(format!(
798 "failed to encode role attestation prepare replay response: {err}"
799 ))
800 })
801 }
802
803 fn decode_role_attestation_prepare_response(
804 receipt: &crate::ops::replay::model::ReplayReceipt,
805 ) -> Result<RoleAttestationPrepareResponse, Error> {
806 let response_schema_version = receipt.response_schema_version.ok_or_else(|| {
807 Error::internal(
808 "role attestation prepare replay receipt is missing response schema version",
809 )
810 })?;
811 if response_schema_version != Self::ROLE_ATTESTATION_REPLAY_RESPONSE_SCHEMA_VERSION {
812 return Err(Error::internal(format!(
813 "unsupported role attestation prepare replay response schema version {response_schema_version}"
814 )));
815 }
816 let response_bytes = receipt.response_bytes.as_deref().ok_or_else(|| {
817 Error::internal("role attestation prepare replay receipt is missing response bytes")
818 })?;
819 decode_one(response_bytes).map_err(|err| {
820 Error::internal(format!(
821 "failed to decode role attestation prepare replay response: {err}"
822 ))
823 })
824 }
825
826 fn map_delegation_replay_decision(
827 decision: ReplayReceiptDecision,
828 ) -> Result<DelegationProofPrepareResponse, Error> {
829 match decision {
830 ReplayReceiptDecision::Fresh(_) => {
831 Err(Error::invariant("fresh delegation replay decision escaped"))
832 }
833 ReplayReceiptDecision::ReturnCommitted(receipt) => {
834 Self::decode_delegation_prepare_response(&receipt)
835 }
836 ReplayReceiptDecision::OperationInProgress => Err(Error::conflict(
837 "delegation proof request is already in progress; retry later with the same request id",
838 )),
839 ReplayReceiptDecision::ActorMismatch => Err(Error::conflict(
840 "delegation proof request id was reused by a different caller",
841 )),
842 ReplayReceiptDecision::PayloadMismatch => Err(Error::conflict(
843 "delegation proof request id was reused with a different payload",
844 )),
845 ReplayReceiptDecision::Expired => Err(Error::conflict(
846 "delegation proof replay receipt expired; retry with a new request id",
847 )),
848 ReplayReceiptDecision::RecoveryRequired(reason) => Err(Error::conflict(format!(
849 "delegation proof request requires recovery before replay: {reason:?}"
850 ))),
851 ReplayReceiptDecision::TerminalFailed {
852 error_code,
853 error_bytes,
854 error_bytes_truncated,
855 } => Err(Error::conflict(format!(
856 "delegation proof request previously failed: {error_code:?}; error_bytes_len={}; truncated={error_bytes_truncated}",
857 error_bytes.len()
858 ))),
859 ReplayReceiptDecision::PendingActorQuotaExceeded { max_pending, .. } => {
860 Err(Error::exhausted(format!(
861 "delegation proof pending replay receipt quota exceeded for caller; max_pending={max_pending}"
862 )))
863 }
864 ReplayReceiptDecision::PendingCommandQuotaExceeded { max_pending, .. } => {
865 Err(Error::exhausted(format!(
866 "delegation proof pending replay receipt quota exceeded for command kind; max_pending={max_pending}"
867 )))
868 }
869 }
870 }
871
872 fn map_delegation_replay_store_error(err: ReplayReceiptStoreError) -> Error {
873 match err {
874 ReplayReceiptStoreError::ReceiptDecodeFailed(message) => Error::internal(format!(
875 "failed to decode delegation replay receipt: {message}"
876 )),
877 }
878 }
879
880 fn encode_delegation_prepare_response(
881 response: &DelegationProofPrepareResponse,
882 ) -> Result<Vec<u8>, Error> {
883 encode_one(response).map_err(|err| {
884 Error::internal(format!(
885 "failed to encode delegation proof prepare replay response: {err}"
886 ))
887 })
888 }
889
890 fn decode_delegation_prepare_response(
891 receipt: &crate::ops::replay::model::ReplayReceipt,
892 ) -> Result<DelegationProofPrepareResponse, Error> {
893 let response_schema_version = receipt.response_schema_version.ok_or_else(|| {
894 Error::internal("delegation replay receipt is missing response schema version")
895 })?;
896 if response_schema_version != Self::DELEGATION_REPLAY_RESPONSE_SCHEMA_VERSION {
897 return Err(Error::internal(format!(
898 "unsupported delegation replay response schema version {response_schema_version}"
899 )));
900 }
901 let response_bytes = receipt.response_bytes.as_deref().ok_or_else(|| {
902 Error::internal("delegation replay receipt is missing response bytes")
903 })?;
904 decode_one(response_bytes).map_err(|err| {
905 Error::internal(format!(
906 "failed to decode delegation proof prepare replay response: {err}"
907 ))
908 })
909 }
910}
911
912impl AuthApi {
913 async fn prepare_delegation_proof_remote(
915 request: DelegationProofIssueRequest,
916 ) -> Result<DelegationProofPrepareResponse, Error> {
917 let root_pid = EnvOps::root_pid().map_err(Error::from)?;
918 RootDelegationProofClient::new(root_pid)
919 .prepare_delegation_proof(request)
920 .await
921 .map_err(Self::map_auth_error)
922 }
923}
924
925#[cfg(test)]
926mod tests {
927 use super::AuthApi;
928 use crate::{
929 cdk::types::Principal,
930 dto::{
931 auth::{
932 AuthRequestMetadata, DelegatedRoleGrant, DelegatedTokenPrepareRequest,
933 DelegationAudience, DelegationProofIssueRequest,
934 },
935 error::ErrorCode,
936 },
937 };
938
939 fn p(id: u8) -> Principal {
940 Principal::from_slice(&[id; 29])
941 }
942
943 fn delegation_request(metadata_id: u8) -> DelegationProofIssueRequest {
944 DelegationProofIssueRequest {
945 metadata: Some(meta(metadata_id, 60_000_000_000)),
946 issuer_pid: p(2),
947 aud: DelegationAudience::Project("test".to_string()),
948 grants: vec![grant("project_instance", &["canic.verify"])],
949 cert_ttl_ns: 60_000_000_000,
950 }
951 }
952
953 fn grant(role: &str, scopes: &[&str]) -> DelegatedRoleGrant {
954 DelegatedRoleGrant {
955 target: crate::ids::CanisterRole::owned(role.to_string()),
956 scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
957 }
958 }
959
960 fn meta(id: u8, ttl_ns: u64) -> AuthRequestMetadata {
961 AuthRequestMetadata {
962 request_id: [id; 32],
963 ttl_ns,
964 }
965 }
966
967 fn token_prepare_request(metadata_id: u8) -> DelegatedTokenPrepareRequest {
968 DelegatedTokenPrepareRequest {
969 metadata: Some(meta(metadata_id, 60_000_000_000)),
970 subject: p(8),
971 aud: DelegationAudience::Project("test".to_string()),
972 grants: vec![grant("project_instance", &["canic.verify"])],
973 ttl_ns: 30_000_000_000,
974 ext: None,
975 }
976 }
977
978 #[test]
979 fn delegation_request_caller_must_match_requested_issuer() {
980 AuthApi::validate_delegation_request_caller(p(2), p(2)).expect("matching issuer");
981
982 let err = AuthApi::validate_delegation_request_caller(p(1), p(2))
983 .expect_err("mismatched caller must fail");
984
985 assert_eq!(err.code, ErrorCode::Forbidden);
986 }
987
988 #[test]
989 fn delegation_replay_metadata_rejects_missing_or_invalid_ttl() {
990 let missing = AuthApi::delegation_replay_metadata(None).expect_err("metadata is required");
991 assert_eq!(missing.code, ErrorCode::OperationIdRequired);
992
993 let zero = AuthApi::delegation_replay_metadata(Some(AuthRequestMetadata {
994 request_id: [1; 32],
995 ttl_ns: 0,
996 }))
997 .expect_err("zero ttl is invalid");
998 assert_eq!(zero.code, ErrorCode::InvalidInput);
999
1000 let too_large = AuthApi::delegation_replay_metadata(Some(AuthRequestMetadata {
1001 request_id: [1; 32],
1002 ttl_ns: AuthApi::MAX_DELEGATION_REPLAY_TTL_NS + 1,
1003 }))
1004 .expect_err("oversized ttl is invalid");
1005 assert_eq!(too_large.code, ErrorCode::InvalidInput);
1006 }
1007
1008 #[test]
1009 fn delegation_replay_payload_hash_ignores_metadata() {
1010 let command_kind = AuthApi::delegation_replay_command_kind();
1011 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1012 let a = delegation_request(1);
1013 let b = delegation_request(9);
1014
1015 assert_eq!(
1016 AuthApi::delegation_replay_payload_hash(&command_kind, &actor, &a),
1017 AuthApi::delegation_replay_payload_hash(&command_kind, &actor, &b)
1018 );
1019 }
1020
1021 #[test]
1022 fn delegated_token_replay_metadata_rejects_missing_or_invalid_ttl() {
1023 let missing =
1024 AuthApi::token_replay_metadata(None, "delegated token prepare").expect_err("required");
1025 assert_eq!(missing.code, ErrorCode::OperationIdRequired);
1026
1027 let zero = AuthApi::token_replay_metadata(Some(meta(1, 0)), "delegated token prepare")
1028 .expect_err("zero ttl is invalid");
1029 assert_eq!(zero.code, ErrorCode::InvalidInput);
1030
1031 let too_large = AuthApi::token_replay_metadata(
1032 Some(meta(1, AuthApi::MAX_TOKEN_REPLAY_TTL_NS + 1)),
1033 "delegated token prepare",
1034 )
1035 .expect_err("oversized ttl is invalid");
1036 assert_eq!(too_large.code, ErrorCode::InvalidInput);
1037 }
1038
1039 #[test]
1040 fn delegation_replay_payload_hash_binds_authoritative_payload() {
1041 let command_kind = AuthApi::delegation_replay_command_kind();
1042 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1043 let a = delegation_request(1);
1044 let mut b = a.clone();
1045 b.cert_ttl_ns += 1;
1046
1047 assert_ne!(
1048 AuthApi::delegation_replay_payload_hash(&command_kind, &actor, &a),
1049 AuthApi::delegation_replay_payload_hash(&command_kind, &actor, &b)
1050 );
1051 }
1052
1053 #[test]
1054 fn delegated_token_prepare_payload_hash_ignores_metadata() {
1055 let command_kind = AuthApi::token_prepare_replay_command_kind();
1056 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1057 let a = token_prepare_request(1);
1058 let b = token_prepare_request(9);
1059
1060 assert_eq!(
1061 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &a),
1062 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &b)
1063 );
1064 }
1065
1066 #[test]
1067 fn delegated_token_prepare_payload_hash_binds_authoritative_payload() {
1068 let command_kind = AuthApi::token_prepare_replay_command_kind();
1069 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1070 let a = token_prepare_request(1);
1071 let mut b = a.clone();
1072 b.ttl_ns += 1;
1073
1074 assert_ne!(
1075 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &a),
1076 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &b)
1077 );
1078 }
1079
1080 #[test]
1081 fn delegated_token_prepare_payload_hash_binds_ext() {
1082 let command_kind = AuthApi::token_prepare_replay_command_kind();
1083 let actor = crate::ops::replay::model::ReplayActor::direct_caller(p(2));
1084 let a = token_prepare_request(1);
1085 let mut b = a.clone();
1086 b.ext = Some(b"app-context".to_vec());
1087
1088 assert_ne!(
1089 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &a),
1090 AuthApi::token_prepare_replay_payload_hash(&command_kind, &actor, &b)
1091 );
1092 }
1093}