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