clawft_kernel/workload_pkg/
pack.rs1use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8use serde_json::Value;
9
10use super::cognitum::COGNITUM_RECORD_KIND;
11use super::manifest::{
12 AttestationRef, COG_ARCHES, CogPackageBody, FileRef, KIND_COG, MANIFEST_FILE, ManifestEnvelope,
13 ManifestError, PackageSource, binary_path,
14};
15use super::verify::MAX_FILE_BYTES;
16
17const MAX_COG_TOML_BYTES: u64 = 256 * 1024;
19pub const COGNITUM_RECORD_PATH: &str = "attestations/cognitum-release-record.json";
21pub const COGNITUM_PROVENANCE_KIND: &str = "cognitum.install.provenance.v1";
26pub const NO_PROVENANCE_KIND: &str = "operator.no-provenance-asserted";
29pub const NO_PROVENANCE_PATH: &str = "attestations/operator-no-provenance.json";
31pub const COGNITUM_PROVENANCE_PATH: &str = "attestations/cognitum-provenance.json";
33const PROVENANCE_FILE: &str = "provenance.json";
35const SHAREABLE_TRUST: [&str; 2] = ["ed25519-signed", "source-build"];
39const TRUST_COGNITUM_SHA256: &str = "cognitum-sha256";
41
42#[derive(Debug, Clone, Default)]
44pub struct CogPackInput {
45 pub cog_dir: PathBuf,
47 pub binaries: Vec<(String, PathBuf)>,
49 pub source: PackageSource,
51 pub cognitum_record: Option<PathBuf>,
53 pub redistributable: bool,
56 pub provenance: Option<PathBuf>,
60 pub allow_no_provenance: bool,
64}
65
66#[derive(Debug, thiserror::Error)]
68pub enum PackError {
69 #[error("pack input: {0}")]
71 Input(String),
72 #[error("pack io on {path}: {msg}")]
74 Io {
75 path: String,
77 msg: String,
79 },
80 #[error(transparent)]
82 Manifest(#[from] ManifestError),
83}
84
85fn io_err(path: &Path, e: impl std::fmt::Display) -> PackError {
86 PackError::Io {
87 path: path.display().to_string(),
88 msg: e.to_string(),
89 }
90}
91
92fn read_limited(path: &Path, max: u64) -> Result<Vec<u8>, PackError> {
93 let meta = std::fs::metadata(path).map_err(|e| io_err(path, e))?;
94 if !meta.is_file() {
95 return Err(io_err(path, "not a regular file"));
96 }
97 if meta.len() > max {
98 return Err(io_err(path, format!("larger than {max} bytes")));
99 }
100 std::fs::read(path).map_err(|e| io_err(path, e))
101}
102
103pub fn cog_identity(toml_text: &str) -> Result<(String, String), PackError> {
105 let doc: toml::Value =
106 toml::from_str(toml_text).map_err(|e| PackError::Input(format!("cog.toml: {e}")))?;
107 let field = |k: &str| {
108 doc.get("cog")
109 .and_then(|c| c.get(k))
110 .and_then(toml::Value::as_str)
111 .map(str::to_string)
112 .ok_or_else(|| PackError::Input(format!("cog.toml: [cog].{k} missing")))
113 };
114 Ok((field("id")?, field("version")?))
115}
116
117fn place(out: &Path, rel: &str, content: &[u8], executable: bool) -> Result<FileRef, PackError> {
119 use std::io::Write;
120 let dest = out.join(rel);
121 if let Some(parent) = dest.parent() {
122 std::fs::create_dir_all(parent).map_err(|e| io_err(parent, e))?;
123 }
124 let mut opts = std::fs::OpenOptions::new();
125 opts.write(true).create_new(true);
126 #[cfg(unix)]
127 {
128 use std::os::unix::fs::OpenOptionsExt;
129 opts.mode(if executable { 0o755 } else { 0o644 });
130 }
131 #[cfg(not(unix))]
132 let _ = executable;
133 let mut f = opts.open(&dest).map_err(|e| io_err(&dest, e))?;
134 f.write_all(content).map_err(|e| io_err(&dest, e))?;
135 Ok(FileRef {
136 path: rel.to_string(),
137 size: content.len() as u64,
138 blake3: blake3::hash(content).to_hex().to_string(),
139 })
140}
141
142struct FoundProvenance {
144 beside: Option<String>,
146 trust: Option<String>,
147 sha256: Option<String>,
148}
149
150fn sha256_hex(bytes: &[u8]) -> String {
151 use sha2::{Digest, Sha256};
152 Sha256::digest(bytes).iter().map(|b| format!("{b:02x}")).collect()
153}
154
155fn find_provenance(input: &CogPackInput) -> Result<Vec<FoundProvenance>, PackError> {
160 let mut candidates: Vec<(Option<String>, PathBuf)> = Vec::new();
161 if let Some(p) = &input.provenance {
162 if !p.is_file() {
163 return Err(PackError::Input(format!("--provenance {}: no such file", p.display())));
164 }
165 candidates.push((None, p.clone()));
166 }
167 candidates.push((None, input.cog_dir.join(PROVENANCE_FILE)));
168 for (arch, bin) in &input.binaries {
169 if let Some(dir) = bin.parent() {
170 candidates.push((Some(arch.clone()), dir.join(PROVENANCE_FILE)));
171 }
172 }
173 let mut seen = std::collections::BTreeSet::new();
174 let mut out = Vec::new();
175 for (beside, path) in candidates {
176 if !path.is_file() {
177 continue;
178 }
179 let canon = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
180 if !seen.insert((canon, beside.clone())) {
182 continue;
183 }
184 let bytes = read_limited(&path, 64 * 1024)?;
185 let doc: Value = serde_json::from_slice(&bytes)
186 .map_err(|e| PackError::Input(format!("{}: {e}", path.display())))?;
187 let get = |k: &str| doc.get(k).and_then(Value::as_str).map(str::to_string);
188 out.push(FoundProvenance { beside, trust: get("trust"), sha256: get("sha256") });
189 }
190 Ok(out)
191}
192
193#[derive(Default)]
195struct ProvenanceDecision {
196 cognitum_stamp: Option<Vec<u8>>,
198 no_provenance_asserted: bool,
200}
201
202fn provenance_decision(
211 input: &CogPackInput,
212 id: &str,
213 version: &str,
214 bin_sha: &BTreeMap<String, String>,
215) -> Result<ProvenanceDecision, PackError> {
216 let found = find_provenance(input)?;
217 if let Some(c) = found.iter().find(|f| f.trust.as_deref() == Some(TRUST_COGNITUM_SHA256)) {
218 if input.redistributable {
219 return Err(PackError::Input(format!(
220 "{PROVENANCE_FILE} marks {id}@{version} as a licensed Cognitum install \
221 (trust = {TRUST_COGNITUM_SHA256}); --redistributable is refused"
222 )));
223 }
224 let stamp = serde_json::json!({
225 "trust": TRUST_COGNITUM_SHA256,
226 "cog_id": id,
227 "version": version,
228 "sha256": c.sha256,
229 });
230 let bytes = serde_json::to_vec_pretty(&stamp)
231 .map_err(|e| PackError::Input(format!("{PROVENANCE_FILE}: {e}")))?;
232 return Ok(ProvenanceDecision { cognitum_stamp: Some(bytes), ..Default::default() });
233 }
234 if !input.redistributable {
235 return Ok(ProvenanceDecision::default());
236 }
237 if found.is_empty() {
238 if input.allow_no_provenance {
239 return Ok(ProvenanceDecision { no_provenance_asserted: true, ..Default::default() });
240 }
241 return Err(PackError::Input(format!(
242 "--redistributable needs a {PROVENANCE_FILE} (--provenance, in the cog dir, or beside a --bin) \
243 showing {id}@{version} is not a licensed Cognitum binary; for your own build from source \
244 pass --no-provenance-ok"
245 )));
246 }
247 for f in &found {
248 match f.trust.as_deref() {
249 Some(t) if SHAREABLE_TRUST.contains(&t) => {}
250 other => {
251 return Err(PackError::Input(format!(
252 "{PROVENANCE_FILE} trust {other:?} is not one of {SHAREABLE_TRUST:?}; --redistributable is refused"
253 )));
254 }
255 }
256 if f.sha256.is_none() {
257 return Err(PackError::Input(format!(
258 "{PROVENANCE_FILE} has no sha256, so it cannot show the binary is shareable; --redistributable is refused"
259 )));
260 }
261 }
262 for (arch, sha) in bin_sha {
263 let covered = found.iter().any(|f| {
264 let same = f.sha256.as_deref().is_some_and(|w| w.eq_ignore_ascii_case(sha));
265 same && f.beside.as_ref().is_none_or(|b| b == arch)
266 });
267 if !covered {
268 return Err(PackError::Input(format!(
269 "no {PROVENANCE_FILE} covers the {arch} binary (sha256 {sha}); --redistributable is refused"
270 )));
271 }
272 }
273 for f in &found {
275 if let (Some(arch), Some(want)) = (&f.beside, &f.sha256)
276 && bin_sha.get(arch).is_none_or(|h| !h.eq_ignore_ascii_case(want))
277 {
278 return Err(PackError::Input(format!(
279 "{PROVENANCE_FILE} beside the {arch} binary does not match it; --redistributable is refused"
280 )));
281 }
282 }
283 Ok(ProvenanceDecision::default())
284}
285
286pub fn write_source_build_provenance(cog_dir: &Path, binary: &Path) -> Result<(), PackError> {
291 let content = read_limited(binary, MAX_FILE_BYTES)?;
292 let doc = serde_json::json!({ "trust": "source-build", "sha256": sha256_hex(&content) });
293 let path = cog_dir.join(PROVENANCE_FILE);
294 let bytes = serde_json::to_vec_pretty(&doc).map_err(|e| PackError::Input(e.to_string()))?;
295 std::fs::write(&path, bytes).map_err(|e| io_err(&path, e))
296}
297
298pub fn pack_cog(input: &CogPackInput, out_dir: &Path) -> Result<ManifestEnvelope, PackError> {
302 if input.binaries.is_empty() {
303 return Err(PackError::Input(
304 "at least one --bin <arch>=<path> is required".into(),
305 ));
306 }
307 let mut seen = BTreeMap::new();
308 for (arch, path) in &input.binaries {
309 if !COG_ARCHES.contains(&arch.as_str()) {
310 return Err(PackError::Input(format!(
311 "unknown arch {arch:?} (expected one of {COG_ARCHES:?})"
312 )));
313 }
314 if seen.insert(arch.clone(), path.clone()).is_some() {
315 return Err(PackError::Input(format!("arch {arch} given twice")));
316 }
317 }
318 if out_dir.exists() {
319 let mut entries = std::fs::read_dir(out_dir).map_err(|e| io_err(out_dir, e))?;
320 if entries.next().is_some() {
321 return Err(PackError::Input(format!(
322 "output directory {} is not empty",
323 out_dir.display()
324 )));
325 }
326 }
327 std::fs::create_dir_all(out_dir).map_err(|e| io_err(out_dir, e))?;
328
329 let toml_path = input.cog_dir.join("cog.toml");
330 let toml_bytes = read_limited(&toml_path, MAX_COG_TOML_BYTES)?;
331 let toml_text = std::str::from_utf8(&toml_bytes)
332 .map_err(|_| PackError::Input("cog.toml is not UTF-8".into()))?;
333 let (id, version) = cog_identity(toml_text)?;
334 let cog_toml = place(out_dir, "cog.toml", &toml_bytes, false)?;
335
336 let mut binaries = BTreeMap::new();
337 let mut bin_sha = BTreeMap::new();
338 for (arch, path) in &seen {
339 let content = read_limited(path, MAX_FILE_BYTES)?;
340 bin_sha.insert(arch.clone(), sha256_hex(&content));
341 binaries.insert(
342 arch.clone(),
343 place(out_dir, &binary_path(arch, &id), &content, true)?,
344 );
345 }
346
347 let mut attestations = Vec::new();
348 if let Some(record_path) = &input.cognitum_record {
349 let content = read_limited(record_path, 256 * 1024)?;
350 serde_json::from_slice::<Value>(&content)
351 .map_err(|e| PackError::Input(format!("cognitum record is not JSON: {e}")))?;
352 attestations.push(AttestationRef {
353 kind: COGNITUM_RECORD_KIND.to_string(),
354 file: place(out_dir, COGNITUM_RECORD_PATH, &content, false)?,
355 });
356 }
357
358 let decision = provenance_decision(input, &id, &version, &bin_sha)?;
359 if let Some(stamp) = decision.cognitum_stamp {
360 attestations.push(AttestationRef {
361 kind: COGNITUM_PROVENANCE_KIND.to_string(),
362 file: place(out_dir, COGNITUM_PROVENANCE_PATH, &stamp, false)?,
363 });
364 }
365 if decision.no_provenance_asserted {
366 let note = serde_json::json!({
367 "asserted_by": "operator",
368 "flag": "--no-provenance-ok",
369 "cog_id": id,
370 "version": version,
371 "claim": "no provenance.json was available; the operator asserts this is not a licensed Cognitum binary",
372 });
373 let bytes = serde_json::to_vec_pretty(¬e)
374 .map_err(|e| PackError::Input(format!("no-provenance assertion: {e}")))?;
375 attestations.push(AttestationRef {
376 kind: NO_PROVENANCE_KIND.to_string(),
377 file: place(out_dir, NO_PROVENANCE_PATH, &bytes, false)?,
378 });
379 }
380
381 let body = CogPackageBody {
382 id,
383 version,
384 cog_toml,
385 binaries,
386 source: input.source.clone(),
387 attestations,
388 redistributable: input.redistributable,
389 };
390 body.validate()?;
391 let value = serde_json::to_value(&body).map_err(|e| ManifestError::Parse(e.to_string()))?;
392 Ok(ManifestEnvelope::new(KIND_COG, value))
393}
394
395pub fn write_manifest(out_dir: &Path, envelope: &ManifestEnvelope) -> Result<PathBuf, PackError> {
397 let path = out_dir.join(MANIFEST_FILE);
398 std::fs::write(&path, envelope.to_pretty_json()?).map_err(|e| io_err(&path, e))?;
399 Ok(path)
400}