canic_core/workflow/rpc/capability/
proof.rs1use crate::{
2 cdk::types::Principal,
3 dto::{
4 capability::{
5 CapabilityProof, CapabilityProofBlob, DelegatedGrantProof, RoleAttestationProof,
6 },
7 error::Error,
8 rpc::{CreateCanisterParent, Request},
9 },
10 ops::{
11 ic::IcOps, storage::children::CanisterChildrenOps,
12 storage::registry::subnet::SubnetRegistryOps,
13 },
14};
15use candid::{decode_one, encode_one};
16use std::convert::TryFrom;
17
18pub(super) fn verify_root_structural_proof(capability: &Request) -> Result<(), Error> {
22 let caller = IcOps::msg_caller();
23
24 if SubnetRegistryOps::get(caller).is_none() {
25 return Err(Error::forbidden(
26 "structural proof requires caller to be registered in subnet registry",
27 ));
28 }
29
30 match capability {
31 Request::Cycles(_) => Ok(()),
32 Request::CreateCanister(request) => verify_root_structural_create(request),
33 Request::UpgradeCanister(request) => {
34 verify_root_structural_child_target(caller, request.canister_pid, "upgrade")
35 }
36 Request::RecycleCanister(request) => {
37 verify_root_structural_child_target(caller, request.canister_pid, "recycle")
38 }
39 Request::IssueRoleAttestation(_) | Request::IssueInternalInvocationProof(_) => {
40 Err(Error::forbidden(
41 "structural proof is only supported for root cycles, child provision, child upgrade, and child recycle capabilities",
42 ))
43 }
44 }
45}
46
47fn verify_root_structural_create(
48 request: &crate::dto::rpc::CreateCanisterRequest,
49) -> Result<(), Error> {
50 if matches!(&request.parent, CreateCanisterParent::ThisCanister) {
51 return Ok(());
52 }
53
54 Err(Error::forbidden(
55 "structural provision proof requires parent=ThisCanister",
56 ))
57}
58
59fn verify_root_structural_child_target(
60 caller: Principal,
61 target_pid: Principal,
62 operation: &str,
63) -> Result<(), Error> {
64 let target = SubnetRegistryOps::get(target_pid).ok_or_else(|| {
65 Error::forbidden(format!(
66 "structural proof requires registered {operation} target"
67 ))
68 })?;
69 if target.parent_pid != Some(caller) {
70 return Err(Error::forbidden(format!(
71 "structural proof requires {operation} target to be a direct child of caller"
72 )));
73 }
74 Ok(())
75}
76
77pub(super) fn verify_nonroot_structural_cycles_proof() -> Result<(), Error> {
81 let caller = IcOps::msg_caller();
82
83 if !CanisterChildrenOps::contains_pid(&caller) {
84 return Err(Error::forbidden(
85 "structural proof requires caller to be a direct child of receiver",
86 ));
87 }
88
89 Ok(())
90}
91
92pub(super) fn verify_capability_hash_binding(
96 target_canister: Principal,
97 capability_version: u16,
98 capability: &Request,
99 capability_hash: [u8; 32],
100) -> Result<(), Error> {
101 let expected = super::root_capability_hash(target_canister, capability_version, capability)?;
102 if capability_hash != expected {
103 return Err(Error::invalid(
104 "capability_hash does not match capability payload",
105 ));
106 }
107
108 Ok(())
109}
110
111pub(super) fn encode_role_attestation_blob(
115 proof: &RoleAttestationProof,
116) -> Result<CapabilityProofBlob, Error> {
117 Ok(CapabilityProofBlob {
118 proof_version: proof.proof_version,
119 capability_hash: proof.capability_hash,
120 payload: encode_one(proof).map_err(|err| {
121 Error::internal(format!("failed to encode role attestation proof: {err}"))
122 })?,
123 })
124}
125
126pub(super) fn decode_role_attestation_blob(
128 blob: &CapabilityProofBlob,
129) -> Result<RoleAttestationProof, Error> {
130 let proof: RoleAttestationProof = decode_one(&blob.payload)
131 .map_err(|err| Error::invalid(format!("failed to decode role attestation proof: {err}")))?;
132
133 if proof.proof_version != blob.proof_version {
134 return Err(Error::invalid(
135 "role attestation proof_version does not match wire header",
136 ));
137 }
138 if proof.capability_hash != blob.capability_hash {
139 return Err(Error::invalid(
140 "role attestation capability_hash does not match wire header",
141 ));
142 }
143
144 Ok(proof)
145}
146
147pub(super) fn encode_delegated_grant_blob(
149 proof: &DelegatedGrantProof,
150) -> Result<CapabilityProofBlob, Error> {
151 Ok(CapabilityProofBlob {
152 proof_version: proof.proof_version,
153 capability_hash: proof.capability_hash,
154 payload: encode_one(proof).map_err(|err| {
155 Error::internal(format!("failed to encode delegated grant proof: {err}"))
156 })?,
157 })
158}
159
160pub(super) fn decode_delegated_grant_blob(
162 blob: &CapabilityProofBlob,
163) -> Result<DelegatedGrantProof, Error> {
164 let proof: DelegatedGrantProof = decode_one(&blob.payload)
165 .map_err(|err| Error::invalid(format!("failed to decode delegated grant proof: {err}")))?;
166
167 if proof.proof_version != blob.proof_version {
168 return Err(Error::invalid(
169 "delegated grant proof_version does not match wire header",
170 ));
171 }
172 if proof.capability_hash != blob.capability_hash {
173 return Err(Error::invalid(
174 "delegated grant capability_hash does not match wire header",
175 ));
176 }
177
178 Ok(proof)
179}
180
181impl TryFrom<RoleAttestationProof> for CapabilityProof {
182 type Error = Error;
183
184 fn try_from(value: RoleAttestationProof) -> Result<Self, Self::Error> {
185 Ok(Self::RoleAttestation(encode_role_attestation_blob(&value)?))
186 }
187}
188
189impl TryFrom<DelegatedGrantProof> for CapabilityProof {
190 type Error = Error;
191
192 fn try_from(value: DelegatedGrantProof) -> Result<Self, Self::Error> {
193 Ok(Self::DelegatedGrant(encode_delegated_grant_blob(&value)?))
194 }
195}