1use std::io::Read;
10use std::path::{Path, PathBuf};
11
12use ed25519_dalek::{Signature, VerifyingKey};
13
14use crate::workload_kind::{KindRegistry, validate_envelope};
15
16use super::codec::hex_decode_exact;
17use super::cognitum;
18use super::manifest::{
19 CogPackageBody, FileRef, KIND_COG, MANIFEST_FILE, MAX_MANIFEST_BYTES, ManifestEnvelope,
20 ManifestError,
21};
22use super::trust::{KeyOrigin, TrustAnchors};
23
24pub const MAX_FILE_BYTES: u64 = 512 * 1024 * 1024;
26
27#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
29pub enum VerifyError {
30 #[error("manifest invalid: {0}")]
32 Manifest(String),
33 #[error("package has no signature")]
35 MissingSignature,
36 #[error("no signature from a pinned signer (saw: {key_ids})")]
38 UntrustedSigner {
39 key_ids: String,
41 },
42 #[error("signature from {key_id} does not verify")]
45 BadSignature {
46 key_id: String,
48 },
49 #[error("package file missing: {path}")]
51 FileMissing {
52 path: String,
54 },
55 #[error("package file tampered: {path} (expected {expected}, got {actual})")]
57 HashMismatch {
58 path: String,
60 expected: String,
62 actual: String,
64 },
65 #[error("cognitum release record rejected: {0}")]
67 CognitumRecord(String),
68 #[error("io error on {path}: {msg}")]
70 Io {
71 path: String,
73 msg: String,
75 },
76}
77
78impl VerifyError {
79 pub fn code(&self) -> &'static str {
81 match self {
82 Self::Manifest(_) => "manifest-invalid",
83 Self::MissingSignature => "missing-signature",
84 Self::UntrustedSigner { .. } => "untrusted-signer",
85 Self::BadSignature { .. } => "bad-signature",
86 Self::FileMissing { .. } => "file-missing",
87 Self::HashMismatch { .. } => "file-tampered",
88 Self::CognitumRecord(_) => "cognitum-record-rejected",
89 Self::Io { .. } => "io-error",
90 }
91 }
92}
93
94impl From<ManifestError> for VerifyError {
95 fn from(e: ManifestError) -> Self {
96 Self::Manifest(e.to_string())
97 }
98}
99
100#[derive(Debug, Clone, Default)]
102pub struct VerifyPolicy {
103 pub accept_cognitum_release: bool,
107 pub record_cog_toml_pins: Vec<String>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct AcceptedSigner {
119 pub key_id: String,
121 pub origin: KeyOrigin,
123}
124
125#[derive(Debug, Clone)]
127pub struct VerifiedPackage {
128 pub package_id: String,
130 pub envelope: ManifestEnvelope,
132 pub body: CogPackageBody,
134 pub signers: Vec<AcceptedSigner>,
136}
137
138pub trait FileSource {
140 fn read(&self, file: &FileRef) -> Result<Vec<u8>, VerifyError>;
142}
143
144pub struct DirSource {
146 root: PathBuf,
147}
148
149impl DirSource {
150 pub fn new(root: impl Into<PathBuf>) -> Self {
152 Self { root: root.into() }
153 }
154}
155
156impl FileSource for DirSource {
157 fn read(&self, file: &FileRef) -> Result<Vec<u8>, VerifyError> {
158 let path = self.root.join(&file.path);
161 if let (Ok(root), Ok(real)) = (self.root.canonicalize(), path.canonicalize())
162 && !real.starts_with(&root)
163 {
164 return Err(VerifyError::Io {
165 path: file.path.clone(),
166 msg: "resolves outside the package".into(),
167 });
168 }
169 read_bounded(&path, &file.path, MAX_FILE_BYTES)
170 }
171}
172
173pub(crate) fn read_bounded(path: &Path, label: &str, max: u64) -> Result<Vec<u8>, VerifyError> {
174 let meta = std::fs::symlink_metadata(path).map_err(|e| io_or_missing(e, label))?;
175 if !meta.is_file() {
176 return Err(VerifyError::Io {
177 path: label.into(),
178 msg: "not a regular file".into(),
179 });
180 }
181 if meta.len() > max {
182 return Err(VerifyError::Io {
183 path: label.into(),
184 msg: format!("larger than {max} bytes"),
185 });
186 }
187 let mut out = Vec::new();
189 std::fs::File::open(path)
190 .map_err(|e| io_or_missing(e, label))?
191 .take(max + 1)
192 .read_to_end(&mut out)
193 .map_err(|e| io_or_missing(e, label))?;
194 if out.len() as u64 > max {
195 return Err(VerifyError::Io {
196 path: label.into(),
197 msg: format!("larger than {max} bytes"),
198 });
199 }
200 Ok(out)
201}
202
203fn io_or_missing(e: std::io::Error, label: &str) -> VerifyError {
204 if e.kind() == std::io::ErrorKind::NotFound {
205 VerifyError::FileMissing { path: label.into() }
206 } else {
207 VerifyError::Io {
208 path: label.into(),
209 msg: e.to_string(),
210 }
211 }
212}
213
214pub fn verify_dir(
216 dir: &Path,
217 anchors: &TrustAnchors,
218 policy: &VerifyPolicy,
219) -> Result<VerifiedPackage, VerifyError> {
220 verify_dir_in(dir, anchors, policy, &KindRegistry::builtin())
221}
222
223pub fn verify_dir_in(
225 dir: &Path,
226 anchors: &TrustAnchors,
227 policy: &VerifyPolicy,
228 kinds: &KindRegistry,
229) -> Result<VerifiedPackage, VerifyError> {
230 let bytes = read_bounded(
231 &dir.join(MANIFEST_FILE),
232 MANIFEST_FILE,
233 MAX_MANIFEST_BYTES as u64,
234 )?;
235 verify_with_source_in(&bytes, &DirSource::new(dir), anchors, policy, kinds)
236}
237
238pub fn verify_with_source(
240 manifest: &[u8],
241 source: &dyn FileSource,
242 anchors: &TrustAnchors,
243 policy: &VerifyPolicy,
244) -> Result<VerifiedPackage, VerifyError> {
245 verify_with_source_in(manifest, source, anchors, policy, &KindRegistry::builtin())
246}
247
248pub fn verify_with_source_in(
250 manifest: &[u8],
251 source: &dyn FileSource,
252 anchors: &TrustAnchors,
253 policy: &VerifyPolicy,
254 kinds: &KindRegistry,
255) -> Result<VerifiedPackage, VerifyError> {
256 let (envelope, body, mut signers) = parse_and_check(manifest, anchors, kinds)?;
257 if policy.accept_cognitum_release && envelope.kind != KIND_COG {
262 return Err(VerifyError::Manifest(
263 "the Cognitum release verifier applies only to the cog kind".into(),
264 ));
265 }
266
267 let record = if policy.accept_cognitum_release {
271 cognitum::check_attached(&body, source, anchors)?
272 } else {
273 None
274 };
275 if signers.is_empty()
276 && let Some(rec) = record
277 {
278 cognitum::require_record_only_binding(&body, &rec.facts, policy)?;
280 signers.push(rec.signer);
281 }
282 if signers.is_empty() {
283 return Err(no_signer_error(&envelope));
284 }
285
286 for file in body.files() {
287 check_file(file, &source.read(file)?)?;
288 }
289 Ok(VerifiedPackage {
290 package_id: envelope.package_id()?,
291 envelope,
292 body,
293 signers,
294 })
295}
296
297pub fn verify_manifest_signatures(
306 manifest: &[u8],
307 anchors: &TrustAnchors,
308) -> Result<VerifiedPackage, VerifyError> {
309 verify_manifest_signatures_in(manifest, anchors, &KindRegistry::builtin())
310}
311
312pub fn verify_manifest_signatures_in(
314 manifest: &[u8],
315 anchors: &TrustAnchors,
316 kinds: &KindRegistry,
317) -> Result<VerifiedPackage, VerifyError> {
318 let (envelope, body, signers) = parse_and_check(manifest, anchors, kinds)?;
319 if signers.is_empty() {
320 return Err(no_signer_error(&envelope));
321 }
322 Ok(VerifiedPackage {
323 package_id: envelope.package_id()?,
324 envelope,
325 body,
326 signers,
327 })
328}
329
330type Checked = (ManifestEnvelope, CogPackageBody, Vec<AcceptedSigner>);
331
332fn parse_and_check(
333 manifest: &[u8],
334 anchors: &TrustAnchors,
335 kinds: &KindRegistry,
336) -> Result<Checked, VerifyError> {
337 let envelope = ManifestEnvelope::from_bytes(manifest)?;
338 validate_envelope(kinds, &envelope)?;
342 let body = envelope.cog_shaped_body()?;
343 let signers = check_signatures(&envelope, anchors)?;
344 Ok((envelope, body, signers))
345}
346
347fn no_signer_error(envelope: &ManifestEnvelope) -> VerifyError {
348 if envelope.signatures.is_empty() {
349 VerifyError::MissingSignature
350 } else {
351 let key_ids: Vec<&str> = envelope
352 .signatures
353 .iter()
354 .map(|s| s.key_id.as_str())
355 .collect();
356 VerifyError::UntrustedSigner {
357 key_ids: key_ids.join(","),
358 }
359 }
360}
361
362pub fn check_file(file: &FileRef, content: &[u8]) -> Result<(), VerifyError> {
364 let actual = blake3::hash(content).to_hex().to_string();
365 if actual != file.blake3 {
366 return Err(VerifyError::HashMismatch {
367 path: file.path.clone(),
368 expected: file.blake3.clone(),
369 actual,
370 });
371 }
372 if content.len() as u64 != file.size {
373 return Err(VerifyError::HashMismatch {
374 path: file.path.clone(),
375 expected: format!("{} bytes", file.size),
376 actual: format!("{} bytes", content.len()),
377 });
378 }
379 Ok(())
380}
381
382pub(crate) fn check_signatures(
390 env: &ManifestEnvelope,
391 anchors: &TrustAnchors,
392) -> Result<Vec<AcceptedSigner>, VerifyError> {
393 let msg = env.signed_statement()?;
394 let mut accepted = Vec::new();
395 for entry in &env.signatures {
396 if entry.algorithm != "ed25519" {
397 return Err(VerifyError::Manifest(format!(
398 "unsupported algorithm {:?}",
399 entry.algorithm
400 )));
401 }
402 let Some(pk) = hex_decode_exact::<32>(&entry.public_key) else {
403 continue;
404 };
405 let Some(pinned) = anchors.signer(&pk) else {
406 continue;
407 };
408 let bad = || VerifyError::BadSignature {
409 key_id: entry.key_id.clone(),
410 };
411 if pinned.key_id != entry.key_id {
412 return Err(bad());
413 }
414 let Some(sig) = hex_decode_exact::<64>(&entry.signature) else {
415 continue;
416 };
417 let vk = VerifyingKey::from_bytes(&pk).map_err(|_| bad())?;
418 vk.verify_strict(&msg, &Signature::from_bytes(&sig))
419 .map_err(|_| bad())?;
420 if !accepted
421 .iter()
422 .any(|a: &AcceptedSigner| a.key_id == pinned.key_id)
423 {
424 accepted.push(AcceptedSigner {
425 key_id: pinned.key_id.clone(),
426 origin: pinned.origin,
427 });
428 }
429 }
430 Ok(accepted)
431}