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