// SPDX-License-Identifier: AGPL-3.0-only
//! The caller's side of `heimdall verify-result`.
//!
//! **Heimdall is a defensive sandbox**, and this is how a caller trusts its
//! verdict: the result file must decode, carry the pinned key id and boot id,
//! verify under the pinned Ed25519 key, optionally match the caller's job
//! ticket (job id + nonce), and every output it lists must match the files on
//! disk with nothing extra beside them.
use std::io::Read;
use std::path::{Path, PathBuf};
use heimdall_core::{
check_output_files, JobTicket, OutputError, PinError, PinnedKey, SignedResult, Verifier,
VerifyError, MAX_WIRE,
};
use crate::outdir::FILES_DIR;
/// A pinned-key file larger than this is refused.
pub const MAX_KEY_FILE_BYTES: u64 = 4096;
/// The prefix of a pinned key record (`PinnedKey::to_text`).
pub const PIN_PREFIX: &str = "heimdall-boot-key ";
/// Why verification did not succeed.
#[derive(Debug, thiserror::Error)]
pub enum VerifyFileError {
/// The result or key file could not be read at all.
#[error("cannot read {path}: {err}")]
Read { path: PathBuf, err: String },
/// The pinned key is not a valid record.
#[error("pinned key: {0}")]
Key(#[from] PinError),
/// The result is not authentic or not for this job.
#[error("result refused: {0}")]
Verify(#[from] VerifyError),
/// The outputs on disk do not match the result.
#[error("outputs refused: {0}")]
Outputs(#[from] OutputError),
}
impl VerifyFileError {
/// True when the inputs were readable but the result is not trustworthy.
pub fn is_rejection(&self) -> bool {
matches!(
self,
VerifyFileError::Verify(_) | VerifyFileError::Outputs(_)
)
}
}
/// A result that verified.
#[derive(Debug, Clone)]
pub struct Verified {
pub signed: SignedResult,
/// The dir whose files were checked, if any.
pub files_dir: Option<PathBuf>,
/// How many output files were checked.
pub files_checked: usize,
}
fn read_capped(path: &Path, cap: u64) -> Result<Vec<u8>, VerifyFileError> {
let rerr = |e: String| VerifyFileError::Read {
path: path.to_path_buf(),
err: e,
};
let meta = std::fs::metadata(path).map_err(|e| rerr(e.to_string()))?;
if !meta.is_file() {
return Err(rerr("not a regular file".into()));
}
let f = std::fs::File::open(path).map_err(|e| rerr(e.to_string()))?;
let mut buf = Vec::new();
f.take(cap.saturating_add(1))
.read_to_end(&mut buf)
.map_err(|e| rerr(e.to_string()))?;
if buf.len() as u64 > cap {
return Err(rerr(format!("larger than {cap} bytes")));
}
Ok(buf)
}
/// The pinned key from `--key`: either the record itself
/// (`heimdall-boot-key v1 ...`) or the path of a file holding it.
pub fn load_pinned_key(arg: &str) -> Result<PinnedKey, VerifyFileError> {
if arg.starts_with(PIN_PREFIX) {
return Ok(PinnedKey::from_text(arg)?);
}
let path = Path::new(arg);
let bytes = read_capped(path, MAX_KEY_FILE_BYTES)?;
let text = std::str::from_utf8(&bytes).map_err(|_| PinError::BadFormat)?;
Ok(PinnedKey::from_text(text)?)
}
/// Read a result file (a regular file, at most `MAX_WIRE` bytes).
pub fn read_result_file(path: &Path) -> Result<Vec<u8>, VerifyFileError> {
read_capped(path, MAX_WIRE as u64)
}
/// The files dir to check by default: `files/` beside the result file.
pub fn default_files_dir(result_file: &Path) -> Option<PathBuf> {
let dir = result_file.parent()?.join(FILES_DIR);
std::fs::symlink_metadata(&dir)
.ok()
.filter(|m| m.is_dir())
.map(|_| dir)
}
/// Verify result wire bytes against a pinned key, optionally bind them to a
/// job ticket, and optionally check the output files in `files_dir`.
pub fn verify_result(
wire: &[u8],
pinned: PinnedKey,
ticket: Option<&JobTicket>,
files_dir: Option<&Path>,
) -> Result<Verified, VerifyFileError> {
let mut v = Verifier::new(pinned)?;
let signed = match ticket {
Some(t) => {
v.accept(wire, t)?;
SignedResult::from_bytes(wire).map_err(VerifyError::from)?
}
None => v.verify(wire)?,
};
let files_checked = match files_dir {
Some(d) => check_output_files(&signed.result, d)?.len(),
None => 0,
};
Ok(Verified {
signed,
files_dir: files_dir.map(Path::to_path_buf),
files_checked,
})
}