use std::io::Read;
use std::path::{Path, PathBuf};
use ed25519_dalek::{Signature, VerifyingKey};
use crate::workload_kind::{KindRegistry, validate_envelope};
use super::codec::hex_decode_exact;
use super::cognitum;
use super::manifest::{
CogPackageBody, FileRef, KIND_COG, MANIFEST_FILE, MAX_MANIFEST_BYTES, ManifestEnvelope,
ManifestError,
};
use super::trust::{KeyOrigin, TrustAnchors};
pub const MAX_FILE_BYTES: u64 = 512 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum VerifyError {
#[error("manifest invalid: {0}")]
Manifest(String),
#[error("package has no signature")]
MissingSignature,
#[error("no signature from a pinned signer (saw: {key_ids})")]
UntrustedSigner {
key_ids: String,
},
#[error("signature from {key_id} does not verify")]
BadSignature {
key_id: String,
},
#[error("package file missing: {path}")]
FileMissing {
path: String,
},
#[error("package file tampered: {path} (expected {expected}, got {actual})")]
HashMismatch {
path: String,
expected: String,
actual: String,
},
#[error("cognitum release record rejected: {0}")]
CognitumRecord(String),
#[error("io error on {path}: {msg}")]
Io {
path: String,
msg: String,
},
}
impl VerifyError {
pub fn code(&self) -> &'static str {
match self {
Self::Manifest(_) => "manifest-invalid",
Self::MissingSignature => "missing-signature",
Self::UntrustedSigner { .. } => "untrusted-signer",
Self::BadSignature { .. } => "bad-signature",
Self::FileMissing { .. } => "file-missing",
Self::HashMismatch { .. } => "file-tampered",
Self::CognitumRecord(_) => "cognitum-record-rejected",
Self::Io { .. } => "io-error",
}
}
}
impl From<ManifestError> for VerifyError {
fn from(e: ManifestError) -> Self {
Self::Manifest(e.to_string())
}
}
#[derive(Debug, Clone, Default)]
pub struct VerifyPolicy {
pub accept_cognitum_release: bool,
pub record_cog_toml_pins: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AcceptedSigner {
pub key_id: String,
pub origin: KeyOrigin,
}
#[derive(Debug, Clone)]
pub struct VerifiedPackage {
pub package_id: String,
pub envelope: ManifestEnvelope,
pub body: CogPackageBody,
pub signers: Vec<AcceptedSigner>,
}
pub trait FileSource {
fn read(&self, file: &FileRef) -> Result<Vec<u8>, VerifyError>;
}
pub struct DirSource {
root: PathBuf,
}
impl DirSource {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
}
impl FileSource for DirSource {
fn read(&self, file: &FileRef) -> Result<Vec<u8>, VerifyError> {
let path = self.root.join(&file.path);
if let (Ok(root), Ok(real)) = (self.root.canonicalize(), path.canonicalize())
&& !real.starts_with(&root)
{
return Err(VerifyError::Io {
path: file.path.clone(),
msg: "resolves outside the package".into(),
});
}
read_bounded(&path, &file.path, MAX_FILE_BYTES)
}
}
pub(crate) fn read_bounded(path: &Path, label: &str, max: u64) -> Result<Vec<u8>, VerifyError> {
let meta = std::fs::symlink_metadata(path).map_err(|e| io_or_missing(e, label))?;
if !meta.is_file() {
return Err(VerifyError::Io {
path: label.into(),
msg: "not a regular file".into(),
});
}
if meta.len() > max {
return Err(VerifyError::Io {
path: label.into(),
msg: format!("larger than {max} bytes"),
});
}
let mut out = Vec::new();
std::fs::File::open(path)
.map_err(|e| io_or_missing(e, label))?
.take(max + 1)
.read_to_end(&mut out)
.map_err(|e| io_or_missing(e, label))?;
if out.len() as u64 > max {
return Err(VerifyError::Io {
path: label.into(),
msg: format!("larger than {max} bytes"),
});
}
Ok(out)
}
fn io_or_missing(e: std::io::Error, label: &str) -> VerifyError {
if e.kind() == std::io::ErrorKind::NotFound {
VerifyError::FileMissing { path: label.into() }
} else {
VerifyError::Io {
path: label.into(),
msg: e.to_string(),
}
}
}
pub fn verify_dir(
dir: &Path,
anchors: &TrustAnchors,
policy: &VerifyPolicy,
) -> Result<VerifiedPackage, VerifyError> {
verify_dir_in(dir, anchors, policy, &KindRegistry::builtin())
}
pub fn verify_dir_in(
dir: &Path,
anchors: &TrustAnchors,
policy: &VerifyPolicy,
kinds: &KindRegistry,
) -> Result<VerifiedPackage, VerifyError> {
let bytes = read_bounded(
&dir.join(MANIFEST_FILE),
MANIFEST_FILE,
MAX_MANIFEST_BYTES as u64,
)?;
verify_with_source_in(&bytes, &DirSource::new(dir), anchors, policy, kinds)
}
pub fn verify_with_source(
manifest: &[u8],
source: &dyn FileSource,
anchors: &TrustAnchors,
policy: &VerifyPolicy,
) -> Result<VerifiedPackage, VerifyError> {
verify_with_source_in(manifest, source, anchors, policy, &KindRegistry::builtin())
}
pub fn verify_with_source_in(
manifest: &[u8],
source: &dyn FileSource,
anchors: &TrustAnchors,
policy: &VerifyPolicy,
kinds: &KindRegistry,
) -> Result<VerifiedPackage, VerifyError> {
let (envelope, body, mut signers) = parse_and_check(manifest, anchors, kinds)?;
if policy.accept_cognitum_release && envelope.kind != KIND_COG {
return Err(VerifyError::Manifest(
"the Cognitum release verifier applies only to the cog kind".into(),
));
}
let record = if policy.accept_cognitum_release {
cognitum::check_attached(&body, source, anchors)?
} else {
None
};
if signers.is_empty()
&& let Some(rec) = record
{
cognitum::require_record_only_binding(&body, &rec.facts, policy)?;
signers.push(rec.signer);
}
if signers.is_empty() {
return Err(no_signer_error(&envelope));
}
for file in body.files() {
check_file(file, &source.read(file)?)?;
}
Ok(VerifiedPackage {
package_id: envelope.package_id()?,
envelope,
body,
signers,
})
}
pub fn verify_manifest_signatures(
manifest: &[u8],
anchors: &TrustAnchors,
) -> Result<VerifiedPackage, VerifyError> {
verify_manifest_signatures_in(manifest, anchors, &KindRegistry::builtin())
}
pub fn verify_manifest_signatures_in(
manifest: &[u8],
anchors: &TrustAnchors,
kinds: &KindRegistry,
) -> Result<VerifiedPackage, VerifyError> {
let (envelope, body, signers) = parse_and_check(manifest, anchors, kinds)?;
if signers.is_empty() {
return Err(no_signer_error(&envelope));
}
Ok(VerifiedPackage {
package_id: envelope.package_id()?,
envelope,
body,
signers,
})
}
type Checked = (ManifestEnvelope, CogPackageBody, Vec<AcceptedSigner>);
fn parse_and_check(
manifest: &[u8],
anchors: &TrustAnchors,
kinds: &KindRegistry,
) -> Result<Checked, VerifyError> {
let envelope = ManifestEnvelope::from_bytes(manifest)?;
validate_envelope(kinds, &envelope)?;
let body = envelope.cog_shaped_body()?;
let signers = check_signatures(&envelope, anchors)?;
Ok((envelope, body, signers))
}
fn no_signer_error(envelope: &ManifestEnvelope) -> VerifyError {
if envelope.signatures.is_empty() {
VerifyError::MissingSignature
} else {
let key_ids: Vec<&str> = envelope
.signatures
.iter()
.map(|s| s.key_id.as_str())
.collect();
VerifyError::UntrustedSigner {
key_ids: key_ids.join(","),
}
}
}
pub fn check_file(file: &FileRef, content: &[u8]) -> Result<(), VerifyError> {
let actual = blake3::hash(content).to_hex().to_string();
if actual != file.blake3 {
return Err(VerifyError::HashMismatch {
path: file.path.clone(),
expected: file.blake3.clone(),
actual,
});
}
if content.len() as u64 != file.size {
return Err(VerifyError::HashMismatch {
path: file.path.clone(),
expected: format!("{} bytes", file.size),
actual: format!("{} bytes", content.len()),
});
}
Ok(())
}
pub(crate) fn check_signatures(
env: &ManifestEnvelope,
anchors: &TrustAnchors,
) -> Result<Vec<AcceptedSigner>, VerifyError> {
let msg = env.signed_statement()?;
let mut accepted = Vec::new();
for entry in &env.signatures {
if entry.algorithm != "ed25519" {
return Err(VerifyError::Manifest(format!(
"unsupported algorithm {:?}",
entry.algorithm
)));
}
let Some(pk) = hex_decode_exact::<32>(&entry.public_key) else {
continue;
};
let Some(pinned) = anchors.signer(&pk) else {
continue;
};
let bad = || VerifyError::BadSignature {
key_id: entry.key_id.clone(),
};
if pinned.key_id != entry.key_id {
return Err(bad());
}
let Some(sig) = hex_decode_exact::<64>(&entry.signature) else {
continue;
};
let vk = VerifyingKey::from_bytes(&pk).map_err(|_| bad())?;
vk.verify_strict(&msg, &Signature::from_bytes(&sig))
.map_err(|_| bad())?;
if !accepted
.iter()
.any(|a: &AcceptedSigner| a.key_id == pinned.key_id)
{
accepted.push(AcceptedSigner {
key_id: pinned.key_id.clone(),
origin: pinned.origin,
});
}
}
Ok(accepted)
}