canic_host/protocol_binding/
mod.rs1mod 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#[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#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct ResolvedProtocolBinding {
41 pub(crate) binding: RegistryProtocolBinding,
42 pub(crate) candid_path: PathBuf,
43}
44
45impl ResolvedProtocolBinding {
46 #[must_use]
48 pub const fn binding(&self) -> &RegistryProtocolBinding {
49 &self.binding
50 }
51
52 #[must_use]
54 pub fn candid_path(&self) -> &std::path::Path {
55 &self.candid_path
56 }
57}
58
59#[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
110pub 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
150pub 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}