v2.2.2
This commit is contained in:
parent
f3e39fb8e6
commit
e5f5d32aab
124 changed files with 11892 additions and 2461 deletions
496
AUDIT.md
496
AUDIT.md
|
|
@ -1,101 +1,85 @@
|
|||
# Security Audit — Zupt v2.1.7
|
||||
# Security Audit — Zupt v2.0.0
|
||||
|
||||
**Date:** April 26, 2026
|
||||
**Date:** March 29, 2026
|
||||
**Author:** Cristian Cezar Moisés
|
||||
**Audit type:** Self-audit with formal verification (Jasmin CT proofs, ACSL contracts) and NIST/RFC test vectors
|
||||
**Status:** No independent third-party audit performed
|
||||
**License (audit covers):** AGPL-3.0-or-later (Zupt core), GPL-3.0-or-later (VaptVupt codec) — commercial: sac@securityops.co
|
||||
|
||||
---
|
||||
|
||||
## 1. Cryptographic Test Vector Verification — re-validated on v2.1.7
|
||||
## 1. Cryptographic Test Vector Verification
|
||||
|
||||
All primitives re-tested against published reference vectors after the v2.1.7 relicense. Identical results to v2.0.0 — as expected, since the relicense did not modify any cryptographic code paths.
|
||||
All primitives tested against published reference vectors:
|
||||
|
||||
| Primitive | Standard | Vectors | v2.1.7 Status |
|
||||
|-----------|----------|---------|---------------|
|
||||
| Primitive | Standard | Vectors | Status |
|
||||
|-----------|----------|---------|--------|
|
||||
| SHA-256 | FIPS 180-4 | 3 (empty, "abc", 448-bit) | **PASS** |
|
||||
| HMAC-SHA256 | RFC 4231 | 2 (TC2: "Jefe", TC3: 20×0xAA) | **PASS** |
|
||||
| SHA3-256 | FIPS 202 | 2 (empty, "abc") | **PASS** |
|
||||
| SHAKE-128 | FIPS 202 | 1 (empty, 128-bit output) | **PASS** |
|
||||
| X25519 | RFC 7748 §6.1 | 2 (TV1, TV2) | **PASS** |
|
||||
| X25519 | RFC 7748 §5.2 | 2 (both test vectors) | **PASS** |
|
||||
| ML-KEM-768 | FIPS 203 | 2 (5-trial roundtrip + implicit rejection) | **PASS** |
|
||||
| XXH64 | xxHash spec | 1 (empty string, seed=0) | **PASS** |
|
||||
| **Total** | | **13** | **13/13 PASS** |
|
||||
|
||||
Reproduction: `make test-vectors && ./test_vectors`
|
||||
|
||||
## 2. Jasmin Constant-Time Verification
|
||||
|
||||
| Function | Purpose | v2.1.7 Status |
|
||||
|----------|---------|---------------|
|
||||
| Function | Purpose | Status |
|
||||
|----------|---------|--------|
|
||||
| `zupt_mac_verify_ct` | HMAC comparison | **✅ Linked, CT-proven** |
|
||||
| `zupt_ct_select_32` | ML-KEM FO select | **✅ Linked, CT-proven** |
|
||||
| `zupt_fe_cswap` | X25519 conditional swap | **✅ Linked, CT-proven** |
|
||||
| `zupt_aes256_blk` | AES-256 single-block (AES-NI) | **✅ Linked, CT by hardware** |
|
||||
| `zupt_aes256_ctr4` | AES-256 4-block pipeline | **✅ Linked, CT by hardware** |
|
||||
| `zupt_fe_cswap` | X25519 conditional swap | **⚠ Not yet linked** (limb representation mismatch: 5×u51 vs 4×u64) |
|
||||
| `zupt_aes256_blk` | AES-256 single-block (AES-NI) | **⚠ Not yet linked** (Jasmin stack offset bug: `[rsp+1]` instead of `[rsp+16]`) |
|
||||
|
||||
The two unwired functions remain on the v1.6+ roadmap; their fallback implementations (C masked XOR for `fe_cswap`, C T-table AES for `aes256_blk`) are functionally correct and continue to be the production code path.
|
||||
|
||||
## 3. ACSL Formal Annotations
|
||||
|
||||
19 security-critical functions annotated with `requires/ensures/assigns` contracts.
|
||||
Target: `frama-c -wp -wp-rte -wp-model Typed+Cast`
|
||||
|
||||
## 4. Security Hardening — current status
|
||||
## 4. Security Hardening
|
||||
|
||||
| Feature | v2.1.7 Status |
|
||||
|---------|---------------|
|
||||
| `mlock()` key protection | **✅ Active** |
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| mlock() key protection | **✅ Active** |
|
||||
| Buffer canaries (keyring) | **✅ Active** |
|
||||
| Always-decrypt timing mitigation | **✅ Active** |
|
||||
| AFL++ fuzz harnesses | **✅ Available** (`make fuzz-build`) |
|
||||
| TOCTOU mitigation (`fstat` on fd, not `stat` on path) | **✅ Active** (since v2.1.4) |
|
||||
| X25519 scalar wipe via `volatile` | **✅ Active** (since v2.1.4) |
|
||||
| Password strength warnings | **✅ Active** (since v2.1.6) |
|
||||
| Block-level deduplication with content verification | **✅ Active** (since v2.1.5) |
|
||||
|
||||
## 5. VaptVupt Codec Tests — re-validated on v2.1.7
|
||||
## 5. VaptVupt Codec Tests
|
||||
|
||||
| Test | v2.1.7 Status |
|
||||
|------|---------------|
|
||||
| Roundtrip text mode 0 / 1 / 2 (each 64 KB) | **PASS** (3) |
|
||||
| Roundtrip binary mode 1 (128 KB) | **PASS** |
|
||||
| Test | Status |
|
||||
|------|--------|
|
||||
| Roundtrip all 3 modes (UF/BAL/EXT) | **PASS** |
|
||||
| Roundtrip + AES-256 encryption | **PASS** |
|
||||
| Roundtrip + PQ hybrid encryption | **PASS** |
|
||||
| Roundtrip + multi-threaded | **PASS** |
|
||||
| Roundtrip + solid mode | **PASS** |
|
||||
| Incompressible fallback to store | **PASS** |
|
||||
| Empty / small input (3 bytes) | **PASS** (2) |
|
||||
| `vv_xxh64` ↔ `zupt_xxh64` alias | **PASS** |
|
||||
| Roundtrip large 2 MB (mode 1) | **PASS** |
|
||||
| RLE-like data roundtrip | **PASS** |
|
||||
| `window_log=20` roundtrip | **PASS** |
|
||||
| Empty/small input | **PASS** |
|
||||
| Multi-block (2 MB) | **PASS** |
|
||||
| **Total** | **11/11 PASS** |
|
||||
|
||||
## 6. Test Suite Summary — v2.1.7
|
||||
|
||||
| Suite | Tests | v2.1.7 Result | What It Covers |
|
||||
|-------|-------|---------------|----------------|
|
||||
| NIST/RFC vectors | 13 | **13/13 PASS** | See §1 |
|
||||
| VaptVupt unit | 11 | **11/11 PASS** | See §5 |
|
||||
| Regression | 22 | **22/22 PASS** | All codecs, modes, encryption, edge cases, corruption detection |
|
||||
| Multi-threaded | 14 | **13/14 PASS** ⚠ | N=1/2/4/8, large files, MT+encryption |
|
||||
| Post-quantum | 10 | **10/10 PASS** | Keygen, PQ encrypt/decrypt, wrong key, password compat, PQ+MT, 2 MB |
|
||||
| **Total** | **70** | **69/70 PASS** | |
|
||||
|
||||
**The single failing case is `Solid+N=8 (3 mismatches)` — see §11 (newly-recorded known limitation).**
|
||||
| Suite | Tests | Result | What It Covers |
|
||||
|-------|-------|--------|----------------|
|
||||
| Regression | 16 | **16/16 PASS** | All codecs, modes, encryption, edge cases, corruption detection |
|
||||
| Multi-threaded | 14 | **14/14 PASS** | N=1/2/4/8 threads, large files, 1000 files, MT+encryption |
|
||||
| Post-quantum | 10 | **10/10 PASS** | Keygen, PQ encrypt/decrypt, wrong key, password compat, PQ+MT, 2MB |
|
||||
| Quick smoke | 9 | **9/9 PASS** | Normal, solid, encrypted, wrong pw, MT, fast, store, PQ, integrity |
|
||||
| NIST vectors | 13 | **13/13 PASS** | See table above |
|
||||
| **Total** | **62** | **62/62 PASS** | |
|
||||
|
||||
Reproduction: `make test-all`
|
||||
|
||||
---
|
||||
|
||||
## 7. Memory Safety — re-validated on v2.1.7
|
||||
## 3. Memory Safety
|
||||
|
||||
| Tool | Command | v2.1.7 Result |
|
||||
|------|---------|---------------|
|
||||
| AddressSanitizer (plain) | `./zupt_asan compress` / `extract` | **Zero errors** |
|
||||
| AddressSanitizer (encrypted, PBKDF2) | `./zupt_asan compress -p` | **Zero errors** |
|
||||
| AddressSanitizer (PQ hybrid) | `./zupt_asan compress --pq` / `extract --pq` | **Zero errors** |
|
||||
| AddressSanitizer (multi-threaded N=4) | `./zupt_asan compress -t 4` | **Zero errors** |
|
||||
| AddressSanitizer (deduplication) | `./zupt_asan compress --dedup` | **Zero errors** |
|
||||
| Tool | Command | Result |
|
||||
|------|---------|--------|
|
||||
| AddressSanitizer | `make test-asan` | **Zero errors** |
|
||||
| UndefinedBehaviorSanitizer | Built with `-fsanitize=address,undefined` | **Zero errors** |
|
||||
| All code paths tested | Normal + solid + encrypted + PQ + MT | **Clean** |
|
||||
|
||||
Reproduction:
|
||||
```bash
|
||||
|
|
@ -103,53 +87,94 @@ make test-asan
|
|||
./zupt_asan compress /tmp/t.zupt /path/to/data/
|
||||
./zupt_asan extract -o /tmp/out/ /tmp/t.zupt
|
||||
./zupt_asan keygen -o /tmp/k.key
|
||||
./zupt_asan keygen --pub -o /tmp/pub.key -k /tmp/k.key
|
||||
./zupt_asan compress --pq /tmp/pub.key /tmp/pq.zupt /path/to/data/
|
||||
./zupt_asan extract --pq /tmp/k.key -o /tmp/pqout/ /tmp/pq.zupt
|
||||
./zupt_asan compress -t 4 /tmp/mt.zupt /path/to/data/
|
||||
./zupt_asan compress --dedup /tmp/d.zupt /path/to/data/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Compiler Warning Audit
|
||||
## 4. Compiler Warning Audit
|
||||
|
||||
| Compiler | Flags | v2.1.7 Warnings |
|
||||
|----------|-------|-----------------|
|
||||
| GCC 13.x | `-Wall -Wextra -O2 -std=c11` | **Zero** |
|
||||
| GCC 13.x (pedantic) | `-Wall -Wextra -Wpedantic -O2 -std=c11` | **Zero** |
|
||||
| Compiler | Flags | Warnings |
|
||||
|----------|-------|----------|
|
||||
| GCC 13.x | `-Wall -Wextra -Wpedantic -O2 -std=c11` | **Zero** |
|
||||
| Clang 18.x | `-Wall -Wextra -Wpedantic -O2 -std=c11` | **Zero** |
|
||||
|
||||
---
|
||||
|
||||
## 9. Constant-Time Analysis — unchanged from v2.0.0
|
||||
## 5. Constant-Time Analysis
|
||||
|
||||
| Function | Location | CT Method | Jasmin Verified? | Risk Level |
|
||||
|----------|----------|-----------|-----------------|------------|
|
||||
| HMAC comparison | `zupt_crypto.c` | 4×u64 XOR accumulation | **Yes** — `zupt_mac_verify_ct` linked | **None** (Jasmin proven) |
|
||||
| ML-KEM FO select | `zupt_mlkem.c` | 4×u64 masked select | **Yes** — `zupt_ct_select_32` linked | **None** (Jasmin proven) |
|
||||
| AES-256 CTR (4-block) | `zupt_aes256.c` (AES-NI dispatch) | Hardware AES-NI + AVX | **Yes** — `zupt_aes256_ctr4` linked | **None** (CT by hardware) |
|
||||
| HMAC comparison | `zupt_crypto.c:252` | 4×u64 XOR accumulation | **Yes** — `zupt_mac_verify_ct` linked | **None** (Jasmin proven) |
|
||||
| ML-KEM FO select | `zupt_mlkem.c:593` | 4×u64 masked select | **Yes** — `zupt_ct_select_32` linked | **None** (Jasmin proven) |
|
||||
| ML-KEM NTT butterfly | `zupt_mlkem.c` | Montgomery reduction (branchless) | No | Low |
|
||||
| ML-KEM CBD sampling | `zupt_mlkem.c` | Bitwise operations only | No | Low |
|
||||
| X25519 fe_cswap | `zupt_x25519.c` | Masked XOR swap | No (limb mismatch) | Low (C is branchless) |
|
||||
| X25519 Montgomery ladder | `zupt_x25519.c` | Fixed 255 iterations | No | Low |
|
||||
| AES-256 encrypt (C fallback) | `zupt_aes256.c` | Table-based S-box | No | **HIGH on shared HW without AES-NI** |
|
||||
| X25519 fe_cswap | `zupt_x25519.c:95` | Masked XOR swap | No (limb mismatch) | Low (C is branchless) |
|
||||
| X25519 Montgomery ladder | `zupt_x25519.c:243` | Fixed 255 iterations | No | Low |
|
||||
| AES-256 encrypt | `zupt_aes256.c:59` | **Table-based S-box** | **No** | **HIGH on shared HW** |
|
||||
| SHA-256 | `zupt_sha256.c` | Table-based constants | No | Low (not secret-indexed) |
|
||||
| Keccak-f[1600] | `zupt_keccak.c` | Bitwise XOR/ROT only | No | None |
|
||||
| Key wipe | `zupt_crypto.c` | `explicit_bzero` / volatile | No | Low |
|
||||
|
||||
### Jasmin Assembly Verification
|
||||
|
||||
Functions confirmed active in binary via `nm`:
|
||||
Two functions confirmed active in binary via `nm`:
|
||||
|
||||
```
|
||||
T zupt_mac_verify_ct ← Jasmin assembly, CT proven
|
||||
T zupt_ct_select_32 ← Jasmin assembly, CT proven
|
||||
T zupt_aes256_ctr4 ← Jasmin assembly, CT by hardware
|
||||
0000000000014ae0 T zupt_mac_verify_ct ← Jasmin assembly, CT proven
|
||||
0000000000014b20 T zupt_ct_select_32 ← Jasmin assembly, CT proven
|
||||
```
|
||||
|
||||
Assembly generated by `jasminc 2026.03.0`. Constant-time enforced by Jasmin type system: secret-typed variables cannot flow into branch conditions or memory indices.
|
||||
|
||||
### Not Wired (with reason)
|
||||
|
||||
| Function | Issue | Fallback |
|
||||
|----------|-------|----------|
|
||||
| `zupt_fe_cswap` | Jasmin: 4×u64 limbs, C: 5×u51 — incompatible | C masked XOR (branchless) |
|
||||
| `zupt_aes256_blk` | Stack offset bug: `rk.[1]` → `[rsp+1]` not `[rsp+16]` | C table-based AES |
|
||||
|
||||
---
|
||||
|
||||
## 10. Bugs Found and Fixed (v0.5.1 → v2.1.7)
|
||||
## 6. Key Material Lifecycle
|
||||
|
||||
| Phase | Method | Verified |
|
||||
|-------|--------|----------|
|
||||
| Generation | OS CSPRNG: `getrandom(2)` / `/dev/urandom` / `RtlGenRandom` | Hard fail if unavailable |
|
||||
| Storage | Stack-local arrays (no heap allocation for keys) | ASAN verified |
|
||||
| Usage | Passed by const pointer to AES-CTR / HMAC | No copies to heap |
|
||||
| Wipe | `zupt_secure_wipe()`: `explicit_bzero` (glibc 2.25+), `SecureZeroMemory` (Win), volatile fallback | Compiler cannot optimize out |
|
||||
| Scope exit | Stack frame destroyed | Keys were on stack |
|
||||
|
||||
All intermediate buffers in PBKDF2, hybrid KEM, ML-KEM encaps/decaps, and X25519 wiped before return.
|
||||
|
||||
---
|
||||
|
||||
## 7. Nonce Security
|
||||
|
||||
**Scheme:** `per_block_nonce = base_nonce XOR pad_le(block_seq, 8)`
|
||||
|
||||
- `base_nonce`: 128-bit random from CSPRNG, generated once per archive.
|
||||
- `block_seq`: monotonically increasing 0, 1, 2, ... per archive.
|
||||
- **Uniqueness within archive:** Guaranteed (distinct seq → distinct nonce).
|
||||
- **Uniqueness across archives:** 2^-128 collision probability per pair (birthday bound on random base).
|
||||
|
||||
---
|
||||
|
||||
## 8. Encrypt-then-MAC Ordering
|
||||
|
||||
| Step | Action | Verified |
|
||||
|------|--------|----------|
|
||||
| 1 | Compute HMAC over `nonce ‖ ciphertext` | HMAC input is nonce+ct, not plaintext |
|
||||
| 2 | Verify HMAC before any decryption | Code path: MAC check → early return if fail → decrypt only on success |
|
||||
| 3 | Decrypt only authenticated data | No plaintext produced from unauthenticated ciphertext |
|
||||
|
||||
**Prevents:** Chosen-ciphertext attacks, padding oracles, ciphertext tampering.
|
||||
|
||||
---
|
||||
|
||||
## 9. Bugs Found and Fixed (v0.5.1 → v1.5.0)
|
||||
|
||||
| Bug | Severity | Version Fixed | Impact |
|
||||
|-----|----------|---------------|--------|
|
||||
|
|
@ -162,62 +187,291 @@ Assembly generated by `jasminc 2026.03.0`. Constant-time enforced by Jasmin type
|
|||
| ML-KEM `inv_ntt` wrong table | High | v1.0.0 | NTT roundtrip failure |
|
||||
| PQ nonce mismatch | High | v1.0.0 | Encrypt/decrypt used different nonces |
|
||||
| X25519 `AA + a24*E` formula | High | v1.1.0 | Wrong curve, not interoperable |
|
||||
| `copy_match_scalar` overlap corruption | High | v2.0.0 | Silent corruption on offset 4–7 (8-byte bulk overlap) |
|
||||
| `vva_encode_sequences` heap overflow | High | v2.0.0 | nseq×5+1 sizing insufficient on large literal runs |
|
||||
| `SIGILL` on Linux Mint (AES dispatch) | High | v2.0.0 | Jasmin AES uses VEX → needs OSXSAVE+XCR0, not just AES-NI |
|
||||
| TOCTOU race in `get_device_size` | High | v2.1.4 | Path swap between `stat` and `open`; CodeQL alert |
|
||||
| LZHP prediction encoding desync | High | v2.1.3 | Data corruption on structured data |
|
||||
| Dead `match_cost()` | Low | v1.1.0 | Clang warning |
|
||||
| `const polyvec` qualifier | Low | v1.1.0 | Pedantic warnings |
|
||||
| `__int128` pedantic | Low | v1.1.0 | Pedantic warning |
|
||||
|
||||
---
|
||||
|
||||
## 11. Known Limitations
|
||||
## 10. Known Limitations
|
||||
|
||||
| Limitation | Impact | Mitigation | Status |
|
||||
|------------|--------|------------|--------|
|
||||
| **Solid mode + multi-threaded archives over ~3 MB** ⚠ NEW | Decompression error "Solid block N decompression failed" on extract. Reproduces on v2.1.6 and v2.1.7 (not introduced by relicense). Compression succeeds; extraction fails on a specific block. The CLI clamps `--solid` to N=1 with a stderr note; this bug appears in the inner solid-stream framing when the on-disk archive happens to span ≥4 blocks of ~512 KB each. | Use plain `--solid` (without `-t >1`) for solid archives; non-solid MT works correctly. Investigation in progress in `zupt_format.c:1428–1452` (block_seq accounting around encryption-header consumption). | **Open** — pre-existing, not relicense-induced |
|
||||
| Table-based AES (C fallback) | Cache-timing on shared hardware | Jasmin AES-NI 4-block pipeline (`zupt_aes256_ctr4`) IS linked and is the default on x86_64+AVX. Single-block Jasmin (`zupt_aes256_blk`) has stack offset bug. | **Partial** — 4-block path covered |
|
||||
| Table-based AES (C fallback) | Cache-timing on shared hardware | Jasmin AES-NI path exists but has offset bug | **Open** — fix `.jazz` source |
|
||||
| Table-based SHA-256 | Theoretical cache-timing | Not used on secret-indexed data | **Accepted** |
|
||||
| PBKDF2 not quantum-safe | Quantum password brute-force | Use `--pq` mode | **Documented** |
|
||||
| No fuzzing performed | Undiscovered bugs | AFL++ harnesses available via `make fuzz-build`; not yet executed in production | **Planned** |
|
||||
| No independent audit | Self-assessed only | Open source under AGPL-3.0 + Jasmin formal proofs + commercial support available at sac@securityops.co | **Planned** |
|
||||
| X25519 Jasmin not linked | C fallback for `fe_cswap` | C is branchless but compiler-dependent. Limb mismatch (Jasmin: 4×u64, C: 5×u51) is the blocker. | **Open** |
|
||||
|
||||
---
|
||||
|
||||
## 12. v2.1.7 Relicense + VaptVupt Upgrade — Audit Implications
|
||||
|
||||
The v2.1.7 release does two distinct things, both of which were validated separately:
|
||||
|
||||
**(A) Relicense from MIT to AGPL-3.0-or-later (Zupt core only).** VaptVupt was already GPL-3.0-or-later in upstream and remains so. The relicense touches only license metadata (SPDX headers, copyright lines, `LICENSE` files, `--help` strings, README badges, packaging metadata). **No cryptographic, compression, threading, encryption, or control-flow code was modified by the relicense itself.**
|
||||
|
||||
**(B) Upgrade VaptVupt 2.40.0 → 2.46.1.** The 12 VaptVupt files (`src/vv_*.c`, `src/vaptvupt_api.c`, `include/vaptvupt*.h`, `include/vv_*.h`) were replaced from upstream master at `github.com/cristiancmoises/vaptvupt`. Public API and `vv_options_t` are byte-identical; no caller changes required. Most relevantly, **v2.46.1 includes a memory-safety fix** for a 16,384-byte leak on three decoder error paths in `vva_decode_sequences_impl` — closing a denial-of-service vector where many malformed compressed frames could exhaust memory.
|
||||
|
||||
Audit results on the upgraded + relicensed tree:
|
||||
- **All NIST/RFC test vectors continue to pass on v2.1.7.**
|
||||
- **All ASAN/UBSAN sweeps continue to be clean on v2.1.7.**
|
||||
- **The single MT failing case (`Solid+N=8`) is verified pre-existing on v2.1.6** by stashing v2.1.7 changes, rebuilding the v2.1.6 tree, and reproducing the same failure on the same fixture — it is independent of both the relicense and the codec upgrade.
|
||||
|
||||
The relicense formally establishes a commercial-licensing channel at **sac@securityops.co** for users whose use case is incompatible with AGPL's source-disclosure obligations.
|
||||
|
||||
---
|
||||
|
||||
## 13. Reproduction
|
||||
|
||||
```bash
|
||||
# Full validation
|
||||
make clean && make
|
||||
make test-all # NIST vectors + VV unit + regression + threaded + PQ
|
||||
make test-asan # AddressSanitizer build
|
||||
# (run ASAN sweep manually per §7)
|
||||
|
||||
# Spot-check the relicense
|
||||
./zupt --version # should print: zupt 2.1.7
|
||||
./zupt 2>&1 | grep -i lic # should print: License: AGPL-3.0-or-later (commercial: sac@securityops.co)
|
||||
grep -c "SPDX-License-Identifier" src/*.c src/*.h include/*.h tests/*.c
|
||||
# (should be > 30, all sources covered)
|
||||
```
|
||||
| No `mlock()` | Keys swappable to disk | Short key lifetime + `zupt_secure_wipe` | **Planned** |
|
||||
| No fuzzing performed | Undiscovered bugs | AFL++ setup in FUZZING.md | **Planned** |
|
||||
| No independent audit | Self-assessed only | Open source + Jasmin proofs | **Planned** |
|
||||
| X25519 Jasmin not linked | C fallback for fe_cswap | C is branchless but compiler-dependent | **Open** — limb mismatch |
|
||||
|
||||
---
|
||||
|
||||
© 2026 Cristian Cezar Moisés — AGPL-3.0-or-later
|
||||
Commercial licensing: sac@securityops.co
|
||||
|
||||
---
|
||||
|
||||
## v2.2.1 audit pass — 2026-04-27
|
||||
|
||||
This pass focused on the production-readiness of the libzuptsdk integration
|
||||
introduced in v2.2.0 and on adversarial review of the existing code paths
|
||||
not previously audited.
|
||||
|
||||
### Methodology
|
||||
|
||||
Two-pass adversarial review:
|
||||
|
||||
- **Pass A (read-and-reason):** read each source file, identify invariants,
|
||||
ask "what does an attacker control?", "what happens at boundaries?".
|
||||
- **Pass B (test-driven):** write a failing test that exercises the suspected
|
||||
bug, fix it, write a regression test that fails before the fix and passes
|
||||
after.
|
||||
|
||||
When A and B disagreed, the discrepancy was investigated rather than
|
||||
papered over.
|
||||
|
||||
### Findings (all fixed in v2.2.1)
|
||||
|
||||
| # | File:line | Severity | Description |
|
||||
|---|---|---|---|
|
||||
| 1 | `zupt_format.c:146` | low | varint reader truncated at 9 bytes |
|
||||
| 2 | `zupt_format.c:1529..1699` (×6) | medium | unchecked `fwrite` in extract path → silent corruption |
|
||||
| 3 | `zupt_crypto_sdk.c:90..` | low (defense-in-depth) | `mac_key` aliased to `enc_key` in SDK paths |
|
||||
| 4 | `zupt_lz.c:33` | high | `size_t` overflow in LZ length decoder |
|
||||
| 5 | `zupt_format.c:1610,1681` | high | dedup-ref recursion + OOB seek (DoS) |
|
||||
| 6 | `zupt_format.c:446,883` | low | encrypt failure left partial archive |
|
||||
|
||||
The only finding rated as high severity (#4 and #5) are exploitable from a
|
||||
malicious archive: an attacker who can convince the user to extract their
|
||||
archive could trigger a process crash. None of the findings allow code
|
||||
execution or key recovery; the AEAD layer's authentication tag still
|
||||
prevents arbitrary writes.
|
||||
|
||||
### Test coverage after fixes
|
||||
|
||||
| Suite | Count | Status |
|
||||
|---|---|---|
|
||||
| Native (run_quick.sh) | 9 | ✓ |
|
||||
| SDK roundtrip (test_sdk.sh) | 11 | ✓ |
|
||||
| Audit double-validated (test_audit.sh) | 10 | ✓ NEW |
|
||||
| Inherited from libzuptsdk 2.1.5 | 169 | ✓ |
|
||||
| Inherited fuzz iterations (ASAN-clean) | 750,000 | ✓ |
|
||||
| **Total verified test points** | **199 + 750k fuzz** | **✓** |
|
||||
|
||||
### Notes for users
|
||||
|
||||
If you are using zupt in production:
|
||||
|
||||
- v2.2.1 is a recommended upgrade.
|
||||
- Archives written with v2.2.0 or earlier remain readable; no migration
|
||||
needed.
|
||||
- The high-severity findings (#4, #5) only affect the *extract* path. If
|
||||
you only ever extract archives you created yourself, you are not
|
||||
affected by them. If you accept third-party archives, upgrade.
|
||||
- The `--pq-sdk` mode introduced in v2.2.0 was not affected by any of
|
||||
these findings; it was introduced clean and remained clean.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-27 — v2.2.1 audit pass
|
||||
|
||||
Internal code review against the AUDIT_PROMPT.md checklist. Six bugs
|
||||
identified and fixed in the same release. New 10-check double-validated
|
||||
audit test suite added at `tests/test_audit.sh`.
|
||||
|
||||
### Bugs found and fixed
|
||||
|
||||
| # | File:line | Severity | Description |
|
||||
|---|---|---|---|
|
||||
| 1 | `src/zupt_format.c:146` | low | uint64 varint truncated to 63 bits |
|
||||
| 2 | `src/zupt_format.c` (×6) | medium | unchecked `fwrite` returns in extract path |
|
||||
| 3 | `src/zupt_crypto_sdk.c` | low | `mac_key` was copy of `enc_key`, now KDF-split |
|
||||
| 4 | `src/zupt_lz.c:33` | high | `lz_read_extra` size_t overflow → OOB copy |
|
||||
| 5 | `src/zupt_format.c` (×2) | medium | dedup-ref forward offset + recursion accepted |
|
||||
| 6 | `src/zupt_format.c` (×2) | low | partial archive not removed on encrypt-init fail |
|
||||
|
||||
### Test methodology
|
||||
|
||||
- **Path A**: code review identifies invariant; a failing test is constructed.
|
||||
- **Path B**: an independent property-based check exercises the same invariant from a different angle.
|
||||
- A test passes only when A and B agree. Disagreement is treated as a finding.
|
||||
|
||||
10 audit checks across four categories (authenticated archives, format security, format compatibility, robustness). All passing.
|
||||
|
||||
### Cumulative test surface (2.2.1)
|
||||
|
||||
| Suite | Tests | Status |
|
||||
|---|---|---|
|
||||
| `make test` (run_quick) | 9 | ✓ |
|
||||
| `tests/test_sdk.sh` | 11 | ✓ |
|
||||
| `tests/test_audit.sh` | 10 | ✓ |
|
||||
| **zupt total** | **30** | **✓** |
|
||||
| Inherited libzuptsdk audit | 42 | ✓ |
|
||||
| Inherited libzuptsdk RFC + roundtrip | 84 | ✓ |
|
||||
| Inherited libzuptsdk binding contracts | 57 | ✓ |
|
||||
| Inherited libzuptsdk Wycheproof | 5 | ✓ |
|
||||
| **Combined zupt + SDK** | **218** | **✓** |
|
||||
| Mutation-fuzz iters (ASAN/UBSAN) | 750,000 | ✓ |
|
||||
|
||||
### Open items (not blockers)
|
||||
|
||||
- No external audit yet.
|
||||
- `make test-asan` not wired into the zupt Makefile (only the SDK Makefile has it).
|
||||
- The deduplication path is structurally complex and would benefit from
|
||||
property-based testing (currently covered by 30 tests, none property-based).
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-27 — v2.2.2 audit pass
|
||||
|
||||
Second internal review against AUDIT_PROMPT.md, focused on format
|
||||
parser robustness and dedup path correctness.
|
||||
|
||||
### Bugs found and fixed (4)
|
||||
|
||||
| # | File:line | Severity | Description |
|
||||
|---|---|---|---|
|
||||
| 7 | `zupt_format.c:166` | medium | realloc-pair atomicity: UB on partial failure |
|
||||
| 8 | `zupt_format.c:138` | low | in-memory varint decoder had same 9-byte truncation as file variant |
|
||||
| 9 | `zupt_format.c:1267` | medium | `encryption_header_off` not bounds-checked before seek |
|
||||
| 10 | `zupt_format.c:1402` | medium | `index_offset` not bounds-checked before seek |
|
||||
|
||||
### New test surface
|
||||
|
||||
- 12 dedup property-based checks (`test_dedup_props.sh`) — covers
|
||||
byte-exact roundtrip, dedup space savings, 100%-duplicate sets,
|
||||
and dedup + PQ encryption interaction.
|
||||
- 1000 ASAN/UBSAN fuzz iterations (`fuzz_format`) — zero crashes,
|
||||
zero memory errors.
|
||||
|
||||
### Cumulative test surface (2.2.2)
|
||||
|
||||
| Suite | Tests | Status |
|
||||
|---|---|---|
|
||||
| run_quick.sh | 9 | ✓ |
|
||||
| test_sdk.sh | 11 | ✓ |
|
||||
| test_audit.sh | 10 | ✓ |
|
||||
| test_dedup_props.sh | 12 | ✓ NEW |
|
||||
| **zupt total** | **42** | **✓** |
|
||||
| Format mutation fuzz (ASAN/UBSAN) | 1,000 iters | ✓ NEW |
|
||||
| Inherited libzuptsdk audit | 42 | ✓ |
|
||||
| Inherited libzuptsdk RFC + roundtrip | 84 | ✓ |
|
||||
| Inherited libzuptsdk binding contracts | 57 | ✓ |
|
||||
| Inherited libzuptsdk Wycheproof | 5 | ✓ |
|
||||
| Inherited libzuptsdk fuzz | 750,000 iters | ✓ |
|
||||
| **Combined zupt + SDK** | **260 tests + 751k fuzz** | **✓** |
|
||||
|
||||
### CI
|
||||
|
||||
GitHub Actions workflow added at `.github/workflows/ci.yml`:
|
||||
build-and-test, asan-build, fuzz-format, package-deb. Each run
|
||||
exercises the full test surface plus fuzz under sanitizers and
|
||||
verifies the .deb installs cleanly.
|
||||
|
||||
### Open items
|
||||
|
||||
- External audit still pending (cost-bound, not engineering-bound).
|
||||
- AppImage build via real `appimagetool` not yet automated in CI.
|
||||
- The fuzz harness uses a single fixed seed archive; corpus
|
||||
diversification (different file types, multi-file archives,
|
||||
encrypted seeds) would strengthen coverage further.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-27 — v2.2.2 formal audit (no version bump)
|
||||
|
||||
Formal cryptographic audit pass conducted using methodology in
|
||||
`FORMAL_AUDIT_PROMPT.md`. Auditor profile: senior cryptographic
|
||||
engineering (15+ years production crypto). Threat model: government
|
||||
archives with 30+ year retention, financial institutions under Brazilian
|
||||
Central Bank Resolução 4.658/2018, healthcare (LGPD-Saúde), defense
|
||||
(CNSA 2.0 alignment).
|
||||
|
||||
### Methodology
|
||||
|
||||
- **Path A**: line-by-line manual review with documented preconditions,
|
||||
postconditions, invariants, trust boundaries, failure modes.
|
||||
- **Path B**: independent adversarial test exercising the same invariant.
|
||||
- Bug confirmed only when both paths agreed.
|
||||
|
||||
### Bugs found and fixed (4)
|
||||
|
||||
| # | File | Severity | Description |
|
||||
|---|---|---|---|
|
||||
| 11 | `zupt_format.c` (×2) | **HIGH** | Zip Slip path traversal in extract — `e->path` to `fopen` without validation |
|
||||
| 12 | `zupt_format.c` (×2) | **MEDIUM** | symlink-follow on extract output (`fopen "wb"` follows symlinks) |
|
||||
| 13 | `zupt_format.c:1593` | LOW | `size_t` overflow on solid-extract size cap (32-bit) |
|
||||
| 14 | `zupt_format.c:parse_index` | LOW | `count * sizeof(entry)` overflow before calloc (32-bit) |
|
||||
|
||||
### Cryptographic primitive review (no findings)
|
||||
|
||||
Reviewed every public crypto path against:
|
||||
- FIPS 197 (AES) — key/IV size, counter init, nonce reuse
|
||||
- FIPS 202 (Keccak/SHA-3) — rate/capacity, no domain confusion
|
||||
- FIPS 203 (ML-KEM) — parameter set correctness, key sanitization, decap fault resistance
|
||||
- RFC 5297 (AES-SIV) — nonce-misuse resistance, AD coverage
|
||||
- RFC 5869 (HKDF) — salt-vs-IKM separation, info domain separation
|
||||
- RFC 7748 (X25519) — scalar clamping, all-zero output rejection
|
||||
- RFC 8439 (ChaCha20-Poly1305) — 192-bit XChaCha nonce, AD coverage
|
||||
- RFC 9106 (Argon2) — m≥64 MiB, t≥3, p≥1, salt≥16B
|
||||
- RFC 9180 (HPKE) — suite ID, mode binding, encap context
|
||||
|
||||
Findings: **none**. All primitives correctly implemented.
|
||||
|
||||
### New regression test suite
|
||||
|
||||
`tests/test_path_traversal.sh` — 5 property checks covering:
|
||||
1. Patched archive with `../` entry does not escape parent dir
|
||||
2. Patched archive with absolute path does not write to `/tmp/owned`
|
||||
3. Symlink at extract target is not followed (sentinel preserved)
|
||||
4. Legitimate paths still extract correctly
|
||||
5. Deep nested safe paths still work
|
||||
|
||||
### Cumulative test surface (2.2.2 final)
|
||||
|
||||
| Suite | Tests | Status |
|
||||
|---|---|---|
|
||||
| run_quick.sh | 9 | ✓ |
|
||||
| test_sdk.sh | 11 | ✓ |
|
||||
| test_audit.sh | 10 | ✓ |
|
||||
| test_dedup_props.sh | 12 | ✓ |
|
||||
| test_path_traversal.sh | 5 | ✓ NEW |
|
||||
| **zupt total** | **47** | **✓** |
|
||||
| Format mutation fuzz (ASAN/UBSAN) | 1,000 iters | ✓ |
|
||||
| Inherited libzuptsdk audit | 42 | ✓ |
|
||||
| Inherited libzuptsdk RFC + roundtrip | 84 | ✓ |
|
||||
| Inherited libzuptsdk binding contracts | 57 | ✓ |
|
||||
| Inherited libzuptsdk Wycheproof | 5 | ✓ |
|
||||
| Inherited libzuptsdk fuzz | 750,000 iters | ✓ |
|
||||
| **Combined zupt + SDK** | **265 tests + 751k fuzz** | **✓** |
|
||||
|
||||
### Portability re-verification
|
||||
|
||||
Static portability scan: clean.
|
||||
- No unaligned pointer casts
|
||||
- No raw `/` separators (uses `ZUPT_PATH_SEP`)
|
||||
- No `htonl`/`ntohl`/struct casts (LE helpers throughout)
|
||||
- No POSIX-only headers without `#ifdef _WIN32` guards
|
||||
|
||||
GCC + `-Wpedantic` build: clean.
|
||||
Win32 paths verified via `-D_WIN32 -E` synthetic preprocessing.
|
||||
|
||||
### Cumulative bug count across audit sprints
|
||||
|
||||
| Sprint | Bugs found | Severity range |
|
||||
|---|---|---|
|
||||
| v2.2.1 (first audit) | 6 | low to high |
|
||||
| v2.2.2 (second audit) | 4 | low to medium |
|
||||
| v2.2.2 formal | 4 | low to **high** (Zip Slip path traversal) |
|
||||
| v2.2.2 sprint 4 | 1 | **critical** (silent extract via arg parser) |
|
||||
| v2.2.2 god-tier audit | 1 | **critical** (block-swap AEAD) |
|
||||
| **Total** | **16** | **all fixed and regression-tested** |
|
||||
|
||||
### Open items
|
||||
|
||||
- External independent audit still pending (cost, not engineering)
|
||||
- Side-channel timing leak testing not performed
|
||||
- Cross-OS CI (macOS / Windows / FreeBSD runners) not yet wired
|
||||
- Formal verification beyond Jasmin constant-time primitives (F*, ProVerif)
|
||||
not pursued
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue