zupt/AUDIT.md
Cristian Cezar Moisés e5f5d32aab v2.2.2
2026-05-01 09:58:47 -03:00

477 lines
19 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 Audit — Zupt v2.0.0
**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
---
## 1. Cryptographic Test Vector Verification
All primitives tested against published reference vectors:
| 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 §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** |
## 2. Jasmin Constant-Time Verification
| 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** |
## 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
| Feature | Status |
|---------|--------|
| mlock() key protection | **✅ Active** |
| Buffer canaries (keyring) | **✅ Active** |
| Always-decrypt timing mitigation | **✅ Active** |
| AFL++ fuzz harnesses | **✅ Available** (`make fuzz-build`) |
## 5. VaptVupt Codec Tests
| 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 | **PASS** |
| Multi-block (2 MB) | **PASS** |
| **Total** | **11/11 PASS** |
| 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`
---
## 3. Memory Safety
| 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
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 compress --pq /tmp/pub.key /tmp/pq.zupt /path/to/data/
./zupt_asan extract --pq /tmp/k.key -o /tmp/pqout/ /tmp/pq.zupt
```
---
## 4. Compiler Warning Audit
| Compiler | Flags | Warnings |
|----------|-------|----------|
| GCC 13.x | `-Wall -Wextra -Wpedantic -O2 -std=c11` | **Zero** |
| Clang 18.x | `-Wall -Wextra -Wpedantic -O2 -std=c11` | **Zero** |
---
## 5. Constant-Time Analysis
| Function | Location | CT Method | Jasmin Verified? | Risk Level |
|----------|----------|-----------|-----------------|------------|
| 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: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
Two functions confirmed active in binary via `nm`:
```
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 |
---
## 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 |
|-----|----------|---------------|--------|
| Huffman Kraft-inequality violation | Critical | v0.5.1 | Data corruption on specific inputs |
| Heap-buffer-overflow in LZ match finder | Critical | v0.5.1 | Potential code execution |
| `rand()` CSPRNG fallback | Critical | v0.5.1 | Predictable encryption keys |
| ML-KEM `poly_basemul` OOB | Critical | v1.0.0 | Buffer overread in NTT |
| ML-KEM missing `poly_tomont` | Critical | v1.0.0 | Public key in wrong domain |
| ML-KEM inverted FO `cmov` | Critical | v1.0.0 | Always selected rejection key |
| 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 |
| 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 |
---
## 10. Known Limitations
| Limitation | Impact | Mitigation | Status |
|------------|--------|------------|--------|
| 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 `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
---
## 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