19 KiB
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:
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-sdkmode 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-asannot 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
appimagetoolnot 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:
- Patched archive with
../entry does not escape parent dir - Patched archive with absolute path does not write to
/tmp/owned - Symlink at extract target is not followed (sentinel preserved)
- Legitimate paths still extract correctly
- 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 (usesZUPT_PATH_SEP) - No
htonl/ntohl/struct casts (LE helpers throughout) - No POSIX-only headers without
#ifdef _WIN32guards
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