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/// 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_regular_bytes(&candid_path) {
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(RegularFileReadError::Io(source)) => {
196            return Err(ProtocolBindingError::ReadCandid {
197                path: candid_path,
198                source,
199            });
200        }
201        Err(_) => return Err(ProtocolBindingError::UnsafeCandid { path: candid_path }),
202    };
203    let observed = derive_protocol_profile_hashes(
204        &binding.release_identity,
205        &binding.role,
206        &binding.capabilities,
207        &candid,
208    );
209    if observed.candid_sha256 != binding.candid_sha256 {
210        return Err(ProtocolBindingError::CandidHashMismatch {
211            canister: target.to_string(),
212        });
213    }
214    if observed.protocol_profile_digest != binding.protocol_profile_digest {
215        return Err(ProtocolBindingError::ProfileDigestMismatch {
216            canister: target.to_string(),
217        });
218    }
219    Ok(ResolvedProtocolBinding {
220        binding,
221        candid_path,
222    })
223}