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:
Cristian Cezar Moisés 2026-07-09 21:15:14 -03:00
commit 124958aea9
24 changed files with 811 additions and 123 deletions

View file

@ -50,7 +50,7 @@
#define ZUPT_PRODUCT_EXTENSION ".zupt" /* on-disk archive extension (kept stable) */
#define ZUPT_PRODUCT_TAGLINE "Post-quantum backup compression"
#define ZUPT_VERSION_STRING "4.1.0"
#define ZUPT_VERSION_STRING "4.2.0"
/* Vendored codec release (upstream tag) — single source for display strings.
* The codec's own VV_VERSION_* is its internal API version, not the release. */
#define ZUPT_CODEC_RELEASE "2.60.4"
@ -104,6 +104,7 @@
#define ZUPT_ENC_PQ_SDK_V2 0x03 /* libzuptsdk v2 header: HKDF combiner + commitment + HPKE binding */
#define ZUPT_ENC_PW_ARGON2 0x04 /* Password-based via libzuptsdk: Argon2id + XChaCha20-Poly1305 */
#define ZUPT_ENC_PQ_BOX_V1 0x05 /* libpqvaptvupt sealed box: HKDF-SHA256 domain-separated combiner */
#define ZUPT_ENC_PQ_ONLY 0x06 /* Full post-quantum: ML-KEM-768 only (no X25519), SHA3-512 KDF (v4.2.0) */
/* Argon2id KDF profile descriptor (v3.4.0).
*
@ -271,6 +272,7 @@ typedef struct {
int pq_mode; /* 1 = post-quantum hybrid KEM mode */
int sdk_mode; /* 1 = use libzuptsdk-backed v3 crypto (HKDF combiner + commitment + HPKE) */
int box_mode; /* 1 = libpqvaptvupt sealed-box mode (ZUPT_ENC_PQ_BOX_V1) */
int pqonly_mode; /* 1 = full post-quantum mode: ML-KEM-768 only (ZUPT_ENC_PQ_ONLY) */
int dedup; /* 1 = block-level deduplication enabled */
int kdf_legacy_pbkdf2; /* v2.4.1: 1 = force PBKDF2-SHA256 enc-header (compat with v2.4.0 and older readers). Default 0 = Argon2id. */
char password[256];
@ -484,6 +486,14 @@ int zupt_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
const uint8_t *enc_hdr, size_t enc_hdr_len);
/* ─── Full post-quantum crypto: ML-KEM-768 only, no X25519 (v4.2.0) ─── */
int zupt_pq_keygen(const char *keyfile);
int zupt_pq_export_pubkey(const char *privfile, const char *pubfile);
int zupt_pq_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
uint8_t *enc_hdr, size_t *enc_hdr_len);
int zupt_pq_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
const uint8_t *enc_hdr, size_t enc_hdr_len);
/* ─── SDK-backed crypto (zupt v2.2+, libzuptsdk under the hood) ─── */
int zupt_sdk_hybrid_keygen(const char *privkeyfile, const char *pubkeyfile);
int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,