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