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