Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/heimdall

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");
}