guests/verify-bundle/src/decode.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! Decoding compressed chunks OUTSIDE the verifier (docs/GATLING-WASM.md).
//! Heimdall is a defensive sandbox.
//!
//! In the split guest the verifier never runs OpenZL itself: it hands the
//! compressed frames of the chunks it needs to a [`ChunkDecoder`], which in the
//! wasm host is the `decode-chunks` import (N isolated decoder instances run
//! by the host, see `crates/heimdall-wasm`). A decoder answers, per chunk, the
//! decoded bytes and their BLAKE3-256, or why the frame does not decode.
//!
//! The verifier trusts nothing a decoder says: the length must be the size
//! the index declares, the digest must be the index checksum, and, unless the
//! decoder's digests are confirmed by trusted code ([`ChunkDecoder::digests_confirmed`],
//! the wasm host hashes what a decoder instance returns itself), the verifier
//! hashes the bytes again. A decoder that answers falsely is refused with
//! [`crate::refusal::GuestCode::DECODER`]; a frame that does not decode is
//! Holger's container refusal, exactly as in the single-instance guest.
/// One compressed chunk handed to a decoder: the whole frame as the archive
/// holds it, and the decoded size the index declares.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkRef<'a> {
pub frame: &'a [u8],
pub size: u64,
/// `false`: only the digest is wanted, `bytes` comes back empty.
pub want_bytes: bool,
}
/// What one chunk decoded to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decoded {
/// BLAKE3-256 of the decoded bytes (32 bytes from an honest decoder).
pub digest: Vec<u8>,
/// The decoded bytes, when they were asked for; else empty.
pub bytes: Vec<u8>,
}
/// One answer per chunk: the decoded chunk, or why its frame does not decode
/// (the reason goes into the container refusal as is).
pub type ChunkResult = Result<Decoded, String>;
/// SHA-256 and BLAKE3-256 of one member (32 bytes each from honest code).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemberDigest {
pub sha256: Vec<u8>,
pub blake3: Vec<u8>,
}
/// Something that decodes compressed chunks for the verifier.
pub trait ChunkDecoder {
/// Decode every chunk; exactly one answer per chunk, in order.
fn decode_chunks(&mut self, chunks: &[ChunkRef<'_>]) -> Vec<ChunkResult>;
/// `true` when trusted code has already checked that every returned
/// `digest` is the BLAKE3-256 of the returned `bytes` (the wasm host does,
/// natively and in parallel). `false`: the verifier hashes them again.
fn digests_confirmed(&self) -> bool {
false
}
/// SHA-256 and BLAKE3-256 of every member, one per member in order, from
/// TRUSTED code (the wasm host, natively and in parallel), or `None` to
/// let the verifier hash them itself. The verifier holds the answer to
/// the signed manifest exactly as if it had hashed the bytes.
fn member_digests(&mut self, members: &[&[u8]]) -> Option<Vec<MemberDigest>> {
let _ = members;
None
}
}
/// Decode one chunk exactly as the single-instance guest does: the frame
/// header must declare the index's size BEFORE the decoder runs, and the
/// decoder must fill exactly that many bytes. The `Err` text is the reason the
/// single-instance guest gives for the same frame.
#[cfg(feature = "openzl")]
pub fn decode_one(c: &ChunkRef<'_>) -> ChunkResult {
let size = usize::try_from(c.size).map_err(|_| {
format!(
"the index declares {} bytes, more than a decoder can address",
c.size
)
})?;
let mut out = Vec::new();
out.try_reserve_exact(size)
.map_err(|_| format!("the decoder cannot allocate the declared {size} bytes"))?;
out.resize(size, 0);
crate::zl::decode_exact(c.frame, &mut out)?;
let digest = blake3::hash(&out).as_bytes().to_vec();
if !c.want_bytes {
out = Vec::new();
}
Ok(Decoded { digest, bytes: out })
}
/// The decoder in this process (feature `openzl`): the same C decoder the
/// single-instance guest links, called through the split path. Used natively
/// to prove the split path gives the single-instance verdicts.
#[cfg(feature = "openzl")]
#[derive(Debug, Default, Clone, Copy)]
pub struct LocalDecoder;
#[cfg(feature = "openzl")]
impl ChunkDecoder for LocalDecoder {
fn decode_chunks(&mut self, chunks: &[ChunkRef<'_>]) -> Vec<ChunkResult> {
chunks.iter().map(decode_one).collect()
}
fn digests_confirmed(&self) -> bool {
true // computed right here, by this process, over these bytes
}
}