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
46
CHANGELOG.md
46
CHANGELOG.md
|
|
@ -1,6 +1,52 @@
|
||||||
# VaptVupt Changelog
|
# VaptVupt Changelog
|
||||||
|
|
||||||
|
|
||||||
|
## [4.2.0] — 2026-07-09 — Full (pure) post-quantum mode; dedup keystream-reuse fix
|
||||||
|
|
||||||
|
### Added — full post-quantum encryption (`--pq-only`)
|
||||||
|
|
||||||
|
- New native **full post-quantum** mode: `--pq-only` uses **ML-KEM-768**
|
||||||
|
(FIPS 203) as the *sole* key-establishment mechanism, with no classical
|
||||||
|
X25519 component. It complements the existing hybrid `--pq` for compliance
|
||||||
|
postures that require a single NIST-standardised PQ primitive with no
|
||||||
|
classical KEM in the envelope (CNSA 2.0-style "PQ-only" requirements).
|
||||||
|
- Wire format: new envelope type `0x06` (`ZUPT_ENC_PQ_ONLY`). The archive key
|
||||||
|
is `SHA3-512(ml_ss || ml_ct || "ZUPT-PQ-ONLY-v1")`. Keypairs use the `ZPQK`
|
||||||
|
magic (1200-byte public, 3600-byte private) and are **not** interchangeable
|
||||||
|
with hybrid `--pq` keys.
|
||||||
|
- Keygen: `vaptvupt keygen --pq-only` and `keygen --pub --pq-only`. Encrypt
|
||||||
|
with `compress --pq-only pub.key`, restore with `extract --pq-only priv.key`.
|
||||||
|
Wrong/tampered ciphertext is rejected via ML-KEM Fujisaki-Okamoto implicit
|
||||||
|
rejection plus the HMAC-SHA256 Encrypt-then-MAC envelope.
|
||||||
|
- Built entirely from the in-tree crypto — no external library, always
|
||||||
|
available in the default source-only build. The security trade-off vs the
|
||||||
|
hybrid is documented explicitly: `--pq-only` has no classical safety net, so
|
||||||
|
a future break of ML-KEM-768 alone breaks the envelope. `--pq` (hybrid)
|
||||||
|
remains the default recommendation.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Deterministic keygen guidance for the SDK path.** `keygen --sdk` /
|
||||||
|
`--box` on a source-only build now fails with a clear message pointing to the
|
||||||
|
native `--pq` / `--pq-only` keygen (or a `WITH_SDK=1` build) instead of an
|
||||||
|
opaque error.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- **Critical — AES-256-CTR keystream reuse under deduplication.** In `--dedup`
|
||||||
|
mode every data block was assigned block sequence 0 (the sentinel that lets
|
||||||
|
cross-file dedup references authenticate consistently). The per-block AEAD
|
||||||
|
nonce was previously derived as `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, from which XOR of ciphertexts leaks
|
||||||
|
plaintext XOR). 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. Nonces are now distinct across all blocks in every mode (regression
|
||||||
|
test `tests/test_dedup_nonce.sh`). Found by an adversarial review of the
|
||||||
|
encryption path and confirmed empirically. Archives written by 4.1.0 and
|
||||||
|
earlier in `--dedup` + encryption mode should be re-encrypted with 4.2.0.
|
||||||
|
|
||||||
|
|
||||||
## [4.1.0] — 2026-07-07 — Source-only build; multithreaded-encryption fix; hardening
|
## [4.1.0] — 2026-07-07 — Source-only build; multithreaded-encryption fix; hardening
|
||||||
|
|
||||||
### Source-only build (no vendored binaries)
|
### Source-only build (no vendored binaries)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Every packaging recipe expects an upstream tarball `vaptvupt-VERSION.tar.gz` pro
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make dist
|
make dist
|
||||||
# → /tmp/vaptvupt-4.1.0.tar.gz
|
# → /tmp/vaptvupt-4.2.0.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
Re-running `make dist` on the same source tree produces an identical sha256 (verified by `tests/test_dist_reproducible.sh`, wired into `make test`). This lets distros pin a stable hash in their recipes.
|
Re-running `make dist` on the same source tree produces an identical sha256 (verified by `tests/test_dist_reproducible.sh`, wired into `make test`). This lets distros pin a stable hash in their recipes.
|
||||||
|
|
@ -56,13 +56,13 @@ Maintainer flow:
|
||||||
```sh
|
```sh
|
||||||
# 1. Produce the upstream tarball
|
# 1. Produce the upstream tarball
|
||||||
make dist
|
make dist
|
||||||
# → /tmp/vaptvupt-4.1.0.tar.gz
|
# → /tmp/vaptvupt-4.2.0.tar.gz
|
||||||
|
|
||||||
# 2. Upload to a stable URL (e.g. git.securityops.co releases)
|
# 2. Upload to a stable URL (e.g. git.securityops.co releases)
|
||||||
|
|
||||||
# 3. Update packaging/aur/PKGBUILD:
|
# 3. Update packaging/aur/PKGBUILD:
|
||||||
# - Set pkgver=4.1.0
|
# - Set pkgver=4.2.0
|
||||||
# - Set sha256sums=("$(sha256sum /tmp/vaptvupt-4.1.0.tar.gz | awk '{print $1}')")
|
# - Set sha256sums=("$(sha256sum /tmp/vaptvupt-4.2.0.tar.gz | awk '{print $1}')")
|
||||||
|
|
||||||
# 4. Generate .SRCINFO
|
# 4. Generate .SRCINFO
|
||||||
cd packaging/aur && makepkg --printsrcinfo > .SRCINFO
|
cd packaging/aur && makepkg --printsrcinfo > .SRCINFO
|
||||||
|
|
@ -73,7 +73,7 @@ makepkg -s
|
||||||
# 6. Push to AUR
|
# 6. Push to AUR
|
||||||
git clone ssh://aur@aur.archlinux.org/vaptvupt.git aur-vaptvupt
|
git clone ssh://aur@aur.archlinux.org/vaptvupt.git aur-vaptvupt
|
||||||
cp packaging/aur/PKGBUILD packaging/aur/.SRCINFO aur-vaptvupt/
|
cp packaging/aur/PKGBUILD packaging/aur/.SRCINFO aur-vaptvupt/
|
||||||
cd aur-vaptvupt && git add -A && git commit -m "v4.1.0" && git push
|
cd aur-vaptvupt && git add -A && git commit -m "v4.2.0" && git push
|
||||||
```
|
```
|
||||||
|
|
||||||
User install:
|
User install:
|
||||||
|
|
@ -117,10 +117,10 @@ The `packaging/debian/` tree is a Debian source-package layout. Maintainer flow:
|
||||||
# 1. Produce the upstream tarball with the standard Debian
|
# 1. Produce the upstream tarball with the standard Debian
|
||||||
# orig.tar.gz naming convention:
|
# orig.tar.gz naming convention:
|
||||||
make dist
|
make dist
|
||||||
cp /tmp/vaptvupt-4.1.0.tar.gz /tmp/vaptvupt_4.1.0.orig.tar.gz
|
cp /tmp/vaptvupt-4.2.0.tar.gz /tmp/vaptvupt_4.2.0.orig.tar.gz
|
||||||
|
|
||||||
# 2. Unpack and overlay the debian/ tree:
|
# 2. Unpack and overlay the debian/ tree:
|
||||||
cd /tmp && tar xzf vaptvupt_4.1.0.orig.tar.gz && cd vaptvupt-4.1.0
|
cd /tmp && tar xzf vaptvupt_4.2.0.orig.tar.gz && cd vaptvupt-4.2.0
|
||||||
cp -a /path/to/vaptvupt/packaging/debian ./debian
|
cp -a /path/to/vaptvupt/packaging/debian ./debian
|
||||||
|
|
||||||
# 3. Build the source package:
|
# 3. Build the source package:
|
||||||
|
|
@ -128,7 +128,7 @@ dpkg-buildpackage -S -us -uc # source-only
|
||||||
dpkg-buildpackage -b -us -uc # binary
|
dpkg-buildpackage -b -us -uc # binary
|
||||||
|
|
||||||
# 4. Lint:
|
# 4. Lint:
|
||||||
lintian vaptvupt_4.1.0-1_*.deb
|
lintian vaptvupt_4.2.0-1_*.deb
|
||||||
|
|
||||||
# 5. Submit via the standard Debian mentors process:
|
# 5. Submit via the standard Debian mentors process:
|
||||||
# https://mentors.debian.net/intro-maintainers/
|
# https://mentors.debian.net/intro-maintainers/
|
||||||
|
|
@ -145,7 +145,7 @@ sudo apt install vaptvupt
|
||||||
```sh
|
```sh
|
||||||
# 1. Produce the tarball
|
# 1. Produce the tarball
|
||||||
make dist
|
make dist
|
||||||
cp /tmp/vaptvupt-4.1.0.tar.gz ~/rpmbuild/SOURCES/
|
cp /tmp/vaptvupt-4.2.0.tar.gz ~/rpmbuild/SOURCES/
|
||||||
|
|
||||||
# 2. Drop the .spec into the SPECS directory:
|
# 2. Drop the .spec into the SPECS directory:
|
||||||
cp packaging/rpm/vaptvupt.spec ~/rpmbuild/SPECS/
|
cp packaging/rpm/vaptvupt.spec ~/rpmbuild/SPECS/
|
||||||
|
|
@ -154,7 +154,7 @@ cp packaging/rpm/vaptvupt.spec ~/rpmbuild/SPECS/
|
||||||
cd ~/rpmbuild && rpmbuild -ba SPECS/vaptvupt.spec
|
cd ~/rpmbuild && rpmbuild -ba SPECS/vaptvupt.spec
|
||||||
|
|
||||||
# 4. Lint:
|
# 4. Lint:
|
||||||
rpmlint RPMS/x86_64/vaptvupt-4.1.0-1.fc*.rpm
|
rpmlint RPMS/x86_64/vaptvupt-4.2.0-1.fc*.rpm
|
||||||
|
|
||||||
# 5. Submit via the Fedora new-package review process:
|
# 5. Submit via the Fedora new-package review process:
|
||||||
# https://docs.fedoraproject.org/en-US/package-maintainers/Package_Review_Process/
|
# https://docs.fedoraproject.org/en-US/package-maintainers/Package_Review_Process/
|
||||||
|
|
@ -177,7 +177,7 @@ The `packaging/opensuse/` tree carries an RPM `.spec` suited to the Open Build S
|
||||||
make dist
|
make dist
|
||||||
|
|
||||||
# 2. In an OBS package checkout (osc), stage the sources and spec:
|
# 2. In an OBS package checkout (osc), stage the sources and spec:
|
||||||
cp /tmp/vaptvupt-4.1.0.tar.gz .
|
cp /tmp/vaptvupt-4.2.0.tar.gz .
|
||||||
cp /path/to/vaptvupt/packaging/opensuse/vaptvupt.spec .
|
cp /path/to/vaptvupt/packaging/opensuse/vaptvupt.spec .
|
||||||
|
|
||||||
# 3. Build locally against a target repository:
|
# 3. Build locally against a target repository:
|
||||||
|
|
@ -230,7 +230,7 @@ nix build github:cristiancmoises/vaptvupt#vaptvupt
|
||||||
nix run github:cristiancmoises/vaptvupt#vaptvupt -- version
|
nix run github:cristiancmoises/vaptvupt#vaptvupt -- version
|
||||||
|
|
||||||
# 2. To consume from another flake:
|
# 2. To consume from another flake:
|
||||||
# inputs.vaptvupt.url = "github:cristiancmoises/vaptvupt?ref=v4.1.0";
|
# inputs.vaptvupt.url = "github:cristiancmoises/vaptvupt?ref=v4.2.0";
|
||||||
# packages.x86_64-linux.default = inputs.vaptvupt.packages.x86_64-linux.vaptvupt;
|
# packages.x86_64-linux.default = inputs.vaptvupt.packages.x86_64-linux.vaptvupt;
|
||||||
|
|
||||||
# 3. To submit to nixpkgs (https://github.com/NixOS/nixpkgs):
|
# 3. To submit to nixpkgs (https://github.com/NixOS/nixpkgs):
|
||||||
|
|
|
||||||
18
INSTALL.md
18
INSTALL.md
|
|
@ -5,13 +5,13 @@ If you're seeing the error:
|
||||||
```
|
```
|
||||||
vaptvupt-gui depende de python3-pyqt6 | python3-pyside6; porém:
|
vaptvupt-gui depende de python3-pyqt6 | python3-pyside6; porém:
|
||||||
Pacote python3-pyqt6 não está instalado.
|
Pacote python3-pyqt6 não está instalado.
|
||||||
vaptvupt-gui depende de vaptvupt (>= 4.1.0); porém:
|
vaptvupt-gui depende de vaptvupt (>= 4.2.0); porém:
|
||||||
Versão de vaptvupt no sistema é 2.1.7-1.
|
Versão de vaptvupt no sistema é 2.1.7-1.
|
||||||
```
|
```
|
||||||
|
|
||||||
This is correct behavior. The `vaptvupt-gui` deb requires:
|
This is correct behavior. The `vaptvupt-gui` deb requires:
|
||||||
- Python 3 with **PyQt6** or **PySide6** (the GUI toolkit)
|
- Python 3 with **PyQt6** or **PySide6** (the GUI toolkit)
|
||||||
- The **vaptvupt CLI 4.1.0** or newer
|
- The **vaptvupt CLI 4.2.0** or newer
|
||||||
|
|
||||||
## The fastest fix — one command (Linux Mint, Ubuntu, Debian)
|
## The fastest fix — one command (Linux Mint, Ubuntu, Debian)
|
||||||
|
|
||||||
|
|
@ -33,8 +33,8 @@ the right order.
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y python3-pyqt6
|
sudo apt install -y python3-pyqt6
|
||||||
|
|
||||||
# 2. Upgrade vaptvupt CLI to 4.1.0
|
# 2. Upgrade vaptvupt CLI to 4.2.0
|
||||||
sudo dpkg -i vaptvupt_4.1.0_amd64.deb
|
sudo dpkg -i vaptvupt_4.2.0_amd64.deb
|
||||||
|
|
||||||
# 3. Install the GUI
|
# 3. Install the GUI
|
||||||
sudo dpkg -i vaptvupt-gui_1.3.0_all.deb
|
sudo dpkg -i vaptvupt-gui_1.3.0_all.deb
|
||||||
|
|
@ -50,7 +50,7 @@ sudo apt --fix-broken install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install -y python3-pyqt6
|
sudo dnf install -y python3-pyqt6
|
||||||
sudo dnf install -y vaptvupt-4.1.0-1.x86_64.rpm vaptvupt-gui-1.3.0-1.noarch.rpm
|
sudo dnf install -y vaptvupt-4.2.0-1.x86_64.rpm vaptvupt-gui-1.3.0-1.noarch.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
(Or build the RPM from the SRPM tarball with `rpmbuild -bb SPECS/vaptvupt.spec`)
|
(Or build the RPM from the SRPM tarball with `rpmbuild -bb SPECS/vaptvupt.spec`)
|
||||||
|
|
@ -98,7 +98,7 @@ Qt6 inside the deb because:
|
||||||
- Bundling would make the deb 80 MB+ instead of 35 KB
|
- Bundling would make the deb 80 MB+ instead of 35 KB
|
||||||
- Distribution-managed Qt gets security updates automatically
|
- Distribution-managed Qt gets security updates automatically
|
||||||
|
|
||||||
## Why does the GUI need vaptvupt 4.1.0?
|
## Why does the GUI need vaptvupt 4.2.0?
|
||||||
|
|
||||||
The GUI calls `vaptvupt --pq` and `vaptvupt keygen` for native
|
The GUI calls `vaptvupt --pq` and `vaptvupt keygen` for native
|
||||||
post-quantum encryption (ML-KEM-768 + X25519, in-tree implementation).
|
post-quantum encryption (ML-KEM-768 + X25519, in-tree implementation).
|
||||||
|
|
@ -108,7 +108,7 @@ fail against them.
|
||||||
## After installing — verify
|
## After installing — verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
vaptvupt version # should show: 4.1.0
|
vaptvupt version # should show: 4.2.0
|
||||||
vaptvupt-gui # should launch the GUI window
|
vaptvupt-gui # should launch the GUI window
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -178,8 +178,8 @@ sudo zypper install gcc make # openSUSE
|
||||||
### Build VaptVupt itself
|
### Build VaptVupt itself
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf vaptvupt-4.1.0-source.tar.gz
|
tar -xzf vaptvupt-4.2.0-source.tar.gz
|
||||||
cd vaptvupt-4.1.0
|
cd vaptvupt-4.2.0
|
||||||
|
|
||||||
make # build the `./vaptvupt` binary
|
make # build the `./vaptvupt` binary
|
||||||
sudo make install # install to /usr/local/bin (override with PREFIX=/usr)
|
sudo make install # install to /usr/local/bin (override with PREFIX=/usr)
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -417,6 +417,7 @@ test: $(TARGET)
|
||||||
$(Q)bash tests/test_path_traversal.sh
|
$(Q)bash tests/test_path_traversal.sh
|
||||||
$(Q)bash tests/test_arg_order.sh
|
$(Q)bash tests/test_arg_order.sh
|
||||||
$(Q)bash tests/test_block_swap.sh
|
$(Q)bash tests/test_block_swap.sh
|
||||||
|
$(Q)bash tests/test_dedup_nonce.sh
|
||||||
$(Q)bash tests/test_f08_topmac.sh
|
$(Q)bash tests/test_f08_topmac.sh
|
||||||
$(Q)bash tests/test_f09_preface.sh
|
$(Q)bash tests/test_f09_preface.sh
|
||||||
$(Q)bash tests/test_f10_kdf_default.sh
|
$(Q)bash tests/test_f10_kdf_default.sh
|
||||||
|
|
@ -475,6 +476,7 @@ check: $(TARGET) test-vectors
|
||||||
$(Q)bash tests/test_path_traversal.sh
|
$(Q)bash tests/test_path_traversal.sh
|
||||||
$(Q)bash tests/test_arg_order.sh
|
$(Q)bash tests/test_arg_order.sh
|
||||||
$(Q)bash tests/test_block_swap.sh
|
$(Q)bash tests/test_block_swap.sh
|
||||||
|
$(Q)bash tests/test_dedup_nonce.sh
|
||||||
$(Q)bash tests/test_f08_topmac.sh
|
$(Q)bash tests/test_f08_topmac.sh
|
||||||
$(Q)bash tests/test_f10_kdf_default.sh
|
$(Q)bash tests/test_f10_kdf_default.sh
|
||||||
$(Q)bash tests/test_f11_authfail_message.sh
|
$(Q)bash tests/test_f11_authfail_message.sh
|
||||||
|
|
|
||||||
111
README.md
111
README.md
|
|
@ -17,28 +17,37 @@ License: AGPL-3.0-or-later (dual-licensed AGPL + commercial).
|
||||||
> command is preserved as a symlink to `vaptvupt` for one major version
|
> command is preserved as a symlink to `vaptvupt` for one major version
|
||||||
> cycle.
|
> cycle.
|
||||||
|
|
||||||
## What's new in 4.1.0
|
## What's new in 4.2.0
|
||||||
|
|
||||||
- **Source-only tree.** The prebuilt vendored libraries `libzuptsdk.so`
|
- **Full (pure) post-quantum mode — `--pq-only`.** ML-KEM-768 (FIPS 203) as
|
||||||
and `libpqvaptvupt.so` have been removed. The default `make` needs only
|
the *sole* key-establishment mechanism, with no classical X25519 component.
|
||||||
a C compiler and make (plus libm/pthread) — no external crypto library —
|
For compliance postures that mandate a single NIST-standardised PQ primitive
|
||||||
and installs no `.so`.
|
with no classical KEM in the envelope (CNSA 2.0-style "PQ-only"). Envelope
|
||||||
- **Native `--pq` is the default post-quantum mode.** ML-KEM-768 + X25519
|
type `0x06`; archive key `SHA3-512(ml_ss ‖ ml_ct ‖ "ZUPT-PQ-ONLY-v1")`.
|
||||||
hybrid KEM, in-tree C implementation, available in the default build.
|
In-tree crypto, available in the default build.
|
||||||
- **SDK-backed modes are optional.** `--pq-sdk`, `--pq-box`, and the
|
- **Hybrid `--pq` stays the default recommendation.** `--pq-only` trades away
|
||||||
Argon2id KDF are only available in an upstream `make WITH_SDK=1` build
|
the hybrid safety net: a future break of ML-KEM-768 *alone* would break a
|
||||||
linked against the separately distributed `libzuptsdk`/`libpqvaptvupt`.
|
`--pq-only` archive, whereas `--pq` would still require breaking X25519 too.
|
||||||
- **Wire/on-disk format is v1.6, unchanged.** Archives created by 4.0.0
|
Use `--pq-only` only when a policy forbids the classical component.
|
||||||
are read and written identically.
|
- **Security fix (critical): AES-CTR keystream reuse under `--dedup`.** Dedup
|
||||||
|
blocks all share sequence 0, and the old nonce (`base_nonce XOR seq`)
|
||||||
|
therefore collapsed to one value across blocks — reusing the CTR keystream
|
||||||
|
(a many-time-pad). Every block now uses a **fresh random 128-bit nonce**.
|
||||||
|
Re-encrypt any `--dedup` + encrypted archives written by ≤ 4.1.0. Regression
|
||||||
|
test: `tests/test_dedup_nonce.sh`.
|
||||||
|
- **Clearer SDK keygen guidance.** `keygen --sdk`/`--box` on the source-only
|
||||||
|
build now points you to native `--pq`/`--pq-only` (or a `WITH_SDK=1` build).
|
||||||
|
- **Wire/on-disk format is v1.6, unchanged.** All 4.x archives interoperate;
|
||||||
|
the new `0x06` envelope is additive.
|
||||||
|
|
||||||
> **F-16 (data loss):** archives created by **≤ 3.8.0** at `-l 8`/`-l 9`
|
> **F-16 (data loss):** archives created by **≤ 3.8.0** at `-l 8`/`-l 9`
|
||||||
> whose inputs included x86/ELF/PE executables may be **undecodable by any
|
> whose inputs included x86/ELF/PE executables may be **undecodable by any
|
||||||
> version** (write-time defect in the old in-tree BCJ encoder). Re-create
|
> version** (write-time defect in the old in-tree BCJ encoder). Re-create
|
||||||
> such archives with 4.1.0 and verify extraction before deleting source
|
> such archives with 4.2.0 and verify extraction before deleting source
|
||||||
> data. Details in [CHANGELOG.md](CHANGELOG.md).
|
> data. Details in [CHANGELOG.md](CHANGELOG.md).
|
||||||
|
|
||||||
Binaries for the CLI (4.1.0) and GUI (1.3.0) are on the
|
Binaries for the CLI (4.2.0) and GUI (1.3.0) are on the
|
||||||
[release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.1.0).
|
[release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.2.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -50,8 +59,10 @@ Binaries for the CLI (4.1.0) and GUI (1.3.0) are on the
|
||||||
`--lzhp`.
|
`--lzhp`.
|
||||||
- **Post-quantum encryption** — `--pq` uses ML-KEM-768 + X25519 hybrid
|
- **Post-quantum encryption** — `--pq` uses ML-KEM-768 + X25519 hybrid
|
||||||
KEM (the approach used by Signal and iMessage), protecting against
|
KEM (the approach used by Signal and iMessage), protecting against
|
||||||
"harvest now, decrypt later" attacks. In-tree, available in the default
|
"harvest now, decrypt later" attacks. `--pq-only` offers a full (pure)
|
||||||
build.
|
ML-KEM-768 mode with no classical component for "PQ-only" compliance
|
||||||
|
postures. Both are in-tree and available in the default build; hybrid
|
||||||
|
`--pq` is the recommended default.
|
||||||
- **AES-NI acceleration** — AES-256-CTR via Jasmin-verified assembly with
|
- **AES-NI acceleration** — AES-256-CTR via Jasmin-verified assembly with
|
||||||
a 4-block interleaved pipeline. AVX detection validates OSXSAVE/XCR0 (no
|
a 4-block interleaved pipeline. AVX detection validates OSXSAVE/XCR0 (no
|
||||||
SIGILL). Falls back to C table-based AES on unsupported hardware.
|
SIGILL). Falls back to C table-based AES on unsupported hardware.
|
||||||
|
|
@ -111,18 +122,18 @@ Argon2id KDF.
|
||||||
### Pre-built packages
|
### Pre-built packages
|
||||||
|
|
||||||
Assets are published on the
|
Assets are published on the
|
||||||
[v4.1.0 release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.1.0)
|
[v4.2.0 release page](https://git.securityops.co/cristiancmoises/vaptvupt/releases/tag/v4.2.0)
|
||||||
and verifiable against the published `SHA256SUMS.txt`.
|
and verifiable against the published `SHA256SUMS.txt`.
|
||||||
|
|
||||||
**Command-line tool (`vaptvupt` 4.1.0):**
|
**Command-line tool (`vaptvupt` 4.2.0):**
|
||||||
|
|
||||||
| Format | File | Distros |
|
| Format | File | Distros |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Debian/Ubuntu | `vaptvupt_4.1.0_amd64.deb` | Debian 11+, Ubuntu 22.04+, Mint 21+ |
|
| Debian/Ubuntu | `vaptvupt_4.2.0_amd64.deb` | Debian 11+, Ubuntu 22.04+, Mint 21+ |
|
||||||
| RPM | `vaptvupt-4.1.0-1.x86_64.rpm` | Fedora 38+, RHEL 9+, openSUSE, AlmaLinux, Rocky, other RPM-based distributions |
|
| RPM | `vaptvupt-4.2.0-1.x86_64.rpm` | Fedora 38+, RHEL 9+, openSUSE, AlmaLinux, Rocky, other RPM-based distributions |
|
||||||
| AppDir tarball | `vaptvupt-4.1.0-x86_64.AppDir.tar.gz` | Any glibc 2.28+ (extract & run, no FUSE) |
|
| AppDir tarball | `vaptvupt-4.2.0-x86_64.AppDir.tar.gz` | Any glibc 2.28+ (extract & run, no FUSE) |
|
||||||
| Source tarball | `vaptvupt-4.1.0.tar.gz` | Build from source on any platform |
|
| Source tarball | `vaptvupt-4.2.0.tar.gz` | Build from source on any platform |
|
||||||
| openSUSE OBS | `vaptvupt-4.1.0-opensuse-obs.tar.gz` | Open Build Service source bundle |
|
| openSUSE OBS | `vaptvupt-4.2.0-opensuse-obs.tar.gz` | Open Build Service source bundle |
|
||||||
|
|
||||||
**Graphical front-end (`vaptvupt-gui` 1.3.0):**
|
**Graphical front-end (`vaptvupt-gui` 1.3.0):**
|
||||||
|
|
||||||
|
|
@ -138,17 +149,17 @@ and verifiable against the published `SHA256SUMS.txt`.
|
||||||
sha256sum -c SHA256SUMS.txt
|
sha256sum -c SHA256SUMS.txt
|
||||||
|
|
||||||
# Debian / Ubuntu / Mint
|
# Debian / Ubuntu / Mint
|
||||||
sudo dpkg -i vaptvupt_4.1.0_amd64.deb
|
sudo dpkg -i vaptvupt_4.2.0_amd64.deb
|
||||||
sudo apt-get install -f # resolve any missing deps
|
sudo apt-get install -f # resolve any missing deps
|
||||||
|
|
||||||
# Fedora / RHEL / openSUSE / AlmaLinux / Rocky and other RPM-based distros
|
# Fedora / RHEL / openSUSE / AlmaLinux / Rocky and other RPM-based distros
|
||||||
sudo rpm -i vaptvupt-4.1.0-1.x86_64.rpm
|
sudo rpm -i vaptvupt-4.2.0-1.x86_64.rpm
|
||||||
# or
|
# or
|
||||||
sudo dnf install ./vaptvupt-4.1.0-1.x86_64.rpm
|
sudo dnf install ./vaptvupt-4.2.0-1.x86_64.rpm
|
||||||
|
|
||||||
# AppDir tarball (no install, no FUSE required)
|
# AppDir tarball (no install, no FUSE required)
|
||||||
tar xzf vaptvupt-4.1.0-x86_64.AppDir.tar.gz
|
tar xzf vaptvupt-4.2.0-x86_64.AppDir.tar.gz
|
||||||
./vaptvupt-4.1.0-x86_64.AppDir/AppRun --help
|
./vaptvupt-4.2.0-x86_64.AppDir/AppRun --help
|
||||||
|
|
||||||
# GUI AppImage (single executable)
|
# GUI AppImage (single executable)
|
||||||
chmod +x VaptVupt-GUI-1.3.0-x86_64.AppImage
|
chmod +x VaptVupt-GUI-1.3.0-x86_64.AppImage
|
||||||
|
|
@ -158,10 +169,10 @@ chmod +x VaptVupt-GUI-1.3.0-x86_64.AppImage
|
||||||
### Building from SRPM (Fedora / RHEL / RPM-based distributions)
|
### Building from SRPM (Fedora / RHEL / RPM-based distributions)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar xzf vaptvupt-4.1.0.srpm.tar.gz
|
tar xzf vaptvupt-4.2.0.srpm.tar.gz
|
||||||
cd ~/rpmbuild # or use rpmbuild --define "_topdir $(pwd)"
|
cd ~/rpmbuild # or use rpmbuild --define "_topdir $(pwd)"
|
||||||
rpmbuild -bb SPECS/vaptvupt.spec
|
rpmbuild -bb SPECS/vaptvupt.spec
|
||||||
sudo rpm -i RPMS/x86_64/vaptvupt-4.1.0-1.*.rpm
|
sudo rpm -i RPMS/x86_64/vaptvupt-4.2.0-1.*.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
### Basic usage
|
### Basic usage
|
||||||
|
|
@ -209,6 +220,13 @@ vaptvupt keygen -o mykey.key
|
||||||
vaptvupt keygen --pub -o pub.key -k mykey.key
|
vaptvupt keygen --pub -o pub.key -k mykey.key
|
||||||
vaptvupt compress --pq pub.key backup.zupt ~/Documents/
|
vaptvupt compress --pq pub.key backup.zupt ~/Documents/
|
||||||
vaptvupt extract --pq mykey.key -o ~/restored/ backup.zupt
|
vaptvupt extract --pq mykey.key -o ~/restored/ backup.zupt
|
||||||
|
|
||||||
|
# Native --pq-only (full/pure ML-KEM-768, no classical component).
|
||||||
|
# Use only for "PQ-only" compliance postures; --pq (hybrid) is safer.
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
The SDK-backed modes below require a `make WITH_SDK=1` build linked against
|
The SDK-backed modes below require a `make WITH_SDK=1` build linked against
|
||||||
|
|
@ -397,8 +415,10 @@ packaging-syntax checks.
|
||||||
|
|
||||||
## Post-Quantum Encryption
|
## Post-Quantum Encryption
|
||||||
|
|
||||||
`--pq` uses hybrid ML-KEM-768 + X25519 key encapsulation per NIST FIPS 203,
|
VaptVupt has two native PQ modes, both in-tree and available in the default
|
||||||
in-tree and available in the default build.
|
build.
|
||||||
|
|
||||||
|
**`--pq` — hybrid ML-KEM-768 + X25519 (envelope `0x02`, recommended):**
|
||||||
|
|
||||||
```
|
```
|
||||||
Public key → ML-KEM-768 Encaps + X25519 ECDH → hybrid shared secret
|
Public key → ML-KEM-768 Encaps + X25519 ECDH → hybrid shared secret
|
||||||
|
|
@ -406,11 +426,27 @@ Public key → ML-KEM-768 Encaps + X25519 ECDH → hybrid shared secret
|
||||||
→ AES-256-CTR + HMAC-SHA256 per block
|
→ AES-256-CTR + HMAC-SHA256 per block
|
||||||
```
|
```
|
||||||
|
|
||||||
Security model: secure if EITHER ML-KEM-768 (post-quantum) OR X25519
|
Security model: secure if **EITHER** ML-KEM-768 (post-quantum) **OR** X25519
|
||||||
(classical) is secure.
|
(classical) is secure. This is the recommended default — it stays safe even
|
||||||
|
if one primitive is later broken.
|
||||||
|
|
||||||
Password mode (`-p`) is not quantum-safe. Use `--pq` for long-term
|
**`--pq-only` — full/pure ML-KEM-768 (envelope `0x06`):**
|
||||||
protection.
|
|
||||||
|
```
|
||||||
|
Public key → ML-KEM-768 Encaps → shared secret ss, ciphertext ct
|
||||||
|
→ archive_key = SHA3-512(ss ‖ ct ‖ "ZUPT-PQ-ONLY-v1")
|
||||||
|
→ AES-256-CTR + HMAC-SHA256 per block
|
||||||
|
```
|
||||||
|
|
||||||
|
Security model: secure if ML-KEM-768 is secure — there is **no classical
|
||||||
|
fallback**. Choose this only when a policy mandates a single NIST-standardised
|
||||||
|
PQ primitive with no classical KEM in the envelope (CNSA 2.0-style "PQ-only").
|
||||||
|
The trade-off is explicit: a future break of ML-KEM-768 *alone* breaks the
|
||||||
|
archive, whereas under `--pq` the attacker must also break X25519. **When in
|
||||||
|
doubt, use `--pq`.**
|
||||||
|
|
||||||
|
Password mode (`-p`) is not quantum-safe. Use `--pq` (or `--pq-only`) for
|
||||||
|
long-term protection.
|
||||||
|
|
||||||
The SDK-backed `--pq-sdk` and `--pq-box` modes are optional and require a
|
The SDK-backed `--pq-sdk` and `--pq-box` modes are optional and require a
|
||||||
`make WITH_SDK=1` build against `libzuptsdk`/`libpqvaptvupt`.
|
`make WITH_SDK=1` build against `libzuptsdk`/`libpqvaptvupt`.
|
||||||
|
|
@ -635,6 +671,7 @@ VaptVupt archives require VaptVupt v2.0+.
|
||||||
| v3.4.0–v3.8.0 | F-15 KDF parameter transparency, measured constant-time MAC comparison (dudect), NIST SP 800-38A AES-CTR vectors, ML-KEM decaps through the CT primitive, consolidated benchmarks |
|
| v3.4.0–v3.8.0 | F-15 KDF parameter transparency, measured constant-time MAC comparison (dudect), NIST SP 800-38A AES-CTR vectors, ML-KEM decaps through the CT primitive, consolidated benchmarks |
|
||||||
| v4.0.0 | Codec 2.60.4 security release (OOB heap write fixed in AVX2 decode fast path), `--pq-box` sealed-box mode, F-16 data-loss disclosure + fix (old in-tree BCJ encoder), CBMC-verified BCJ filters with auto ELF/PE/Mach-O detection, SHA-NI acceleration. Wire format v1.6 |
|
| v4.0.0 | Codec 2.60.4 security release (OOB heap write fixed in AVX2 decode fast path), `--pq-box` sealed-box mode, F-16 data-loss disclosure + fix (old in-tree BCJ encoder), CBMC-verified BCJ filters with auto ELF/PE/Mach-O detection, SHA-NI acceleration. Wire format v1.6 |
|
||||||
| v4.1.0 | Source-only tree (prebuilt libzuptsdk/libpqvaptvupt removed); default build needs only a C compiler + make; native `--pq` is the default PQ mode; `--pq-sdk`/`--pq-box`/Argon2id gated behind `make WITH_SDK=1`. Wire format stays v1.6 |
|
| v4.1.0 | Source-only tree (prebuilt libzuptsdk/libpqvaptvupt removed); default build needs only a C compiler + make; native `--pq` is the default PQ mode; `--pq-sdk`/`--pq-box`/Argon2id gated behind `make WITH_SDK=1`. Wire format stays v1.6 |
|
||||||
|
| v4.2.0 | Full (pure) post-quantum mode `--pq-only` (ML-KEM-768 only, envelope 0x06); critical fix for AES-CTR keystream reuse under `--dedup` (fresh random per-block nonce); clearer SDK keygen guidance. Wire format stays v1.6 |
|
||||||
|
|
||||||
See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes.
|
See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes.
|
||||||
|
|
||||||
|
|
|
||||||
59
SECURITY.md
59
SECURITY.md
|
|
@ -1,4 +1,4 @@
|
||||||
# Security Policy — VaptVupt 4.1.0
|
# Security Policy — VaptVupt 4.2.0
|
||||||
|
|
||||||
## Reporting Vulnerabilities
|
## Reporting Vulnerabilities
|
||||||
|
|
||||||
|
|
@ -26,7 +26,8 @@ deployments, treat it as "reviewed but unaudited" and do your own review.
|
||||||
| Mode | CLI Flag | Algorithm | PQ-Safe? | Use Case |
|
| Mode | CLI Flag | Algorithm | PQ-Safe? | Use Case |
|
||||||
|------|----------|-----------|----------|----------|
|
|------|----------|-----------|----------|----------|
|
||||||
| Password | `-p` | PBKDF2-SHA256 → AES-256-CTR + HMAC-SHA256 | No | Short-term backups, personal use |
|
| Password | `-p` | PBKDF2-SHA256 → AES-256-CTR + HMAC-SHA256 | No | Short-term backups, personal use |
|
||||||
| PQ Hybrid | `--pq` | ML-KEM-768 + X25519 → AES-256-CTR + HMAC-SHA256 | Yes | Long-term archives, high-value data |
|
| PQ Hybrid | `--pq` | ML-KEM-768 + X25519 → AES-256-CTR + HMAC-SHA256 | Yes | Long-term archives, high-value data (**recommended**) |
|
||||||
|
| PQ Only | `--pq-only` | ML-KEM-768 only → AES-256-CTR + HMAC-SHA256 | Yes | "PQ-only" compliance postures (no classical KEM) |
|
||||||
| None | (default) | No encryption (compression only) | N/A | Non-sensitive data |
|
| None | (default) | No encryption (compression only) | N/A | Non-sensitive data |
|
||||||
|
|
||||||
Password mode (`-p`) is not quantum-safe. For protection against "harvest
|
Password mode (`-p`) is not quantum-safe. For protection against "harvest
|
||||||
|
|
@ -34,6 +35,17 @@ now, decrypt later" quantum attacks, use `--pq` — the recommended
|
||||||
post-quantum mode. `--pq` is native and in-tree; it needs no external
|
post-quantum mode. `--pq` is native and in-tree; it needs no external
|
||||||
library.
|
library.
|
||||||
|
|
||||||
|
`--pq-only` (envelope type `0x06`) uses ML-KEM-768 as the *sole* key
|
||||||
|
mechanism, with no classical X25519 component. It exists for compliance
|
||||||
|
postures that mandate a single NIST-standardised PQ primitive with no
|
||||||
|
classical KEM in the envelope (CNSA 2.0-style "PQ-only"). **This is a
|
||||||
|
deliberate reduction in defence-in-depth:** unlike `--pq`, there is no
|
||||||
|
classical fallback, so a future cryptanalytic break of ML-KEM-768 alone is
|
||||||
|
sufficient to break the archive. Under `--pq`, an attacker must break *both*
|
||||||
|
ML-KEM-768 and X25519. **Unless a policy forbids the classical component,
|
||||||
|
prefer `--pq`.** Both modes are native, in-tree, and need no external
|
||||||
|
library.
|
||||||
|
|
||||||
Optional SDK modes (`--pq-sdk`, `--pq-box`) are available only in an
|
Optional SDK modes (`--pq-sdk`, `--pq-box`) are available only in an
|
||||||
upstream `make WITH_SDK=1` build linked against the separately distributed
|
upstream `make WITH_SDK=1` build linked against the separately distributed
|
||||||
libzuptsdk / libpqvaptvupt libraries. They are not part of the default
|
libzuptsdk / libpqvaptvupt libraries. They are not part of the default
|
||||||
|
|
@ -51,7 +63,8 @@ build and are not defaults.
|
||||||
| Password KDF (WITH_SDK=1 option) | Argon2id | RFC 9106 | OWASP minimums | Password-dependent, memory-hard |
|
| Password KDF (WITH_SDK=1 option) | Argon2id | RFC 9106 | OWASP minimums | Password-dependent, memory-hard |
|
||||||
| Post-quantum KEM | ML-KEM-768 | FIPS 203 | 1184B pk / 2400B sk | NIST Level 3 |
|
| Post-quantum KEM | ML-KEM-768 | FIPS 203 | 1184B pk / 2400B sk | NIST Level 3 |
|
||||||
| Classical KEM | X25519 | RFC 7748 | 32B scalar | ~128-bit classical |
|
| Classical KEM | X25519 | RFC 7748 | 32B scalar | ~128-bit classical |
|
||||||
| Hybrid KDF | SHA3-512 | FIPS 202 | 512-bit output | Secure if either KEM holds |
|
| Hybrid KDF (`--pq`) | SHA3-512 | FIPS 202 | 512-bit output | Secure if either KEM holds |
|
||||||
|
| PQ-only KDF (`--pq-only`) | SHA3-512 | FIPS 202 | 512-bit output | Secure if ML-KEM-768 holds (no classical fallback) |
|
||||||
| Integrity | XXH64 | xxHash spec | 64-bit checksum | Non-cryptographic |
|
| Integrity | XXH64 | xxHash spec | 64-bit checksum | Non-cryptographic |
|
||||||
| Hashing | SHA3-256, SHA3-512 | FIPS 202 | 256/512-bit | Standard |
|
| Hashing | SHA3-256, SHA3-512 | FIPS 202 | 256/512-bit | Standard |
|
||||||
| Random | OS CSPRNG | getrandom(2) / RtlGenRandom | N/A | Hard fail if unavailable |
|
| Random | OS CSPRNG | getrandom(2) / RtlGenRandom | N/A | Hard fail if unavailable |
|
||||||
|
|
@ -66,14 +79,28 @@ Argon2id is available only in a `make WITH_SDK=1` build.
|
||||||
### Per-Block Authenticated Encryption
|
### Per-Block Authenticated Encryption
|
||||||
|
|
||||||
```
|
```
|
||||||
For each data block (sequence 0, 1, 2, ...):
|
For each data block:
|
||||||
|
|
||||||
nonce = base_nonce XOR pad_le(block_seq, 8) [16 bytes]
|
nonce = CSPRNG(16) [16 bytes, fresh per block]
|
||||||
ciphertext = AES-256-CTR(enc_key, nonce, plaintext)
|
ciphertext = AES-256-CTR(enc_key, nonce, plaintext)
|
||||||
mac = HMAC-SHA256(mac_key, nonce ‖ ciphertext) [32 bytes]
|
mac = HMAC-SHA256(mac_key, aad ‖ nonce ‖ ciphertext) [32 bytes]
|
||||||
stored = nonce ‖ ciphertext ‖ mac
|
stored = nonce ‖ ciphertext ‖ mac
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The nonce is a **fresh 128-bit random value per block**, stored in the block
|
||||||
|
prefix and bound into the block MAC. The block sequence number is bound into
|
||||||
|
the MAC AAD (not into the nonce), so reordering, splicing, or replaying blocks
|
||||||
|
is still detected.
|
||||||
|
|
||||||
|
> **History (fixed in 4.2.0):** earlier releases derived the nonce as
|
||||||
|
> `base_nonce XOR pad_le(block_seq, 8)`. In `--dedup` mode every data block is
|
||||||
|
> assigned sequence 0 (the sentinel that keeps cross-file dedup references
|
||||||
|
> authenticating consistently), so the nonce collapsed to a single value across
|
||||||
|
> all dedup blocks — reusing the AES-CTR keystream across distinct plaintexts
|
||||||
|
> (a many-time-pad). Switching to a fresh random per-block nonce closes this.
|
||||||
|
> Regression test: `tests/test_dedup_nonce.sh`. Re-encrypt any `--dedup` +
|
||||||
|
> encrypted archives written by ≤ 4.1.0.
|
||||||
|
|
||||||
### Encrypt-then-MAC
|
### Encrypt-then-MAC
|
||||||
|
|
||||||
HMAC is computed over `nonce ‖ ciphertext` and verified **before** any
|
HMAC is computed over `nonce ‖ ciphertext` and verified **before** any
|
||||||
|
|
@ -106,6 +133,26 @@ The `--pq-sdk` mode (WITH_SDK=1 only) uses an HKDF-SHA3-256 combiner, a
|
||||||
32-byte key commitment tag, HPKE-style context binding (RFC 9180 §5),
|
32-byte key commitment tag, HPKE-style context binding (RFC 9180 §5),
|
||||||
anti-fault double decapsulation, and XChaCha20-Poly1305 AEAD.
|
anti-fault double decapsulation, and XChaCha20-Poly1305 AEAD.
|
||||||
|
|
||||||
|
### Full Post-Quantum KEM (`--pq-only`)
|
||||||
|
|
||||||
|
```
|
||||||
|
Encapsulation:
|
||||||
|
ML-KEM-768.Encaps(pk) → ml_ct[1088], ml_ss[32]
|
||||||
|
archive_key = SHA3-512(ml_ss ‖ ml_ct ‖ "ZUPT-PQ-ONLY-v1")
|
||||||
|
enc_key = archive_key[0:32]
|
||||||
|
mac_key = archive_key[32:64]
|
||||||
|
```
|
||||||
|
|
||||||
|
Security model: secure if ML-KEM-768 (post-quantum, NIST Level 3) remains
|
||||||
|
unbroken. **There is no classical component**, so — unlike `--pq` — a break of
|
||||||
|
ML-KEM-768 alone is sufficient to compromise the archive key. This mode exists
|
||||||
|
only for compliance postures that mandate a single NIST-standardised PQ
|
||||||
|
primitive with no classical KEM in the envelope (CNSA 2.0-style "PQ-only").
|
||||||
|
Decapsulation uses ML-KEM Fujisaki-Okamoto implicit rejection: a wrong or
|
||||||
|
tampered `ml_ct` yields a pseudorandom shared secret, so decryption fails
|
||||||
|
closed at the HMAC check rather than leaking a decapsulation-validity oracle.
|
||||||
|
**Unless a policy forbids the classical component, prefer `--pq`.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Constant-Time Guarantees
|
## Constant-Time Guarantees
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,15 @@ a substitute for full-disk encryption.
|
||||||
(600k iterations). Argon2id is available only in an upstream
|
(600k iterations). Argon2id is available only in an upstream
|
||||||
`make WITH_SDK=1` build against the separately distributed
|
`make WITH_SDK=1` build against the separately distributed
|
||||||
libraries.
|
libraries.
|
||||||
- `--pq`: native post-quantum mode (ML-KEM-768 + X25519), the PQ mode
|
- `--pq`: native post-quantum **hybrid** mode (ML-KEM-768 + X25519), the
|
||||||
in the default build. The ML-KEM-768 implementation is in-tree.
|
recommended PQ mode in the default build. The ML-KEM-768 implementation
|
||||||
|
is in-tree.
|
||||||
|
- `--pq-only`: native **full/pure** post-quantum mode (ML-KEM-768 only, no
|
||||||
|
X25519), also in the default build. For compliance postures that mandate a
|
||||||
|
single NIST-standardised PQ primitive with no classical KEM in the envelope.
|
||||||
|
Its threat profile differs from `--pq` in exactly one axis: it has no
|
||||||
|
classical fallback, so a break of ML-KEM-768 alone breaks the archive
|
||||||
|
(see §5 and "Cryptographic assumptions").
|
||||||
- `--pq-sdk` / `--pq-box`: optional post-quantum modes backed by the
|
- `--pq-sdk` / `--pq-box`: optional post-quantum modes backed by the
|
||||||
separately distributed `libzuptsdk` / `libpqvaptvupt` libraries.
|
separately distributed `libzuptsdk` / `libpqvaptvupt` libraries.
|
||||||
Available only in a `make WITH_SDK=1` build. Key files for these
|
Available only in a `make WITH_SDK=1` build. Key files for these
|
||||||
|
|
@ -99,7 +106,7 @@ also constant (HMAC is always run, branchless return).
|
||||||
The detailed cause is available via `--verbose` for debugging on
|
The detailed cause is available via `--verbose` for debugging on
|
||||||
machines under the user's own control.
|
machines under the user's own control.
|
||||||
|
|
||||||
### 5. Post-quantum forward secrecy (`--pq` and optional `--pq-sdk`)
|
### 5. Post-quantum forward secrecy (`--pq`, `--pq-only`, and optional `--pq-sdk`)
|
||||||
|
|
||||||
The native `--pq` mode uses ML-KEM-768 (FIPS 203) hybridized with
|
The native `--pq` mode uses ML-KEM-768 (FIPS 203) hybridized with
|
||||||
X25519 via an HKDF combiner. Archives encrypted today cannot be
|
X25519 via an HKDF combiner. Archives encrypted today cannot be
|
||||||
|
|
@ -111,8 +118,18 @@ assuming:
|
||||||
- X25519 hybridization protects against an unforeseen ML-KEM break
|
- X25519 hybridization protects against an unforeseen ML-KEM break
|
||||||
- The recipient's private key is not later compromised
|
- The recipient's private key is not later compromised
|
||||||
|
|
||||||
The optional `--pq-sdk` mode provides the same hybrid guarantee via
|
The native `--pq-only` mode (envelope type `0x06`) provides the same
|
||||||
the separately distributed SDK libraries.
|
harvest-now-decrypt-later protection using ML-KEM-768 as the *sole* key
|
||||||
|
mechanism. It exists for compliance postures that mandate a single
|
||||||
|
NIST-standardised PQ primitive with no classical KEM in the envelope
|
||||||
|
(CNSA 2.0-style "PQ-only"). **The trade-off is a loss of the second
|
||||||
|
assumption above:** there is no X25519 hybridization, so an unforeseen
|
||||||
|
break of ML-KEM-768 alone is sufficient to recover the archive key. For
|
||||||
|
that reason `--pq` (hybrid) is the recommended default, and `--pq-only`
|
||||||
|
should be used only when a policy forbids the classical component.
|
||||||
|
|
||||||
|
The optional `--pq-sdk` mode provides the same hybrid guarantee as
|
||||||
|
`--pq` via the separately distributed SDK libraries.
|
||||||
|
|
||||||
### 6. Side-channel resistance for cryptographic primitives
|
### 6. Side-channel resistance for cryptographic primitives
|
||||||
|
|
||||||
|
|
@ -274,8 +291,8 @@ VaptVupt's security rests on the following standard assumptions:
|
||||||
| AES-256-CTR is a secure stream cipher | All encrypted archives become readable |
|
| AES-256-CTR is a secure stream cipher | All encrypted archives become readable |
|
||||||
| HMAC-SHA256 is a secure PRF / MAC | Tamper detection fails; integrity can be forged |
|
| HMAC-SHA256 is a secure PRF / MAC | Tamper detection fails; integrity can be forged |
|
||||||
| PBKDF2-SHA256 (or Argon2id, WITH_SDK) is a secure password KDF | Password-mode archives become brute-forceable faster |
|
| PBKDF2-SHA256 (or Argon2id, WITH_SDK) is a secure password KDF | Password-mode archives become brute-forceable faster |
|
||||||
| ML-KEM-768 retains NIST Category 3 security | `--pq` / `--pq-sdk` reduce to the X25519 layer |
|
| ML-KEM-768 retains NIST Category 3 security | `--pq` / `--pq-sdk` reduce to the X25519 layer; **`--pq-only` has no fallback and is broken** |
|
||||||
| X25519 retains 128-bit security (no quantum) | PQ modes reduce to the ML-KEM layer; classical password mode unaffected |
|
| X25519 retains 128-bit security (no quantum) | Hybrid PQ modes reduce to the ML-KEM layer; `--pq-only` and classical password mode unaffected |
|
||||||
| HKDF-SHA256 is a secure key-derivation construction | Combined PQ + classical keys may be predictable |
|
| HKDF-SHA256 is a secure key-derivation construction | Combined PQ + classical keys may be predictable |
|
||||||
| SHA3 / SHAKE retain pre-image and collision resistance | Auxiliary protocol bindings may be forged |
|
| SHA3 / SHAKE retain pre-image and collision resistance | Auxiliary protocol bindings may be forged |
|
||||||
|
|
||||||
|
|
@ -306,6 +323,6 @@ normally.
|
||||||
## Document version
|
## Document version
|
||||||
|
|
||||||
This threat model covers archive format v1.6 as shipped in VaptVupt
|
This threat model covers archive format v1.6 as shipped in VaptVupt
|
||||||
4.1.0. It is part of the source tree (`THREAT_MODEL.md`) and
|
4.2.0. It is part of the source tree (`THREAT_MODEL.md`) and
|
||||||
versioned with the project; this section will be updated as the
|
versioned with the project; this section will be updated as the
|
||||||
format evolves.
|
format evolves.
|
||||||
|
|
|
||||||
147
doc/vaptvupt.1
147
doc/vaptvupt.1
|
|
@ -1,7 +1,7 @@
|
||||||
.\" Manpage for vaptvupt (formerly zupt; INPI Brasil trademark rename in v3.0.0)
|
.\" Manpage for vaptvupt (formerly zupt; INPI Brasil trademark rename in v3.0.0)
|
||||||
.\" SPDX-License-Identifier: AGPL-3.0-or-later
|
.\" SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
.\" Copyright (c) 2025-2026 Cristian Cezar Moisés
|
.\" 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
|
.SH NAME
|
||||||
vaptvupt \- post-quantum backup compression utility (formerly zupt)
|
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
|
.B info
|
||||||
Print archive header metadata without requiring the decryption key.
|
Print archive header metadata without requiring the decryption key.
|
||||||
Reports: format version, codec, encryption type (none / PBKDF2 /
|
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
|
creation timestamp, archive UUID, AIT presence. Safe to run on an
|
||||||
untrusted archive.
|
untrusted archive.
|
||||||
|
|
||||||
|
|
@ -157,18 +158,32 @@ after each block.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B keygen
|
.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
|
.B --sdk
|
||||||
generates an ML-KEM-768 + X25519 hybrid keypair suitable for
|
generates a keypair for the optional
|
||||||
.B --pq-sdk
|
.B --pq-sdk
|
||||||
mode. With
|
mode, and with
|
||||||
.B --box
|
.B --box
|
||||||
generates a libpqvaptvupt sealed-box keypair (writes \fIfile\fR and
|
a libpqvaptvupt sealed-box keypair for
|
||||||
\fIfile\fR.pub) for
|
|
||||||
.B --pq-box
|
.B --pq-box
|
||||||
mode. With
|
(both need a
|
||||||
|
.B WITH_SDK=1
|
||||||
|
build). With
|
||||||
.B --pub
|
.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
|
.SH GLOBAL OPTIONS
|
||||||
|
|
||||||
|
|
@ -233,10 +248,55 @@ Choose key-derivation function for password mode:
|
||||||
Use a 32-byte raw key file (generated with
|
Use a 32-byte raw key file (generated with
|
||||||
.BR "vaptvupt keygen" ).
|
.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
|
.TP
|
||||||
.B --pq-box \fIpub\fR
|
.B --pq-box \fIpub\fR
|
||||||
Enable post-quantum sealed-box encryption via the vendored libpqvaptvupt
|
Enable post-quantum sealed-box encryption via libpqvaptvupt (envelope
|
||||||
(v4.0.0+, envelope type 0x05). ML-KEM-768 + X25519 shared secrets are
|
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
|
combined through HKDF-SHA256 with a domain-separating info string
|
||||||
("pqvv-seal-v1"); the box carries AES-256-CTR + HMAC-SHA256
|
("pqvv-seal-v1"); the box carries AES-256-CTR + HMAC-SHA256
|
||||||
Encrypt-then-MAC. The
|
Encrypt-then-MAC. The
|
||||||
|
|
@ -249,8 +309,13 @@ On extraction, pass the secret key:
|
||||||
\fIpriv\fR.
|
\fIpriv\fR.
|
||||||
.TP
|
.TP
|
||||||
.B --pq-sdk \fIpub\fR
|
.B --pq-sdk \fIpub\fR
|
||||||
Enable post-quantum hybrid encryption. Uses ML-KEM-768 + X25519 with
|
Enable post-quantum hybrid encryption via libzuptsdk (envelope type
|
||||||
HKDF combiner, HPKE binding, and key commitment. The
|
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
|
.I pub
|
||||||
argument is the recipient's public-key file generated by
|
argument is the recipient's public-key file generated by
|
||||||
.BR "vaptvupt keygen --sdk" .
|
.BR "vaptvupt keygen --sdk" .
|
||||||
|
|
@ -293,11 +358,16 @@ flag of the same name.
|
||||||
.SH POST-QUANTUM ENCRYPTION
|
.SH POST-QUANTUM ENCRYPTION
|
||||||
|
|
||||||
.B vaptvupt
|
.B vaptvupt
|
||||||
implements a hybrid KEM as specified in FIPS 203 (ML-KEM) combined
|
offers two native post-quantum modes, both built entirely from the
|
||||||
with X25519 (RFC 7748). The session key is derived as:
|
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
|
.RS
|
||||||
.nf
|
.nf
|
||||||
|
|
||||||
ss_pq = ML-KEM-768.decaps(sk_pq, ct_pq)
|
ss_pq = ML-KEM-768.decaps(sk_pq, ct_pq)
|
||||||
ss_ec = X25519(sk_ec, pk_ec_peer)
|
ss_ec = X25519(sk_ec, pk_ec_peer)
|
||||||
session = HKDF-SHA256(ss_pq || ss_ec,
|
session = HKDF-SHA256(ss_pq || ss_ec,
|
||||||
|
|
@ -305,10 +375,38 @@ session = HKDF-SHA256(ss_pq || ss_ec,
|
||||||
salt = archive_uuid)
|
salt = archive_uuid)
|
||||||
.fi
|
.fi
|
||||||
.RE
|
.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
|
(ss_pq, ct_pq) = ML-KEM-768.encaps(pk_pq)
|
||||||
the strongest of {ML-KEM-768, X25519}: an attacker must break both
|
archive_key = SHA3-512(ss_pq || ct_pq || "ZUPT-PQ-ONLY-v1")
|
||||||
to recover the key.
|
.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
|
.PP
|
||||||
.B Key commitment:
|
.B Key commitment:
|
||||||
|
|
@ -537,6 +635,19 @@ $ vaptvupt compress --pq mykey.pub backup.zupt ~/Documents
|
||||||
.fi
|
.fi
|
||||||
.RE
|
.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:
|
Backup a block device, sparse-aware:
|
||||||
|
|
||||||
.RS
|
.RS
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ The GUI calls the vaptvupt CLI binary — all cryptography runs in native C, not
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- **vaptvupt** v4.1.0 — Cristian Cezar Moisés ([github](https://git.securityops.co/cristiancmoises/vaptvupt))
|
- **vaptvupt** v4.2.0 — Cristian Cezar Moisés ([github](https://git.securityops.co/cristiancmoises/vaptvupt))
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
#define ZUPT_PRODUCT_EXTENSION ".zupt" /* on-disk archive extension (kept stable) */
|
#define ZUPT_PRODUCT_EXTENSION ".zupt" /* on-disk archive extension (kept stable) */
|
||||||
#define ZUPT_PRODUCT_TAGLINE "Post-quantum backup compression"
|
#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.
|
/* Vendored codec release (upstream tag) — single source for display strings.
|
||||||
* The codec's own VV_VERSION_* is its internal API version, not the release. */
|
* The codec's own VV_VERSION_* is its internal API version, not the release. */
|
||||||
#define ZUPT_CODEC_RELEASE "2.60.4"
|
#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_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_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_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).
|
/* Argon2id KDF profile descriptor (v3.4.0).
|
||||||
*
|
*
|
||||||
|
|
@ -271,6 +272,7 @@ typedef struct {
|
||||||
int pq_mode; /* 1 = post-quantum hybrid KEM mode */
|
int pq_mode; /* 1 = post-quantum hybrid KEM mode */
|
||||||
int sdk_mode; /* 1 = use libzuptsdk-backed v3 crypto (HKDF combiner + commitment + HPKE) */
|
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 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 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. */
|
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];
|
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,
|
int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
|
||||||
const uint8_t *enc_hdr, size_t enc_hdr_len);
|
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) ─── */
|
/* ─── 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_keygen(const char *privkeyfile, const char *pubkeyfile);
|
||||||
int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
|
int zupt_sdk_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
# zupt-VERSION.tar.gz alongside the PKGBUILD.
|
# zupt-VERSION.tar.gz alongside the PKGBUILD.
|
||||||
|
|
||||||
pkgname=vaptvupt
|
pkgname=vaptvupt
|
||||||
pkgver=4.1.0
|
pkgver=4.2.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
provides=('zupt')
|
provides=('zupt')
|
||||||
replaces=('zupt')
|
replaces=('zupt')
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,27 @@
|
||||||
|
vaptvupt (4.2.0-1) UNRELEASED; urgency=high
|
||||||
|
|
||||||
|
* New native full (pure) post-quantum mode --pq-only: ML-KEM-768 (FIPS
|
||||||
|
203) as the sole key-establishment mechanism, no classical X25519
|
||||||
|
component (envelope type 0x06; 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"). Keys via keygen --pq-only (ZPQK magic;
|
||||||
|
not interchangeable with hybrid --pq keys). Hybrid --pq remains the
|
||||||
|
recommended default; --pq-only has no classical fallback, so a break
|
||||||
|
of ML-KEM-768 alone breaks the archive. In-tree, default build.
|
||||||
|
* Security (critical): AES-256-CTR keystream reuse under --dedup. Dedup
|
||||||
|
blocks all use sequence 0, so the previous nonce (base_nonce XOR seq)
|
||||||
|
collapsed to a single value across blocks, reusing the CTR keystream
|
||||||
|
(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.
|
||||||
|
* 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).
|
||||||
|
* Wire format v1.6 unchanged; the 0x06 envelope is additive.
|
||||||
|
|
||||||
|
-- Cristian Cezar Moisés <sac@securityops.co> Wed, 09 Jul 2026 12:00:00 +0000
|
||||||
|
|
||||||
vaptvupt (4.1.0-1) UNRELEASED; urgency=high
|
vaptvupt (4.1.0-1) UNRELEASED; urgency=high
|
||||||
|
|
||||||
* Source-only build: the prebuilt vendored libraries libzuptsdk.so and
|
* Source-only build: the prebuilt vendored libraries libzuptsdk.so and
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@
|
||||||
class Vaptvupt < Formula
|
class Vaptvupt < Formula
|
||||||
desc "Post-quantum backup compression utility (ML-KEM-768 + AES-256-CTR + HMAC-SHA256)"
|
desc "Post-quantum backup compression utility (ML-KEM-768 + AES-256-CTR + HMAC-SHA256)"
|
||||||
homepage "https://git.securityops.co/cristiancmoises/zupt"
|
homepage "https://git.securityops.co/cristiancmoises/zupt"
|
||||||
url "https://git.securityops.co/cristiancmoises/zupt/releases/download/v4.1.0/vaptvupt-4.1.0.tar.gz"
|
url "https://git.securityops.co/cristiancmoises/zupt/releases/download/v4.2.0/vaptvupt-4.2.0.tar.gz"
|
||||||
version "4.1.0"
|
version "4.2.0"
|
||||||
sha256 "REPLACE_WITH_SHA256_OF_RELEASE_TARBALL"
|
sha256 "REPLACE_WITH_SHA256_OF_RELEASE_TARBALL"
|
||||||
license "AGPL-3.0-or-later"
|
license "AGPL-3.0-or-later"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
zupt = pkgs.stdenv.mkDerivation {
|
zupt = pkgs.stdenv.mkDerivation {
|
||||||
pname = "vaptvupt";
|
pname = "vaptvupt";
|
||||||
version = "4.1.0";
|
version = "4.2.0";
|
||||||
|
|
||||||
# When publishing, replace this with `fetchurl` against the
|
# When publishing, replace this with `fetchurl` against the
|
||||||
# release tarball. For local development the flake assumes it
|
# release tarball. For local development the flake assumes it
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# openSUSE Build Service update for `home:cabelo:innovators/vaptvupt`
|
# openSUSE Build Service update for `home:cabelo:innovators/vaptvupt`
|
||||||
|
|
||||||
This directory contains the three files needed to build vaptvupt `4.1.0`
|
This directory contains the three files needed to build vaptvupt `4.2.0`
|
||||||
in OBS:
|
in OBS:
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|---------------|-------------------------------------------------------------------------|
|
|---------------|-------------------------------------------------------------------------|
|
||||||
| `_service` | `revision` pinned to `v4.1.0`. Format unchanged (still `tar_scm`). |
|
| `_service` | `revision` pinned to `v4.2.0`. Format unchanged (still `tar_scm`). |
|
||||||
| `vaptvupt.spec` | `Version: 4.1.0`. `License: AGPL-3.0-or-later`. `%check` calls `make check`. |
|
| `vaptvupt.spec` | `Version: 4.2.0`. `License: AGPL-3.0-or-later`. `%check` calls `make check`. |
|
||||||
| `vaptvupt.changes`| Changelog for the 4.x series. Older history preserved verbatim. |
|
| `vaptvupt.changes`| Changelog for the 4.x series. Older history preserved verbatim. |
|
||||||
|
|
||||||
## Spec notes
|
## Spec notes
|
||||||
|
|
@ -58,22 +58,22 @@ cp /path/to/vaptvupt-source/packaging/opensuse/_service .
|
||||||
cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.spec .
|
cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.spec .
|
||||||
cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.changes .
|
cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.changes .
|
||||||
|
|
||||||
# 3. Trigger the service locally to fetch v4.1.0 from GitHub
|
# 3. Trigger the service locally to fetch v4.2.0 from GitHub
|
||||||
osc service runall
|
osc service runall
|
||||||
# Produces vaptvupt-4.1.0.tar.gz in the current directory.
|
# Produces vaptvupt-4.2.0.tar.gz in the current directory.
|
||||||
|
|
||||||
# 4. (Optional) Local build to verify before committing
|
# 4. (Optional) Local build to verify before committing
|
||||||
osc build openSUSE_Tumbleweed x86_64
|
osc build openSUSE_Tumbleweed x86_64
|
||||||
|
|
||||||
# 5. Commit upstream
|
# 5. Commit upstream
|
||||||
osc status # confirm vaptvupt-4.1.0.tar.gz is staged alongside the
|
osc status # confirm vaptvupt-4.2.0.tar.gz is staged alongside the
|
||||||
# three text files
|
# three text files
|
||||||
osc commit -m "Update to 4.1.0"
|
osc commit -m "Update to 4.2.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes for future updates
|
## Notes for future updates
|
||||||
|
|
||||||
* The `_service` `revision` is pinned to `v4.1.0`. To track a new
|
* The `_service` `revision` is pinned to `v4.2.0`. To track a new
|
||||||
release, edit that one line and re-run `osc service runall`.
|
release, edit that one line and re-run `osc service runall`.
|
||||||
* The spec's `Version:` field is hard-coded — when you bump `_service`
|
* The spec's `Version:` field is hard-coded — when you bump `_service`
|
||||||
`revision`, also bump `Version:` to match.
|
`revision`, also bump `Version:` to match.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<service name="tar_scm" mode="manual">
|
<service name="tar_scm" mode="manual">
|
||||||
<param name="url">https://github.com/cristiancmoises/zupt</param>
|
<param name="url">https://github.com/cristiancmoises/zupt</param>
|
||||||
<param name="scm">git</param>
|
<param name="scm">git</param>
|
||||||
<param name="revision">v4.1.0</param>
|
<param name="revision">v4.2.0</param>
|
||||||
<param name="versionformat">@PARENT_TAG@</param>
|
<param name="versionformat">@PARENT_TAG@</param>
|
||||||
<param name="versionrewrite-pattern">v(.*)</param>
|
<param name="versionrewrite-pattern">v(.*)</param>
|
||||||
<param name="submodules">enable</param>
|
<param name="submodules">enable</param>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,26 @@
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Wed Jul 9 12:00:00 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org>
|
||||||
|
|
||||||
|
- Update to 4.2.0:
|
||||||
|
* New native full (pure) post-quantum mode --pq-only: ML-KEM-768
|
||||||
|
(FIPS 203) as the sole key-establishment mechanism, with no
|
||||||
|
classical X25519 component (envelope type 0x06; archive key
|
||||||
|
SHA3-512(ml_ss || ml_ct || "ZUPT-PQ-ONLY-v1")). For compliance
|
||||||
|
postures that mandate a single NIST-standardised PQ primitive with
|
||||||
|
no classical KEM in the envelope (CNSA 2.0-style "PQ-only"). Keys
|
||||||
|
via keygen --pq-only. In-tree crypto, built in the default
|
||||||
|
source-only package. Hybrid --pq remains the recommended default;
|
||||||
|
--pq-only has no classical fallback.
|
||||||
|
* Security (critical): fixed AES-256-CTR keystream reuse under
|
||||||
|
--dedup. Dedup blocks all use sequence 0, so the previous nonce
|
||||||
|
(base_nonce XOR seq) collapsed to a single value across blocks,
|
||||||
|
reusing the CTR keystream. Each block now uses a fresh random
|
||||||
|
128-bit nonce. Re-encrypt any --dedup encrypted archives written
|
||||||
|
by <= 4.1.0.
|
||||||
|
* keygen --sdk / --box now gives clear guidance toward native --pq /
|
||||||
|
--pq-only on a source-only build.
|
||||||
|
* Wire format v1.6 unchanged; the 0x06 envelope is additive.
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Tue Jul 7 12:00:00 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org>
|
Tue Jul 7 12:00:00 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
|
|
||||||
Name: vaptvupt
|
Name: vaptvupt
|
||||||
Version: 4.1.0
|
Version: 4.2.0
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: Post-quantum backup compression with AES-256 + ML-KEM-768 hybrid encryption
|
Summary: Post-quantum backup compression with AES-256 + ML-KEM-768 hybrid encryption
|
||||||
License: AGPL-3.0-or-later
|
License: AGPL-3.0-or-later
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
# in the base.
|
# in the base.
|
||||||
|
|
||||||
Name: vaptvupt
|
Name: vaptvupt
|
||||||
Version: 4.1.0
|
Version: 4.2.0
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Post-quantum backup compression utility (AES-256 + ML-KEM-768 + Argon2id, formerly Zupt)
|
Summary: Post-quantum backup compression utility (AES-256 + ML-KEM-768 + Argon2id, formerly Zupt)
|
||||||
|
|
||||||
|
|
@ -112,6 +112,16 @@ ln -sf libpqvaptvupt.so.0.6.0 %{buildroot}%{_libdir}/%{name}/libpqvaptvupt.so
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Wed Jul 09 2026 Cristian Cezar Moisés <sac@securityops.co> - 4.2.0-1
|
||||||
|
- New native full (pure) post-quantum mode --pq-only: ML-KEM-768 as the
|
||||||
|
sole KEM, no classical X25519 (envelope 0x06). For "PQ-only" compliance
|
||||||
|
postures; hybrid --pq remains the recommended default. In-tree crypto.
|
||||||
|
- Security (critical): fixed AES-256-CTR keystream reuse under --dedup
|
||||||
|
(every block now uses a fresh random 128-bit nonce). Re-encrypt any
|
||||||
|
--dedup encrypted archives written by <= 4.1.0.
|
||||||
|
- Clearer keygen --sdk/--box guidance on source-only builds.
|
||||||
|
- Wire format v1.6 unchanged.
|
||||||
|
|
||||||
* Tue May 20 2025 Cristian Cezar Moisés <sac@securityops.co> - 2.4.4-1
|
* Tue May 20 2025 Cristian Cezar Moisés <sac@securityops.co> - 2.4.4-1
|
||||||
- Initial Fedora/EPEL RPM package.
|
- Initial Fedora/EPEL RPM package.
|
||||||
- Tracks upstream v2.4.4: distribution packaging release; archive
|
- Tracks upstream v2.4.4: distribution packaging release; archive
|
||||||
|
|
|
||||||
|
|
@ -391,11 +391,23 @@ uint8_t *zupt_encrypt_buffer_aad(const zupt_keyring_t *kr,
|
||||||
uint8_t *pkg = (uint8_t *)malloc(*olen);
|
uint8_t *pkg = (uint8_t *)malloc(*olen);
|
||||||
if (!pkg) return NULL;
|
if (!pkg) return NULL;
|
||||||
|
|
||||||
/* Derive per-block nonce */
|
/* Per-block nonce: a fresh random 128-bit value for every block.
|
||||||
|
*
|
||||||
|
* SECURITY FIX (v4.2.0): the previous scheme derived the nonce as
|
||||||
|
* 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 MAC
|
||||||
|
* the same way). That collapsed every dedup block's nonce to the single
|
||||||
|
* per-archive base_nonce, reusing the AES-256-CTR keystream across distinct
|
||||||
|
* plaintext blocks — a many-time-pad that leaks plaintext to a
|
||||||
|
* ciphertext-only attacker, in every encryption mode (password, hybrid PQ,
|
||||||
|
* full PQ). A random 128-bit nonce is unique with overwhelming probability
|
||||||
|
* regardless of dedup or thread scheduling. The nonce is stored in the
|
||||||
|
* package prefix and bound by the HMAC, and decrypt reads it back directly,
|
||||||
|
* so this is an encrypt-side change only — the on-disk format, the MAC
|
||||||
|
* transcript (which still uses block_seq as aad_seq), and the decrypt path
|
||||||
|
* are all unchanged, and pre-4.2 archives still extract byte-exact. */
|
||||||
uint8_t nonce[16];
|
uint8_t nonce[16];
|
||||||
memcpy(nonce, kr->base_nonce, 16);
|
zupt_random_bytes(nonce, 16);
|
||||||
for (int i = 0; i < 8; i++)
|
|
||||||
nonce[i] ^= (uint8_t)(block_seq >> (i * 8));
|
|
||||||
|
|
||||||
/* Store nonce */
|
/* Store nonce */
|
||||||
memcpy(pkg, nonce, 16);
|
memcpy(pkg, nonce, 16);
|
||||||
|
|
@ -850,3 +862,207 @@ int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════
|
||||||
|
* FULL POST-QUANTUM KEM: ML-KEM-768 only (v4.2.0)
|
||||||
|
*
|
||||||
|
* Unlike the hybrid --pq mode (ML-KEM-768 + X25519), this mode uses
|
||||||
|
* ML-KEM-768 ALONE — no classical X25519 component. It is "fully
|
||||||
|
* post-quantum": confidentiality of the archive key rests solely on
|
||||||
|
* ML-KEM (FIPS 203, IND-CCA2 with the Fujisaki-Okamoto transform /
|
||||||
|
* implicit rejection that zupt_mlkem768_decaps implements).
|
||||||
|
*
|
||||||
|
* SECURITY NOTE: the hybrid --pq mode remains the recommended default.
|
||||||
|
* A pure-PQ scheme has NO classical fallback, so a future break of
|
||||||
|
* ML-KEM-768 leaves no second layer. Use --pq-only only when a strictly
|
||||||
|
* post-quantum construction is a hard requirement (e.g. a policy that
|
||||||
|
* forbids classical primitives entirely).
|
||||||
|
*
|
||||||
|
* Key file (ZPQK):
|
||||||
|
* [4B] "ZPQK"
|
||||||
|
* [1B] version 0x01
|
||||||
|
* [1B] flags: bit0 = has_private
|
||||||
|
* [2B] reserved
|
||||||
|
* [1184B] ml_kem_pk
|
||||||
|
* [2400B] ml_kem_sk (only if has_private)
|
||||||
|
* [8B] xxh64 of everything above
|
||||||
|
*
|
||||||
|
* enc_hdr (ZUPT_ENC_PQ_ONLY = 0x06), 1105 bytes:
|
||||||
|
* [1B] 0x06
|
||||||
|
* [1088B] ml_kem_ciphertext
|
||||||
|
* [16B] base_nonce
|
||||||
|
*
|
||||||
|
* archive_key[64] = SHA3-512(ml_ss ‖ ml_ct ‖ "ZUPT-PQ-ONLY-v1")
|
||||||
|
* enc_key = archive_key[0:32], mac_key = archive_key[32:64]
|
||||||
|
* The ML-KEM ciphertext is bound into the KDF transcript (defense in
|
||||||
|
* depth) alongside the domain separator, which also prevents cross-mode
|
||||||
|
* key reuse with the hybrid path (different label).
|
||||||
|
* ═══════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
#define ZPQK_MAGIC "ZPQK"
|
||||||
|
#define ZPQK_VERSION 0x01
|
||||||
|
#define ZPQK_FLAG_PRIVATE 0x01
|
||||||
|
#define ZPQK_HDR 8
|
||||||
|
#define ZPQK_PUB_SIZE (ZPQK_HDR + 1184)
|
||||||
|
#define ZPQK_PRIV_SIZE (ZPQK_HDR + 1184 + 2400)
|
||||||
|
#define ZUPT_PQ_ONLY_LABEL "ZUPT-PQ-ONLY-v1" /* 15 bytes */
|
||||||
|
|
||||||
|
int zupt_pq_keygen(const char *keyfile) {
|
||||||
|
uint8_t ml_pk[MLKEM_PUBLICKEYBYTES], ml_sk[MLKEM_SECRETKEYBYTES];
|
||||||
|
if (zupt_mlkem768_keygen(ml_pk, ml_sk) != 0) return -1;
|
||||||
|
|
||||||
|
FILE *f = fopen(keyfile, "wb");
|
||||||
|
if (!f) { zupt_secure_wipe(ml_sk, sizeof(ml_sk)); return -1; }
|
||||||
|
|
||||||
|
size_t total = ZPQK_PRIV_SIZE;
|
||||||
|
uint8_t *buf = (uint8_t *)calloc(total + 8, 1);
|
||||||
|
if (!buf) { fclose(f); zupt_secure_wipe(ml_sk, sizeof(ml_sk)); return -1; }
|
||||||
|
|
||||||
|
memcpy(buf, ZPQK_MAGIC, 4);
|
||||||
|
buf[4] = ZPQK_VERSION;
|
||||||
|
buf[5] = ZPQK_FLAG_PRIVATE;
|
||||||
|
buf[6] = buf[7] = 0;
|
||||||
|
memcpy(buf + ZPQK_HDR, ml_pk, 1184);
|
||||||
|
memcpy(buf + ZPQK_HDR + 1184, ml_sk, 2400);
|
||||||
|
|
||||||
|
uint64_t ck = zupt_xxh64(buf, total, 0);
|
||||||
|
zupt_le64_put(buf + total, ck);
|
||||||
|
|
||||||
|
size_t written = fwrite(buf, 1, total + 8, f);
|
||||||
|
if (fclose(f) != 0) written = 0;
|
||||||
|
|
||||||
|
zupt_secure_wipe(ml_sk, sizeof(ml_sk));
|
||||||
|
zupt_secure_wipe(buf, total + 8);
|
||||||
|
free(buf);
|
||||||
|
return (written == total + 8) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int zupt_pq_export_pubkey(const char *privfile, const char *pubfile) {
|
||||||
|
FILE *f = fopen(privfile, "rb");
|
||||||
|
if (!f) return -1;
|
||||||
|
uint8_t hdr[ZPQK_HDR];
|
||||||
|
if (fread(hdr, 1, ZPQK_HDR, f) != ZPQK_HDR || memcmp(hdr, ZPQK_MAGIC, 4) != 0 ||
|
||||||
|
!(hdr[5] & ZPQK_FLAG_PRIVATE)) { fclose(f); return -1; }
|
||||||
|
uint8_t ml_pk[1184];
|
||||||
|
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
FILE *out = fopen(pubfile, "wb");
|
||||||
|
if (!out) return -1;
|
||||||
|
size_t total = ZPQK_PUB_SIZE;
|
||||||
|
uint8_t buf[ZPQK_PUB_SIZE + 8];
|
||||||
|
memcpy(buf, ZPQK_MAGIC, 4);
|
||||||
|
buf[4] = ZPQK_VERSION;
|
||||||
|
buf[5] = 0;
|
||||||
|
buf[6] = buf[7] = 0;
|
||||||
|
memcpy(buf + ZPQK_HDR, ml_pk, 1184);
|
||||||
|
uint64_t ck = zupt_xxh64(buf, total, 0);
|
||||||
|
zupt_le64_put(buf + total, ck);
|
||||||
|
size_t written = fwrite(buf, 1, total + 8, out);
|
||||||
|
if (fclose(out) != 0) written = 0;
|
||||||
|
return (written == total + 8) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int read_pq_pubkey(const char *path, uint8_t ml_pk[1184]) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return -1;
|
||||||
|
uint8_t hdr[ZPQK_HDR];
|
||||||
|
if (fread(hdr, 1, ZPQK_HDR, f) != ZPQK_HDR || memcmp(hdr, ZPQK_MAGIC, 4) != 0) {
|
||||||
|
fclose(f); return -1;
|
||||||
|
}
|
||||||
|
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int read_pq_privkey(const char *path, uint8_t ml_pk[1184], uint8_t ml_sk[2400]) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return -1;
|
||||||
|
uint8_t hdr[ZPQK_HDR];
|
||||||
|
if (fread(hdr, 1, ZPQK_HDR, f) != ZPQK_HDR || memcmp(hdr, ZPQK_MAGIC, 4) != 0 ||
|
||||||
|
!(hdr[5] & ZPQK_FLAG_PRIVATE)) { fclose(f); return -1; }
|
||||||
|
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
|
||||||
|
if (fread(ml_sk, 1, 2400, f) != 2400) { fclose(f); return -1; }
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* archive_key = SHA3-512(ml_ss ‖ ml_ct ‖ label). Shared by encrypt/decrypt. */
|
||||||
|
static void pq_only_derive(const uint8_t ml_ss[32], const uint8_t ml_ct[1088],
|
||||||
|
uint8_t archive_key[64]) {
|
||||||
|
uint8_t kdf_input[32 + 1088 + 15];
|
||||||
|
memcpy(kdf_input, ml_ss, 32);
|
||||||
|
memcpy(kdf_input + 32, ml_ct, 1088);
|
||||||
|
memcpy(kdf_input + 32 + 1088, ZUPT_PQ_ONLY_LABEL, 15);
|
||||||
|
zupt_sha3_512(kdf_input, sizeof(kdf_input), archive_key);
|
||||||
|
zupt_secure_wipe(kdf_input, sizeof(kdf_input));
|
||||||
|
}
|
||||||
|
|
||||||
|
int zupt_pq_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
|
||||||
|
uint8_t *enc_hdr, size_t *enc_hdr_len) {
|
||||||
|
uint8_t ml_pk[1184];
|
||||||
|
if (read_pq_pubkey(pubkeyfile, ml_pk) != 0) return -1;
|
||||||
|
|
||||||
|
uint8_t ml_ct[1088], ml_ss[32];
|
||||||
|
if (zupt_mlkem768_encaps(ml_ct, ml_ss, ml_pk) != 0) return -1;
|
||||||
|
|
||||||
|
uint8_t archive_key[64];
|
||||||
|
pq_only_derive(ml_ss, ml_ct, archive_key);
|
||||||
|
|
||||||
|
kr->canary_head = ZUPT_CANARY;
|
||||||
|
memcpy(kr->enc_key, archive_key, 32);
|
||||||
|
memcpy(kr->mac_key, archive_key + 32, 32);
|
||||||
|
zupt_random_bytes(kr->base_nonce, ZUPT_NONCE_SIZE);
|
||||||
|
kr->iterations = 0;
|
||||||
|
kr->active = 1;
|
||||||
|
kr->canary_tail = ZUPT_CANARY;
|
||||||
|
zupt_mlock_keys(kr->enc_key, ZUPT_AES_KEY_SIZE);
|
||||||
|
zupt_mlock_keys(kr->mac_key, ZUPT_HMAC_SIZE);
|
||||||
|
|
||||||
|
enc_hdr[0] = ZUPT_ENC_PQ_ONLY;
|
||||||
|
memcpy(enc_hdr + 1, ml_ct, 1088);
|
||||||
|
memcpy(enc_hdr + 1 + 1088, kr->base_nonce, 16);
|
||||||
|
*enc_hdr_len = 1 + 1088 + 16; /* 1105 bytes */
|
||||||
|
|
||||||
|
zupt_secure_wipe(ml_ss, 32);
|
||||||
|
zupt_secure_wipe(archive_key, 64);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int zupt_pq_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
|
||||||
|
const uint8_t *enc_hdr, size_t enc_hdr_len) {
|
||||||
|
if (enc_hdr_len < 1 + 1088 + 16) return -1;
|
||||||
|
if (enc_hdr[0] != ZUPT_ENC_PQ_ONLY) return -1;
|
||||||
|
const uint8_t *ml_ct = enc_hdr + 1;
|
||||||
|
const uint8_t *nonce = enc_hdr + 1 + 1088;
|
||||||
|
|
||||||
|
uint8_t ml_pk[1184], ml_sk[2400];
|
||||||
|
if (read_pq_privkey(privkeyfile, ml_pk, ml_sk) != 0) {
|
||||||
|
zupt_secure_wipe(ml_sk, sizeof(ml_sk)); /* wipe any partial secret from a truncated key file */
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ML-KEM-768 decapsulation (FO implicit rejection: an invalid ciphertext
|
||||||
|
* yields a pseudorandom shared secret, so a wrong/tampered ct produces a
|
||||||
|
* wrong archive key and the per-block HMAC fails-closed at extract time). */
|
||||||
|
uint8_t ml_ss[32];
|
||||||
|
zupt_mlkem768_decaps(ml_ss, ml_ct, ml_sk);
|
||||||
|
|
||||||
|
uint8_t archive_key[64];
|
||||||
|
pq_only_derive(ml_ss, ml_ct, archive_key);
|
||||||
|
|
||||||
|
kr->canary_head = ZUPT_CANARY;
|
||||||
|
memcpy(kr->enc_key, archive_key, 32);
|
||||||
|
memcpy(kr->mac_key, archive_key + 32, 32);
|
||||||
|
memcpy(kr->base_nonce, nonce, ZUPT_NONCE_SIZE);
|
||||||
|
kr->iterations = 0;
|
||||||
|
kr->active = 1;
|
||||||
|
kr->canary_tail = ZUPT_CANARY;
|
||||||
|
zupt_mlock_keys(kr->enc_key, ZUPT_AES_KEY_SIZE);
|
||||||
|
zupt_mlock_keys(kr->mac_key, ZUPT_HMAC_SIZE);
|
||||||
|
|
||||||
|
zupt_secure_wipe(ml_sk, sizeof(ml_sk));
|
||||||
|
zupt_secure_wipe(ml_ss, 32);
|
||||||
|
zupt_secure_wipe(archive_key, 64);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,35 @@ zupt_error_t write_enc_header(FILE *out, zupt_archive_header_t *hdr,
|
||||||
|
|
||||||
if (!opts->quiet)
|
if (!opts->quiet)
|
||||||
fprintf(stderr, " Encryption: SDK-v2 PQ Hybrid + XChaCha20-Poly1305 (commitment + HPKE)\n\n");
|
fprintf(stderr, " Encryption: SDK-v2 PQ Hybrid + XChaCha20-Poly1305 (commitment + HPKE)\n\n");
|
||||||
|
} else if (opts->pqonly_mode) {
|
||||||
|
/* ─── FULL POST-QUANTUM MODE (ML-KEM-768 only, no X25519) ─── */
|
||||||
|
hdr->global_flags |= ZUPT_FLAG_PQ_HYBRID; /* generic PQ indicator; enc_type distinguishes */
|
||||||
|
|
||||||
|
uint8_t enc_hdr_buf[1200];
|
||||||
|
size_t enc_hdr_len = 0;
|
||||||
|
if (!opts->quiet)
|
||||||
|
fprintf(stderr, " Full post-quantum key encapsulation (ML-KEM-768, no classical layer)...\n");
|
||||||
|
if (zupt_pq_encrypt_init(&opts->keyring, opts->keyfile,
|
||||||
|
enc_hdr_buf, &enc_hdr_len) != 0) {
|
||||||
|
fprintf(stderr, "Error: full-PQ key encapsulation failed (wrong key file?).\n");
|
||||||
|
return ZUPT_ERR_AUTH_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
zupt_w8(out, ZUPT_BLOCK_MAGIC_0); zupt_w8(out, ZUPT_BLOCK_MAGIC_1);
|
||||||
|
zupt_w8(out, ZUPT_BLOCK_ENC_HEADER);
|
||||||
|
zupt_w16le(out, ZUPT_CODEC_STORE); zupt_w16le(out, 0);
|
||||||
|
zupt_write_varint(out, enc_hdr_len);
|
||||||
|
zupt_write_varint(out, enc_hdr_len);
|
||||||
|
zupt_w64le(out, zupt_xxh64(enc_hdr_buf, enc_hdr_len, 0));
|
||||||
|
if (fwrite(enc_hdr_buf, 1, enc_hdr_len, out) != enc_hdr_len)
|
||||||
|
return ZUPT_ERR_IO;
|
||||||
|
|
||||||
|
fseeko(out, 0, SEEK_SET);
|
||||||
|
if (fwrite(hdr, sizeof(*hdr), 1, out) != 1) return ZUPT_ERR_IO;
|
||||||
|
fseeko(out, 0, SEEK_END);
|
||||||
|
|
||||||
|
if (!opts->quiet)
|
||||||
|
fprintf(stderr, " Encryption: Full PQ (ML-KEM-768) + AES-256-CTR + HMAC-SHA256\n\n");
|
||||||
} else if (opts->pq_mode) {
|
} else if (opts->pq_mode) {
|
||||||
/* ─── PQ HYBRID MODE ─── */
|
/* ─── PQ HYBRID MODE ─── */
|
||||||
hdr->global_flags |= ZUPT_FLAG_PQ_HYBRID;
|
hdr->global_flags |= ZUPT_FLAG_PQ_HYBRID;
|
||||||
|
|
@ -2022,6 +2051,21 @@ zupt_error_t read_enc_header(FILE *f, zupt_archive_header_t *hdr, zupt_options_t
|
||||||
}
|
}
|
||||||
free(eb.payload);
|
free(eb.payload);
|
||||||
return ZUPT_OK;
|
return ZUPT_OK;
|
||||||
|
} else if (enc_type == ZUPT_ENC_PQ_ONLY) {
|
||||||
|
/* ─── FULL POST-QUANTUM MODE (ML-KEM-768 only) ─── */
|
||||||
|
if (opts->keyfile[0] == '\0') {
|
||||||
|
fprintf(stderr, "Error: Archive uses full post-quantum encryption. Use --pq-only <keyfile>.\n");
|
||||||
|
free(eb.payload);
|
||||||
|
return ZUPT_ERR_AUTH_FAIL;
|
||||||
|
}
|
||||||
|
if (zupt_pq_decrypt_init(&opts->keyring, opts->keyfile,
|
||||||
|
eb.payload, (size_t)eb.compressed_size) != 0) {
|
||||||
|
fprintf(stderr, "Error: full-PQ decryption key derivation failed (wrong key?).\n");
|
||||||
|
free(eb.payload);
|
||||||
|
return ZUPT_ERR_AUTH_FAIL;
|
||||||
|
}
|
||||||
|
free(eb.payload);
|
||||||
|
return ZUPT_OK;
|
||||||
} else if (enc_type == ZUPT_ENC_PBKDF2) {
|
} else if (enc_type == ZUPT_ENC_PBKDF2) {
|
||||||
/* ─── PASSWORD MODE (v0.7+ format with enc_type prefix) ─── */
|
/* ─── PASSWORD MODE (v0.7+ format with enc_type prefix) ─── */
|
||||||
if (opts->password[0] == '\0') {
|
if (opts->password[0] == '\0') {
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,9 @@ static void usage(void) {
|
||||||
" Use 'pbkdf2' for v2.4.0-and-older reader compatibility.\n"
|
" Use 'pbkdf2' for v2.4.0-and-older reader compatibility.\n"
|
||||||
" -c, --comment <TEXT> Embed a free-form archive comment (v2.4.3+).\n"
|
" -c, --comment <TEXT> Embed a free-form archive comment (v2.4.3+).\n"
|
||||||
" --comment-file <FILE> Read comment from file (max 4096 bytes).\n"
|
" --comment-file <FILE> Read comment from file (max 4096 bytes).\n"
|
||||||
" --pq <pubkey> Post-quantum encryption (legacy XOR+SHA3 combiner)\n"
|
" --pq <pubkey> Post-quantum HYBRID encryption (ML-KEM-768 + X25519) [recommended]\n"
|
||||||
" --pq-sdk <pubkey> Post-quantum encryption via libzuptsdk\n"
|
" --pq-only <pubkey> FULL post-quantum encryption (ML-KEM-768 only, no classical layer)\n"
|
||||||
|
" --pq-sdk <pubkey> Post-quantum encryption via libzuptsdk (WITH_SDK=1 builds only)\n"
|
||||||
" --pq-box <pubkey> Post-quantum sealed box via libpqvaptvupt (HKDF combiner)\n"
|
" --pq-box <pubkey> Post-quantum sealed box via libpqvaptvupt (HKDF combiner)\n"
|
||||||
" (HKDF combiner + key commitment + HPKE binding\n"
|
" (HKDF combiner + key commitment + HPKE binding\n"
|
||||||
" + Argon2id; recommended for new archives)\n"
|
" + Argon2id; recommended for new archives)\n"
|
||||||
|
|
@ -88,8 +89,9 @@ static void usage(void) {
|
||||||
"Extract/List/Test Options:\n"
|
"Extract/List/Test Options:\n"
|
||||||
" -o, --output <DIR> Output directory (extract only)\n"
|
" -o, --output <DIR> Output directory (extract only)\n"
|
||||||
" -p, --password <PW> Decryption password\n"
|
" -p, --password <PW> Decryption password\n"
|
||||||
" --pq <privkey> Post-quantum decryption (legacy combiner)\n"
|
" --pq <privkey> Post-quantum HYBRID decryption (ML-KEM-768 + X25519)\n"
|
||||||
" --pq-sdk <privkey> Post-quantum decryption via libzuptsdk\n"
|
" --pq-only <privkey> FULL post-quantum decryption (ML-KEM-768 only)\n"
|
||||||
|
" --pq-sdk <privkey> Post-quantum decryption via libzuptsdk (WITH_SDK=1 builds only)\n"
|
||||||
" --pq-box <privkey> Post-quantum sealed-box decryption (libpqvaptvupt)\n"
|
" --pq-box <privkey> Post-quantum sealed-box decryption (libpqvaptvupt)\n"
|
||||||
" -v, --verbose Verbose output\n"
|
" -v, --verbose Verbose output\n"
|
||||||
" -t, --threads <N> Thread count for decompression\n"
|
" -t, --threads <N> Thread count for decompression\n"
|
||||||
|
|
@ -98,9 +100,11 @@ static void usage(void) {
|
||||||
" -o <file> Output keyfile path (required)\n"
|
" -o <file> Output keyfile path (required)\n"
|
||||||
" --pub Export public key from existing private key (-k)\n"
|
" --pub Export public key from existing private key (-k)\n"
|
||||||
" -k <privkey> Source private keyfile (with --pub)\n"
|
" -k <privkey> Source private keyfile (with --pub)\n"
|
||||||
" --sdk, --pq-sdk Generate SDK v2 keypair (writes <file> and <file>.pub)\n"
|
" (default) Generate HYBRID keypair (ML-KEM-768 + X25519) for --pq\n"
|
||||||
" --box, --pq-box Generate pq-box keypair (libpqvaptvupt; writes <file> and <file>.pub)\n"
|
" --pq-only Generate FULL post-quantum keypair (ML-KEM-768 only) for --pq-only\n"
|
||||||
" Use these keys with --pq-sdk / --pq-box respectively.\n"
|
" --sdk, --pq-sdk Generate SDK v2 keypair (libzuptsdk; WITH_SDK=1 builds only)\n"
|
||||||
|
" --box, --pq-box Generate pq-box keypair (libpqvaptvupt; WITH_SDK=1 builds only)\n"
|
||||||
|
" Use each key with its matching mode.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Directories are traversed recursively.\n"
|
"Directories are traversed recursively.\n"
|
||||||
"\n");
|
"\n");
|
||||||
|
|
@ -268,6 +272,9 @@ int main(int argc, char **argv) {
|
||||||
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
||||||
|
opts.pqonly_mode=1; opts.encrypt=1;
|
||||||
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
|
@ -410,6 +417,9 @@ int main(int argc, char **argv) {
|
||||||
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
||||||
|
opts.pqonly_mode=1; opts.encrypt=1;
|
||||||
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
|
@ -446,6 +456,9 @@ int main(int argc, char **argv) {
|
||||||
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
||||||
|
opts.pqonly_mode=1; opts.encrypt=1;
|
||||||
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
|
@ -481,6 +494,9 @@ int main(int argc, char **argv) {
|
||||||
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
||||||
|
opts.pqonly_mode=1; opts.encrypt=1;
|
||||||
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
|
@ -743,6 +759,9 @@ int main(int argc, char **argv) {
|
||||||
opts.verbose=1;
|
opts.verbose=1;
|
||||||
} else if ((streq(argv[ai],"-t")||streq(argv[ai],"--threads"))&&ai+1<argc) {
|
} else if ((streq(argv[ai],"-t")||streq(argv[ai],"--threads"))&&ai+1<argc) {
|
||||||
opts.threads=atoi(argv[++ai]);
|
opts.threads=atoi(argv[++ai]);
|
||||||
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
||||||
|
opts.pqonly_mode=1; opts.encrypt=1;
|
||||||
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
||||||
opts.pq_mode=1; opts.encrypt=1;
|
opts.pq_mode=1; opts.encrypt=1;
|
||||||
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
||||||
|
|
@ -821,6 +840,7 @@ int main(int argc, char **argv) {
|
||||||
int export_pub = 0;
|
int export_pub = 0;
|
||||||
int sdk_mode = 0;
|
int sdk_mode = 0;
|
||||||
int box_mode = 0;
|
int box_mode = 0;
|
||||||
|
int pqonly_mode = 0;
|
||||||
int ai = 2;
|
int ai = 2;
|
||||||
while (ai < argc && isopt(argv[ai])) {
|
while (ai < argc && isopt(argv[ai])) {
|
||||||
if ((streq(argv[ai],"-o")||streq(argv[ai],"--output")) && ai+1 < argc)
|
if ((streq(argv[ai],"-o")||streq(argv[ai],"--output")) && ai+1 < argc)
|
||||||
|
|
@ -833,6 +853,8 @@ int main(int argc, char **argv) {
|
||||||
sdk_mode = 1;
|
sdk_mode = 1;
|
||||||
else if (streq(argv[ai],"--box")||streq(argv[ai],"--pq-box"))
|
else if (streq(argv[ai],"--box")||streq(argv[ai],"--pq-box"))
|
||||||
box_mode = 1;
|
box_mode = 1;
|
||||||
|
else if (streq(argv[ai],"--pq-only")||streq(argv[ai],"--pqonly"))
|
||||||
|
pqonly_mode = 1;
|
||||||
else { fprintf(stderr, "Unknown option '%s'\n", argv[ai]); return 1; }
|
else { fprintf(stderr, "Unknown option '%s'\n", argv[ai]); return 1; }
|
||||||
ai++;
|
ai++;
|
||||||
}
|
}
|
||||||
|
|
@ -848,10 +870,22 @@ int main(int argc, char **argv) {
|
||||||
if (export_pub) {
|
if (export_pub) {
|
||||||
if (!privfile) { fprintf(stderr, "Error: --pub requires -k <private_keyfile>\n"); return 1; }
|
if (!privfile) { fprintf(stderr, "Error: --pub requires -k <private_keyfile>\n"); return 1; }
|
||||||
fprintf(stderr, " Exporting public key from: %s\n", privfile);
|
fprintf(stderr, " Exporting public key from: %s\n", privfile);
|
||||||
if (zupt_hybrid_export_pubkey(privfile, outfile) != 0) {
|
int erc = pqonly_mode ? zupt_pq_export_pubkey(privfile, outfile)
|
||||||
fprintf(stderr, "Error: Failed to export public key.\n"); return 1;
|
: zupt_hybrid_export_pubkey(privfile, outfile);
|
||||||
|
if (erc != 0) {
|
||||||
|
fprintf(stderr, "Error: Failed to export public key%s.\n",
|
||||||
|
pqonly_mode ? "" : " (for full-PQ keys use: keygen --pub --pq-only)");
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
fprintf(stderr, " Public key written to: %s\n", outfile);
|
fprintf(stderr, " Public key written to: %s\n", outfile);
|
||||||
|
} else if (pqonly_mode) {
|
||||||
|
fprintf(stderr, " Generating ML-KEM-768 keypair (full post-quantum, no X25519)...\n");
|
||||||
|
if (zupt_pq_keygen(outfile) != 0) {
|
||||||
|
fprintf(stderr, "Error: full-PQ key generation failed.\n"); return 1;
|
||||||
|
}
|
||||||
|
fprintf(stderr, " Private key written to: %s\n", outfile);
|
||||||
|
fprintf(stderr, " SECURITY: Keep this file secret. Back it up securely.\n");
|
||||||
|
fprintf(stderr, " To export public key: vaptvupt keygen --pub --pq-only -o pub.key -k %s\n", outfile);
|
||||||
} else if (box_mode) {
|
} else if (box_mode) {
|
||||||
fprintf(stderr, " Generating ML-KEM-768 + X25519 keypair (pq-box format)...\n");
|
fprintf(stderr, " Generating ML-KEM-768 + X25519 keypair (pq-box format)...\n");
|
||||||
char pubfile[512];
|
char pubfile[512];
|
||||||
|
|
@ -867,7 +901,14 @@ int main(int argc, char **argv) {
|
||||||
char pubfile[512];
|
char pubfile[512];
|
||||||
snprintf(pubfile, sizeof(pubfile), "%s.pub", outfile);
|
snprintf(pubfile, sizeof(pubfile), "%s.pub", outfile);
|
||||||
if (zupt_sdk_hybrid_keygen(outfile, pubfile) != 0) {
|
if (zupt_sdk_hybrid_keygen(outfile, pubfile) != 0) {
|
||||||
fprintf(stderr, "Error: SDK key generation failed.\n"); return 1;
|
fprintf(stderr,
|
||||||
|
"Error: SDK-v2 key generation is unavailable in this build.\n"
|
||||||
|
" --pq-sdk needs libzuptsdk, which is not part of the source-only\n"
|
||||||
|
" build. For post-quantum keys use one of the native modes:\n"
|
||||||
|
" vaptvupt keygen -o key # hybrid ML-KEM-768 + X25519 (--pq)\n"
|
||||||
|
" vaptvupt keygen --pq-only -o key # full PQ, ML-KEM-768 only (--pq-only)\n"
|
||||||
|
" (Rebuild upstream with 'make WITH_SDK=1' to enable --pq-sdk.)\n");
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
fprintf(stderr, " Private key: %s\n", outfile);
|
fprintf(stderr, " Private key: %s\n", outfile);
|
||||||
fprintf(stderr, " Public key: %s\n", pubfile);
|
fprintf(stderr, " Public key: %s\n", pubfile);
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,14 @@ E=$(find "$T/o7" -name a.txt -type f 2>/dev/null|head -1); [ -n "$E" ] && diff -
|
||||||
$Z keygen -o "$T/k.key" 2>/dev/null && $Z keygen --pub -o "$T/p.key" -k "$T/k.key" 2>/dev/null
|
$Z keygen -o "$T/k.key" 2>/dev/null && $Z keygen --pub -o "$T/p.key" -k "$T/k.key" 2>/dev/null
|
||||||
$Z compress --pq "$T/p.key" "$T/8.zupt" "$T/d/" 2>/dev/null && $Z extract --pq "$T/k.key" -o "$T/o8" "$T/8.zupt" 2>/dev/null
|
$Z compress --pq "$T/p.key" "$T/8.zupt" "$T/d/" 2>/dev/null && $Z extract --pq "$T/k.key" -o "$T/o8" "$T/8.zupt" 2>/dev/null
|
||||||
E=$(find "$T/o8" -name a.txt -type f 2>/dev/null|head -1); [ -n "$E" ] && diff -q "$T/d/a.txt" "$E" >/dev/null 2>&1 && ok "PQ" || fl "PQ"
|
E=$(find "$T/o8" -name a.txt -type f 2>/dev/null|head -1); [ -n "$E" ] && diff -q "$T/d/a.txt" "$E" >/dev/null 2>&1 && ok "PQ" || fl "PQ"
|
||||||
|
# Full post-quantum (ML-KEM-768 only, no X25519) round-trip.
|
||||||
|
$Z keygen --pq-only -o "$T/kq.key" 2>/dev/null && $Z keygen --pub --pq-only -o "$T/pq.key" -k "$T/kq.key" 2>/dev/null
|
||||||
|
$Z compress --pq-only "$T/pq.key" "$T/9.zupt" "$T/d/" 2>/dev/null && $Z extract --pq-only "$T/kq.key" -o "$T/o9" "$T/9.zupt" 2>/dev/null
|
||||||
|
E=$(find "$T/o9" -name a.txt -type f 2>/dev/null|head -1); [ -n "$E" ] && diff -q "$T/d/a.txt" "$E" >/dev/null 2>&1 && ok "PQ-only" || fl "PQ-only"
|
||||||
R=$($Z test "$T/1.zupt" 2>&1); echo "$R"|grep -q "0 failed" && ok "Integrity" || fl "Integrity"
|
R=$($Z test "$T/1.zupt" 2>&1); echo "$R"|grep -q "0 failed" && ok "Integrity" || fl "Integrity"
|
||||||
# F-01 (2.2.4): every help command verb starts its own line.
|
# F-01 (2.2.4): every help command verb starts its own line.
|
||||||
# Pre-fix output ran "keygen … Key generation zupt version" on one
|
# Pre-fix output ran "keygen … Key generation zupt version" on one
|
||||||
# wrapped line due to a missing \n in src/zupt_main.c:41.
|
# wrapped line due to a missing \n in src/zupt_main.c:41.
|
||||||
HC=$($Z help 2>&1 | grep -cE '^ (vaptvupt|zupt) ')
|
HC=$($Z help 2>&1 | grep -cE '^ (vaptvupt|zupt) ')
|
||||||
[ "$HC" -ge 10 ] && ok "Help command lines ($HC)" || fl "Help command lines ($HC, need ≥10)"
|
[ "$HC" -ge 10 ] && ok "Help command lines ($HC)" || fl "Help command lines ($HC, need ≥10)"
|
||||||
echo ""; echo " Results: $P passed, $F failed (10 tests)"; [ "$F" -eq 0 ] && exit 0 || exit 1
|
echo ""; echo " Results: $P passed, $F failed (11 tests)"; [ "$F" -eq 0 ] && exit 0 || exit 1
|
||||||
|
|
|
||||||
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