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