#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Host-only malformed-input tests for the read-only SIECAF parser.""" from __future__ import annotations import argparse from io import BytesIO import importlib.util from pathlib import Path import sys from types import ModuleType from typing import Callable def load_module(name: str, path: Path) -> ModuleType: spec = importlib.util.spec_from_file_location(name, path) if spec is None or spec.loader is None: raise RuntimeError(f"could not load {path}") module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) return module def require(condition: bool, message: str) -> None: if not condition: raise RuntimeError(message) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--root", type=Path, required=True) args = parser.parse_args() module = load_module( "siecaf_inspector", args.root / "tools/inspect_siecaf_header.py" ) def build_archive( *, count: int = 1, magic: bytes | None = None, unknown_u64: int = 1, version: int = 3, metadata_ids: list[int] | None = None, hash_ids: list[int] | None = None, overlap: bool = False, out_of_range: bool = False, hash_seed: int = 1, trailing: int = 0, aligned_length: int | None = None, unaligned_length: int = 123, ) -> bytes: alignment = module.ALIGNMENT file_offset = alignment segment_length = alignment if aligned_length is None else aligned_length data_size = count * alignment source_size = file_offset + data_size + trailing metadata_ids = metadata_ids or list(range(count)) hash_ids = hash_ids or list(range(count)) header = module.HEADER.pack( magic or module.MAGIC, unknown_u64, 1, 0, version, 0, bytes(range(16)), bytes(range(12)), 0, count, file_offset, data_size, ) metadata = bytearray() for index in range(count): offset = file_offset + index * alignment if overlap and index == 1: offset = file_offset if out_of_range and index == count - 1: offset = file_offset + data_size metadata.extend( module.SEGMENT_META.pack( metadata_ids[index], 0, 0, offset, segment_length, 3, 1, b"\x00" * 12, 0, unaligned_length, ) ) hashes = bytearray() for index in range(count): hashes.extend( module.SECTION_HASH.pack( hash_ids[index], 0, bytes([(hash_seed + index) % 256]) * 16, b"\x00" * 24, ) ) tables = header + metadata + hashes require(len(tables) <= file_offset, "synthetic tables exceed preamble") return ( tables + b"\x00" * (file_offset - len(tables)) + b"\xa5" * data_size + b"\xee" * trailing )[:source_size] def inspect(value: bytes) -> dict[str, object]: return module.inspect_siecaf( BytesIO(value), len(value), source_label="synthetic" ) cases: list[tuple[str, Callable[[], None]]] = [] def case(name: str) -> Callable[[Callable[[], None]], Callable[[], None]]: def register(function: Callable[[], None]) -> Callable[[], None]: cases.append((name, function)) return function return register @case("01 valid structure parses") def _() -> None: result = inspect(build_archive()) require( result["classification"] == "SIECAF_VALID_STRUCTURE", str(result), ) require(not result["errors"], "valid structure returned errors") require( "key_or_unknown_16_hex" not in result["header"] and "raw_hex" not in result["header"] and result["header"]["key_or_unknown_16_redacted"] is True, "cryptographic header material was exposed", ) @case("02 magic must be exact") def _() -> None: result = inspect(build_archive(magic=b"BADCAF\x00\x00")) require(result["classification"] == "SIECAF_MALFORMED", str(result)) @case("03 unsupported version fails closed") def _() -> None: result = inspect(build_archive(version=7)) require( result["classification"] == "SIECAF_UNSUPPORTED_VERSION", str(result), ) @case("04 version six uses the published fixed-width layout") def _() -> None: result = inspect(build_archive(version=6)) require( result["classification"] == "SIECAF_VALID_STRUCTURE", str(result), ) @case("05 truncated header is rejected") def _() -> None: result = inspect(b"SIECAF\x00\x00") require(result["classification"] == "SIECAF_MALFORMED", str(result)) @case("06 segment-count multiplication overflow is rejected") def _() -> None: header = module.HEADER.pack( module.MAGIC, 1, 1, 0, 3, 0, b"\x00" * 16, b"\x00" * 12, 0, module.UINT64_MAX, module.ALIGNMENT, module.ALIGNMENT, ) result = module.inspect_siecaf( BytesIO(header), len(header), source_label="overflow" ) require(result["classification"] == "SIECAF_MALFORMED", str(result)) require( any("overflow" in error for error in result["errors"]), "overflow was not reported", ) @case("06 out-of-range offsets are rejected") def _() -> None: result = inspect(build_archive(out_of_range=True)) require(result["classification"] == "SIECAF_MALFORMED", str(result)) require( any("outside" in error for error in result["errors"]), "out-of-range section was not reported", ) @case("07 overlaps are reported") def _() -> None: result = inspect(build_archive(count=2, overlap=True)) require(result["classification"] == "SIECAF_MALFORMED", str(result)) require(bool(result["overlaps"]), "overlap record is absent") @case("08 duplicate metadata IDs are reported") def _() -> None: result = inspect(build_archive(count=2, metadata_ids=[0, 0], hash_ids=[0, 1])) require(result["classification"] == "SIECAF_MALFORMED", str(result)) require( result["duplicate_metadata_section_ids"] == [0], "duplicate metadata ID was not preserved", ) @case("09 metadata and hash ID sets must agree") def _() -> None: result = inspect(build_archive(count=2, metadata_ids=[0, 1], hash_ids=[0, 2])) require(result["classification"] == "SIECAF_MALFORMED", str(result)) require( any("table indexes" in error for error in result["errors"]), "mismatched ID sets were not reported", ) @case("10 repeated section IDs are valid when part numbers differ") def _() -> None: value = bytearray(build_archive(count=2, metadata_ids=[7, 7])) second_meta = module.HEADER.size + module.SEGMENT_META.size fields = list( module.SEGMENT_META.unpack( value[second_meta : second_meta + module.SEGMENT_META.size] ) ) fields[2] = 1 value[second_meta : second_meta + module.SEGMENT_META.size] = ( module.SEGMENT_META.pack(*fields) ) result = inspect(bytes(value)) require( result["classification"] == "SIECAF_VALID_STRUCTURE", str(result), ) require( result["repeated_metadata_section_ids"] == [7], "multi-part ID was not reported", ) @case("11 aligned length must be coherent") def _() -> None: result = inspect(build_archive(aligned_length=module.ALIGNMENT - 1)) require(result["classification"] == "SIECAF_MALFORMED", str(result)) @case("12 unaligned length cannot exceed aligned length") def _() -> None: result = inspect(build_archive(unaligned_length=module.ALIGNMENT + 1)) require(result["classification"] == "SIECAF_MALFORMED", str(result)) @case("13 zero-length sections are coherent") def _() -> None: result = inspect(build_archive(aligned_length=0, unaligned_length=0)) require( result["classification"] == "SIECAF_VALID_STRUCTURE", str(result), ) @case("14 trailing data is visible but not silently discarded") def _() -> None: result = inspect(build_archive(trailing=7)) require( result["classification"] == "SIECAF_VALID_STRUCTURE", str(result), ) require(result["trailing_data_bytes"] == 7, "trailing bytes not counted") require(bool(result["warnings"]), "trailing bytes not warned") @case("15 structural exact requires all structural hashes") def _() -> None: left = inspect(build_archive()) right = inspect(build_archive()) comparison = module.compare_structures(left, right) require( comparison["classification"] == "SIECAF_STRUCTURAL_EXACT", str(comparison), ) @case("16 equal layout with different hashes is not structural exact") def _() -> None: left = inspect(build_archive(hash_seed=1)) right = inspect(build_archive(hash_seed=9)) comparison = module.compare_structures(left, right) require( comparison["classification"] == "SIECAF_LAYOUT_MATCH_HASHES_DIFFER", str(comparison), ) @case("17 different segment layout is classified") def _() -> None: left = inspect(build_archive(count=1)) right = inspect(build_archive(count=2)) comparison = module.compare_structures(left, right) require( comparison["classification"] == "SIECAF_LAYOUT_DIFFERENT", str(comparison), ) failures: list[str] = [] for name, function in cases: try: function() except Exception as error: failures.append(f"{name}: {error}") if failures: for failure in failures: print(f"FAIL: {failure}") return 1 print(f"SIECAF header parser tests: {len(cases)}/{len(cases)} PASS") return 0 if __name__ == "__main__": raise SystemExit(main())