Build from source with no vendored binaries: - Remove prebuilt libzuptsdk.so / libpqvaptvupt.so (and a stray .pyc). The default build needs only a C compiler + make; it links no external library and installs no .so. The libzuptsdk-backed modes (Argon2id KDF, --pq-sdk, --pq-box) are gated behind an opt-in `make WITH_SDK=1`. The default password KDF is PBKDF2-SHA256 and --pq (native ML-KEM-768 + X25519) is the built-in PQ mode. openSUSE/RPM/deb/AUR/Homebrew/Nix recipes bumped to 4.1.0; the openSUSE spec now builds source-only (%files ships no .so, %build/%install WITH_SDK=0). Fix: multithreaded encrypted archives were unextractable on the native AEAD path. The parallel compress/decompress workers skipped the F-09 frame-preface AAD that the serial path and the archive's AAD_PREFACE flag bind into every block MAC, so each multithreaded block failed authentication. The workers now bind the preface via a shared serializer; output is byte-identical across thread counts and interoperates with single-threaded archives (also fixes `--kdf pbkdf2 -t N` in any build). Security hardening (crafted-archive memory safety + crypto): - LZH raw code-length stack overflow + huff_lut OOB write - overflow-safe bounds in parse_index and solid-mode extract (heap OOB read) - SEQ decoder safe-zone heap overflow (litlen+matchlen reserve) - require the per-block ENCRYPTED flag on encrypted archives (plaintext forgery) - cap archive-supplied PBKDF2 iteration count (KDF-amplification DoS) - non-elidable secret wipe in the SDK path; restored disk images created 0600 Docs: remove AUDIT.md / BENCHMARKS.md / ROADMAP.md; trim marketing/AI-styled text and correct KDF/PQ facts across README, SECURITY, INSTALL, DISTRIBUTION, THREAT_MODEL, THIRD-PARTY-NOTICES, the man page, and packaging READMEs. Wire format v1.6 unchanged.
271 lines
12 KiB
Markdown
271 lines
12 KiB
Markdown
# Security Policy — VaptVupt 4.1.0
|
|
|
|
## Reporting Vulnerabilities
|
|
|
|
Report privately by email to **zupt@riseup.net** with `[security]` in the
|
|
subject. Do not open a public issue on the project's git server.
|
|
|
|
Include:
|
|
|
|
- Version (`vaptvupt --version`) and platform.
|
|
- Description, impact assessment, and a reproduction (a minimal archive or
|
|
a code snippet).
|
|
|
|
Disclosure SLA: acknowledgement within 5 business days; target fix within
|
|
30 days for high-severity issues. Coordinated disclosure preferred; the
|
|
timeline is discussed case by case. A PGP key is on the project's
|
|
keyserver entry.
|
|
|
|
The project has not had an external independent audit. For high-stakes
|
|
deployments, treat it as "reviewed but unaudited" and do your own review.
|
|
|
|
---
|
|
|
|
## Encryption Modes
|
|
|
|
| Mode | CLI Flag | Algorithm | PQ-Safe? | Use Case |
|
|
|------|----------|-----------|----------|----------|
|
|
| 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 |
|
|
| None | (default) | No encryption (compression only) | N/A | Non-sensitive data |
|
|
|
|
Password mode (`-p`) is not quantum-safe. For protection against "harvest
|
|
now, decrypt later" quantum attacks, use `--pq` — the recommended
|
|
post-quantum mode. `--pq` is native and in-tree; it needs no external
|
|
library.
|
|
|
|
Optional SDK modes (`--pq-sdk`, `--pq-box`) are available only in an
|
|
upstream `make WITH_SDK=1` build linked against the separately distributed
|
|
libzuptsdk / libpqvaptvupt libraries. They are not part of the default
|
|
build and are not defaults.
|
|
|
|
---
|
|
|
|
## Cryptographic Algorithms
|
|
|
|
| Component | Algorithm | Standard | Key Size | Security Level |
|
|
|-----------|-----------|----------|----------|---------------|
|
|
| Symmetric encryption | AES-256-CTR | FIPS 197 | 256-bit | 128-bit post-quantum (Grover) |
|
|
| Authentication | HMAC-SHA256 | RFC 2104 | 256-bit | 128-bit post-quantum (Grover) |
|
|
| Password KDF (default) | PBKDF2-SHA256 | RFC 8018 | 600K iterations | Password-dependent |
|
|
| 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 |
|
|
| Classical KEM | X25519 | RFC 7748 | 32B scalar | ~128-bit classical |
|
|
| Hybrid KDF | SHA3-512 | FIPS 202 | 512-bit output | Secure if either KEM holds |
|
|
| Integrity | XXH64 | xxHash spec | 64-bit checksum | Non-cryptographic |
|
|
| Hashing | SHA3-256, SHA3-512 | FIPS 202 | 256/512-bit | Standard |
|
|
| Random | OS CSPRNG | getrandom(2) / RtlGenRandom | N/A | Hard fail if unavailable |
|
|
|
|
The default build uses PBKDF2-SHA256 (600k iterations) for password mode.
|
|
Argon2id is available only in a `make WITH_SDK=1` build.
|
|
|
|
---
|
|
|
|
## Security Architecture
|
|
|
|
### Per-Block Authenticated Encryption
|
|
|
|
```
|
|
For each data block (sequence 0, 1, 2, ...):
|
|
|
|
nonce = base_nonce XOR pad_le(block_seq, 8) [16 bytes]
|
|
ciphertext = AES-256-CTR(enc_key, nonce, plaintext)
|
|
mac = HMAC-SHA256(mac_key, nonce ‖ ciphertext) [32 bytes]
|
|
stored = nonce ‖ ciphertext ‖ mac
|
|
```
|
|
|
|
### Encrypt-then-MAC
|
|
|
|
HMAC is computed over `nonce ‖ ciphertext` and verified **before** any
|
|
decryption. This prevents:
|
|
|
|
- Chosen-ciphertext attacks
|
|
- Padding oracle attacks
|
|
- Processing of tampered data
|
|
|
|
### Hybrid Post-Quantum KEM (`--pq`)
|
|
|
|
```
|
|
Encapsulation:
|
|
ML-KEM-768.Encaps(pk) → ml_ct[1088], ml_ss[32]
|
|
eph_sk ← CSPRNG(32)
|
|
eph_pk = X25519(eph_sk, basepoint)
|
|
x25519_ss = X25519(eph_sk, recipient_pk)
|
|
hybrid_ikm = ml_ss XOR x25519_ss
|
|
archive_key = SHA3-512(hybrid_ikm ‖ ml_ct ‖ eph_pk ‖ "ZUPT-HYBRID-v1")
|
|
enc_key = archive_key[0:32]
|
|
mac_key = archive_key[32:64]
|
|
```
|
|
|
|
Security model: secure if EITHER ML-KEM-768 (post-quantum, NIST Level 3)
|
|
OR X25519 (classical, ~128-bit) remains unbroken. Both must be compromised
|
|
simultaneously to recover the archive key. Same approach as Signal
|
|
(PQXDH), Apple iMessage (PQ3), and OpenSSH 9.0+.
|
|
|
|
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),
|
|
anti-fault double decapsulation, and XChaCha20-Poly1305 AEAD.
|
|
|
|
---
|
|
|
|
## Constant-Time Guarantees
|
|
|
|
### Jasmin-Verified (assembly linked into binary)
|
|
|
|
| Function | Purpose | Proof |
|
|
|----------|---------|-------|
|
|
| `zupt_mac_verify_ct` | HMAC comparison (32 bytes) | Jasmin type system: no branch on diff value |
|
|
| `zupt_ct_select_32` | ML-KEM FO implicit rejection | Jasmin type system: no branch on cond value |
|
|
|
|
These functions are compiled from Jasmin source to x86-64 assembly. The
|
|
Jasmin compiler enforces that no secret-typed variable flows into branch
|
|
conditions or memory addresses. This guarantee holds at the machine code
|
|
level — no C compiler optimization can introduce timing leaks.
|
|
|
|
### C Constant-Time (branchless, compiler-dependent)
|
|
|
|
| Function | Method | Risk |
|
|
|----------|--------|------|
|
|
| X25519 `fe_cswap` | Masked XOR (`mask & (a ^ b)`) | Low — branchless but compiler may optimize |
|
|
| ML-KEM NTT/basemul | Montgomery reduction (no branches) | Low |
|
|
| ML-KEM CBD sampling | Bitwise operations only | Low |
|
|
| Key wipe (`zupt_secure_wipe`) | `explicit_bzero` / volatile | Low |
|
|
|
|
### NOT Constant-Time (documented risks)
|
|
|
|
| Function | Risk | Mitigation |
|
|
|----------|------|------------|
|
|
| AES-256 block encrypt | HIGH on shared hardware — S-box table lookups leak via cache timing | Jasmin AES-NI path planned; do not use on multi-tenant VMs |
|
|
| SHA-256 | Low — table constants are public, not indexed by secret data | Accepted |
|
|
|
|
---
|
|
|
|
## Threat Model
|
|
|
|
### What VaptVupt Protects
|
|
|
|
| Asset | Protection |
|
|
|-------|-----------|
|
|
| File contents | AES-256-CTR encryption |
|
|
| File names, sizes, structure | Encrypted in central index block, HMAC-protected |
|
|
| Archive integrity (payloads + index) | Per-block HMAC-SHA256 |
|
|
| Archive integrity (header + footer metadata) | v1.5+ archives: 32-byte archive-integrity-trailer HMAC-SHA256 over `hdr ‖ ft[0..23]`. v1.4 archives: not covered, downgrade warning on extract. |
|
|
| Against stolen backups | AES-256 requires key/password to read |
|
|
| Against tampering of file contents, names, sizes, offsets | HMAC detects any modification |
|
|
| Against tampering of per-block frame preface bytes (codec_id, block_flags, varints, plaintext-XXH64) | v1.6: per-block MAC binds the canonical preface AAD; encryption-header block validated structurally |
|
|
| Against tampering of archive comment (when present) | Comment block goes through the same per-block AEAD pipeline as data (AES-256-CTR + HMAC-SHA256 + preface AAD); `hdr.comment_offset` pointer is in the AIT-signed region |
|
|
| Against block-swap (reorder) attacks | MAC binds an 8-byte position AAD; a block moved to another position fails verification and its partial output is unlinked. Dedup refs use sentinel seq=0 and rely on plaintext XXH64 for per-block integrity. |
|
|
| Against malicious archive entries (Zip Slip / path traversal) | `zupt_path_is_safe()` rejects `..`, absolute paths, Windows drive/UNC paths, embedded NULs |
|
|
| Against symlink at extract target (TOCTOU) | `zupt_safe_fopen_output()` uses `O_NOFOLLOW` on POSIX. Windows relies on directory ACLs (documented limitation). |
|
|
| Against quantum adversary | `--pq` mode: ML-KEM-768 (NIST Level 3) hybridized with X25519 |
|
|
|
|
The wire/on-disk format is v1.6. See CHANGELOG.md for the per-release
|
|
finding history behind these protections.
|
|
|
|
### What VaptVupt Does NOT Protect Against
|
|
|
|
| Threat | Reason | Mitigation Path |
|
|
|--------|--------|----------------|
|
|
| Attacker who knows the password or has the private key | Fundamental to encryption | Use strong passwords (12+ chars); protect key files |
|
|
| Endpoint compromise (keylogger, malware on the host) | Outside the archive's trust boundary | Secure the machine where you type the password or hold the key |
|
|
| Cache-timing side channels (C AES) | Table-based S-box lookups | Build with Jasmin AES-NI when available; avoid multi-tenant VMs |
|
|
| Memory forensics during operation | Keys on stack during compress/extract | `zupt_secure_wipe()` on completion; `mlock()` planned |
|
|
| Deniability | Archive header identifies format | `.zupt` magic bytes visible; ENCRYPTED flag in header |
|
|
| Weak passwords | PBKDF2-SHA256 (600k) is the default KDF; Argon2id (memory-hard) is available in a WITH_SDK=1 build | Use `--pq` mode for critical data — keys are random, not derived from a password |
|
|
| Traffic analysis / metadata | Archive size reveals data volume; file list, sizes, mtimes not padded | Outside VaptVupt's scope |
|
|
| File permission/ownership | Not stored in archive | Documented in README.md |
|
|
| Spectre-class side channels in callers | Below the constant-time primitive layer | Host OS / compiler mitigations |
|
|
|
|
### Quantum Threat Analysis
|
|
|
|
Scenario: adversary captures an encrypted archive today, stores it, and
|
|
attempts decryption when a cryptographically-relevant quantum computer is
|
|
available.
|
|
|
|
| Mode | Classical Security | Quantum Security | Verdict |
|
|
|------|-------------------|-----------------|---------|
|
|
| Password (`-p`) | Password-dependent + 256-bit AES | ~128-bit (Grover on AES), PBKDF2 accelerated | Vulnerable — use `--pq` |
|
|
| PQ Hybrid (`--pq`) | ~128-bit (X25519) | NIST Level 3 (ML-KEM-768) | Protected |
|
|
|
|
In `--pq` mode: even if Shor's algorithm breaks X25519, ML-KEM-768
|
|
protects the archive; even if a novel classical attack breaks ML-KEM,
|
|
X25519 still provides ~128-bit security. The hybrid design is secure if
|
|
either component holds.
|
|
|
|
### Extracting untrusted archives — operational guidance
|
|
|
|
The in-binary defenses are the primary control; the following are defense
|
|
in depth:
|
|
|
|
1. Extract into a dedicated empty directory (not `~/Downloads` or `/tmp`).
|
|
2. Audit symlinks in the target directory before extraction.
|
|
3. Run extraction as a low-privilege user, never root.
|
|
4. On Windows, pre-create the target directory with restrictive ACLs
|
|
(the `O_NOFOLLOW` defense is POSIX-only).
|
|
|
|
### Out of scope
|
|
|
|
- External independent audit.
|
|
- Side-channel testing on production hardware (timing leaks).
|
|
- Formal verification beyond the Jasmin constant-time primitives.
|
|
|
|
---
|
|
|
|
## CSPRNG Policy
|
|
|
|
| Platform | Primary Source | Fallback | Failure Mode |
|
|
|----------|---------------|----------|--------------|
|
|
| Linux | `getrandom(2)` | `/dev/urandom` | Hard exit — no encryption without CSPRNG |
|
|
| macOS | `/dev/urandom` | None | Hard exit |
|
|
| Windows | `RtlGenRandom` | None | Hard exit |
|
|
|
|
There is no `rand()`, `srand()`, or any weak PRNG fallback anywhere in the
|
|
codebase. If the OS CSPRNG is unavailable, VaptVupt exits with an error.
|
|
This is a deliberate design choice — weak random keys are worse than no
|
|
encryption.
|
|
|
|
---
|
|
|
|
## Supported Platforms
|
|
|
|
| Platform | Compiler | Threading | CSPRNG | Status |
|
|
|----------|----------|-----------|--------|--------|
|
|
| Linux x86-64 | GCC 5+ / Clang 3.5+ | pthreads | `getrandom(2)` | Primary |
|
|
| Linux ARM64 | GCC 5+ | pthreads | `getrandom(2)` | Tested |
|
|
| macOS x86-64/ARM64 | Apple Clang | pthreads | `/dev/urandom` | Tested |
|
|
| Windows x86-64 | MinGW / MSVC 2015+ | Win32 threads | `RtlGenRandom` | Tested |
|
|
| FreeBSD | GCC / Clang | pthreads | `/dev/urandom` | Untested (expected to work) |
|
|
|
|
---
|
|
|
|
## Verification Commands
|
|
|
|
Anyone can verify the security claims. The default build needs only a C
|
|
compiler + make (plus libm/pthread); no external crypto library.
|
|
|
|
```bash
|
|
# Build
|
|
make
|
|
|
|
# Functional tests
|
|
make test-all
|
|
|
|
# Memory safety
|
|
make test-asan
|
|
|
|
# NIST/RFC test vectors
|
|
make test-vectors && ./test_vectors
|
|
|
|
# Verify Jasmin symbols are active
|
|
nm vaptvupt | grep "zupt_mac_verify_ct\|zupt_ct_select_32"
|
|
# Expected: T zupt_mac_verify_ct
|
|
# T zupt_ct_select_32
|
|
|
|
# Verify Jasmin compilation (requires jasminc)
|
|
jasminc -arch x86-64 -o /dev/null jasmin/zupt_mac_verify.jazz
|
|
jasminc -arch x86-64 -o /dev/null jasmin/zupt_mlkem_select.jazz
|
|
```
|
|
|
|
---
|
|
|
|
© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later (dual-licensed AGPL + commercial)
|