zupt/tests/test_f09_preface.sh
2026-08-31 14:14:36 -03:00

163 lines
5.4 KiB
Shell
Executable file

#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
#
# F-09 regression test (VaptVupt 2.3.1).
#
# F-09 closed the per-block frame preface tamper window by:
# 1. Binding the canonical preface (block_type, codec_id, block_flags,
# sizes, plaintext-XXH64) into the per-block HMAC via the new v1.6
# ZUPT_FLAG_AAD_PREFACE policy.
# 2. Adding strict structural validation of the encryption-header
# block's frame preface in read_enc_header (same pattern as F-07
# for the index block in v2.2.5).
#
# This test flips every serialized block-preface byte in a small v1.6 PBKDF2
# archive and asserts that each mutation is rejected. With pre-F-09 code this
# would show silent acceptances; post-F-09 it must show zero. A PQ-SDK archive
# receives the historical full-archive byte sweep when system libvuptsdk is
# enabled.
#
# Plaintext archives have no HMAC (XXH64 best-effort only), so per-byte
# coverage is intentionally weaker and a different, separately-tracked
# promise.
set -Eeuo pipefail
repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd -P)
ZUPT="${ZUPT_BIN:-$repo_root/zupt}"
case "$ZUPT" in
/*) ;;
*) ZUPT="$PWD/$ZUPT" ;;
esac
if [ ! -x "$ZUPT" ]; then
echo "$ZUPT not found — run 'make' first" >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo ' ✗ python3 is required for byte-level archive mutations' >&2
exit 1
fi
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
cd "$TMPDIR" || exit 1
printf 'F-09 regression test payload\n' > input.txt
printf 'source-only-preface-password\n' > password.txt
chmod 600 password.txt
run_sweep() {
local label=$1
local archive=$2
local scope=$3
shift 3
local -a auth_options=("$@")
local -a positions=()
local size
local position
local positions_output
local tested=0
local undetected_positions=''
local undetected_count
local clean_dir="clean-$label"
size=$(wc -c < "$archive")
if [ "$size" -lt 100 ] || [ "$size" -gt 10000 ]; then
echo "$label archive has unexpected size $size" >&2
return 1
fi
mkdir "$clean_dir"
if ! "$ZUPT" extract "${auth_options[@]}" -o "$clean_dir" "$archive" \
>/dev/null 2>&1 ||
! cmp input.txt "$clean_dir/input.txt" >/dev/null 2>&1; then
echo " ✗ clean $label archive does not extract byte-exact" >&2
return 1
fi
if [ "$scope" = preface ]; then
if ! positions_output=$(python3 \
"$repo_root/tests/archive_surgery.py" preface-positions \
"$archive") || [ -z "$positions_output" ]; then
echo " ✗ could not locate $label block prefaces" >&2
return 1
fi
while IFS= read -r position; do
[ -n "$position" ] && positions+=("$position")
done <<<"$positions_output"
echo " [F-09: all block-preface bytes in $size-byte $label archive]"
elif [ "$scope" = full ]; then
for ((position = 0; position < size; position++)); do
positions+=("$position")
done
echo " [F-09: exhaustive byte sweep of $size-byte $label archive]"
else
echo " ✗ internal error: unknown sweep scope $scope" >&2
return 1
fi
for position in "${positions[@]}"; do
if ! python3 - "$archive" t.zupt "$position" <<'PY'
import pathlib
import sys
source = pathlib.Path(sys.argv[1]).read_bytes()
mutated = bytearray(source)
mutated[int(sys.argv[3])] ^= 0x01
pathlib.Path(sys.argv[2]).write_bytes(mutated)
PY
then
echo " ✗ could not mutate $label archive byte $position" >&2
return 1
fi
tested=$((tested + 1))
if "$ZUPT" test "${auth_options[@]}" t.zupt >/dev/null 2>&1; then
undetected_positions="$undetected_positions $position"
fi
done
undetected_count=$(printf '%s\n' "$undetected_positions" | wc -w)
if [ "$undetected_count" -ne 0 ]; then
echo "$label: $undetected_count silent-accepted positions (must be 0)"
echo " positions:$undetected_positions"
return 1
fi
echo "$label: $tested tamper positions tested, 0 accepted"
}
FAIL=0
if ! "$ZUPT" compress --store --kdf pbkdf2 --pass-file password.txt \
pbkdf2.zupt input.txt >/dev/null 2>&1; then
echo ' ✗ could not create source-only PBKDF2 archive' >&2
exit 1
fi
if ! run_sweep PBKDF2 pbkdf2.zupt preface --pass-file password.txt; then
FAIL=$((FAIL + 1))
fi
version=$("$ZUPT" --version 2>&1)
if grep -Fq 'libvuptsdk=enabled' <<<"$version"; then
if ! "$ZUPT" keygen --sdk -o k.priv >/dev/null 2>&1 ||
! "$ZUPT" compress --store --pq-sdk k.priv.pub pq-sdk.zupt \
input.txt >/dev/null 2>&1; then
echo ' ✗ could not create PQ-SDK archive' >&2
FAIL=$((FAIL + 1))
elif ! run_sweep PQ-SDK pq-sdk.zupt full --pq-sdk k.priv; then
FAIL=$((FAIL + 1))
fi
else
echo ' SKIP: additional PQ-SDK sweep needs WITH_SDK=1 and system libvuptsdk'
fi
echo
echo " ───────────────────────────────────────"
if [ "$FAIL" -eq 0 ]; then
echo " F-09 regression: PASS"
else
echo " F-09 regression: FAIL ($FAIL archive variants)"
fi
echo " ───────────────────────────────────────"
[ "$FAIL" -eq 0 ]