Skip to main content

canic_core/workflow/rpc/capability/
proof.rs

1use crate::{
2    cdk::types::Principal,
3    dto::{
4        capability::{CapabilityProof, CapabilityProofBlob, DelegatedGrantProof},
5        error::Error,
6        rpc::{CreateCanisterParent, Request},
7    },
8    ops::{
9        ic::IcOps, storage::children::CanisterChildrenOps,
10        storage::registry::subnet::SubnetRegistryOps,
11    },
12};
13use candid::{decode_one, encode_one};
14use std::convert::TryFrom;
15
16/// verify_root_structural_proof
17///
18/// Verify structural proof constraints for capability families that allow it.
19pub(super) fn verify_root_structural_proof(capability: &Request) -> Result<(), Error> {
20    let caller = IcOps::msg_caller();
21
22    if SubnetRegistryOps::get(caller).is_none() {
23        return Err(Error::forbidden(
24            "structural proof requires caller to be registered in subnet registry",
25        ));
26    }
27
28    match capability {
29        Request::Cycles(_) => Ok(()),
30        Request::CreateCanister(request) => verify_root_structural_create(request),
31        Request::UpgradeCanister(request) => {
32            verify_root_structural_child_target(caller, request.canister_pid, "upgrade")
33        }
34        Request::RecycleCanister(request) => {
35            verify_root_structural_child_target(caller, request.canister_pid, "recycle")
36        }
37        Request::IssueRoleAttestation(_) | Request::IssueInternalInvocationProof(_) => {
38            Err(Error::forbidden(
39                "structural proof is only supported for root cycles, child provision, child upgrade, and child recycle capabilities",
40            ))
41        }
42    }
43}
44
45fn verify_root_structural_create(
46    request: &crate::dto::rpc::CreateCanisterRequest,
47) -> Result<(), Error> {
48    if matches!(&request.parent, CreateCanisterParent::ThisCanister) {
49        return Ok(());
50    }
51
52    Err(Error::forbidden(
53        "structural provision proof requires parent=ThisCanister",
54    ))
55}
56
57fn verify_root_structural_child_target(
58    caller: Principal,
59    target_pid: Principal,
60    operation: &str,
61) -> Result<(), Error> {
62    let target = SubnetRegistryOps::get(target_pid).ok_or_else(|| {
63        Error::forbidden(format!(
64            "structural proof requires registered {operation} target"
65        ))
66    })?;
67    if target.parent_pid != Some(caller) {
68        return Err(Error::forbidden(format!(
69            "structural proof requires {operation} target to be a direct child of caller"
70        )));
71    }
72    Ok(())
73}
74
75/// verify_nonroot_structural_cycles_proof
76///
77/// Verify that a structural cycles request came from a cached direct child.
78pub(super) fn verify_nonroot_structural_cycles_proof() -> Result<(), Error> {
79    let caller = IcOps::msg_caller();
80
81    if !CanisterChildrenOps::contains_pid(&caller) {
82        return Err(Error::forbidden(
83            "structural proof requires caller to be a direct child of receiver",
84        ));
85    }
86
87    Ok(())
88}
89
90/// verify_capability_hash_binding
91///
92/// Ensure the proof hash matches canonical capability payload bytes.
93pub(super) fn verify_capability_hash_binding(
94    target_canister: Principal,
95    capability_version: u16,
96    capability: &Request,
97    capability_hash: [u8; 32],
98) -> Result<(), Error> {
99    let expected = super::root_capability_hash(target_canister, capability_version, capability)?;
100    if capability_hash != expected {
101        return Err(Error::invalid(
102            "capability_hash does not match capability payload",
103        ));
104    }
105
106    Ok(())
107}
108
109// --- Wire Encoding ------------------------------------------------------
110
111// Encode the full delegated-grant proof into the compact shared wire blob.
112pub(super) fn encode_delegated_grant_blob(
113    proof: &DelegatedGrantProof,
114) -> Result<CapabilityProofBlob, Error> {
115    Ok(CapabilityProofBlob {
116        proof_version: proof.proof_version,
117        capability_hash: proof.capability_hash,
118        payload: encode_one(proof).map_err(|err| {
119            Error::internal(format!("failed to encode delegated grant proof: {err}"))
120        })?,
121    })
122}
123
124// Decode a delegated-grant wire blob back into its concrete proof payload.
125pub(super) fn decode_delegated_grant_blob(
126    blob: &CapabilityProofBlob,
127) -> Result<DelegatedGrantProof, Error> {
128    let proof: DelegatedGrantProof = decode_one(&blob.payload)
129        .map_err(|err| Error::invalid(format!("failed to decode delegated grant proof: {err}")))?;
130
131    if proof.proof_version != blob.proof_version {
132        return Err(Error::invalid(
133            "delegated grant proof_version does not match wire header",
134        ));
135    }
136    if proof.capability_hash != blob.capability_hash {
137        return Err(Error::invalid(
138            "delegated grant capability_hash does not match wire header",
139        ));
140    }
141
142    Ok(proof)
143}
144
145impl TryFrom<DelegatedGrantProof> for CapabilityProof {
146    type Error = Error;
147
148    fn try_from(value: DelegatedGrantProof) -> Result<Self, Self::Error> {
149        Ok(Self::DelegatedGrant(encode_delegated_grant_blob(&value)?))
150    }
151}