Skip to main content

canic_core/api/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::{Request, RequestFamily},
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    if capability.family() == RequestFamily::RequestCycles {
31        return Ok(());
32    }
33
34    if let Some(request) = capability.upgrade_request() {
35        let target = SubnetRegistryOps::get(request.canister_pid).ok_or_else(|| {
36            Error::forbidden("structural proof requires registered upgrade target")
37        })?;
38        if target.parent_pid != Some(caller) {
39            return Err(Error::forbidden(
40                "structural proof requires upgrade target to be a direct child of caller",
41            ));
42        }
43        return Ok(());
44    }
45
46    Err(Error::forbidden(
47        "structural proof is only supported for root cycles and upgrade capabilities",
48    ))
49}
50
51/// verify_nonroot_structural_cycles_proof
52///
53/// Verify that a structural cycles request came from a cached direct child.
54pub(super) fn verify_nonroot_structural_cycles_proof() -> Result<(), Error> {
55    let caller = IcOps::msg_caller();
56
57    if !CanisterChildrenOps::contains_pid(&caller) {
58        return Err(Error::forbidden(
59            "structural proof requires caller to be a direct child of receiver",
60        ));
61    }
62
63    Ok(())
64}
65
66/// verify_capability_hash_binding
67///
68/// Ensure the proof hash matches canonical capability payload bytes.
69pub(super) fn verify_capability_hash_binding(
70    target_canister: Principal,
71    capability_version: u16,
72    capability: &Request,
73    capability_hash: [u8; 32],
74) -> Result<(), Error> {
75    let expected = super::root_capability_hash(target_canister, capability_version, capability)?;
76    if capability_hash != expected {
77        return Err(Error::invalid(
78            "capability_hash does not match capability payload",
79        ));
80    }
81
82    Ok(())
83}
84
85// --- Wire Encoding ------------------------------------------------------
86
87// Encode the full role-attestation proof into the compact shared wire blob.
88pub(super) fn encode_role_attestation_blob(
89    proof: &RoleAttestationProof,
90) -> Result<CapabilityProofBlob, Error> {
91    Ok(CapabilityProofBlob {
92        proof_version: proof.proof_version,
93        capability_hash: proof.capability_hash,
94        payload: encode_one(proof).map_err(|err| {
95            Error::internal(format!("failed to encode role attestation proof: {err}"))
96        })?,
97    })
98}
99
100// Decode a role-attestation wire blob back into its concrete proof payload.
101pub(super) fn decode_role_attestation_blob(
102    blob: &CapabilityProofBlob,
103) -> Result<RoleAttestationProof, Error> {
104    let proof: RoleAttestationProof = decode_one(&blob.payload)
105        .map_err(|err| Error::invalid(format!("failed to decode role attestation proof: {err}")))?;
106
107    if proof.proof_version != blob.proof_version {
108        return Err(Error::invalid(
109            "role attestation proof_version does not match wire header",
110        ));
111    }
112    if proof.capability_hash != blob.capability_hash {
113        return Err(Error::invalid(
114            "role attestation capability_hash does not match wire header",
115        ));
116    }
117
118    Ok(proof)
119}
120
121// Encode the full delegated-grant proof into the compact shared wire blob.
122pub(super) fn encode_delegated_grant_blob(
123    proof: &DelegatedGrantProof,
124) -> Result<CapabilityProofBlob, Error> {
125    Ok(CapabilityProofBlob {
126        proof_version: proof.proof_version,
127        capability_hash: proof.capability_hash,
128        payload: encode_one(proof).map_err(|err| {
129            Error::internal(format!("failed to encode delegated grant proof: {err}"))
130        })?,
131    })
132}
133
134// Decode a delegated-grant wire blob back into its concrete proof payload.
135pub(super) fn decode_delegated_grant_blob(
136    blob: &CapabilityProofBlob,
137) -> Result<DelegatedGrantProof, Error> {
138    let proof: DelegatedGrantProof = decode_one(&blob.payload)
139        .map_err(|err| Error::invalid(format!("failed to decode delegated grant proof: {err}")))?;
140
141    if proof.proof_version != blob.proof_version {
142        return Err(Error::invalid(
143            "delegated grant proof_version does not match wire header",
144        ));
145    }
146    if proof.capability_hash != blob.capability_hash {
147        return Err(Error::invalid(
148            "delegated grant capability_hash does not match wire header",
149        ));
150    }
151
152    Ok(proof)
153}
154
155impl TryFrom<RoleAttestationProof> for CapabilityProof {
156    type Error = Error;
157
158    fn try_from(value: RoleAttestationProof) -> Result<Self, Self::Error> {
159        Ok(Self::RoleAttestation(encode_role_attestation_blob(&value)?))
160    }
161}
162
163impl TryFrom<DelegatedGrantProof> for CapabilityProof {
164    type Error = Error;
165
166    fn try_from(value: DelegatedGrantProof) -> Result<Self, Self::Error> {
167        Ok(Self::DelegatedGrant(encode_delegated_grant_blob(&value)?))
168    }
169}