// SPDX-License-Identifier: AGPL-3.0-only
//! Property tests: no mutation of a real bundle, of its members, or of the
//! parameters can make the guest accept content other than what Holger signed,
//! and no input at all can make it panic past its own guard.
mod common;
use common::zwriter::RawArchive;
use common::*;
use std::sync::OnceLock;
use heimdall_guest_verify_bundle::pathsafe::validate_member_path;
use heimdall_guest_verify_bundle::znippy::Archive;
use proptest::prelude::*;
struct Golden {
fx: Fixture,
archive: Vec<u8>,
params: Vec<u8>,
accepted: Outcome,
}
fn golden() -> &'static Golden {
static G: OnceLock<Golden> = OnceLock::new();
G.get_or_init(|| {
let fx = good();
let archive = fx.pack();
let params = fx.params();
let accepted = verify_bundle(&archive, ¶ms);
assert!(accepted.is_accepted());
Golden {
fx,
archive,
params,
accepted,
}
})
}
/// The integrity property: refused with nothing emitted, or accepted with
/// exactly the signed content.
fn assert_integrity(o: &Outcome) {
let g = golden();
if o.is_accepted() {
assert_eq!(o.emitted.len(), g.accepted.emitted.len());
for e in &o.emitted[1..] {
assert_eq!(
&e.bytes,
member_for(&g.fx.members, &e.name),
"accepted ALTERED content in {}",
e.name
);
}
} else {
assert!(o.emitted.is_empty(), "a refusal emitted outputs");
assert!(
o.reason().unwrap().starts_with('['),
"refusal without a code"
);
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 512, ..ProptestConfig::default() })]
#[test]
fn any_single_byte_change_of_the_archive_never_yields_altered_content(pos in any::<prop::sample::Index>(), x in 1u8..=255) {
let g = golden();
let mut b = g.archive.clone();
let i = pos.index(b.len());
b[i] ^= x;
assert_integrity(&verify_bundle(&b, &g.params));
}
#[test]
fn several_random_byte_changes_never_yield_altered_content(edits in prop::collection::vec((any::<prop::sample::Index>(), 1u8..=255), 1..16)) {
let g = golden();
let mut b = g.archive.clone();
for (pos, x) in edits {
let i = pos.index(b.len());
b[i] ^= x;
}
assert_integrity(&verify_bundle(&b, &g.params));
}
#[test]
fn every_proper_prefix_of_the_archive_is_refused(cut in any::<prop::sample::Index>()) {
let g = golden();
let n = cut.index(g.archive.len());
let o = verify_bundle(&g.archive[..n], &g.params);
prop_assert!(!o.is_accepted());
}
#[test]
fn bytes_inserted_anywhere_never_yield_altered_content(pos in any::<prop::sample::Index>(), ins in prop::collection::vec(any::<u8>(), 1..64)) {
let g = golden();
let mut b = g.archive.clone();
let i = pos.index(b.len() + 1);
b.splice(i..i, ins);
assert_integrity(&verify_bundle(&b, &g.params));
}
#[test]
fn arbitrary_bytes_are_never_accepted(b in prop::collection::vec(any::<u8>(), 0..4096)) {
let g = golden();
let o = verify_bundle(&b, &g.params);
prop_assert!(!o.is_accepted());
prop_assert!(o.emitted.is_empty());
}
#[test]
fn arbitrary_bytes_behind_a_valid_footer_are_never_accepted(b in prop::collection::vec(any::<u8>(), 0..4096), off in any::<u64>()) {
let g = golden();
let mut v = b.clone();
v.extend_from_slice(b"ZNPYMIDX");
v.extend_from_slice(&(off % (b.len() as u64 + 1)).to_le_bytes());
prop_assert!(!verify_bundle(&v, &g.params).is_accepted());
}
#[test]
fn changing_any_member_byte_without_a_key_is_always_refused(which in any::<prop::sample::Index>(), pos in any::<prop::sample::Index>(), x in 1u8..=255) {
let g = golden();
let names: Vec<&String> = g.fx.members.keys().filter(|k| !g.fx.members[*k].is_empty()).collect();
let name = names[which.index(names.len())].clone();
let mut members = g.fx.members.clone();
let m = members.get_mut(&name).unwrap();
let i = pos.index(m.len());
m[i] ^= x;
// Rebuilt honestly at the container layer: the archive itself is sound,
// only the signed content changed.
let bytes = RawArchive::from_members(&members, 4096).encode();
let o = verify_bundle(&bytes, &g.params);
prop_assert!(!o.is_accepted(), "a change in {} was accepted", name);
}
#[test]
fn a_stowaway_member_under_any_name_is_refused(name in "[a-zA-Z0-9_./\\\\-]{1,40}", body in prop::collection::vec(any::<u8>(), 0..64)) {
let g = golden();
let mut members = g.fx.members.clone();
prop_assume!(!members.contains_key(&name));
members.insert(name.clone(), body);
let bytes = RawArchive::from_members(&members, 4096).encode();
let o = verify_bundle(&bytes, &g.params);
prop_assert!(!o.is_accepted(), "stowaway {:?} accepted", name);
}
#[test]
fn a_mutated_parameter_document_never_yields_altered_content(pos in any::<prop::sample::Index>(), x in 1u8..=255) {
let g = golden();
let mut p = g.params.clone();
let i = pos.index(p.len());
p[i] ^= x;
assert_integrity(&verify_bundle(&g.archive, &p));
}
#[test]
fn the_index_parser_never_panics_on_arbitrary_input(b in prop::collection::vec(any::<u8>(), 0..2048)) {
let _ = Archive::parse(&b);
}
#[test]
fn an_accepted_member_path_never_escapes_its_root(s in "\\PC{0,24}") {
if validate_member_path(&s).is_ok() {
prop_assert!(!s.starts_with('/'));
prop_assert!(!s.contains('\\'));
for seg in s.split('/') {
prop_assert!(!seg.is_empty() && seg != "." && seg != "..");
}
prop_assert!(!s.chars().any(|c| c.is_control()));
}
}
}