guests/verify-bundle/tests/split.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The split path in the library: the verifier hands compressed chunks to a
//! [`ChunkDecoder`] and believes nothing it says unchecked. Heimdall is a
//! defensive sandbox; these decoders lie on purpose, to prove the verifier
//! catches them. (Every other test in this crate also runs the split path
//! with an honest decoder, through `common::verify_bundle`.)
#![cfg(feature = "openzl")]
mod common;
use common::zwriter::RawArchive;
use common::*;
use heimdall_guest_verify_bundle::decode::{
decode_one, ChunkDecoder, ChunkRef, ChunkResult, Decoded, LocalDecoder, MemberDigest,
};
use heimdall_guest_verify_bundle::verify_bundle_split;
const DECODER: &str = "heimdall-bundle-7";
/// An honest decode, then `lie` applied to the answers.
struct Liar<F: FnMut(&mut Vec<ChunkResult>)> {
lie: F,
confirmed: bool,
calls: usize,
}
impl<F: FnMut(&mut Vec<ChunkResult>)> ChunkDecoder for Liar<F> {
fn decode_chunks(&mut self, chunks: &[ChunkRef<'_>]) -> Vec<ChunkResult> {
self.calls += 1;
let mut out: Vec<ChunkResult> = chunks.iter().map(decode_one).collect();
(self.lie)(&mut out);
out
}
fn digests_confirmed(&self) -> bool {
self.confirmed
}
}
fn liar<F: FnMut(&mut Vec<ChunkResult>)>(lie: F) -> Liar<F> {
Liar {
lie,
confirmed: false,
calls: 0,
}
}
fn first_ok(out: &mut [ChunkResult]) -> &mut Decoded {
out.iter_mut()
.find_map(|r| r.as_mut().ok())
.expect("an OpenZL chunk")
}
fn openzl_bundle() -> (Fixture, Vec<u8>) {
let fx = good();
let a = pack_holger(&fx.members);
(fx, a)
}
#[test]
fn an_honest_local_decoder_gives_the_single_instance_outcome_on_a_multi_chunk_openzl_bundle() {
let fx = build(Spec {
payloads: vec![(
"firmware.bin".into(),
(0..600_000u32)
.map(|i| (i % 251) as u8 ^ (i >> 9) as u8)
.collect(),
"application/octet-stream".into(),
)],
..Spec::default()
});
let a = RawArchive::from_members_openzl(&fx.members, 4096).encode();
let single = heimdall_guest_verify_bundle::verify_bundle(&a, &fx.params());
assert!(single.is_accepted(), "{:?}", single.reason());
let split = verify_bundle_split(&a, &fx.params(), &mut LocalDecoder);
assert_eq!(split, single);
}
#[test]
fn the_compressed_chunks_of_a_phase_go_to_the_decoder_in_at_most_one_call() {
let (fx, a) = openzl_bundle();
let mut d = liar(|_| {});
assert!(verify_bundle_split(&a, &fx.params(), &mut d).is_accepted());
// At most one call for the control members and one for the content
// (a phase with nothing compressed makes none).
assert!((1..=2).contains(&d.calls), "{} calls", d.calls);
}
#[test]
fn a_decoder_returning_wrong_bytes_under_the_right_digest_is_refused() {
let (fx, a) = openzl_bundle();
let mut d = liar(|out| first_ok(out).bytes[0] ^= 0x5a);
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(&o, DECODER, "is not the BLAKE3 of the bytes it returned");
}
#[test]
fn a_decoder_returning_the_right_bytes_under_a_wrong_digest_is_refused() {
let (fx, a) = openzl_bundle();
let mut d = liar(|out| first_ok(out).digest[0] ^= 1);
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(&o, DECODER, "is not the BLAKE3 of the bytes it returned");
}
#[test]
fn a_decoder_lying_consistently_is_caught_by_the_index_checksum_with_holgers_code() {
let (fx, a) = openzl_bundle();
let mut d = liar(|out| {
let x = first_ok(out);
x.bytes[0] ^= 0x5a;
x.digest = blake3::hash(&x.bytes).as_bytes().to_vec();
});
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(
&o,
holger_errcode::UPDATE_CONTAINER.code,
"does not match its blake3 checksum",
);
}
#[test]
fn a_decoder_lying_consistently_is_caught_even_when_its_digests_are_trusted() {
let (fx, a) = openzl_bundle();
let mut d = liar(|out| {
let x = first_ok(out);
x.bytes[0] ^= 0x5a;
x.digest = blake3::hash(&x.bytes).as_bytes().to_vec();
});
d.confirmed = true;
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(
&o,
holger_errcode::UPDATE_CONTAINER.code,
"does not match its blake3 checksum",
);
}
#[test]
fn a_decoder_returning_too_few_or_too_many_bytes_is_refused() {
let (fx, a) = openzl_bundle();
for grow in [false, true] {
let mut d = liar(move |out| {
let x = first_ok(out);
if grow {
x.bytes.push(0);
} else {
x.bytes.pop();
}
x.digest = blake3::hash(&x.bytes).as_bytes().to_vec();
});
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(&o, DECODER, "the index declares");
}
}
#[test]
fn a_decoder_returning_a_short_digest_is_refused() {
let (fx, a) = openzl_bundle();
let mut d = liar(|out| first_ok(out).digest.truncate(31));
d.confirmed = true;
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(&o, DECODER, "31-byte digest");
}
#[test]
fn a_decoder_returning_the_wrong_number_of_answers_is_refused() {
let (fx, a) = openzl_bundle();
for extra in [false, true] {
let mut d = liar(move |out| {
if extra {
let first = out[0].clone();
out.push(first);
} else {
out.pop();
}
});
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(&o, DECODER, "answers for");
}
}
#[test]
fn a_decoder_that_says_a_good_frame_does_not_decode_gets_holgers_container_refusal() {
let (fx, a) = openzl_bundle();
let mut d = liar(|out| out[0] = Err("frame refused by the decoder".into()));
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(
&o,
holger_errcode::UPDATE_CONTAINER.code,
"frame refused by the decoder",
);
}
#[test]
fn a_stored_bundle_never_calls_the_decoder() {
let fx = good();
let mut d = liar(|_| panic!("a stored bundle has nothing to decode"));
assert!(verify_bundle_split(&fx.pack(), &fx.params(), &mut d).is_accepted());
assert_eq!(d.calls, 0);
}
/// An honest decoder whose member digests (the host's native hashing in the
/// wasm split guest) are `lie`d about.
struct DigestLiar<F: FnMut(&mut Vec<MemberDigest>)>(F);
impl<F: FnMut(&mut Vec<MemberDigest>)> ChunkDecoder for DigestLiar<F> {
fn decode_chunks(&mut self, chunks: &[ChunkRef<'_>]) -> Vec<ChunkResult> {
LocalDecoder.decode_chunks(chunks)
}
fn digests_confirmed(&self) -> bool {
true
}
fn member_digests(&mut self, members: &[&[u8]]) -> Option<Vec<MemberDigest>> {
let mut v: Vec<MemberDigest> = members
.iter()
.map(|b| MemberDigest {
sha256: heimdall_guest_verify_bundle::digest::sha256(b).to_vec(),
blake3: blake3::hash(b).as_bytes().to_vec(),
})
.collect();
(self.0)(&mut v);
Some(v)
}
}
#[test]
fn honest_member_digests_from_outside_give_the_single_instance_outcome() {
let (fx, a) = openzl_bundle();
let single = heimdall_guest_verify_bundle::verify_bundle(&a, &fx.params());
let o = verify_bundle_split(&a, &fx.params(), &mut DigestLiar(|_| {}));
assert_eq!(o, single);
}
#[test]
fn a_wrong_member_digest_from_outside_is_held_to_the_signed_manifest() {
let (fx, a) = openzl_bundle();
let at = fx
.members
.keys()
.position(|k| k.starts_with("payload/"))
.unwrap();
for which in [0usize, 1] {
let mut d = DigestLiar(move |v: &mut Vec<MemberDigest>| {
if which == 0 {
v[at].sha256[0] ^= 1
} else {
v[at].blake3[0] ^= 1
}
});
let o = verify_bundle_split(&a, &fx.params(), &mut d);
assert_refused(
&o,
holger_errcode::UPDATE_DIGEST_MISMATCH.code,
"digest mismatch",
);
}
}
#[test]
fn a_short_or_missing_member_digest_from_outside_is_refused() {
let (fx, a) = openzl_bundle();
let o = verify_bundle_split(
&a,
&fx.params(),
&mut DigestLiar(|v| v[0].sha256.truncate(8)),
);
assert_refused(&o, DECODER, "not 32");
let o = verify_bundle_split(
&a,
&fx.params(),
&mut DigestLiar(|v| {
v.pop();
}),
);
assert_refused(&o, DECODER, "member digests for");
}