325 lines
11 KiB
Python
325 lines
11 KiB
Python
#!/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())
|