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