v4.2.0: full (pure) post-quantum mode + critical dedup nonce fix
Add a native full post-quantum encryption mode and fix a critical keystream-reuse bug in deduplicated encrypted archives. Full post-quantum mode (--pq-only) - New envelope type 0x06 (ZUPT_ENC_PQ_ONLY): ML-KEM-768 (FIPS 203) as the sole key-establishment mechanism, with no classical X25519 component. Archive key = SHA3-512(ml_ss || ml_ct || "ZUPT-PQ-ONLY-v1"). - For compliance postures that require a single NIST-standardised PQ primitive with no classical KEM in the envelope (CNSA 2.0-style "PQ-only"). Hybrid --pq stays the recommended default; --pq-only has no classical fallback, so a break of ML-KEM-768 alone breaks it. - keygen --pq-only / keygen --pub --pq-only (ZPQK magic, 1200B pub / 3600B priv; not interchangeable with hybrid --pq keys). Wrong or tampered ciphertext is rejected via ML-KEM FO implicit rejection plus the HMAC-SHA256 Encrypt-then-MAC envelope. In-tree, default build. Security (critical): AES-256-CTR keystream reuse under --dedup - Dedup assigns block sequence 0 to every data block (the sentinel that keeps cross-file dedup references authenticating consistently). The per-block nonce was base_nonce XOR block_seq, so under --dedup every block collapsed to the same nonce, reusing the CTR keystream across distinct plaintexts (a many-time-pad). Each block now uses a fresh random 128-bit nonce stored in the block prefix and bound into the block MAC; block_seq is still bound as MAC AAD. Regression test: tests/test_dedup_nonce.sh. Re-encrypt any --dedup encrypted archives written by <= 4.1.0. Other - keygen --sdk / --box on a source-only build now fails with a clear message pointing to native --pq / --pq-only (or a WITH_SDK=1 build). - Documentation: README, SECURITY, THREAT_MODEL, man page, CHANGELOG, and all packaging recipes updated for the new mode and the security fix; version bumped to 4.2.0. Wire format v1.6 unchanged (0x06 is additive). Validation: make check 16/16, quick suite 11/11 (incl. PQ-only), dedup-nonce regression (all block nonces distinct), cppcheck clean.
This commit is contained in:
parent
31fa4028aa
commit
124958aea9
24 changed files with 811 additions and 123 deletions
56
tests/test_dedup_nonce.sh
Normal file
56
tests/test_dedup_nonce.sh
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Regression: dedup-encrypted archives must NOT reuse the AES-256-CTR nonce
|
||||
# across blocks.
|
||||
#
|
||||
# v4.2.0 fix: the old per-block nonce was base_nonce XOR block_seq, but dedup
|
||||
# mode hard-codes block_seq==0 for every data block (the sentinel needed so
|
||||
# cross-file dedup references authenticate consistently). That collapsed every
|
||||
# dedup block's nonce to a single value, reusing the CTR keystream across
|
||||
# distinct plaintexts — a many-time-pad. The nonce is now a fresh random 128-bit
|
||||
# value per block. This test asserts every encrypted DATA block in a
|
||||
# dedup-encrypted archive carries a distinct stored nonce.
|
||||
set -u
|
||||
ZUPT="${ZUPT_BIN:-./zupt}"
|
||||
echo "Dedup nonce uniqueness (keystream-reuse regression)"
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo " - skipped: python3 not available"; exit 0
|
||||
fi
|
||||
|
||||
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
||||
# 1 MiB of random data => many distinct 128 KiB blocks (random never dedups).
|
||||
head -c 1048576 /dev/urandom > "$T/f.bin"
|
||||
"$ZUPT" compress --dedup -p testpw "$T/a.zupt" "$T/f.bin" >/dev/null 2>&1
|
||||
|
||||
python3 - "$T/a.zupt" <<'PY'
|
||||
import sys
|
||||
d = open(sys.argv[1], 'rb').read()
|
||||
nonces = []; i = 0
|
||||
def rv(p):
|
||||
v = s = 0
|
||||
while True:
|
||||
b = d[p]; p += 1; v |= (b & 127) << s
|
||||
if not (b & 128): break
|
||||
s += 7
|
||||
return v, p
|
||||
while True:
|
||||
j = d.find(b'\xbb\x01', i)
|
||||
if j < 0 or j + 7 > len(d): break
|
||||
bt = d[j+2]; flags = d[j+5] | (d[j+6] << 8)
|
||||
if bt == 0 and (flags & 1): # DATA + ENCRYPTED
|
||||
p = j + 7
|
||||
_, p = rv(p); _, p = rv(p); p += 8 # skip usz, csz, xxh64
|
||||
nonces.append(bytes(d[p:p+16])) # 16-byte nonce prefix
|
||||
i = j + 2
|
||||
if len(nonces) < 2:
|
||||
print(" - inconclusive: only %d encrypted block(s) parsed" % len(nonces)); sys.exit(0)
|
||||
if len(set(nonces)) == len(nonces):
|
||||
print(" ✓ %d encrypted dedup blocks, all %d nonces distinct" % (len(nonces), len(set(nonces))))
|
||||
sys.exit(0)
|
||||
print(" ✗ %d blocks but only %d distinct nonces — CTR KEYSTREAM REUSE" % (len(nonces), len(set(nonces))))
|
||||
sys.exit(1)
|
||||
PY
|
||||
rc=$?
|
||||
[ $rc -eq 0 ] && echo " Dedup nonce: 1 passed, 0 failed" || echo " Dedup nonce: 0 passed, 1 failed"
|
||||
exit $rc
|
||||
Loading…
Reference in a new issue