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

19 KiB
Raw Blame History

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 Yeszupt_mac_verify_ct linked None (Jasmin proven)
ML-KEM FO select zupt_mlkem.c:593 4×u64 masked select Yeszupt_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