1use std::fs::File;
19use std::io::Read;
20use std::path::Path;
21
22use crate::mesh_artifact::{ArtifactExchange, ExchangeError};
23use crate::mesh_artifact_transfer::{FetchError, PeerSet};
24use crate::mesh_artifact_wire::{ArtifactId, ArtifactKey};
25use crate::mesh_swarm_state::GrantOrigin;
26use crate::workload_kind::KindRegistry;
27use crate::workload_pkg::codec::{hex_decode_exact, hex_encode};
28use crate::workload_pkg::manifest::{FileRef, MANIFEST_FILE, MAX_MANIFEST_BYTES};
29use crate::workload_pkg::verify::{
30 VerifiedPackage, VerifyError, read_bounded, verify_manifest_signatures_in,
31};
32use crate::workload_pkg::{TrustAnchors, VerifyPolicy, verify_stored_in};
33
34#[derive(Debug, Clone)]
36pub struct ExchangedPackage {
37 pub package_id: String,
39 pub manifest_hash: String,
41 pub files: Vec<(String, ArtifactId)>,
43 pub verified: VerifiedPackage,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
49pub enum PackageExchangeError {
50 #[error(transparent)]
52 Verify(#[from] VerifyError),
53 #[error(transparent)]
55 Exchange(#[from] ExchangeError),
56 #[error(transparent)]
58 Fetch(#[from] FetchError),
59}
60
61pub(crate) fn signer_keys(verified: &VerifiedPackage, anchors: &TrustAnchors) -> Vec<String> {
64 verified
65 .signers
66 .iter()
67 .filter_map(|s| {
68 anchors
69 .signers
70 .iter()
71 .chain(anchors.cognitum.iter())
72 .find(|k| k.key_id == s.key_id)
73 })
74 .map(|k| hex_encode(&k.public_key))
75 .collect()
76}
77
78pub(crate) fn grant_origin(verified: &VerifiedPackage) -> GrantOrigin {
92 let cognitum_record = verified
93 .body
94 .attestations
95 .iter()
96 .any(|a| a.kind.to_ascii_lowercase().starts_with("cognitum."));
97 let cognitum_url = verified
98 .body
99 .source
100 .release_url
101 .as_deref()
102 .is_some_and(|u| u.to_ascii_lowercase().contains("cognitum"));
103 if cognitum_record || cognitum_url {
104 GrantOrigin::Cognitum {
105 cog_id: verified.body.id.clone(),
106 version: verified.body.version.clone(),
107 }
108 } else if verified.body.redistributable {
109 GrantOrigin::OptIn
110 } else {
111 GrantOrigin::NotFlagged
112 }
113}
114
115pub(crate) fn pinned(file: &FileRef) -> Result<([u8; 32], u64), VerifyError> {
116 let hash = hex_decode_exact::<32>(&file.blake3)
117 .ok_or_else(|| VerifyError::Manifest(format!("{}: bad blake3", file.path)))?;
118 Ok((hash, file.size))
119}
120
121impl ArtifactExchange {
122 pub fn seed_package(
125 &self,
126 manifest: &[u8],
127 anchors: &TrustAnchors,
128 open: &mut dyn FnMut(&FileRef) -> std::io::Result<Box<dyn Read>>,
129 ) -> Result<ExchangedPackage, PackageExchangeError> {
130 self.seed_package_in(manifest, anchors, &KindRegistry::builtin(), open)
131 }
132
133 pub fn seed_package_in(
135 &self,
136 manifest: &[u8],
137 anchors: &TrustAnchors,
138 kinds: &KindRegistry,
139 open: &mut dyn FnMut(&FileRef) -> std::io::Result<Box<dyn Read>>,
140 ) -> Result<ExchangedPackage, PackageExchangeError> {
141 let verified = verify_manifest_signatures_in(manifest, anchors, kinds)?;
142 self.refuse_if_revoked(&verified, anchors, blake3::hash(manifest).as_bytes())?;
143 let mut files = Vec::new();
144 for file in verified.body.files() {
145 let expect = pinned(file)?;
146 let mut reader = open(file).map_err(|e| VerifyError::Io {
147 path: file.path.clone(),
148 msg: e.to_string(),
149 })?;
150 let d = self
151 .seed_reader(reader.as_mut(), Some(expect))
152 .map_err(|e| match e {
153 ExchangeError::Mismatch(m) => {
154 PackageExchangeError::Verify(VerifyError::HashMismatch {
155 path: file.path.clone(),
156 expected: file.blake3.clone(),
157 actual: m,
158 })
159 }
160 other => other.into(),
161 })?;
162 files.push((file.path.clone(), d.id()));
163 }
164 let m = self.seed_bytes(manifest)?;
165 self.materialize(&m.id())?;
166 for (_, id) in &files {
167 self.materialize(id)?;
168 }
169 Ok(self.authorize(verified, m.content_hash, files, anchors))
170 }
171
172 pub fn seed_package_dir(
174 &self,
175 dir: &Path,
176 anchors: &TrustAnchors,
177 ) -> Result<ExchangedPackage, PackageExchangeError> {
178 self.seed_package_dir_in(dir, anchors, &KindRegistry::builtin())
179 }
180
181 pub fn seed_package_dir_in(
183 &self,
184 dir: &Path,
185 anchors: &TrustAnchors,
186 kinds: &KindRegistry,
187 ) -> Result<ExchangedPackage, PackageExchangeError> {
188 let manifest = read_bounded(
189 &dir.join(MANIFEST_FILE),
190 MANIFEST_FILE,
191 MAX_MANIFEST_BYTES as u64,
192 )?;
193 self.seed_package_dir_manifest(dir, &manifest, anchors, kinds)
194 }
195
196 pub fn seed_package_dir_manifest(
200 &self,
201 dir: &Path,
202 manifest: &[u8],
203 anchors: &TrustAnchors,
204 kinds: &KindRegistry,
205 ) -> Result<ExchangedPackage, PackageExchangeError> {
206 let root = dir
207 .canonicalize()
208 .map_err(|e| ExchangeError::Io(e.to_string()))?;
209 self.seed_package_in(manifest, anchors, kinds, &mut |file| {
210 let real = root.join(&file.path).canonicalize()?;
213 if !real.starts_with(&root) {
214 return Err(std::io::Error::other("resolves outside the package"));
215 }
216 Ok(Box::new(File::open(real)?) as Box<dyn Read>)
217 })
218 }
219
220 pub async fn fetch_package(
225 &self,
226 peers: &mut PeerSet,
227 manifest_hash: &str,
228 anchors: &TrustAnchors,
229 ) -> Result<ExchangedPackage, PackageExchangeError> {
230 self.fetch_package_in(peers, manifest_hash, anchors, &KindRegistry::builtin())
231 .await
232 }
233
234 pub async fn fetch_package_in(
238 &self,
239 peers: &mut PeerSet,
240 manifest_hash: &str,
241 anchors: &TrustAnchors,
242 kinds: &KindRegistry,
243 ) -> Result<ExchangedPackage, PackageExchangeError> {
244 let mh = hex_decode_exact::<32>(manifest_hash).ok_or_else(|| {
245 VerifyError::Manifest("manifest hash must be 64 lower-case hex".into())
246 })?;
247 let m = self.fetch(peers, ArtifactKey::Content(mh)).await?;
248 if m.descriptor.total_size > MAX_MANIFEST_BYTES as u64 {
249 return Err(VerifyError::Manifest("manifest too large".into()).into());
250 }
251 let manifest = self.read_all(&m.id)?;
252 let verified = verify_manifest_signatures_in(&manifest, anchors, kinds)?;
253 self.refuse_if_revoked(&verified, anchors, &mh)?;
254 let grant = self.authorize(verified.clone(), mh, Vec::new(), anchors);
257
258 let mut files = Vec::new();
259 let mut all_small = true;
260 for file in verified.body.files() {
261 let (hash, size) = pinned(file)?;
262 let got = self.fetch(peers, ArtifactKey::Content(hash)).await?;
263 if got.descriptor.total_size != size {
264 return Err(VerifyError::HashMismatch {
265 path: file.path.clone(),
266 expected: format!("{size} bytes"),
267 actual: format!("{} bytes", got.descriptor.total_size),
268 }
269 .into());
270 }
271 all_small &= size <= self.config().materialize_limit;
272 files.push((file.path.clone(), got.id));
273 }
274 if all_small {
275 verify_stored_in(
277 self.store(),
278 &hex_encode(&mh),
279 anchors,
280 &VerifyPolicy::default(),
281 kinds,
282 )?;
283 }
284 Ok(ExchangedPackage { files, ..grant })
285 }
286
287 pub(crate) fn refuse_if_revoked(
289 &self,
290 verified: &VerifiedPackage,
291 anchors: &TrustAnchors,
292 manifest_hash: &[u8; 32],
293 ) -> Result<(), ExchangeError> {
294 let signers = signer_keys(verified, anchors);
295 if self.is_revoked_subject(&verified.package_id, &signers, manifest_hash) {
296 return Err(ExchangeError::Revoked(format!(
297 "package {} (or its signer or manifest) is revoked",
298 verified.package_id
299 )));
300 }
301 Ok(())
302 }
303
304 pub(crate) fn authorize(
305 &self,
306 verified: VerifiedPackage,
307 manifest_hash: [u8; 32],
308 files: Vec<(String, ArtifactId)>,
309 anchors: &TrustAnchors,
310 ) -> ExchangedPackage {
311 let signers = signer_keys(&verified, anchors);
312 let origin = grant_origin(&verified);
313 self.grant_with(manifest_hash, &verified.package_id, signers.clone(), origin.clone());
314 for file in verified.body.files() {
315 if let Ok((hash, _)) = pinned(file) {
316 self.grant_with(hash, &verified.package_id, signers.clone(), origin.clone());
317 }
318 }
319 ExchangedPackage {
320 package_id: verified.package_id.clone(),
321 manifest_hash: hex_encode(&manifest_hash),
322 files,
323 verified,
324 }
325 }
326}