Skip to main content

canic_host/protocol_binding/
mod.rs

1//! Module: protocol_binding
2//!
3//! Responsibility: validate exact host-side protocol bindings before Canister transport.
4//! Does not own: Fleet discovery, artifact production, or live Overview negotiation.
5//! Boundary: immutable release/Directory evidence must reproduce the local Candid profile.
6
7use crate::{
8    icp::existing_local_canister_candid_path, registry::RegistryEntry,
9    release_set::CanicInfrastructureArtifactEntry,
10};
11use canic_core::{
12    ids::CanisterRole,
13    role_contract::{ProtocolProfileDigest, RoleCapabilityKey, derive_protocol_profile_hashes},
14};
15use std::{collections::BTreeSet, fs, io, path::PathBuf};
16use thiserror::Error as ThisError;
17
18/// Complete immutable protocol identity selected before one role call.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct RegistryProtocolBinding {
21    pub release_identity: String,
22    pub role: CanisterRole,
23    pub capabilities: BTreeSet<RoleCapabilityKey>,
24    pub candid_sha256: [u8; 32],
25    pub protocol_profile_digest: ProtocolProfileDigest,
26}
27
28/// Validated binding plus the exact local Candid sidecar admitted for transport.
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct ResolvedProtocolBinding {
31    pub(crate) binding: RegistryProtocolBinding,
32    pub(crate) candid_path: PathBuf,
33}
34
35impl ResolvedProtocolBinding {
36    /// Return the exact immutable identity verified by the resolver.
37    #[must_use]
38    pub const fn binding(&self) -> &RegistryProtocolBinding {
39        &self.binding
40    }
41
42    /// Return the exact verified local Candid sidecar admitted for transport.
43    #[must_use]
44    pub fn candid_path(&self) -> &std::path::Path {
45        &self.candid_path
46    }
47}
48
49/// Failure to select an exact protocol profile before transport.
50#[derive(Debug, ThisError)]
51pub enum ProtocolBindingError {
52    #[error("Canister {canister} has no exact protocol binding in protected registry metadata")]
53    MissingBinding { canister: String },
54
55    #[error("Canister {canister} has no exact role in protected registry metadata")]
56    MissingRole { canister: String },
57
58    #[error(
59        "Canister {canister} registry role {registry_role} conflicts with protocol role {protocol_role}"
60    )]
61    RoleMismatch {
62        canister: String,
63        registry_role: String,
64        protocol_role: String,
65    },
66
67    #[error("Canister {canister} protocol release identity is empty")]
68    MissingReleaseIdentity { canister: String },
69
70    #[error("Canister {canister} has no local Candid sidecar for exact role {role}")]
71    MissingCandid { canister: String, role: String },
72
73    #[error("failed to read exact Candid sidecar {path}: {source}")]
74    ReadCandid {
75        path: PathBuf,
76        #[source]
77        source: io::Error,
78    },
79
80    #[error("Canister {canister} local Candid hash conflicts with protected registry metadata")]
81    CandidHashMismatch { canister: String },
82
83    #[error(
84        "Canister {canister} protocol-profile digest conflicts with protected registry metadata"
85    )]
86    ProfileDigestMismatch { canister: String },
87
88    #[error(
89        "infrastructure artifact role {artifact_role} conflicts with protocol role {protocol_role}"
90    )]
91    InfrastructureRoleMismatch {
92        artifact_role: String,
93        protocol_role: String,
94    },
95}
96
97/// Select and verify one exact registry-owned local binding before transport.
98pub fn resolve_registry_protocol_binding(
99    icp_root: &std::path::Path,
100    artifact_environment: &str,
101    entry: &RegistryEntry,
102) -> Result<ResolvedProtocolBinding, ProtocolBindingError> {
103    let binding =
104        entry
105            .protocol_binding
106            .clone()
107            .ok_or_else(|| ProtocolBindingError::MissingBinding {
108                canister: entry.pid.clone(),
109            })?;
110    let registry_role = entry
111        .role
112        .as_deref()
113        .ok_or_else(|| ProtocolBindingError::MissingRole {
114            canister: entry.pid.clone(),
115        })?;
116    if binding.release_identity.trim().is_empty() {
117        return Err(ProtocolBindingError::MissingReleaseIdentity {
118            canister: entry.pid.clone(),
119        });
120    }
121    if registry_role != binding.role.as_str() {
122        return Err(ProtocolBindingError::RoleMismatch {
123            canister: entry.pid.clone(),
124            registry_role: registry_role.to_string(),
125            protocol_role: binding.role.to_string(),
126        });
127    }
128    resolve_local_protocol_binding(icp_root, artifact_environment, &entry.pid, binding)
129}
130
131/// Select and verify one exact immutable infrastructure-artifact binding before transport.
132pub fn resolve_infrastructure_protocol_binding(
133    icp_root: &std::path::Path,
134    artifact_environment: &str,
135    artifact: &CanicInfrastructureArtifactEntry,
136) -> Result<ResolvedProtocolBinding, ProtocolBindingError> {
137    if artifact.protocol_role.as_str() != artifact.role.protocol_role_name() {
138        return Err(ProtocolBindingError::InfrastructureRoleMismatch {
139            artifact_role: artifact.role.as_str().to_string(),
140            protocol_role: artifact.protocol_role.to_string(),
141        });
142    }
143    resolve_local_protocol_binding(
144        icp_root,
145        artifact_environment,
146        artifact.role.as_str(),
147        RegistryProtocolBinding {
148            release_identity: artifact.protocol_release_identity.clone(),
149            role: artifact.protocol_role.clone(),
150            capabilities: artifact.protocol_capabilities.clone(),
151            candid_sha256: artifact.candid_sha256,
152            protocol_profile_digest: artifact.protocol_profile_digest,
153        },
154    )
155}
156
157fn resolve_local_protocol_binding(
158    icp_root: &std::path::Path,
159    artifact_environment: &str,
160    target: &str,
161    binding: RegistryProtocolBinding,
162) -> Result<ResolvedProtocolBinding, ProtocolBindingError> {
163    if binding.release_identity.trim().is_empty() {
164        return Err(ProtocolBindingError::MissingReleaseIdentity {
165            canister: target.to_string(),
166        });
167    }
168    let candid_path =
169        existing_local_canister_candid_path(icp_root, artifact_environment, binding.role.as_str())
170            .ok_or_else(|| ProtocolBindingError::MissingCandid {
171                canister: target.to_string(),
172                role: binding.role.to_string(),
173            })?;
174    let candid = fs::read(&candid_path).map_err(|source| ProtocolBindingError::ReadCandid {
175        path: candid_path.clone(),
176        source,
177    })?;
178    let observed = derive_protocol_profile_hashes(
179        &binding.release_identity,
180        &binding.role,
181        &binding.capabilities,
182        &candid,
183    );
184    if observed.candid_sha256 != binding.candid_sha256 {
185        return Err(ProtocolBindingError::CandidHashMismatch {
186            canister: target.to_string(),
187        });
188    }
189    if observed.protocol_profile_digest != binding.protocol_profile_digest {
190        return Err(ProtocolBindingError::ProfileDigestMismatch {
191            canister: target.to_string(),
192        });
193    }
194    Ok(ResolvedProtocolBinding {
195        binding,
196        candid_path,
197    })
198}
199
200#[cfg(test)]
201mod tests;