zupt/SECURITY.md
Cristian Cezar Moisés 544a2cd647
Some checks failed
CI / build-and-test (clang) (push) Has been cancelled
CI / build-and-test (gcc) (push) Has been cancelled
CI / strict-warnings (clang, -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -O2 -std=c11 -Werror) (push) Has been cancelled
CI / strict-warnings (gcc, -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -Wformat-security -Wlogical-op -Wjump-misses-init -Wdouble-promotion -O2 -std=c11 -Werror) (push) Has been cancelled
CI / sanitizers (push) Has been cancelled
CI / pie-hardening (push) Has been cancelled
CI / cross-aarch64 (push) Has been cancelled
CI / dist-reproducibility (push) Has been cancelled
CI / packaging-syntax (push) Has been cancelled
CI / release (push) Has been cancelled
v4.0.0: codec 2.60.4 security release, --pq-box sealed-box mode, F-16 fix
Major release. Highlights:

- Codec: vendored VaptVupt codec moves to canonical 2.60.4 security
  release. Fixes a high-severity OOB heap write in the AVX2 decode fast
  path (reachable on a valid stream sized to exactly content_size, both
  tail variants). Brings CBMC-formally-verified BCJ filters with
  automatic ELF/PE/Mach-O detection. Compressed output stays
  byte-identical (ratio gate Δ 0.00%); wire format unchanged at v1.6.
- New --pq-box sealed-box recipient mode (vendored libpqvaptvupt 0.6.0):
  ML-KEM-768 + X25519 combined via HKDF-SHA256 with domain separation,
  AES-256-CTR + HMAC-SHA256 EtM. Legacy --pq and --pq-sdk stay readable.
- F-16: discloses and fixes a pre-existing data-loss defect in the
  <= 3.8.0 in-tree BCJ encoder. Full back-compat matrix decodes
  byte-exact under 4.0.0; every readable pre-4.0 archive remains readable.

Repository hygiene:
- Sync full 4.0.0 source tree (codec, crypto, SDK, GUI, packaging, tests).
- Remove internal scratch files (PROMPT.md, FORMAL_AUDIT_PROMPT.md)
  and superseded version-specific docs (INTEGRATION_PROTOCOL_2.60.4.md,
  docs/FINDINGS-2.x.md) and a stray test binary.
- Refresh README download/install section to real 4.0.0 release assets;
  bump version badge to 4.0.0.
- Add .gitignore for build outputs (keeps vendored prebuilt libraries).
2026-06-10 18:48:58 -03:00

