use std::fs::File;
use std::io::{self, Read, Seek};
use std::path::Path;
use onelf_format::EntryKind;
use crate::extract::decompress_entry;
use crate::info::read_footer_and_manifest;
pub fn verify(binary: &Path, key: Option<&Path>, sig: Option<&Path>) -> io::Result<()> {
if let (Some(key), Some(sig)) = (key, sig) {
verify_detached_over(binary, key, sig)?;
} else if key.is_some() != sig.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"--key and --sig go together: checking a detached signature needs both",
));
}
let (footer, manifest) = read_footer_and_manifest(binary)?;
let mut file = File::open(binary)?;
if footer.format_version < onelf_format::footer::FORMAT_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"format version {} predates structure hashes: the runtime \
prefix and footer are covered by no hash, so this package cannot \
be verified; repack it with a current onelf",
footer.format_version
),
));
}
let mut prefix = vec![0u8; footer.manifest_offset as usize];
file.read_exact(&mut prefix)?;
let prefix_hash: [u8; 32] = blake3::hash(&prefix).into();
if prefix_hash != manifest.header.prefix_hash {
return Err(io::Error::other(format!(
"runtime prefix hash mismatch: expected {}, got {}; the executable \
body was modified",
hex32(&manifest.header.prefix_hash),
hex32(&prefix_hash)
)));
}
let mut footer_buf = [0u8; onelf_format::FOOTER_SIZE];
file.seek(std::io::SeekFrom::End(-(onelf_format::FOOTER_SIZE as i64)))?;
file.read_exact(&mut footer_buf)?;
let footer_from_file = onelf_format::Footer::from_bytes(&footer_buf)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("bad footer: {e}")))?;
let footer_hash = footer_from_file.structure_hash();
if footer_hash != manifest.header.footer_hash {
return Err(io::Error::other(format!(
"footer hash mismatch: expected {}, got {}; the footer was modified",
hex32(&manifest.header.footer_hash),
hex32(&footer_hash)
)));
}
println!(
"Structure: OK (format {}, prefix and footer intact)",
footer.format_version
);
let dict = crate::info::read_dict(&mut file, &footer)?;
let file_entries: Vec<(usize, String)> = manifest
.entries
.iter()
.enumerate()
.filter(|(_, e)| e.kind == EntryKind::File)
.map(|(i, _)| (i, manifest.entry_path(i)))
.collect();
let total = file_entries.len();
let mut failed: Vec<(String, String)> = Vec::new();
for (idx, rel_path) in &file_entries {
let entry = &manifest.entries[*idx];
let data = match decompress_entry(&mut file, &footer, entry, dict.as_deref()) {
Ok(data) => data,
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
failed.push((rel_path.clone(), e.to_string()));
continue;
}
Err(e) => return Err(e),
};
let actual: [u8; 32] = blake3::hash(&data).into();
if actual != entry.content_hash {
failed.push((
rel_path.clone(),
format!(
"expected {}, got {}",
hex32(&entry.content_hash),
hex32(&actual)
),
));
}
}
println!("Checked {total} file(s)");
if failed.is_empty() {
println!("OK");
Ok(())
} else {
eprintln!("{} file(s) failed verification:", failed.len());
for (path, msg) in &failed {
eprintln!(" {path}: {msg}");
}
Err(io::Error::other("verification failed"))
}
}
fn hex32(bytes: &[u8; 32]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn verify_detached_over(binary: &Path, key: &Path, sig: &Path) -> io::Result<()> {
use ed25519_compact::{PublicKey, Signature};
let key_bytes = std::fs::read(key)?;
let not_a_key = || {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{} is not a public key: expected one or more {}-byte raw keys, found {} bytes",
key.display(),
PublicKey::BYTES,
key_bytes.len()
),
)
};
let (keys, rest) = key_bytes.as_chunks::<{ PublicKey::BYTES }>();
if keys.is_empty() || !rest.is_empty() {
return Err(not_a_key());
}
let ring: Vec<PublicKey> = keys.iter().map(|k| PublicKey::new(*k)).collect();
let sig_bytes = std::fs::read(sig)?;
let signature = Signature::from_slice(&sig_bytes).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{} is not a signature: expected {} raw bytes, found {}",
sig.display(),
Signature::BYTES,
sig_bytes.len()
),
)
})?;
let message = std::fs::read(binary)?;
if !ring
.iter()
.any(|pk| pk.verify(&message, &signature).is_ok())
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"the signature over {} does not verify: the file changed after signing, \
or the key is not the signer's",
binary.display()
),
));
}
println!("Signature: OK ({})", key.display());
Ok(())
}