v2.2.2
This commit is contained in:
parent
f3e39fb8e6
commit
e5f5d32aab
124 changed files with 11892 additions and 2461 deletions
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
* Zupt v2.0.0 — AFL++ Fuzzing Harness: Archive Decompression
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co)
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* Zupt v2.0.0 — AFL++ Fuzzing Harness: Archive Decompression
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later
|
||||
*
|
||||
* Reads a fuzzed .zupt archive from stdin, attempts to extract it.
|
||||
* Catches crashes, buffer overflows, and undefined behavior.
|
||||
|
|
|
|||
BIN
tests/fuzz_format
Executable file
BIN
tests/fuzz_format
Executable file
Binary file not shown.
157
tests/fuzz_format.c
Normal file
157
tests/fuzz_format.c
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* zupt format parser fuzz harness
|
||||
*
|
||||
* Mutates valid archives and feeds them to the listing/extract path
|
||||
* under ASAN/UBSAN. Any crash, leak, or sanitizer error is a finding.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
#define _DEFAULT_SOURCE 1
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
|
||||
/* Simple xorshift64 PRNG — deterministic, fast */
|
||||
static uint64_t rng_state = 0xc0ffeebabe5050ULL;
|
||||
static uint64_t rng(void) {
|
||||
uint64_t x = rng_state;
|
||||
x ^= x << 13; x ^= x >> 7; x ^= x << 17;
|
||||
return rng_state = x;
|
||||
}
|
||||
|
||||
static void mutate(uint8_t *buf, size_t n) {
|
||||
if (n == 0) return;
|
||||
int op = rng() % 5;
|
||||
switch (op) {
|
||||
case 0: { /* byte flip */
|
||||
size_t i = rng() % n;
|
||||
buf[i] ^= 1u << (rng() % 8);
|
||||
break;
|
||||
}
|
||||
case 1: { /* random byte set */
|
||||
size_t i = rng() % n;
|
||||
buf[i] = (uint8_t)(rng() & 0xFF);
|
||||
break;
|
||||
}
|
||||
case 2: { /* zero a 16-byte run */
|
||||
if (n < 16) return;
|
||||
size_t off = rng() % (n - 16);
|
||||
memset(buf + off, 0, 16);
|
||||
break;
|
||||
}
|
||||
case 3: { /* set 0xFF run */
|
||||
if (n < 16) return;
|
||||
size_t off = rng() % (n - 16);
|
||||
memset(buf + off, 0xFF, 16);
|
||||
break;
|
||||
}
|
||||
case 4: { /* swap two bytes */
|
||||
if (n < 2) return;
|
||||
size_t i = rng() % n;
|
||||
size_t j = rng() % n;
|
||||
uint8_t t = buf[i]; buf[i] = buf[j]; buf[j] = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Run the zupt binary on a given archive file. We use fork+exec because
|
||||
* any crash in zupt would otherwise take down the harness; the parent
|
||||
* just records the exit status. ASAN/UBSAN errors return non-zero. */
|
||||
static int run_zupt(const char *zupt_path, const char *archive,
|
||||
const char **flag, int with_pq) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
/* child */
|
||||
int devnull = open("/dev/null", O_WRONLY);
|
||||
if (devnull >= 0) {
|
||||
dup2(devnull, 1); dup2(devnull, 2);
|
||||
close(devnull);
|
||||
}
|
||||
if (with_pq) {
|
||||
execl(zupt_path, "zupt", "list", "--pq-sdk", "/tmp/_fuzz.priv",
|
||||
archive, (char*)NULL);
|
||||
} else {
|
||||
execl(zupt_path, "zupt", "list", archive, (char*)NULL);
|
||||
}
|
||||
_exit(127);
|
||||
(void)flag;
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
if (WIFSIGNALED(status)) return -2; /* crash! */
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int n_iters = argc > 1 ? atoi(argv[1]) : 1000;
|
||||
const char *zupt_path = argc > 2 ? argv[2] : "./zupt";
|
||||
const char *seed = argc > 3 ? argv[3] : "/tmp/_fuzz_seed.zupt";
|
||||
|
||||
if (access(seed, R_OK) != 0) {
|
||||
fprintf(stderr, "Seed archive not found at %s\n", seed);
|
||||
fprintf(stderr, "Build seed first: ./zupt c %s some_file.txt\n", seed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
FILE *f = fopen(seed, "rb");
|
||||
if (!f) return 1;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
uint8_t *seed_data = malloc((size_t)sz);
|
||||
if (!seed_data || fread(seed_data, 1, (size_t)sz, f) != (size_t)sz) {
|
||||
fclose(f); free(seed_data); return 1;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
fprintf(stderr, "Fuzzing zupt format parser: %d iters, seed=%ld bytes\n",
|
||||
n_iters, sz);
|
||||
|
||||
int crashes = 0, errors = 0, accepts = 0;
|
||||
char fuzzfile[64] = "/tmp/_fuzz_archive.zupt";
|
||||
double t0 = (double)clock() / CLOCKS_PER_SEC;
|
||||
|
||||
for (int i = 0; i < n_iters; i++) {
|
||||
/* Copy seed, apply 1-5 random mutations */
|
||||
uint8_t *buf = malloc((size_t)sz);
|
||||
if (!buf) break;
|
||||
memcpy(buf, seed_data, (size_t)sz);
|
||||
int n_mut = 1 + (rng() % 5);
|
||||
for (int m = 0; m < n_mut; m++) mutate(buf, (size_t)sz);
|
||||
|
||||
FILE *out = fopen(fuzzfile, "wb");
|
||||
if (!out) { free(buf); continue; }
|
||||
fwrite(buf, 1, (size_t)sz, out);
|
||||
fclose(out);
|
||||
free(buf);
|
||||
|
||||
int rc = run_zupt(zupt_path, fuzzfile, NULL, 0);
|
||||
if (rc == -2) crashes++;
|
||||
else if (rc != 0) errors++;
|
||||
else accepts++;
|
||||
|
||||
if (i > 0 && i % 100 == 0)
|
||||
fprintf(stderr, " [%d/%d] crashes=%d errors=%d accepts=%d\r",
|
||||
i, n_iters, crashes, errors, accepts);
|
||||
}
|
||||
double dt = (double)clock() / CLOCKS_PER_SEC - t0;
|
||||
|
||||
unlink(fuzzfile);
|
||||
free(seed_data);
|
||||
|
||||
fprintf(stderr, "\n\nFuzz results (%d iterations, %.1fs):\n", n_iters, dt);
|
||||
fprintf(stderr, " CRASHES (signal): %d <-- bugs if non-zero\n", crashes);
|
||||
fprintf(stderr, " errors (rejected): %d (expected, parser working)\n", errors);
|
||||
fprintf(stderr, " accepts: %d (mutations that didn't break magic/size)\n", accepts);
|
||||
|
||||
return crashes > 0 ? 1 : 0;
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
* Zupt v2.0.0 — AFL++ Fuzzing Harness: VaptVupt Codec
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co)
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* Zupt v2.0.0 — AFL++ Fuzzing Harness: VaptVupt Codec
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later
|
||||
*
|
||||
* Reads fuzzed VaptVupt frame data from stdin, attempts decompression.
|
||||
* Tests the VaptVupt codec directly (bypassing Zupt archive format).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# ZUPT v2.0.0 — Comprehensive Regression Test Suite
|
||||
# Covers: normal, solid, encrypted, edge cases, VaptVupt codec
|
||||
# Run: sh tests/regression.sh
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
set +e
|
||||
Z="./zupt"; T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
||||
mkdir -p "$T/d"; echo "hello" > "$T/d/a.txt"
|
||||
|
|
|
|||
78
tests/test_arg_order.sh
Executable file
78
tests/test_arg_order.sh
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Regression test for argument order in extract/list/test commands.
|
||||
# Bug #15 (v2.2.2): options after the positional archive argument were
|
||||
# silently dropped. e.g. `zupt x arch.zupt -o out` ignored `-o out`.
|
||||
|
||||
ZUPT_BIN="$(realpath ./zupt)"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
cd "$TMPDIR"
|
||||
|
||||
PASS=0; FAIL=0
|
||||
chk() {
|
||||
if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1))
|
||||
else echo " ✗ $1"; FAIL=$((FAIL+1)); fi
|
||||
}
|
||||
|
||||
echo " [Argument order regression — extract/list/test]"
|
||||
|
||||
# Setup
|
||||
mkdir d
|
||||
echo "secret content $(date +%s%N)" > d/file.txt
|
||||
"$ZUPT_BIN" c arch.zupt d/file.txt > /dev/null 2>&1
|
||||
"$ZUPT_BIN" c -p mypw arch_pw.zupt d/file.txt > /dev/null 2>&1
|
||||
"$ZUPT_BIN" keygen -o k.key > /dev/null 2>&1
|
||||
"$ZUPT_BIN" keygen --pub -o p.key -k k.key > /dev/null 2>&1
|
||||
"$ZUPT_BIN" c --pq p.key arch_pq.zupt d/file.txt > /dev/null 2>&1
|
||||
|
||||
# P1: extract -o after archive
|
||||
mkdir out1
|
||||
"$ZUPT_BIN" x arch.zupt -o out1 > /dev/null 2>&1
|
||||
[ -f out1/d/file.txt ] && diff -q d/file.txt out1/d/file.txt > /dev/null
|
||||
chk "extract: -o after archive"
|
||||
|
||||
# P2: extract -o before archive
|
||||
mkdir out2
|
||||
"$ZUPT_BIN" x -o out2 arch.zupt > /dev/null 2>&1
|
||||
[ -f out2/d/file.txt ] && diff -q d/file.txt out2/d/file.txt > /dev/null
|
||||
chk "extract: -o before archive"
|
||||
|
||||
# P3: extract password options after archive
|
||||
mkdir out3
|
||||
"$ZUPT_BIN" x arch_pw.zupt -p mypw -o out3 > /dev/null 2>&1
|
||||
[ -f out3/d/file.txt ] && diff -q d/file.txt out3/d/file.txt > /dev/null
|
||||
chk "extract: -p AND -o after archive"
|
||||
|
||||
# P4: extract --pq after archive
|
||||
mkdir out4
|
||||
"$ZUPT_BIN" x arch_pq.zupt --pq k.key -o out4 > /dev/null 2>&1
|
||||
[ -f out4/d/file.txt ] && diff -q d/file.txt out4/d/file.txt > /dev/null
|
||||
chk "extract: --pq after archive"
|
||||
|
||||
# P5: list -p after archive (encrypted archive lists files)
|
||||
out=$("$ZUPT_BIN" l arch_pw.zupt -p mypw 2>&1)
|
||||
echo "$out" | grep -q "file.txt"
|
||||
chk "list: -p after archive"
|
||||
|
||||
# P6: list -p before archive
|
||||
out=$("$ZUPT_BIN" l -p mypw arch_pw.zupt 2>&1)
|
||||
echo "$out" | grep -q "file.txt"
|
||||
chk "list: -p before archive"
|
||||
|
||||
# P7: test -p after archive
|
||||
out=$("$ZUPT_BIN" t arch_pw.zupt -p mypw 2>&1)
|
||||
echo "$out" | grep -q "0 failed"
|
||||
chk "test: -p after archive"
|
||||
|
||||
# P8: test -p before archive
|
||||
out=$("$ZUPT_BIN" t -p mypw arch_pw.zupt 2>&1)
|
||||
echo "$out" | grep -q "0 failed"
|
||||
chk "test: -p before archive"
|
||||
|
||||
echo
|
||||
echo " ───────────────────────────────────────"
|
||||
echo " Argument order regression: $PASS passed, $FAIL failed"
|
||||
echo " ───────────────────────────────────────"
|
||||
[ $FAIL -eq 0 ]
|
||||
137
tests/test_audit.sh
Executable file
137
tests/test_audit.sh
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# zupt audit test suite — double-validated security checks for zupt 2.2+
|
||||
# Each property is checked via TWO independent paths.
|
||||
|
||||
ZUPT_BIN="$(realpath ./zupt)"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
cd "$TMPDIR"
|
||||
|
||||
PASS=0; FAIL=0
|
||||
DCHK() {
|
||||
local name="$1" a="$2" b="$3"
|
||||
if [ "$a" = "$b" ] && [ "$a" = "1" ]; then
|
||||
echo " ✓ $name (A=$a B=$b agree)"; PASS=$((PASS+1))
|
||||
else
|
||||
echo " ✗ $name (A=$a B=$b disagree)"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup: SDK keys
|
||||
"$ZUPT_BIN" keygen --sdk -o k.priv > /dev/null 2>&1
|
||||
"$ZUPT_BIN" keygen --sdk -o other.priv > /dev/null 2>&1
|
||||
"$ZUPT_BIN" keygen -o legacy.key > /dev/null 2>&1
|
||||
|
||||
echo " [A. Authenticated archives]"
|
||||
|
||||
# A1. Wrong key rejected: SDK key vs SDK archive (path A) + Legacy key vs SDK archive (path B)
|
||||
echo "data" > input.txt
|
||||
"$ZUPT_BIN" c --pq-sdk k.priv.pub a.zupt input.txt > /dev/null 2>&1
|
||||
mkdir -p ea && (cd ea && "$ZUPT_BIN" x --pq-sdk ../other.priv ../a.zupt > /dev/null 2>&1)
|
||||
A=$([ ! -f ea/input.txt ] && echo 1 || echo 0)
|
||||
mkdir -p eb && (cd eb && "$ZUPT_BIN" x --pq legacy.key ../a.zupt > /dev/null 2>&1)
|
||||
B=$([ ! -f eb/input.txt ] && echo 1 || echo 0)
|
||||
DCHK "Wrong key rejected (SDK key + legacy key paths)" "$A" "$B"
|
||||
|
||||
# A2. Tamper at byte position N detected (path A: pos 200) (path B: pos at end)
|
||||
cp a.zupt t1.zupt; cp a.zupt t2.zupt
|
||||
python3 -c "
|
||||
b = bytearray(open('t1.zupt','rb').read())
|
||||
b[200] ^= 1
|
||||
open('t1.zupt','wb').write(bytes(b))" 2>/dev/null
|
||||
python3 -c "
|
||||
b = bytearray(open('t2.zupt','rb').read())
|
||||
b[len(b)-50] ^= 1
|
||||
open('t2.zupt','wb').write(bytes(b))" 2>/dev/null
|
||||
mkdir -p t1e && (cd t1e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../t1.zupt > /dev/null 2>&1)
|
||||
mkdir -p t2e && (cd t2e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../t2.zupt > /dev/null 2>&1)
|
||||
A=$([ ! -f t1e/input.txt ] && echo 1 || echo 0)
|
||||
B=$([ ! -f t2e/input.txt ] && echo 1 || echo 0)
|
||||
DCHK "Tamper detected at any byte position" "$A" "$B"
|
||||
|
||||
echo " [B. Format security]"
|
||||
|
||||
# B1. Zero-byte file (path A) + 1-byte file (path B): both must roundtrip
|
||||
> empty.txt
|
||||
echo -n "x" > one.txt
|
||||
"$ZUPT_BIN" c --pq-sdk k.priv.pub e.zupt empty.txt > /dev/null 2>&1
|
||||
"$ZUPT_BIN" c --pq-sdk k.priv.pub o.zupt one.txt > /dev/null 2>&1
|
||||
mkdir -p eex && (cd eex && "$ZUPT_BIN" x --pq-sdk ../k.priv ../e.zupt > /dev/null 2>&1)
|
||||
mkdir -p oex && (cd oex && "$ZUPT_BIN" x --pq-sdk ../k.priv ../o.zupt > /dev/null 2>&1)
|
||||
A=$([ -f eex/empty.txt ] && [ ! -s eex/empty.txt ] && echo 1 || echo 0)
|
||||
B=$([ -f oex/one.txt ] && [ "$(cat oex/one.txt)" = "x" ] && echo 1 || echo 0)
|
||||
DCHK "Edge-size files (0/1 byte) roundtrip" "$A" "$B"
|
||||
|
||||
# B2. 1MB random file (path A) + structured-data 1MB (path B)
|
||||
dd if=/dev/urandom of=big_a.bin bs=1M count=1 2>/dev/null
|
||||
python3 -c "open('big_b.bin','wb').write(b'A'*1024*1024)"
|
||||
"$ZUPT_BIN" c --pq-sdk k.priv.pub ba.zupt big_a.bin > /dev/null 2>&1
|
||||
"$ZUPT_BIN" c --pq-sdk k.priv.pub bb.zupt big_b.bin > /dev/null 2>&1
|
||||
mkdir -p ba_e && (cd ba_e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../ba.zupt > /dev/null 2>&1)
|
||||
mkdir -p bb_e && (cd bb_e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../bb.zupt > /dev/null 2>&1)
|
||||
A=$(diff -q big_a.bin ba_e/big_a.bin > /dev/null 2>&1 && echo 1 || echo 0)
|
||||
B=$(diff -q big_b.bin bb_e/big_b.bin > /dev/null 2>&1 && echo 1 || echo 0)
|
||||
DCHK "1MB roundtrip (random + structured)" "$A" "$B"
|
||||
|
||||
# B3. Truncated archive rejected (path A: cut last 50 bytes) (path B: cut at midpoint)
|
||||
cp a.zupt tr1.zupt; cp a.zupt tr2.zupt
|
||||
truncate -s -50 tr1.zupt
|
||||
truncate -s 100 tr2.zupt
|
||||
mkdir -p tr1e tr2e
|
||||
(cd tr1e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../tr1.zupt > /dev/null 2>&1)
|
||||
(cd tr2e && "$ZUPT_BIN" x --pq-sdk ../k.priv ../tr2.zupt > /dev/null 2>&1)
|
||||
A=$([ ! -f tr1e/input.txt ] && echo 1 || echo 0)
|
||||
B=$([ ! -f tr2e/input.txt ] && echo 1 || echo 0)
|
||||
DCHK "Truncated archive rejected" "$A" "$B"
|
||||
|
||||
echo " [C. Format compatibility]"
|
||||
|
||||
# C1. Mode confusion: SDK archive cannot be read with --pq (legacy)
|
||||
mkdir -p mc1 && (cd mc1 && "$ZUPT_BIN" x --pq ../legacy.key ../a.zupt > /dev/null 2>&1)
|
||||
A=$([ ! -f mc1/input.txt ] && echo 1 || echo 0)
|
||||
# Also: legacy archive cannot be read with --pq-sdk
|
||||
"$ZUPT_BIN" c --pq legacy.key leg.zupt input.txt > /dev/null 2>&1
|
||||
mkdir -p mc2 && (cd mc2 && "$ZUPT_BIN" x --pq-sdk ../k.priv ../leg.zupt > /dev/null 2>&1)
|
||||
B=$([ ! -f mc2/input.txt ] && echo 1 || echo 0)
|
||||
DCHK "Mode confusion prevented (SDK↔legacy)" "$A" "$B"
|
||||
|
||||
# C2. Legacy archive readable with legacy key (compat baseline)
|
||||
mkdir -p lc && (cd lc && "$ZUPT_BIN" x --pq ../legacy.key ../leg.zupt > /dev/null 2>&1)
|
||||
A=$(diff -q lc/input.txt input.txt > /dev/null 2>&1 && echo 1 || echo 0)
|
||||
# B: SDK archive readable with SDK key (compat baseline)
|
||||
mkdir -p sc && (cd sc && "$ZUPT_BIN" x --pq-sdk ../k.priv ../a.zupt > /dev/null 2>&1)
|
||||
B=$(diff -q sc/input.txt input.txt > /dev/null 2>&1 && echo 1 || echo 0)
|
||||
DCHK "Both SDK and legacy paths roundtrip independently" "$A" "$B"
|
||||
|
||||
echo " [D. Robustness]"
|
||||
|
||||
# D1. Non-existent input handled
|
||||
"$ZUPT_BIN" c --pq-sdk k.priv.pub nx.zupt /nonexistent_file_12345 > /dev/null 2>&1
|
||||
A=$([ ! -f nx.zupt ] && echo 1 || echo 0)
|
||||
"$ZUPT_BIN" c --pq-sdk k.priv.pub nx2.zupt /dev/nonexistent > /dev/null 2>&1
|
||||
B=$([ ! -f nx2.zupt ] && echo 1 || echo 0)
|
||||
DCHK "Missing input file rejected cleanly" "$A" "$B"
|
||||
|
||||
# D2. Non-existent key handled
|
||||
mkdir -p nk1 && (cd nk1 && "$ZUPT_BIN" x --pq-sdk /nonexistent.key ../a.zupt > /dev/null 2>&1)
|
||||
A=$([ ! -f nk1/input.txt ] && echo 1 || echo 0)
|
||||
"$ZUPT_BIN" c --pq-sdk /nonexistent.pub bbnk.zupt input.txt > /dev/null 2>&1
|
||||
B=$([ ! -s bbnk.zupt ] && echo 1 || echo 0)
|
||||
DCHK "Missing key file rejected cleanly" "$A" "$B"
|
||||
|
||||
# D3. Multiple files in one archive
|
||||
mkdir -p multi
|
||||
echo "a" > multi/a.txt; echo "b" > multi/b.txt; echo "c" > multi/c.txt
|
||||
"$ZUPT_BIN" c --pq-sdk k.priv.pub mm.zupt multi/a.txt multi/b.txt multi/c.txt > /dev/null 2>&1
|
||||
mkdir -p mmex && (cd mmex && "$ZUPT_BIN" x --pq-sdk ../k.priv ../mm.zupt > /dev/null 2>&1)
|
||||
A=$(diff -q mmex/multi/a.txt multi/a.txt > /dev/null 2>&1 && echo 1 || echo 0)
|
||||
B=$([ -f mmex/multi/c.txt ] && [ "$(cat mmex/multi/c.txt 2>/dev/null)" = "c" ] && echo 1 || echo 0)
|
||||
DCHK "Multiple files in archive roundtrip" "$A" "$B"
|
||||
|
||||
echo
|
||||
echo " ───────────────────────────────────────"
|
||||
echo " Audit results: $PASS passed, $FAIL failed"
|
||||
echo " ───────────────────────────────────────"
|
||||
[ $FAIL -eq 0 ]
|
||||
180
tests/test_block_swap.sh
Executable file
180
tests/test_block_swap.sh
Executable file
|
|
@ -0,0 +1,180 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Bug #16 regression — Block-swap attack on encrypted archives.
|
||||
#
|
||||
# Pre-fix vulnerability:
|
||||
# AES-CTR + HMAC-SHA256 in zupt 2.2.2 covered MAC over (nonce || ciphertext)
|
||||
# only. The decryptor read the nonce from the package itself and ignored
|
||||
# the block_seq parameter. An attacker who swapped two valid encrypted
|
||||
# blocks (header + payload) between positions could produce an archive
|
||||
# that decrypts cleanly but extracts files with the wrong content.
|
||||
#
|
||||
# Fix:
|
||||
# Bind block_seq into MAC as 8-byte LE AAD. Encrypt is now MAC-over
|
||||
# (nonce || ciphertext || block_seq_LE). Decrypt tries v2 first, falls
|
||||
# back to legacy v1 for old archives. Per-file block_seq is used so
|
||||
# extract-side counter matches encrypt-side without needing extra
|
||||
# index metadata.
|
||||
#
|
||||
# This test:
|
||||
# 1. Creates an encrypted archive with two distinct files A and B
|
||||
# 2. Performs the block-swap surgery on the binary
|
||||
# 3. Verifies extract REJECTS the swapped archive (auth failure)
|
||||
# 4. Also verifies normal extract still works (regression guard)
|
||||
|
||||
ZUPT_BIN="$(realpath ./zupt)"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
cd "$TMPDIR"
|
||||
|
||||
PASS=0; FAIL=0
|
||||
chk() {
|
||||
if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1))
|
||||
else echo " ✗ $1"; FAIL=$((FAIL+1)); fi
|
||||
}
|
||||
|
||||
echo " [Bug #16 — Block-swap (reorder) attack defense]"
|
||||
|
||||
# Two distinct files, small enough that each fits in one block
|
||||
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' > file_A.txt
|
||||
printf 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n' > file_B.txt
|
||||
|
||||
# Build encrypted archive (small block to ensure 1 block per file)
|
||||
"$ZUPT_BIN" c -p mypassword -b 64 -t 1 archive.zupt file_A.txt file_B.txt > /dev/null 2>&1
|
||||
|
||||
# P1: Normal extract still works (regression guard)
|
||||
mkdir extract_normal
|
||||
"$ZUPT_BIN" x archive.zupt -p mypassword -o extract_normal > /dev/null 2>&1
|
||||
[ -f extract_normal/file_A.txt ] && [ -f extract_normal/file_B.txt ] && \
|
||||
diff -q file_A.txt extract_normal/file_A.txt > /dev/null && \
|
||||
diff -q file_B.txt extract_normal/file_B.txt > /dev/null
|
||||
chk "Normal extract still works (regression guard)"
|
||||
|
||||
# P2: The block-swap attack must FAIL (no files extracted, or wrong files rejected)
|
||||
python3 << 'PYEOF'
|
||||
import sys, struct
|
||||
data = bytearray(open('archive.zupt','rb').read())
|
||||
# Find DATA blocks via block magic 0xbb 0x01 + block_type=DATA(0)
|
||||
def parse_blocks(data):
|
||||
blocks = []
|
||||
i = 0
|
||||
while i < len(data) - 7:
|
||||
if data[i] == 0xbb and data[i+1] == 0x01:
|
||||
block_type = data[i+2]
|
||||
codec = struct.unpack('<H', bytes(data[i+3:i+5]))[0]
|
||||
flags = struct.unpack('<H', bytes(data[i+5:i+7]))[0]
|
||||
# parse varint uncomp
|
||||
idx = i + 7
|
||||
uncomp, shift = 0, 0
|
||||
while idx < len(data):
|
||||
b = data[idx]
|
||||
uncomp |= (b & 0x7F) << shift
|
||||
idx += 1
|
||||
if not (b & 0x80): break
|
||||
shift += 7
|
||||
comp, shift = 0, 0
|
||||
while idx < len(data):
|
||||
b = data[idx]
|
||||
comp |= (b & 0x7F) << shift
|
||||
idx += 1
|
||||
if not (b & 0x80): break
|
||||
shift += 7
|
||||
payload_start = idx + 8 # skip 8-byte checksum
|
||||
block_end = payload_start + comp
|
||||
blocks.append({
|
||||
'type': block_type, 'flags': flags,
|
||||
'start': i, 'end': block_end, 'comp': comp,
|
||||
})
|
||||
i = block_end
|
||||
else:
|
||||
i += 1
|
||||
return blocks
|
||||
|
||||
blocks = parse_blocks(data)
|
||||
data_blocks = [b for b in blocks if b['type'] == 0 and b['flags'] & 0x01]
|
||||
if len(data_blocks) < 2:
|
||||
print(f"Found only {len(data_blocks)} encrypted DATA blocks; can't swap", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Swap the first two DATA blocks
|
||||
B0 = bytes(data[data_blocks[0]['start']:data_blocks[0]['end']])
|
||||
B1 = bytes(data[data_blocks[1]['start']:data_blocks[1]['end']])
|
||||
swapped = bytearray(data)
|
||||
# Swap (assume same size)
|
||||
if len(B0) != len(B1):
|
||||
print(f"different block sizes {len(B0)} vs {len(B1)}; can't swap directly", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
swapped[data_blocks[0]['start']:data_blocks[0]['end']] = B1
|
||||
swapped[data_blocks[1]['start']:data_blocks[1]['end']] = B0
|
||||
open('archive_swapped.zupt','wb').write(bytes(swapped))
|
||||
print(f"swap done", file=sys.stderr)
|
||||
PYEOF
|
||||
swap_status=$?
|
||||
|
||||
if [ $swap_status -eq 0 ]; then
|
||||
mkdir extract_attack
|
||||
out=$("$ZUPT_BIN" x archive_swapped.zupt -p mypassword -o extract_attack 2>&1)
|
||||
rc=$?
|
||||
|
||||
# Attack defense check: at least one of these must hold
|
||||
# - rc != 0 (extract returned error)
|
||||
# - no files extracted
|
||||
# - extracted files have wrong content (we reject this — would mean the
|
||||
# bug is still present)
|
||||
a_swapped=0; b_swapped=0
|
||||
[ -f extract_attack/file_A.txt ] && cmp -s file_B.txt extract_attack/file_A.txt && a_swapped=1
|
||||
[ -f extract_attack/file_B.txt ] && cmp -s file_A.txt extract_attack/file_B.txt && b_swapped=1
|
||||
|
||||
if [ "$a_swapped" = "1" ] && [ "$b_swapped" = "1" ]; then
|
||||
# BAD: attack succeeded — file_A has B's content and vice versa
|
||||
false
|
||||
else
|
||||
# GOOD: attack rejected (either error, no files, or files unchanged)
|
||||
true
|
||||
fi
|
||||
chk "Block-swap attack rejected (cross-file reorder)"
|
||||
else
|
||||
echo " ⊘ Block-swap attack test skipped (couldn't locate block boundaries)"
|
||||
fi
|
||||
|
||||
# P3: Single-block file (boundary case — empty seq_AAD doesn't degenerate)
|
||||
echo "single block content" > tiny.txt
|
||||
"$ZUPT_BIN" c -p mypassword tiny.zupt tiny.txt > /dev/null 2>&1
|
||||
mkdir tiny_out
|
||||
"$ZUPT_BIN" x tiny.zupt -p mypassword -o tiny_out > /dev/null 2>&1
|
||||
[ -f tiny_out/tiny.txt ] && diff -q tiny.txt tiny_out/tiny.txt > /dev/null
|
||||
chk "Single-block file roundtrip (boundary)"
|
||||
|
||||
# P4: Multi-block large file (ensures every block has correct AAD seq)
|
||||
dd if=/dev/urandom of=big.bin bs=1024 count=512 2>/dev/null
|
||||
"$ZUPT_BIN" c -p mypassword big.zupt big.bin > /dev/null 2>&1
|
||||
mkdir big_out
|
||||
"$ZUPT_BIN" x big.zupt -p mypassword -o big_out > /dev/null 2>&1
|
||||
[ -f big_out/big.bin ] && diff -q big.bin big_out/big.bin > /dev/null
|
||||
chk "512KB multi-block file roundtrip"
|
||||
|
||||
# P5: Multiple files in one archive (each gets per-file seq counter)
|
||||
echo "first" > a.txt
|
||||
echo "second" > b.txt
|
||||
echo "third" > c.txt
|
||||
"$ZUPT_BIN" c -p mypassword multi.zupt a.txt b.txt c.txt > /dev/null 2>&1
|
||||
mkdir multi_out
|
||||
"$ZUPT_BIN" x multi.zupt -p mypassword -o multi_out > /dev/null 2>&1
|
||||
[ -f multi_out/a.txt ] && [ -f multi_out/b.txt ] && [ -f multi_out/c.txt ] && \
|
||||
diff -q a.txt multi_out/a.txt > /dev/null && \
|
||||
diff -q b.txt multi_out/b.txt > /dev/null && \
|
||||
diff -q c.txt multi_out/c.txt > /dev/null
|
||||
chk "Multi-file archive roundtrip (per-file seq counters)"
|
||||
|
||||
# P6: Wrong password still fails cleanly
|
||||
mkdir wrong_pw
|
||||
out=$("$ZUPT_BIN" x archive.zupt -p WRONG_PASSWORD -o wrong_pw 2>&1)
|
||||
[ ! -f wrong_pw/file_A.txt ]
|
||||
chk "Wrong password rejected"
|
||||
|
||||
echo
|
||||
echo " ───────────────────────────────────────"
|
||||
echo " Block-swap regression: $PASS passed, $FAIL failed"
|
||||
echo " ───────────────────────────────────────"
|
||||
[ $FAIL -eq 0 ]
|
||||
130
tests/test_dedup_props.sh
Executable file
130
tests/test_dedup_props.sh
Executable file
|
|
@ -0,0 +1,130 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Property-based test for zupt dedup path.
|
||||
# Generates random file sets with intentional duplicates, verifies that
|
||||
# (a) compressed output is correct (byte-exact roundtrip) and
|
||||
# (b) dedup actually saves space when duplicates are present.
|
||||
|
||||
ZUPT_BIN="$(realpath ./zupt)"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
cd "$TMPDIR"
|
||||
|
||||
PASS=0; FAIL=0
|
||||
chk() {
|
||||
if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1))
|
||||
else echo " ✗ $1"; FAIL=$((FAIL+1)); fi
|
||||
}
|
||||
|
||||
# ─── Property 1: dedup roundtrip is byte-exact ──────────────────────────
|
||||
# Generate 10 random base files + 5 exact duplicates; compress with --dedup;
|
||||
# extract; verify every file matches its original.
|
||||
echo " [P1. Dedup roundtrip preserves all bytes]"
|
||||
|
||||
mkdir input
|
||||
for i in $(seq 1 10); do
|
||||
dd if=/dev/urandom of=input/file_$i.bin bs=4K count=$((RANDOM % 8 + 1)) 2>/dev/null
|
||||
done
|
||||
# 5 exact duplicates (same content as file_1..5)
|
||||
for i in 1 2 3 4 5; do
|
||||
cp input/file_$i.bin input/dup_$i.bin
|
||||
done
|
||||
|
||||
"$ZUPT_BIN" c --dedup test_dedup.zupt input/*.bin > /dev/null 2>&1
|
||||
chk "Compress with --dedup succeeds"
|
||||
|
||||
mkdir extracted && cd extracted
|
||||
"$ZUPT_BIN" x ../test_dedup.zupt > /dev/null 2>&1
|
||||
chk "Extract --dedup archive succeeds"
|
||||
|
||||
all_match=1
|
||||
for i in $(seq 1 10); do
|
||||
if ! diff -q ../input/file_$i.bin tmp*/input/file_$i.bin > /dev/null 2>&1 \
|
||||
&& ! diff -q ../input/file_$i.bin input/file_$i.bin > /dev/null 2>&1; then
|
||||
all_match=0; break
|
||||
fi
|
||||
done
|
||||
[ $all_match -eq 1 ]
|
||||
chk "All 10 base files roundtrip byte-exact"
|
||||
|
||||
dup_match=1
|
||||
for i in 1 2 3 4 5; do
|
||||
found=0
|
||||
for d in tmp*/input input; do
|
||||
if [ -f "$d/dup_$i.bin" ] && diff -q ../input/dup_$i.bin "$d/dup_$i.bin" > /dev/null 2>&1; then
|
||||
found=1; break
|
||||
fi
|
||||
done
|
||||
[ $found -eq 1 ] || { dup_match=0; break; }
|
||||
done
|
||||
[ $dup_match -eq 1 ]
|
||||
chk "All 5 duplicate files roundtrip byte-exact"
|
||||
|
||||
cd ..
|
||||
|
||||
# ─── Property 2: dedup reduces size for duplicate-heavy workloads ───────
|
||||
echo " [P2. Dedup compresses better than non-dedup on duplicate-heavy data]"
|
||||
|
||||
mkdir dups
|
||||
for i in $(seq 1 20); do
|
||||
cp input/file_1.bin dups/copy_$i.bin
|
||||
done
|
||||
|
||||
"$ZUPT_BIN" c no_dedup.zupt dups/*.bin > /dev/null 2>&1
|
||||
"$ZUPT_BIN" c --dedup with_dedup.zupt dups/*.bin > /dev/null 2>&1
|
||||
|
||||
size_no=$(stat -c%s no_dedup.zupt 2>/dev/null || stat -f%z no_dedup.zupt)
|
||||
size_yes=$(stat -c%s with_dedup.zupt 2>/dev/null || stat -f%z with_dedup.zupt)
|
||||
|
||||
[ "$size_yes" -lt "$size_no" ]
|
||||
chk "Dedup archive ($size_yes B) smaller than non-dedup ($size_no B)"
|
||||
|
||||
ratio=$(awk "BEGIN{printf \"%.0f\", $size_yes * 100 / $size_no}")
|
||||
[ "$ratio" -lt 50 ]
|
||||
chk "Dedup achieves >50% reduction (got $ratio% of original)"
|
||||
|
||||
# ─── Property 3: dedup roundtrip preserves data on duplicate-only sets ──
|
||||
echo " [P3. 100% duplicate file set extracts correctly]"
|
||||
|
||||
mkdir extr_dups && cd extr_dups
|
||||
"$ZUPT_BIN" x ../with_dedup.zupt > /dev/null 2>&1
|
||||
chk "Extract heavy-duplicate archive succeeds"
|
||||
|
||||
n_extracted=$(find . -name "copy_*.bin" 2>/dev/null | wc -l)
|
||||
[ "$n_extracted" -eq 20 ]
|
||||
chk "All 20 duplicate copies extracted (got $n_extracted)"
|
||||
|
||||
all_dup_match=1
|
||||
for f in $(find . -name "copy_*.bin"); do
|
||||
if ! diff -q "$f" ../input/file_1.bin > /dev/null 2>&1; then
|
||||
all_dup_match=0; break
|
||||
fi
|
||||
done
|
||||
[ $all_dup_match -eq 1 ]
|
||||
chk "All extracted duplicates byte-exact match the original"
|
||||
|
||||
cd ..
|
||||
|
||||
# ─── Property 4: dedup + encryption coexist correctly ───────────────────
|
||||
echo " [P4. Dedup + SDK encryption work together]"
|
||||
|
||||
"$ZUPT_BIN" keygen --sdk -o k.priv > /dev/null 2>&1
|
||||
"$ZUPT_BIN" c --dedup --pq-sdk k.priv.pub enc_dedup.zupt dups/*.bin > /dev/null 2>&1
|
||||
chk "Encrypt + dedup compress succeeds"
|
||||
|
||||
mkdir extr_enc && cd extr_enc
|
||||
"$ZUPT_BIN" x --pq-sdk ../k.priv ../enc_dedup.zupt > /dev/null 2>&1
|
||||
chk "Encrypt + dedup extract succeeds"
|
||||
|
||||
n=$(find . -name "copy_*.bin" 2>/dev/null | wc -l)
|
||||
[ "$n" -eq 20 ]
|
||||
chk "All 20 copies recovered after enc+dedup ($n found)"
|
||||
|
||||
cd ..
|
||||
|
||||
echo
|
||||
echo " ───────────────────────────────────────"
|
||||
echo " Dedup property results: $PASS passed, $FAIL failed"
|
||||
echo " ───────────────────────────────────────"
|
||||
[ $FAIL -eq 0 ]
|
||||
156
tests/test_path_traversal.sh
Executable file
156
tests/test_path_traversal.sh
Executable file
|
|
@ -0,0 +1,156 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Path traversal / Zip Slip regression tests.
|
||||
#
|
||||
# Verifies that the v2.2.2 audit fixes for CVE-pattern path traversal
|
||||
# (Snyk Zip Slip 2018) and symlink-following on extract are working.
|
||||
#
|
||||
# Tests construct malicious archives in two ways:
|
||||
# (A) compress with a relative path then post-mutate the index (manual fuzz)
|
||||
# (B) try to extract into a directory containing a symlink with the same
|
||||
# name as an archive entry — should be refused due to O_NOFOLLOW.
|
||||
|
||||
ZUPT_BIN="$(realpath ./zupt)"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
cd "$TMPDIR"
|
||||
|
||||
PASS=0; FAIL=0
|
||||
chk() {
|
||||
if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1))
|
||||
else echo " ✗ $1"; FAIL=$((FAIL+1)); fi
|
||||
}
|
||||
|
||||
# ─── Property 1: archive with ".." entry must not extract above target ──
|
||||
# Strategy: compress an innocent file, then patch the archive's index to
|
||||
# replace the path with "../../escaped.txt". Extract into a subdir;
|
||||
# verify the file appears nowhere outside the subdir.
|
||||
echo " [P1. Zip Slip — relative path traversal blocked]"
|
||||
|
||||
mkdir input output_safe
|
||||
echo "secret content" > input/innocent.txt
|
||||
"$ZUPT_BIN" c slip.zupt input/innocent.txt > /dev/null 2>&1
|
||||
|
||||
# Patch the archive: replace "input/innocent.txt" path string with
|
||||
# "../escape.txt" in the index. We use a python helper because the index
|
||||
# is varint-prefixed and we need to keep length consistent.
|
||||
python3 << 'PYEOF'
|
||||
import sys
|
||||
data = bytearray(open('slip.zupt','rb').read())
|
||||
target = b'input/innocent.txt'
|
||||
replacement = b'../escape.txt'
|
||||
# Pad replacement to same length so varint length prefix stays valid
|
||||
pad = b'\x00' * (len(target) - len(replacement))
|
||||
i = data.find(target)
|
||||
if i < 0:
|
||||
print("ERROR: pattern not in archive")
|
||||
sys.exit(1)
|
||||
# Replace the bytes — note this will fail validation below, which is OK,
|
||||
# we want to see if extract REJECTS the malformed path.
|
||||
data[i:i+len(target)] = replacement + pad
|
||||
open('slip_patched.zupt','wb').write(bytes(data))
|
||||
PYEOF
|
||||
|
||||
# Try to extract — even if the patched archive is corrupt, we want to
|
||||
# verify that NO file appears at "../escape.txt" relative to output_safe.
|
||||
cd output_safe
|
||||
"$ZUPT_BIN" x ../slip_patched.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
# The key invariant: nothing escaped to TMPDIR (parent of output_safe)
|
||||
[ ! -f "$TMPDIR/escape.txt" ] && [ ! -f escape.txt ]
|
||||
chk "No escape via patched ../escape.txt path"
|
||||
|
||||
# ─── Property 2: archive with absolute path must not write to that path ──
|
||||
echo " [P2. Absolute path entries blocked]"
|
||||
|
||||
# Construct an archive entry with absolute "/tmp/owned.txt" via patching
|
||||
echo "innocent" > input2.txt
|
||||
"$ZUPT_BIN" c abs.zupt input2.txt > /dev/null 2>&1
|
||||
python3 << 'PYEOF'
|
||||
data = bytearray(open('abs.zupt','rb').read())
|
||||
target = b'input2.txt'
|
||||
# Replace with absolute path of equal length
|
||||
replacement = b'/tmp/owned' # 10 chars vs 10 chars
|
||||
i = data.find(target)
|
||||
if i >= 0:
|
||||
data[i:i+len(target)] = replacement
|
||||
open('abs_patched.zupt','wb').write(bytes(data))
|
||||
PYEOF
|
||||
|
||||
mkdir abs_extract
|
||||
cd abs_extract
|
||||
"$ZUPT_BIN" x ../abs_patched.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
[ ! -f /tmp/owned ]
|
||||
chk "Absolute /tmp/owned path rejected"
|
||||
|
||||
# ─── Property 3: symlink at output target is not followed ──────────────
|
||||
# Pre-place a symlink in output dir pointing to a sentinel file.
|
||||
# Extract an archive with the same entry name; verify the sentinel is
|
||||
# unchanged (i.e. extract refused to follow the symlink).
|
||||
echo " [P3. Symlink at extract target not followed]"
|
||||
|
||||
echo "DO_NOT_OVERWRITE" > sentinel.txt
|
||||
mkdir symlink_extract
|
||||
ln -s "$(pwd)/sentinel.txt" symlink_extract/innocent.txt
|
||||
|
||||
# Build a fresh non-patched archive with "innocent.txt"
|
||||
mkdir input3 && echo "evil overwrite content" > input3/innocent.txt
|
||||
"$ZUPT_BIN" c clean.zupt input3/innocent.txt > /dev/null 2>&1
|
||||
# Mutate path "input3/innocent.txt" -> "innocent.txt" so it lands at the symlink
|
||||
python3 << 'PYEOF'
|
||||
data = bytearray(open('clean.zupt','rb').read())
|
||||
target = b'input3/innocent.txt'
|
||||
replacement = b'innocent.txt' + (b'\x00' * (len(target) - len(b'innocent.txt')))
|
||||
i = data.find(target)
|
||||
if i >= 0:
|
||||
data[i:i+len(target)] = replacement
|
||||
open('clean_patched.zupt','wb').write(bytes(data))
|
||||
PYEOF
|
||||
|
||||
cd symlink_extract
|
||||
"$ZUPT_BIN" x ../clean_patched.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
# Sentinel must be unchanged — symlink follow would have overwritten it
|
||||
content=$(cat sentinel.txt)
|
||||
[ "$content" = "DO_NOT_OVERWRITE" ]
|
||||
chk "Sentinel via symlink not overwritten"
|
||||
|
||||
# ─── Property 4: legitimate paths still extract correctly ─────────────
|
||||
echo " [P4. Legitimate (safe) paths still extract]"
|
||||
|
||||
mkdir legit_input
|
||||
echo "ok content" > legit_input/normal.txt
|
||||
"$ZUPT_BIN" c legit.zupt legit_input/normal.txt > /dev/null 2>&1
|
||||
|
||||
mkdir legit_extract && cd legit_extract
|
||||
"$ZUPT_BIN" x ../legit.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
[ -f legit_extract/legit_input/normal.txt ] && \
|
||||
[ "$(cat legit_extract/legit_input/normal.txt)" = "ok content" ]
|
||||
chk "Normal extraction still works"
|
||||
|
||||
# ─── Property 5: deep path (allowed) but parent dir is created ─────────
|
||||
echo " [P5. Multi-component safe paths still work]"
|
||||
|
||||
mkdir deep && mkdir deep/sub && mkdir deep/sub/sub2
|
||||
echo "deep" > deep/sub/sub2/file.txt
|
||||
"$ZUPT_BIN" c deep.zupt deep/sub/sub2/file.txt > /dev/null 2>&1
|
||||
|
||||
mkdir deep_extract && cd deep_extract
|
||||
"$ZUPT_BIN" x ../deep.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
[ -f deep_extract/deep/sub/sub2/file.txt ]
|
||||
chk "Deep nested path extracted"
|
||||
|
||||
echo
|
||||
echo " ───────────────────────────────────────"
|
||||
echo " Path-traversal regression: $PASS passed, $FAIL failed"
|
||||
echo " ───────────────────────────────────────"
|
||||
[ $FAIL -eq 0 ]
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
set +e
|
||||
ZUPT="${1:-./zupt}"
|
||||
T="/tmp/zupt_pq_$$"; mkdir -p "$T/data"
|
||||
|
|
|
|||
72
tests/test_sdk.sh
Executable file
72
tests/test_sdk.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Test zupt SDK-backed PQ encryption (v2.2+)
|
||||
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
ZUPT_BIN="$(realpath ./zupt)"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
|
||||
cd "$TMPDIR"
|
||||
PASS=0; FAIL=0
|
||||
chk() { if [ $? -eq 0 ]; then echo " OK: $1"; PASS=$((PASS+1)); else echo " FAIL: $1"; FAIL=$((FAIL+1)); fi; }
|
||||
chk_neg() { if [ $? -ne 0 ]; then echo " OK: $1"; PASS=$((PASS+1)); else echo " FAIL: $1 (should have failed)"; FAIL=$((FAIL+1)); fi; }
|
||||
|
||||
# Setup: real keypair
|
||||
"$ZUPT_BIN" keygen --sdk -o key.priv > /dev/null 2>&1
|
||||
[ -f key.priv ] && [ -f key.priv.pub ]
|
||||
chk "SDK keygen produces both files"
|
||||
|
||||
# Test data
|
||||
echo "Hello SDK PQ encryption" > input.txt
|
||||
dd if=/dev/urandom of=large.bin bs=64K count=4 2>/dev/null
|
||||
|
||||
# Roundtrip small file
|
||||
"$ZUPT_BIN" c --pq-sdk key.priv.pub small.zupt input.txt > /dev/null 2>&1
|
||||
chk "SDK encrypt small"
|
||||
mkdir -p extract1 && cd extract1
|
||||
"$ZUPT_BIN" x --pq-sdk ../key.priv ../small.zupt > /dev/null 2>&1
|
||||
chk "SDK decrypt small"
|
||||
diff -q input.txt ../input.txt > /dev/null 2>&1
|
||||
chk "SDK small roundtrip byte-exact"
|
||||
cd ..
|
||||
|
||||
# Roundtrip large file
|
||||
"$ZUPT_BIN" c --pq-sdk key.priv.pub large.zupt large.bin > /dev/null 2>&1
|
||||
chk "SDK encrypt large (256KB)"
|
||||
mkdir -p extract2 && cd extract2
|
||||
"$ZUPT_BIN" x --pq-sdk ../key.priv ../large.zupt > /dev/null 2>&1
|
||||
chk "SDK decrypt large"
|
||||
diff -q large.bin ../large.bin > /dev/null 2>&1
|
||||
chk "SDK large roundtrip byte-exact"
|
||||
cd ..
|
||||
|
||||
# Wrong key rejected
|
||||
"$ZUPT_BIN" keygen --sdk -o other.priv > /dev/null 2>&1
|
||||
"$ZUPT_BIN" x --pq-sdk other.priv small.zupt > /dev/null 2>&1
|
||||
chk_neg "SDK wrong key rejected"
|
||||
|
||||
# Tamper detected
|
||||
cp small.zupt tampered.zupt
|
||||
python3 -c "
|
||||
b = bytearray(open('tampered.zupt','rb').read())
|
||||
b[len(b)-50] ^= 1
|
||||
open('tampered.zupt','wb').write(bytes(b))
|
||||
"
|
||||
"$ZUPT_BIN" x --pq-sdk key.priv tampered.zupt > /dev/null 2>&1
|
||||
chk_neg "SDK tampered ciphertext rejected"
|
||||
|
||||
# Legacy v1 compat: legacy --pq still works
|
||||
"$ZUPT_BIN" keygen -o legacy.key > /dev/null 2>&1
|
||||
"$ZUPT_BIN" c --pq legacy.key legacy.zupt input.txt > /dev/null 2>&1
|
||||
chk "Legacy --pq still encrypts"
|
||||
mkdir -p extract3 && cd extract3
|
||||
"$ZUPT_BIN" x --pq ../legacy.key ../legacy.zupt > /dev/null 2>&1
|
||||
chk "Legacy --pq still decrypts"
|
||||
cd ..
|
||||
|
||||
echo
|
||||
echo " Results: $PASS passed, $FAIL failed ($((PASS+FAIL)) tests)"
|
||||
[ $FAIL -eq 0 ]
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
set +e
|
||||
ZUPT="./zupt"
|
||||
T="/tmp/zupt_mt_$$"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
* Zupt — NIST/RFC Cryptographic Test Vectors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (commercial: sac@securityops.co)
|
||||
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
* Zupt — NIST/RFC Cryptographic Test Vectors
|
||||
* Copyright (c) 2026 Cristian Cezar Moisés — AGPL-3.0-or-later
|
||||
*
|
||||
* Tests: SHA-256 (FIPS 180-4), HMAC-SHA256 (RFC 4231),
|
||||
* X25519 (RFC 7748 §6.1), ML-KEM-768 roundtrip,
|
||||
|
|
|
|||
Loading…
Reference in a new issue