386 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Security Policy — Zupt
## Reporting Vulnerabilities
**Be free to report vulnerabilities. For high-risk send an email.**
Email: **zupt@riseup.net**
Include: description, reproduction steps, impact assessment.
Response within 48 hours. Fix within 30 days for critical issues.
---
## 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` mode.
---
## 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 | PBKDF2-SHA256 | RFC 8018 | 600K iterations | Password-dependent |
| 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 |
---
## 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
```
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+.
---
## 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 Zupt 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 (v2.2.5+ verifier — see CHANGELOG F-06) |
| 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 (see F-08 / CHANGELOG 2.3.0). |
| Against stolen backups | AES-256 requires key/password to read |
| Against tampering of file contents, names, sizes, offsets | HMAC detects any modification (overwhelming probability after F-06 fix) |
| Against tampering of header+footer metadata | **v1.5: top-MAC detects tamper.** v1.4: not detected (legacy; re-archive with v2.3.0+ to upgrade). |
| Against tampering of per-block frame preface bytes (codec_id, block_flags, varints, plaintext-XXH64) | **v1.6 (F-09)**: per-block MAC binds the canonical preface AAD; encryption-header block validated structurally. v1.5 and older: partial detection only (parser/decoder rejects malformed values; explicit MAC coverage was v1.6 work). |
| Against tampering of archive comment (when present) | v2.4.3 (F-12): 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. Both payload and pointer are MAC-covered end-to-end. |
| Against quantum adversary | `--pq` mode: ML-KEM-768 (NIST Level 3) |
### What Zupt 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 |
| Cache-timing side channels (C AES) | Table-based S-box lookups | Build with Jasmin AES-NI when available |
| 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 | Argon2id (default, v2.4.1+) is memory-hard and adds ~2530 bits of work factor vs ~20 for PBKDF2. PBKDF2-SHA256 with 600k iterations available via `--kdf pbkdf2` for legacy reader compatibility. | Use `--pq` or `--pq-sdk` mode for critical data — keys are random, not derived from a password. |
| Traffic analysis | Archive size reveals data volume | Outside Zupt's scope |
| File permission/ownership | Not stored in archive | Documented in README.md (Architecture & platform support) |
### Quantum Threat Analysis
**Scenario:** Adversary captures 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) but 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 ensures the archive is secure if **either** component holds.
---
## 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, Zupt 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) |
---
## Disclosure Timeline
| Date | Event |
|------|-------|
| 2026-01-01 | v0.1.0 — Initial release |
| 2026-03-21 | v0.5.1 — 16 security bug fixes including CSPRNG hardening |
| 2026-03-21 | v1.0.0 — 5 critical ML-KEM bugs fixed, format frozen |
| 2026-03-28 | v1.1.0 — X25519 formula bug fixed (not interoperable with RFC 7748) |
| 2026-03-28 | v1.5.0 — Jasmin assembly linked (MAC verify + ML-KEM select) |
---
## Verification Commands
Anyone can verify every security claim:
```bash
# Build
make # Zero warnings
# All functional tests
make test-all # 62/62 pass
# Memory safety
make test-asan # Zero ASAN/UBSAN errors
# NIST/RFC test vectors
make test-vectors && ./test_vectors # 13/13 pass
# Verify Jasmin symbols are active
nm zupt | 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
## Production deployment notes (v2.2.1)
Zupt is deployed in production environments. The following supported
configurations are considered current and receive security fixes:
| Channel | Supported | Notes |
|---|---|---|
| 2.2.x (latest) | Yes | Recommended for new deployments |
| 2.1.x | Yes (security only) | Supported through 2026-Q4 |
| 2.0.x | No | End of life |
| 1.x | No | End of life |
### Recommended configuration
For new archives, use the libzuptsdk-backed mode:
```bash
zupt keygen --sdk -o key.priv
zupt c --pq-sdk key.priv.pub backup.zupt /path/to/data
zupt x --pq-sdk key.priv backup.zupt
```
This selects:
- ML-KEM-768 + X25519 hybrid KEM with HKDF-SHA3-256 combiner (RFC-style
KDF rather than ad-hoc XOR construction)
- 32-byte HKDF-derived key commitment tag (protects against partitioning
oracle attacks across recipients)
- HPKE-style context binding (RFC 9180 §5)
- Anti-fault double ML-KEM decapsulation
- XChaCha20-Poly1305 AEAD with 24-byte random nonces
- Argon2id (RFC 9106 OWASP minimums) when password mode is used
### Threat model
Zupt assumes:
- The recipient's private key file is kept secret and is not exfiltrated.
- The execution environment has a working `getrandom(2)` / `/dev/urandom`.
- The archive metadata (file list, sizes, mtimes) is not considered
confidential. Padding to hide file sizes is not implemented.
- An attacker may have full write access to the archive in transit; AEAD
+ commitment + HPKE binding ensures any modification is detected.
Zupt does **not** defend against:
- Endpoint compromise (keylogger, malware on the machine where you type
the password or hold the private key).
- Side-channel attacks against the host OS that bypass the constant-time
Jasmin-verified primitives (e.g. Spectre v1 in callers).
- Quantum attacks against X25519 alone — but the ML-KEM-768 component
guarantees post-quantum security via the hybrid KDF.
### Reporting findings
If you find a security issue:
1. **Do not** open a public issue on the project's git server.
2. Email `zupt@riseup.net` with subject `SECURITY: <brief>`.
3. Include the version (`zupt --version`), platform, and a
reproduction (a minimal archive or a code snippet).
4. Expect acknowledgement within 7 days. Coordinated disclosure
timeline will be discussed case by case.
### Disclosure history
| Date | Version | Findings | Severity |
|---|---|---|---|
| 2026-04-27 | 2.2.1 | 6 internally-found bugs (audit pass) | 2 high, 1 medium, 3 low |
---
## v2.2.1 audit findings
The 2.2.1 release fixed six bugs found by code review and added a 10-check
double-validated audit test suite. Detailed root-cause analysis for each
finding is in `CHANGELOG.md` under the 2.2.1 entry.
| # | Severity | Component | Bug |
|---|---|---|---|
| 1 | Low (correctness) | format parser | varint reader truncated values at 2^63 |
| 2 | Medium (data loss) | extract path | unchecked `fwrite` in 6 call sites — silent corruption on disk-full |
| 3 | Low (defense-in-depth) | SDK keyring | `mac_key` was a copy of `enc_key` rather than KDF-derived |
| 4 | High (memory safety) | LZ decoder | `size_t` overflow in length accumulator could enable out-of-bounds copy |
| 5 | Medium (DoS / amplification) | dedup ref blocks | unbounded forward offset + recursion accepted |
| 6 | Low (UX) | encrypt path | partial archive left on disk after encrypt-init failure |
All six fixed in 2.2.1. Regression tests added.
## Reporting vulnerabilities
Email `zupt@riseup.net` with `[security]` in the subject. PGP key on the
project's keyserver entry. Coordinated disclosure preferred; we will
acknowledge within 5 business days and aim for a fix within 30 days for
high-severity issues.
The project does not yet have an external audit. The 2.2.1 audit pass was
internal code review combined with the 169-check libzuptsdk audit suite
inherited via vendored linkage. For high-stakes deployments, treat this as
"reviewed but unaudited" and do your own review.
---
## v2.2.2 formal audit findings (2026-04-27)
A formal cryptographic audit pass was conducted using the methodology
documented in `FORMAL_AUDIT_PROMPT.md` (auditor profile: senior
cryptographic engineer with 15+ years of production crypto systems
experience). Two security-relevant bugs and two robustness bugs found
and fixed; version unchanged at 2.2.2 — same release with hardened
internals.
| # | Severity | Component | Bug |
|---|---|---|---|
| 11 | **HIGH** | extract path | Zip Slip / path traversal — `e->path` from archive used directly in `fopen` |
| 12 | **MEDIUM** | extract output | symlink-following — `fopen "wb"` followed symlinks at output target |
| 13 | LOW (32-bit only) | size cap | 4 GiB cap exceeds `size_t` on 32-bit |
| 14 | LOW (32-bit only) | calloc on parsed count | `count * sizeof(entry)` overflowed `size_t` before calloc internal check |
All four fixed and regression-tested.
### Threat model coverage (post-audit)
The following attack vectors are now explicitly defended against:
- **Malicious archive with path-traversal entries** (Zip Slip 2018 pattern):
rejected by `zupt_path_is_safe()` — blocks `..`, absolute paths, Windows
drive letters, UNC paths, embedded NULs.
- **Symlink at extract target** (TOCTOU pre-extraction): refused by
`zupt_safe_fopen_output()` using `O_NOFOLLOW` on POSIX. Windows path
unchanged — relies on directory ACLs (documented limitation).
- **Malformed archive headers**: bounds-checked offsets (`encryption_header_off`,
`index_offset`); rejected if outside file size.
- **Format parser overflow**: varint truncation, dedup-ref recursion,
realloc-pair atomicity, length-overflow in LZ decoder — all fixed in
prior 2.2.x sprints.
- **Cryptographic key reuse / nonce misuse**: per-block nonce is `base ⊕
block_seq`; `base_nonce` is per-archive random; mac_key is KDF-split
from enc_key (defense in depth even though SDK path doesn't use it).
- **Block-swap (reorder) attack on encrypted archives** (bug #16, fixed
in 2.2.2 god-tier audit): MAC binds 8-byte AAD seq computed as
`((file_index_in_archive + 1) << 32) | per_file_block_seq`. An attacker
who swaps two valid encrypted blocks between positions in the archive
produces blocks whose AAD no longer matches their position; both MAC
candidates (v2 with AAD, v1 legacy fallback) reject the swapped block.
Empty/partial output files are `unlink()`'d on auth failure.
Limitation: dedup mode uses sentinel seq=0 (refs can't derive source
AAD); plaintext XXH64 still provides per-block integrity.
### Path traversal — operational guidance
Even with the in-binary defenses, operators extracting untrusted archives
should:
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).
These are belt-and-suspenders — the in-binary defenses are the primary
control, but defense in depth is good practice.
### Out-of-scope (still)
- External independent audit (cost-bound, on roadmap)
- Side-channel testing on production hardware (timing leaks)
- Formal verification beyond Jasmin constant-time primitives