Skip to main content

canic_core/workflow/rpc/capability/
proof.rs

1use 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
18/// verify_root_structural_proof
19///
20/// Verify structural proof constraints for capability families that allow it.
21pub(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
77/// verify_nonroot_structural_cycles_proof
78///
79/// Verify that a structural cycles request came from a cached direct child.
80pub(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
92/// verify_capability_hash_binding
93///
94/// Ensure the proof hash matches canonical capability payload bytes.
95pub(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
111// --- Wire Encoding ------------------------------------------------------
112
113// Encode the full role-attestation proof into the compact shared wire blob.
114pub(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
126// Decode a role-attestation wire blob back into its concrete proof payload.
127pub(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
147// Encode the full delegated-grant proof into the compact shared wire blob.
148pub(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
160// Decode a delegated-grant wire blob back into its concrete proof payload.
161pub(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}