1use crate::{
2 cdk::{candid::CandidType, types::Principal},
3 dto::{
4 auth::SignedRoleAttestation,
5 capability::{
6 CAPABILITY_VERSION_V1, CapabilityProof, CapabilityRequestMetadata, CapabilityService,
7 DelegatedGrant, DelegatedGrantProof, PROOF_VERSION_V1, RootCapabilityEnvelopeV1,
8 RootCapabilityResponseV1,
9 },
10 error::Error,
11 rpc::{
12 CreateCanisterParent, CreateCanisterResponse, Request, Response, RootRequestMetadata,
13 UpgradeCanisterResponse,
14 },
15 },
16 ids::CanisterRole,
17 log,
18 log::Topic,
19 ops::{
20 ic::{IcOps, ecdsa::EcdsaOps},
21 runtime::metrics::root_capability::{
22 RootCapabilityMetricEvent, RootCapabilityMetricKey, RootCapabilityMetrics,
23 },
24 storage::{auth::DelegationStateOps, registry::subnet::SubnetRegistryOps},
25 },
26 workflow::rpc::request::{RpcRequestWorkflow, handler::RootResponseWorkflow},
27};
28use candid::encode_one;
29use sha2::{Digest, Sha256};
30
31const CAPABILITY_HASH_DOMAIN_V1: &[u8] = b"CANIC_CAPABILITY_V1";
32const DELEGATED_GRANT_SIGNING_DOMAIN_V1: &[u8] = b"CANIC_DELEGATED_GRANT_V1";
33const REPLAY_REQUEST_ID_DOMAIN_V1: &[u8] = b"CANIC_REPLAY_REQUEST_ID_V1";
34const MAX_CAPABILITY_CLOCK_SKEW_SECONDS: u64 = 30;
35const DELEGATED_GRANT_KEY_ID_V1: u32 = 1;
36
37pub struct RpcApi;
57
58impl RpcApi {
59 pub async fn create_canister_request<A>(
60 canister_role: &CanisterRole,
61 parent: CreateCanisterParent,
62 extra: Option<A>,
63 ) -> Result<CreateCanisterResponse, Error>
64 where
65 A: CandidType + Send + Sync,
66 {
67 RpcRequestWorkflow::create_canister_request(canister_role, parent, extra)
68 .await
69 .map_err(Error::from)
70 }
71
72 pub async fn upgrade_canister_request(
73 canister_pid: Principal,
74 ) -> Result<UpgradeCanisterResponse, Error> {
75 RpcRequestWorkflow::upgrade_canister_request(canister_pid)
76 .await
77 .map_err(Error::from)
78 }
79
80 pub async fn response_attested(
81 request: Request,
82 attestation: SignedRoleAttestation,
83 min_accepted_epoch: u64,
84 ) -> Result<Response, Error> {
85 crate::api::auth::DelegationApi::verify_role_attestation(&attestation, min_accepted_epoch)
86 .await?;
87 RootResponseWorkflow::response(request)
88 .await
89 .map_err(Error::from)
90 }
91
92 pub async fn response_capability_v1(
93 envelope: RootCapabilityEnvelopeV1,
94 ) -> Result<RootCapabilityResponseV1, Error> {
95 let RootCapabilityEnvelopeV1 {
96 service,
97 capability_version,
98 capability,
99 proof,
100 metadata,
101 } = envelope;
102
103 let capability_key = root_capability_metric_key(&capability);
104 if let Err(err) = validate_root_capability_envelope(service, capability_version, &proof) {
105 RootCapabilityMetrics::record(
106 capability_key,
107 RootCapabilityMetricEvent::EnvelopeRejected,
108 );
109 log!(
110 Topic::Rpc,
111 Warn,
112 "root capability envelope rejected (capability={}, caller={}, service={:?}, capability_version={}, proof_mode={}): {}",
113 root_capability_family(&capability),
114 IcOps::msg_caller(),
115 service,
116 capability_version,
117 capability_proof_mode_label(&proof),
118 err
119 );
120 return Err(err);
121 }
122 RootCapabilityMetrics::record(capability_key, RootCapabilityMetricEvent::EnvelopeValidated);
123
124 if let Err(err) =
125 verify_root_capability_proof(&capability, capability_version, &proof).await
126 {
127 RootCapabilityMetrics::record(capability_key, RootCapabilityMetricEvent::ProofRejected);
128 log!(
129 Topic::Rpc,
130 Warn,
131 "root capability proof rejected (capability={}, caller={}, service={:?}, capability_version={}, proof_mode={}): {}",
132 root_capability_family(&capability),
133 IcOps::msg_caller(),
134 service,
135 capability_version,
136 capability_proof_mode_label(&proof),
137 err
138 );
139 return Err(err);
140 }
141 RootCapabilityMetrics::record(capability_key, RootCapabilityMetricEvent::ProofVerified);
142
143 let replay_metadata = project_replay_metadata(metadata, IcOps::now_secs())?;
144
145 let capability = with_root_request_metadata(capability, replay_metadata);
146
147 let response = RootResponseWorkflow::response_replay_first(capability)
148 .await
149 .map_err(Error::from)?;
150
151 Ok(RootCapabilityResponseV1 { response })
152 }
153}
154
155fn validate_root_capability_envelope(
156 service: CapabilityService,
157 capability_version: u16,
158 proof: &CapabilityProof,
159) -> Result<(), Error> {
160 if service != CapabilityService::Root {
161 return Err(Error::invalid(
162 "capability envelope service must be Root for root dispatch",
163 ));
164 }
165
166 if capability_version != CAPABILITY_VERSION_V1 {
167 return Err(Error::invalid(format!(
168 "unsupported capability_version: {capability_version}",
169 )));
170 }
171
172 match proof {
173 CapabilityProof::Structural => Ok(()),
174 CapabilityProof::RoleAttestation(proof) => {
175 if proof.proof_version != PROOF_VERSION_V1 {
176 return Err(Error::invalid(format!(
177 "unsupported role attestation proof_version: {}",
178 proof.proof_version
179 )));
180 }
181 Ok(())
182 }
183 CapabilityProof::DelegatedGrant(proof) => {
184 if proof.proof_version != PROOF_VERSION_V1 {
185 return Err(Error::invalid(format!(
186 "unsupported delegated grant proof_version: {}",
187 proof.proof_version
188 )));
189 }
190 Ok(())
191 }
192 }
193}
194
195async fn verify_root_capability_proof(
196 capability: &Request,
197 capability_version: u16,
198 proof: &CapabilityProof,
199) -> Result<(), Error> {
200 let target_canister = IcOps::canister_self();
201
202 match proof {
203 CapabilityProof::Structural => verify_root_structural_proof(capability),
204 CapabilityProof::RoleAttestation(proof) => {
205 verify_capability_hash_binding(
206 target_canister,
207 capability_version,
208 capability,
209 proof.capability_hash,
210 )?;
211
212 crate::api::auth::DelegationApi::verify_role_attestation(&proof.attestation, 0).await
213 }
214 CapabilityProof::DelegatedGrant(proof) => {
215 verify_capability_hash_binding(
216 target_canister,
217 capability_version,
218 capability,
219 proof.capability_hash,
220 )?;
221 verify_delegated_grant_hash_binding(proof)?;
222 verify_root_delegated_grant_proof(
223 capability,
224 proof,
225 IcOps::msg_caller(),
226 target_canister,
227 IcOps::now_secs(),
228 )
229 }
230 }
231}
232
233fn verify_root_structural_proof(capability: &Request) -> Result<(), Error> {
234 let caller = IcOps::msg_caller();
235
236 if SubnetRegistryOps::get(caller).is_none() {
237 return Err(Error::forbidden(
238 "structural proof requires caller to be registered in subnet registry",
239 ));
240 }
241
242 match capability {
243 Request::Cycles(_) => Ok(()),
244 Request::UpgradeCanister(req) => {
245 let target = SubnetRegistryOps::get(req.canister_pid).ok_or_else(|| {
246 Error::forbidden("structural proof requires registered upgrade target")
247 })?;
248 if target.parent_pid != Some(caller) {
249 return Err(Error::forbidden(
250 "structural proof requires upgrade target to be a direct child of caller",
251 ));
252 }
253 Ok(())
254 }
255 _ => Err(Error::forbidden(
256 "structural proof is only supported for root cycles and upgrade capabilities",
257 )),
258 }
259}
260
261fn verify_capability_hash_binding(
262 target_canister: Principal,
263 capability_version: u16,
264 capability: &Request,
265 capability_hash: [u8; 32],
266) -> Result<(), Error> {
267 let expected = root_capability_hash(target_canister, capability_version, capability)?;
268 if capability_hash != expected {
269 return Err(Error::invalid(
270 "capability_hash does not match capability payload",
271 ));
272 }
273
274 Ok(())
275}
276
277const fn root_capability_metric_key(capability: &Request) -> RootCapabilityMetricKey {
278 match capability {
279 Request::CreateCanister(_) => RootCapabilityMetricKey::Provision,
280 Request::UpgradeCanister(_) => RootCapabilityMetricKey::Upgrade,
281 Request::Cycles(_) => RootCapabilityMetricKey::MintCycles,
282 Request::IssueDelegation(_) => RootCapabilityMetricKey::IssueDelegation,
283 Request::IssueRoleAttestation(_) => RootCapabilityMetricKey::IssueRoleAttestation,
284 }
285}
286
287const fn capability_proof_mode_label(proof: &CapabilityProof) -> &'static str {
288 match proof {
289 CapabilityProof::Structural => "Structural",
290 CapabilityProof::RoleAttestation(_) => "RoleAttestation",
291 CapabilityProof::DelegatedGrant(_) => "DelegatedGrant",
292 }
293}
294
295fn verify_delegated_grant_hash_binding(proof: &DelegatedGrantProof) -> Result<(), Error> {
296 if proof.grant.capability_hash != proof.capability_hash {
297 return Err(Error::invalid(
298 "delegated grant capability_hash does not match proof capability_hash",
299 ));
300 }
301
302 Ok(())
303}
304
305fn verify_root_delegated_grant_proof(
306 capability: &Request,
307 proof: &DelegatedGrantProof,
308 caller: Principal,
309 target_canister: Principal,
310 now_secs: u64,
311) -> Result<(), Error> {
312 verify_root_delegated_grant_claims(capability, proof, caller, target_canister, now_secs)?;
313 verify_root_delegated_grant_signature(&proof.grant, &proof.grant_sig)
314}
315
316fn verify_root_delegated_grant_claims(
317 capability: &Request,
318 proof: &DelegatedGrantProof,
319 caller: Principal,
320 target_canister: Principal,
321 now_secs: u64,
322) -> Result<(), Error> {
323 if proof.key_id != DELEGATED_GRANT_KEY_ID_V1 {
324 return Err(Error::invalid(format!(
325 "unsupported delegated grant key_id: {}",
326 proof.key_id
327 )));
328 }
329
330 let grant = &proof.grant;
331 if grant.issuer != target_canister {
332 return Err(Error::forbidden(
333 "delegated grant issuer must match target canister",
334 ));
335 }
336 if grant.subject != caller {
337 return Err(Error::forbidden(
338 "delegated grant subject must match caller",
339 ));
340 }
341 if !grant.audience.contains(&target_canister) {
342 return Err(Error::forbidden(
343 "delegated grant audience must include target canister",
344 ));
345 }
346 if grant.scope.service != CapabilityService::Root {
347 return Err(Error::forbidden(
348 "delegated grant scope service must be Root",
349 ));
350 }
351 let expected_family = root_capability_family(capability);
352 if grant.scope.capability_family != expected_family {
353 return Err(Error::forbidden(format!(
354 "delegated grant scope capability_family '{}' does not match '{}'",
355 grant.scope.capability_family, expected_family
356 )));
357 }
358 if grant.quota == 0 {
359 return Err(Error::invalid(
360 "delegated grant quota must be greater than zero",
361 ));
362 }
363 if grant.expires_at <= grant.issued_at {
364 return Err(Error::invalid(
365 "delegated grant expires_at must be greater than issued_at",
366 ));
367 }
368 if now_secs < grant.issued_at {
369 return Err(Error::forbidden(
370 "delegated grant is not valid yet for current time",
371 ));
372 }
373 if now_secs > grant.expires_at {
374 return Err(Error::forbidden("delegated grant has expired"));
375 }
376
377 Ok(())
378}
379
380fn verify_root_delegated_grant_signature(
381 grant: &DelegatedGrant,
382 signature: &[u8],
383) -> Result<(), Error> {
384 if signature.is_empty() {
385 return Err(Error::forbidden("delegated grant signature is required"));
386 }
387
388 let root_public_key = DelegationStateOps::root_public_key()
389 .ok_or_else(|| Error::forbidden("delegated grant root public key unavailable"))?;
390 let grant_hash = delegated_grant_hash(grant)?;
391 EcdsaOps::verify_signature(&root_public_key, grant_hash, signature)
392 .map_err(|err| Error::forbidden(format!("delegated grant signature invalid: {err}")))?;
393
394 Ok(())
395}
396
397const fn root_capability_family(capability: &Request) -> &'static str {
398 match capability {
399 Request::CreateCanister(_) => "Provision",
400 Request::UpgradeCanister(_) => "Upgrade",
401 Request::Cycles(_) => "MintCycles",
402 Request::IssueDelegation(_) => "IssueDelegation",
403 Request::IssueRoleAttestation(_) => "IssueRoleAttestation",
404 }
405}
406
407fn delegated_grant_hash(grant: &DelegatedGrant) -> Result<[u8; 32], Error> {
408 let payload = encode_one(grant)
409 .map_err(|err| Error::internal(format!("failed to encode delegated grant: {err}")))?;
410 let mut hasher = Sha256::new();
411 hasher.update(DELEGATED_GRANT_SIGNING_DOMAIN_V1);
412 hasher.update(payload);
413 Ok(hasher.finalize().into())
414}
415
416fn root_capability_hash(
417 target_canister: Principal,
418 capability_version: u16,
419 capability: &Request,
420) -> Result<[u8; 32], Error> {
421 let canonical = strip_request_metadata(capability.clone());
422 let payload = encode_one(&(
423 target_canister,
424 CapabilityService::Root,
425 capability_version,
426 canonical,
427 ))
428 .map_err(|err| Error::internal(format!("failed to encode capability payload: {err}")))?;
429 let mut hasher = Sha256::new();
430 hasher.update(CAPABILITY_HASH_DOMAIN_V1);
431 hasher.update(payload);
432 Ok(hasher.finalize().into())
433}
434
435fn with_root_request_metadata(request: Request, metadata: RootRequestMetadata) -> Request {
436 match request {
437 Request::CreateCanister(mut req) => {
438 req.metadata = Some(metadata);
439 Request::CreateCanister(req)
440 }
441 Request::UpgradeCanister(mut req) => {
442 req.metadata = Some(metadata);
443 Request::UpgradeCanister(req)
444 }
445 Request::Cycles(mut req) => {
446 req.metadata = Some(metadata);
447 Request::Cycles(req)
448 }
449 Request::IssueDelegation(mut req) => {
450 req.metadata = Some(metadata);
451 Request::IssueDelegation(req)
452 }
453 Request::IssueRoleAttestation(mut req) => {
454 req.metadata = Some(metadata);
455 Request::IssueRoleAttestation(req)
456 }
457 }
458}
459
460fn strip_request_metadata(request: Request) -> Request {
461 match request {
462 Request::CreateCanister(mut req) => {
463 req.metadata = None;
464 Request::CreateCanister(req)
465 }
466 Request::UpgradeCanister(mut req) => {
467 req.metadata = None;
468 Request::UpgradeCanister(req)
469 }
470 Request::Cycles(mut req) => {
471 req.metadata = None;
472 Request::Cycles(req)
473 }
474 Request::IssueDelegation(mut req) => {
475 req.metadata = None;
476 Request::IssueDelegation(req)
477 }
478 Request::IssueRoleAttestation(mut req) => {
479 req.metadata = None;
480 Request::IssueRoleAttestation(req)
481 }
482 }
483}
484
485fn project_replay_metadata(
486 metadata: CapabilityRequestMetadata,
487 now_secs: u64,
488) -> Result<RootRequestMetadata, Error> {
489 if metadata.ttl_seconds == 0 {
490 return Err(Error::invalid(
491 "capability metadata ttl_seconds must be greater than zero",
492 ));
493 }
494
495 if metadata.issued_at > now_secs.saturating_add(MAX_CAPABILITY_CLOCK_SKEW_SECONDS) {
496 return Err(Error::invalid(
497 "capability metadata issued_at is too far in the future",
498 ));
499 }
500
501 let expires_at = metadata
502 .issued_at
503 .checked_add(u64::from(metadata.ttl_seconds))
504 .ok_or_else(|| Error::invalid("capability metadata expiry overflow"))?;
505 if now_secs > expires_at {
506 return Err(Error::conflict("capability metadata has expired"));
507 }
508
509 Ok(RootRequestMetadata {
510 request_id: replay_request_id(metadata.request_id, metadata.nonce),
511 ttl_seconds: u64::from(metadata.ttl_seconds),
512 })
513}
514
515fn replay_request_id(request_id: [u8; 16], nonce: [u8; 16]) -> [u8; 32] {
516 let mut hasher = Sha256::new();
517 hasher.update(REPLAY_REQUEST_ID_DOMAIN_V1);
518 hasher.update(request_id);
519 hasher.update(nonce);
520 hasher.finalize().into()
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use crate::dto::{
527 auth::{RoleAttestation, SignedRoleAttestation},
528 capability::DelegatedGrantScope,
529 rpc::{CyclesRequest, RootRequestMetadata},
530 };
531 use k256::ecdsa::{Signature, SigningKey, signature::hazmat::PrehashSigner};
532
533 fn p(id: u8) -> Principal {
534 Principal::from_slice(&[id; 29])
535 }
536
537 fn sample_request(cycles: u128) -> Request {
538 Request::Cycles(CyclesRequest {
539 cycles,
540 metadata: None,
541 })
542 }
543
544 fn sample_metadata(
545 request_id: u8,
546 nonce: u8,
547 issued_at: u64,
548 ttl_seconds: u32,
549 ) -> CapabilityRequestMetadata {
550 CapabilityRequestMetadata {
551 request_id: [request_id; 16],
552 nonce: [nonce; 16],
553 issued_at,
554 ttl_seconds,
555 }
556 }
557
558 #[test]
559 fn root_capability_hash_changes_with_payload() {
560 let hash_a =
561 root_capability_hash(p(1), CAPABILITY_VERSION_V1, &sample_request(10)).expect("hash a");
562 let hash_b =
563 root_capability_hash(p(1), CAPABILITY_VERSION_V1, &sample_request(11)).expect("hash b");
564 assert_ne!(hash_a, hash_b);
565 }
566
567 #[test]
568 fn root_capability_hash_binds_target_canister() {
569 let req = sample_request(10);
570 let hash_a = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &req).expect("hash a");
571 let hash_b = root_capability_hash(p(2), CAPABILITY_VERSION_V1, &req).expect("hash b");
572 assert_ne!(hash_a, hash_b);
573 }
574
575 #[test]
576 fn root_capability_hash_binds_capability_version() {
577 let req = sample_request(10);
578 let hash_a = root_capability_hash(p(1), 1, &req).expect("hash a");
579 let hash_b = root_capability_hash(p(1), 2, &req).expect("hash b");
580 assert_ne!(hash_a, hash_b);
581 }
582
583 #[test]
584 fn root_capability_hash_ignores_request_metadata() {
585 let req_a = Request::Cycles(CyclesRequest {
586 cycles: 10,
587 metadata: Some(RootRequestMetadata {
588 request_id: [1u8; 32],
589 ttl_seconds: 60,
590 }),
591 });
592 let req_b = Request::Cycles(CyclesRequest {
593 cycles: 10,
594 metadata: Some(RootRequestMetadata {
595 request_id: [2u8; 32],
596 ttl_seconds: 120,
597 }),
598 });
599
600 let hash_a = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &req_a).expect("hash a");
601 let hash_b = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &req_b).expect("hash b");
602 assert_eq!(hash_a, hash_b);
603 }
604
605 #[test]
606 fn project_replay_metadata_rejects_expired_metadata() {
607 let err = project_replay_metadata(sample_metadata(1, 2, 900, 50), 1_000)
608 .expect_err("expired metadata must fail");
609 assert!(err.message.contains("expired"));
610 }
611
612 #[test]
613 fn project_replay_metadata_rejects_future_metadata_beyond_skew() {
614 let err = project_replay_metadata(sample_metadata(1, 2, 1_031, 60), 1_000)
615 .expect_err("future metadata must fail");
616 assert!(err.message.contains("future"));
617 }
618
619 #[test]
620 fn project_replay_metadata_binds_nonce_into_request_id() {
621 let a = project_replay_metadata(sample_metadata(3, 1, 1_000, 60), 1_000).expect("a");
622 let b = project_replay_metadata(sample_metadata(3, 2, 1_000, 60), 1_000).expect("b");
623 assert_ne!(a.request_id, b.request_id);
624 }
625
626 #[test]
627 fn with_root_request_metadata_overrides_existing_metadata() {
628 let request = Request::Cycles(CyclesRequest {
629 cycles: 10,
630 metadata: Some(RootRequestMetadata {
631 request_id: [7u8; 32],
632 ttl_seconds: 10,
633 }),
634 });
635 let metadata = RootRequestMetadata {
636 request_id: [9u8; 32],
637 ttl_seconds: 60,
638 };
639
640 let updated = with_root_request_metadata(request, metadata);
641 match updated {
642 Request::Cycles(req) => assert_eq!(req.metadata, Some(metadata)),
643 _ => panic!("expected cycles request"),
644 }
645 }
646
647 fn sample_signed_attestation() -> SignedRoleAttestation {
648 SignedRoleAttestation {
649 payload: RoleAttestation {
650 subject: p(1),
651 role: crate::ids::CanisterRole::ROOT,
652 subnet_id: None,
653 audience: Some(p(2)),
654 issued_at: 1_000,
655 expires_at: 2_000,
656 epoch: 1,
657 },
658 signature: vec![],
659 key_id: 1,
660 }
661 }
662
663 fn sample_delegated_grant_proof(
664 capability: &Request,
665 caller: Principal,
666 target_canister: Principal,
667 now_secs: u64,
668 ) -> DelegatedGrantProof {
669 let capability_hash =
670 root_capability_hash(target_canister, CAPABILITY_VERSION_V1, capability).expect("hash");
671 DelegatedGrantProof {
672 proof_version: PROOF_VERSION_V1,
673 capability_hash,
674 grant: DelegatedGrant {
675 issuer: target_canister,
676 subject: caller,
677 audience: vec![target_canister],
678 scope: DelegatedGrantScope {
679 service: CapabilityService::Root,
680 capability_family: root_capability_family(capability).to_string(),
681 },
682 capability_hash,
683 quota: 1,
684 issued_at: now_secs.saturating_sub(10),
685 expires_at: now_secs.saturating_add(10),
686 epoch: 0,
687 },
688 grant_sig: vec![1],
689 key_id: DELEGATED_GRANT_KEY_ID_V1,
690 }
691 }
692
693 fn sign_delegated_grant(seed: u8, grant: &DelegatedGrant) -> (Vec<u8>, Vec<u8>) {
694 let signing_key = SigningKey::from_bytes((&[seed; 32]).into()).expect("signing key");
695 let signature: Signature = signing_key
696 .sign_prehash(&delegated_grant_hash(grant).expect("hash"))
697 .expect("prehash signature");
698 let public_key = signing_key
699 .verifying_key()
700 .to_encoded_point(true)
701 .as_bytes()
702 .to_vec();
703 (public_key, signature.to_bytes().to_vec())
704 }
705
706 #[test]
707 fn validate_root_capability_envelope_rejects_service_mismatch() {
708 let err = validate_root_capability_envelope(
709 CapabilityService::Cycles,
710 CAPABILITY_VERSION_V1,
711 &CapabilityProof::Structural,
712 )
713 .expect_err("service mismatch must fail");
714 assert!(err.message.contains("service"));
715 }
716
717 #[test]
718 fn validate_root_capability_envelope_rejects_capability_version_mismatch() {
719 let err = validate_root_capability_envelope(
720 CapabilityService::Root,
721 CAPABILITY_VERSION_V1 + 1,
722 &CapabilityProof::Structural,
723 )
724 .expect_err("unsupported capability version must fail");
725 assert!(err.message.contains("capability_version"));
726 }
727
728 #[test]
729 fn validate_root_capability_envelope_rejects_role_attestation_proof_version_mismatch() {
730 let err = validate_root_capability_envelope(
731 CapabilityService::Root,
732 CAPABILITY_VERSION_V1,
733 &CapabilityProof::RoleAttestation(crate::dto::capability::RoleAttestationProof {
734 proof_version: PROOF_VERSION_V1 + 1,
735 capability_hash: [0u8; 32],
736 attestation: sample_signed_attestation(),
737 }),
738 )
739 .expect_err("unsupported role proof version must fail");
740 assert!(err.message.contains("proof_version"));
741 }
742
743 #[test]
744 fn verify_capability_hash_binding_rejects_mismatch() {
745 let err = verify_capability_hash_binding(
746 p(1),
747 CAPABILITY_VERSION_V1,
748 &sample_request(10),
749 [0u8; 32],
750 )
751 .expect_err("mismatched hash must fail");
752 assert!(err.message.contains("capability_hash"));
753 }
754
755 #[test]
756 fn verify_capability_hash_binding_accepts_match() {
757 let request = sample_request(10);
758 let hash = root_capability_hash(p(1), CAPABILITY_VERSION_V1, &request).expect("hash");
759 verify_capability_hash_binding(p(1), CAPABILITY_VERSION_V1, &request, hash)
760 .expect("matching hash must verify");
761 }
762
763 #[test]
764 fn verify_delegated_grant_hash_binding_rejects_mismatch() {
765 let proof = DelegatedGrantProof {
766 proof_version: PROOF_VERSION_V1,
767 capability_hash: [1u8; 32],
768 grant: crate::dto::capability::DelegatedGrant {
769 issuer: p(1),
770 subject: p(2),
771 audience: vec![p(3)],
772 scope: crate::dto::capability::DelegatedGrantScope {
773 service: CapabilityService::Root,
774 capability_family: "root".to_string(),
775 },
776 capability_hash: [2u8; 32],
777 quota: 1,
778 issued_at: 1,
779 expires_at: 2,
780 epoch: 0,
781 },
782 grant_sig: vec![],
783 key_id: 1,
784 };
785
786 let err = verify_delegated_grant_hash_binding(&proof)
787 .expect_err("mismatched delegated grant hash must fail");
788 assert!(err.message.contains("capability_hash"));
789 }
790
791 #[test]
792 fn delegated_grant_hash_changes_with_payload() {
793 let grant_a = DelegatedGrant {
794 issuer: p(1),
795 subject: p(2),
796 audience: vec![p(1)],
797 scope: DelegatedGrantScope {
798 service: CapabilityService::Root,
799 capability_family: "MintCycles".to_string(),
800 },
801 capability_hash: [1u8; 32],
802 quota: 1,
803 issued_at: 10,
804 expires_at: 20,
805 epoch: 0,
806 };
807 let mut grant_b = grant_a.clone();
808 grant_b.quota = 2;
809
810 let hash_a = delegated_grant_hash(&grant_a).expect("hash a");
811 let hash_b = delegated_grant_hash(&grant_b).expect("hash b");
812 assert_ne!(hash_a, hash_b);
813 }
814
815 #[test]
816 fn verify_root_delegated_grant_claims_accepts_matching_scope() {
817 let now_secs = 100;
818 let caller = p(2);
819 let target_canister = p(1);
820 let capability = sample_request(10);
821 let proof = sample_delegated_grant_proof(&capability, caller, target_canister, now_secs);
822
823 verify_root_delegated_grant_claims(&capability, &proof, caller, target_canister, now_secs)
824 .expect("matching delegated grant claims must verify");
825 }
826
827 #[test]
828 fn verify_root_delegated_grant_claims_rejects_subject_mismatch() {
829 let now_secs = 100;
830 let caller = p(2);
831 let target_canister = p(1);
832 let capability = sample_request(10);
833 let mut proof =
834 sample_delegated_grant_proof(&capability, caller, target_canister, now_secs);
835 proof.grant.subject = p(3);
836
837 let err = verify_root_delegated_grant_claims(
838 &capability,
839 &proof,
840 caller,
841 target_canister,
842 now_secs,
843 )
844 .expect_err("subject mismatch must fail");
845 assert!(err.message.contains("subject"));
846 }
847
848 #[test]
849 fn verify_root_delegated_grant_claims_rejects_scope_family_mismatch() {
850 let now_secs = 100;
851 let caller = p(2);
852 let target_canister = p(1);
853 let capability = sample_request(10);
854 let mut proof =
855 sample_delegated_grant_proof(&capability, caller, target_canister, now_secs);
856 proof.grant.scope.capability_family = "Upgrade".to_string();
857
858 let err = verify_root_delegated_grant_claims(
859 &capability,
860 &proof,
861 caller,
862 target_canister,
863 now_secs,
864 )
865 .expect_err("scope family mismatch must fail");
866 assert!(err.message.contains("capability_family"));
867 }
868
869 #[test]
870 fn verify_root_delegated_grant_claims_rejects_key_id_mismatch() {
871 let now_secs = 100;
872 let caller = p(2);
873 let target_canister = p(1);
874 let capability = sample_request(10);
875 let mut proof =
876 sample_delegated_grant_proof(&capability, caller, target_canister, now_secs);
877 proof.key_id = DELEGATED_GRANT_KEY_ID_V1 + 1;
878
879 let err = verify_root_delegated_grant_claims(
880 &capability,
881 &proof,
882 caller,
883 target_canister,
884 now_secs,
885 )
886 .expect_err("unsupported key_id must fail");
887 assert!(err.message.contains("key_id"));
888 }
889
890 #[test]
891 fn verify_root_delegated_grant_signature_accepts_valid_signature() {
892 let capability = sample_request(10);
893 let proof = sample_delegated_grant_proof(&capability, p(2), p(1), 100);
894 let (public_key, signature) = sign_delegated_grant(7, &proof.grant);
895 DelegationStateOps::set_root_public_key(public_key);
896
897 verify_root_delegated_grant_signature(&proof.grant, &signature)
898 .expect("valid delegated grant signature must verify");
899 }
900
901 #[test]
902 fn verify_root_delegated_grant_signature_rejects_invalid_signature() {
903 let capability = sample_request(10);
904 let proof = sample_delegated_grant_proof(&capability, p(2), p(1), 100);
905 let (public_key, _signature) = sign_delegated_grant(7, &proof.grant);
906 let (_, wrong_signature) = sign_delegated_grant(8, &proof.grant);
907 DelegationStateOps::set_root_public_key(public_key);
908
909 let err = verify_root_delegated_grant_signature(&proof.grant, &wrong_signature)
910 .expect_err("invalid signature must fail");
911 assert!(err.message.contains("signature invalid"));
912 }
913}