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

@ -1,7 +1,7 @@
.\" Manpage for vaptvupt (formerly zupt; INPI Brasil trademark rename in v3.0.0)
.\" SPDX-License-Identifier: AGPL-3.0-or-later
.\" Copyright (c) 2025-2026 Cristian Cezar Moisés
.TH VAPTVUPT 1 "July 2026" "vaptvupt 4.1.0" "User Commands"
.TH VAPTVUPT 1 "July 2026" "vaptvupt 4.2.0" "User Commands"
.SH NAME
vaptvupt \- post-quantum backup compression utility (formerly zupt)
@ -125,7 +125,8 @@ archive without restoring it. Exit code is non-zero on any failure.
.B info
Print archive header metadata without requiring the decryption key.
Reports: format version, codec, encryption type (none / PBKDF2 /
Argon2id / ML-KEM-768+X25519), KDF iteration count, file count,
Argon2id / ML-KEM-768+X25519 hybrid / ML-KEM-768 pure-PQ), KDF
iteration count, file count,
creation timestamp, archive UUID, AIT presence. Safe to run on an
untrusted archive.
@ -157,18 +158,32 @@ after each block.
.TP
.B keygen
Generate a key file for keyfile-mode encryption. With
Generate a key file. With no PQ flag, writes a 32-byte raw key for
keyfile-mode encryption. With
.B --pq
generates a native ML-KEM-768 + X25519 \fBhybrid\fR keypair for
.B --pq
encryption (in-tree crypto; no external library). With
.B --pq-only
generates a native pure ML-KEM-768 keypair (magic
.BR ZPQK )
for
.B --pq-only
encryption. With
.B --sdk
generates an ML-KEM-768 + X25519 hybrid keypair suitable for
generates a keypair for the optional
.B --pq-sdk
mode. With
mode, and with
.B --box
generates a libpqvaptvupt sealed-box keypair (writes \fIfile\fR and
\fIfile\fR.pub) for
a libpqvaptvupt sealed-box keypair for
.B --pq-box
mode. With
(both need a
.B WITH_SDK=1
build). With
.B --pub
extracts the public key from an existing private key.
extracts the public key from an existing private key (combine with the
matching PQ flag, e.g.
.BR "keygen --pub --pq-only" ).
.SH GLOBAL OPTIONS
@ -233,10 +248,55 @@ Choose key-derivation function for password mode:
Use a 32-byte raw key file (generated with
.BR "vaptvupt keygen" ).
.TP
.B --pq \fIpub\fR
Enable native post-quantum \fBhybrid\fR encryption (envelope type 0x02,
recommended). Combines ML-KEM-768 (FIPS 203) with X25519 (RFC 7748) so
the archive key is secure unless \fBboth\fR the lattice KEM and the
elliptic-curve exchange are broken. Uses the in-tree crypto only — no
external library, always available. The
.I pub
argument is the recipient's public-key file from
.BR "vaptvupt keygen" .
On extraction, pass the secret key:
.B --pq
\fIpriv\fR.
.TP
.B --pq-only \fIpub\fR
Enable native \fBfull\fR (pure) post-quantum encryption (envelope type
0x06). ML-KEM-768 is the \fIsole\fR key-establishment mechanism — no
X25519 component. Choose this only when a policy mandates a single
NIST-standardised PQ primitive with no classical KEM in the envelope
(e.g. CNSA 2.0-style "PQ-only" postures). The archive key is
SHA3-512(ml_ss || ml_ct || "ZUPT-PQ-ONLY-v1"). Note the deliberate
trade-off: unlike
.BR --pq ,
a future weakness in ML-KEM-768 alone is sufficient to break the
envelope, because there is no classical KEM to fall back on. When in
doubt use
.B --pq
(hybrid). Keys are generated with
.BR "vaptvupt keygen --pq-only" ;
the private and public key files (magic
.BR ZPQK )
are not interchangeable with hybrid
.B --pq
keys. On extraction, pass the secret key:
.B --pq-only
\fIpriv\fR.
.TP
.B --pq-box \fIpub\fR
Enable post-quantum sealed-box encryption via the vendored libpqvaptvupt
(v4.0.0+, envelope type 0x05). ML-KEM-768 + X25519 shared secrets are
Enable post-quantum sealed-box encryption via libpqvaptvupt (envelope
type 0x05). \fBRequires an optional\fR \fBWITH_SDK=1\fR \fBbuild\fR: the
default source-only tree ships no vendored library, so this mode is
absent unless you build against libpqvaptvupt yourself. Prefer the
native
.B --pq
or
.B --pq-only
modes, which need no external library. ML-KEM-768 + X25519 shared secrets are
combined through HKDF-SHA256 with a domain-separating info string
("pqvv-seal-v1"); the box carries AES-256-CTR + HMAC-SHA256
Encrypt-then-MAC. The
@ -249,8 +309,13 @@ On extraction, pass the secret key:
\fIpriv\fR.
.TP
.B --pq-sdk \fIpub\fR
Enable post-quantum hybrid encryption. Uses ML-KEM-768 + X25519 with
HKDF combiner, HPKE binding, and key commitment. The
Enable post-quantum hybrid encryption via libzuptsdk (envelope type
0x03). \fBRequires an optional\fR \fBWITH_SDK=1\fR \fBbuild\fR and is
absent from the default source-only tree; use the native
.B --pq
instead, which provides the same ML-KEM-768 + X25519 hybrid with no
external dependency. Uses ML-KEM-768 + X25519 with HKDF combiner, HPKE
binding, and key commitment. The
.I pub
argument is the recipient's public-key file generated by
.BR "vaptvupt keygen --sdk" .
@ -293,11 +358,16 @@ flag of the same name.
.SH POST-QUANTUM ENCRYPTION
.B vaptvupt
implements a hybrid KEM as specified in FIPS 203 (ML-KEM) combined
with X25519 (RFC 7748). The session key is derived as:
offers two native post-quantum modes, both built entirely from the
in-tree crypto (no external library):
.RS
.IP "\fB--pq\fR (hybrid, recommended)" 4
A hybrid KEM combining ML-KEM-768 (FIPS 203) with X25519 (RFC 7748).
The archive key is derived as:
.RS
.nf
ss_pq = ML-KEM-768.decaps(sk_pq, ct_pq)
ss_ec = X25519(sk_ec, pk_ec_peer)
session = HKDF-SHA256(ss_pq || ss_ec,
@ -305,10 +375,38 @@ session = HKDF-SHA256(ss_pq || ss_ec,
salt = archive_uuid)
.fi
.RE
The hybrid combiner means the session key is at least as strong as the
strongest of {ML-KEM-768, X25519}: an attacker must break \fBboth\fR to
recover the key. This is the default recommendation and the right choice
for almost every user, because it stays secure even if one primitive is
later found weak.
.IP "\fB--pq-only\fR (full / pure PQ)" 4
ML-KEM-768 as the \fIsole\fR key-establishment mechanism, with no
classical component. The archive key is derived as:
.RS
.nf
The hybrid combiner means the session key is at least as strong as
the strongest of {ML-KEM-768, X25519}: an attacker must break both
to recover the key.
(ss_pq, ct_pq) = ML-KEM-768.encaps(pk_pq)
archive_key = SHA3-512(ss_pq || ct_pq || "ZUPT-PQ-ONLY-v1")
.fi
.RE
Use this only when a compliance posture requires a single
NIST-standardised PQ primitive with no classical KEM in the envelope
(for example CNSA 2.0-style "PQ-only" requirements). The deliberate
trade-off is that the envelope has \fBno hybrid safety net\fR: a future
cryptanalytic break of ML-KEM-768 alone breaks the archive, whereas
under
.B --pq
the attacker would still have to break X25519 as well. Unless a policy
forbids the classical component, prefer
.BR --pq .
.RE
.PP
Both modes carry the same authenticated envelope as password mode:
per-block AES-256-CTR with a fresh random 128-bit nonce, HMAC-SHA256
Encrypt-then-MAC, and ML-KEM Fujisaki-Okamoto implicit rejection, so a
wrong or tampered ciphertext is rejected rather than yielding garbage.
.PP
.B Key commitment:
@ -537,6 +635,19 @@ $ vaptvupt compress --pq mykey.pub backup.zupt ~/Documents
.fi
.RE
Compress with full (pure) post-quantum encryption — ML-KEM-768 only,
no classical component (compliance postures that mandate a single PQ
primitive):
.RS
.nf
$ vaptvupt keygen --pq-only -o pqkey
$ vaptvupt keygen --pub --pq-only -o pqkey.pub -k pqkey
$ vaptvupt compress --pq-only pqkey.pub backup.zupt ~/Documents
$ vaptvupt extract --pq-only pqkey -o restored backup.zupt
.fi
.RE
Backup a block device, sparse-aware:
.RS