163 lines
5.4 KiB
Shell
Executable file
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 ]
|