Security tests & how to help
Regression suite, fuzzing harnesses, audit rounds, and concrete contribution paths for external researchers.
LUKSbox is a young codebase. The cryptography is built on standardised primitives (NIST FIPS 203, RFC 8452, RFC 5869, RFC 9106), and mature Rust libraries (RustCrypto, libfido2, tss-esapi), but the integration layer, and the on-disk format are ours. This page lists every automated check that runs before a release and tells you how to push the project past where our imagination stops.
If you find a way to make LUKSbox misbehave on a crafted input, a malicious peer, or a surprising filesystem state, even if it isn't a classical vulnerability, please tell us. Open a GitHub issue with reproduction steps. We respond.
Regression suites
Every commit runs the workspace test suite plus a dedicated "security-regressions" CI gate that fails if any of the historic-bug guards trip.
| Surface | What it pins down | Test crate |
|---|---|---|
| Argon2id DoS | Hostile-cost params rejected before stretching starts | luksbox-core / argon2_dos_guard |
| Seed-file DoS | Oversized .kyber files rejected by length cap |
luksbox-pq / seed_file_dos_guard |
| Rogue / MITM FIDO2 authenticator | Wrong hmac-secret response fails AEAD; no oracle | luksbox-fido2 / rogue_authenticator |
| Postcard length-prefix OOM | Attacker length prefix can't balloon allocation | luksbox-vfs / metadata_format_v2 |
| FIPS 203 ML-KEM conformance | Encapsulation/decapsulation against NIST ACVP vectors | luksbox-pq / fips203_conformance |
| Hybrid FIDO + PQ end-to-end | All four hybrid permutations round-trip | luksbox-pq / end_to_end_hybrid |
| HKDF info-string uniqueness | No two subkey purposes share an info string | luksbox-core / security_invariants |
| Slot AAD coverage | Every security-sensitive slot byte is in the AAD | luksbox-core / security_invariants |
| Header-tamper exhaustive coverage | Every byte of the header (excluding the MAC) is authenticated | luksbox-format / security_invariants |
| Concurrent open serialisation | Second opener gets VaultLocked before header read |
luksbox-format / security_invariants |
| Detached header lock pair | Inline + detached opens both lock all involved files | luksbox-format / security_invariants |
| Cross-file chunk substitution | Chunk AAD prevents file ID swaps | luksbox-vfs / security_invariants |
| Chunk position swap | Chunk AAD prevents chunk_index swaps |
luksbox-vfs / security_invariants |
| Generation rollback | Chunk AAD generation mismatch makes AEAD fail |
luksbox-vfs / security_invariants |
| Malicious authenticated metadata | Wrapping chunk IDs, missing inodes, dangling children rejected before VFS uses them | luksbox-vfs / vfs::tests |
| Symlink-target overwrite on extract | luksbox get refuses pre-existing symlink destinations (O_NOFOLLOW) |
luksbox-core / file_util |
| Path substitution after open | Post-lock path-inode re-stat opens the CANONICAL path with O_NOFOLLOW (R12-11), rejects rename-during-open swaps |
luksbox-format / verify_path_inode_rejects_substituted_path |
| AES-NI startup warning | CLI complains visibly when running AES-GCM on a CPU without AES-NI | luksbox-cli / functional |
Helper subprocess --header canonicalization (R12-03) |
Helper rejects a dangling-symlink header path before opening | luksbox-format / round12_findings::r12_03_helper_rejects_symlinked_header_path |
| Helper subprocess mountpoint probe (R12-05) | Helper rejects a symlinked mountpoint with the same O_DIRECTORY|O_NOFOLLOW probe as the parent CLI |
luksbox-format / round12_findings::r12_05_helper_mountpoint_probe_rejects_symlink |
Hybrid sidecar O_NOFOLLOW (R12-06) |
read_bundle refuses symlinked .hybrid sidecars (ELOOP) |
luksbox-format / round12_findings::r12_06_hybrid_sidecar_open_rejects_symlink |
| Deniable envelope opacity / multi-slot (R12-01) | Envelope open under attacker passphrase across all 8-slot occupancy permutations stays opaque, no panic | luksbox-format / round12_findings + deniable_envelope_multi_slot libFuzzer + AFL++ targets |
| openat-bound extraction (R13-01) | secure_create_or_truncate refuses a symlinked basename via openat(parent_dir_fd, basename, O_NOFOLLOW); legitimate intermediate symlink still works; mode 0600 + narrowing preserved |
luksbox-core / round13_file_util (4 tests) |
| Header-restore via container handle (R13-02) | Container::restore_header_bytes writes the supplied bytes byte-for-byte through the verified file handle |
luksbox-format / round13_findings::r13_02_restore_header_bytes_writes_via_container_handle |
| Hide-size real-size DoS guard (R13-03) | Chunk-0 real-size header clamped to allocated capacity in Vfs::real_size; hostile metadata cannot panic stat/read |
covered indirectly by luksbox-vfs / round13_findings |
| Header durability (R13-04) | Container::persist_header round-trips revoke+re-open without leaving a half-written header |
luksbox-format / round13_findings::r13_04_persist_header_returns_clean_after_revoke |
.kyber seed-file safe read (R13-05) |
Symlink swap refused, oversize swap refused, non-regular file refused; legitimate seed still loads | luksbox-pq / round13_seed_file (3 tests) |
| Hybrid sidecar size preflight (R13-06) | Oversize .hybrid files rejected before allocation |
luksbox-format / round13_findings::r13_06_hybrid_sidecar_rejects_oversize_file |
| VFS file-size cap (R13-07) | Vfs::write and Vfs::truncate past MAX_FILE_SIZE return Error::FileSizeExceedsCap; legitimate writes still succeed |
luksbox-vfs / round13_findings (3 tests) |
SecretBox::clone hygiene (R13-09) |
Clone preserves secret-memory backing kind + equal content (no by-value Copy) | luksbox-format / round13_findings::r13_09_secretbox_clone_preserves_backing |
Run them locally:
cargo test --workspace
The full test taxonomy is in
TESTING.md.
Fuzzing campaigns
Every parser that touches attacker-controlled bytes has a libFuzzer
harness in fuzz/
and an AFL++ harness in
fuzz-afl/.
PR CI runs each libFuzzer target for 5 minutes on the persistent
corpus; a dedicated server runs the AFL++ campaigns for hours per
release.
| Target | What it fuzzes | Status |
|---|---|---|
header_parse |
8 KB header parsing, magic + length checks | Clean |
keyslot_parse |
Per-slot binary parsing, all 15 slot kinds (passphrase, FIDO2 x2, TPM x3, hybrid x8) | Clean |
metadata_parse |
Postcard metadata blob decoding | Clean |
hybrid_sidecar_parse |
.hybrid sidecar v1/v2/v3 parsing + binding check |
Clean |
seed_file_parse |
.kyber seed file parsing |
Clean |
auth_then_process |
Full Container::open() path with random ciphertext |
Clean |
header_roundtrip |
Header serialise/deserialise idempotency | Clean |
winfsp_path_parse |
Windows path / share name classifier | Clean |
webauthn_device_path |
Windows Hello device-path classifier | Clean |
vfs_ops |
Differential VFS ops (mkdir/create/rename/unlink/lookup/readdir/write/flush) | Clean (about 130K iters / iteration) |
deniable_header_parse |
v2 deniable envelope-open path with random 36 KiB+ buffer | Clean (Round 11) |
slot_payload_decode / slot_payload_roundtrip |
Deniable slot-payload codec + per-field caps | Clean (Round 11) |
chunk_aead_decrypt |
Per-chunk AEAD open at production callsite with attacker AAD | Clean |
anchor_parse |
Both standard (48 B) and deniable (256 B) anchor readers | Clean |
deniable_envelope_multi_slot |
Multi-slot deniable envelope opacity (real header, fuzzer-selected slot occupancy) | Clean (Round 12) |
Total fuzz iterations across all targets at the time of writing: 30 million plus. Bugs that these harnesses originally found and helped fix (Argon2id DoS, cred_id OOM, postcard-decode OOM) are pinned down by the regression suites listed above.
Run fuzz locally:
cargo install cargo-fuzz
cd fuzz
cargo +nightly fuzz run header_parse -- -max_total_time=300
# Round 12 multi-slot deniable envelope opacity target
cargo +nightly fuzz run deniable_envelope_multi_slot -- -max_total_time=300
The full fuzz taxonomy and per-target invariants are in
FUZZING.md.
Constant-time benches (dudect)
Statistical timing tests run on demand (cargo bench, not part of
the default CI run) over four production code paths plus one
known-leaky control for t-stat calibration.
| Bench | Pins | Reproduce |
|---|---|---|
dudect_hmac_verify |
Header::verify_hmac (subtle::ConstantTimeEq) |
cargo bench --bench dudect_hmac_verify -p luksbox-core |
dudect_aead_open |
AEAD-open rejection path | cargo bench --bench dudect_aead_open -p luksbox-core |
dudect_slot_unlock |
Keyslot post-Argon2id unwrap | cargo bench --bench dudect_slot_unlock -p luksbox-core |
dudect_deniable_envelope |
Deniable envelope-discovery loop (Round 12 R12-01) | cargo bench --bench dudect_deniable_envelope -p luksbox-format |
dudect_reference_leaky |
Known-leaky control | cargo bench --bench dudect_reference_leaky -p luksbox-core |
The acceptance bar is |t| < 3.0 sustained across 5 K - 50 K samples.
dudect_deniable_envelope was added in Round 12 as the regression
gate for R12-01 (the deniable envelope discovery loop's timing
leak). It is expected to PASS on current main now that the constant-
time fix (always-allocate fixed scratch + subtle::Choice-driven
byte selection + single-shot SlotPayload::decode on the chosen
slot) has shipped. See
docs/SECURITY_AUDIT_ROUND_12.md.
Internal audit rounds
Thirteen rounds of internal audit have shipped to date. Each round focuses on a specific surface; the per-round notes (vulnerabilities found, mitigations, regression tests added) are in the audit history on the website and the recent ground-truth snapshot is at the architecture page on the website.
| Round | Surface | Significant outputs |
|---|---|---|
| 1 | Header / keyslot binary format | AAD coverage formalised; HMAC region locked |
| 2 | Argon2id parameter handling | DoS guard; FIPS-aligned upper bounds |
| 3 | FIDO2 protocol (CTAP2 hmac-secret) | cred_id OOM; rogue-authenticator harness |
| 4 | Metadata encoding | bincode->postcard migration; length-prefix guard |
| 5 | Mount / FUSE / WinFsp surface | Symlink prohibition; reparse-point ignore |
| 6 | Generation/replay invariants | Per-chunk generation in AAD; cross-file substitution test |
| 7 | TPM 2.0 integration (Linux) | Permission diagnostic; mock + swtpm conformance |
| 8 | Concurrency, crash safety, secret-memory hygiene | Lock-before-read; parent-dir fsync (Unix + Windows); MVK-lifecycle Zeroizing audit; symlink-target overwrite guard |
| 11 | Deniable v2 cryptographic sweep | Per-vault salt mixed into the inner-header AAD; envelope plaintext wrapped in Zeroizing; Zeroizing<[u8;32]> propagated through deniable_pq_decap; 5 workflow tests + 3 fuzz targets added |
| 12 | FUSE-T subprocess + deniable v2 timing + filesystem TOCTOU + memory safety | 1 CRITICAL + 5 HIGH + 7 MEDIUM + 6 LOW; all 19 findings shipped fixes same revision (R12-14 formally superseded by R12-11). Headline fix: R12-01's deniable envelope discovery loop is now constant-time via subtle::Choice-driven byte selection. Other notable fixes: HmacSecret newtype with Zeroize + ZeroizeOnDrop (R12-19), canonical-path verify with O_NOFOLLOW (R12-11), pre-mount inode re-probe (R12-08), Windows FILE_FLAG_OPEN_REPARSE_POINT (R12-15), MasterVolumeKey::from_zeroizing constructor (R12-17). Regression coverage: dudect_deniable_envelope bench + deniable_envelope_multi_slot libFuzzer / AFL++ target + round12_findings.rs regression suite (7 tests, 0 ignored). Full report at docs/SECURITY_AUDIT_ROUND_12.md. |
| 13 | Local filesystem boundary races + header durability + sidecar DoS + secret-copy hygiene | 0 CRITICAL + 2 HIGH + 5 MEDIUM + 2 LOW + 1 INFO; all 9 findings shipped fixes same revision. Headline fixes: R13-01 closes the intermediate-directory symlink swap in secure_create_or_truncate via openat(parent_dir_fd, basename, O_NOFOLLOW); R13-02 routes luksbox header restore through the container's verified handle (Container::restore_header_bytes). Other notable fixes: Vfs::real_size clamps the chunk-0 authenticated size against allocated capacity (R13-03); Container::persist_header uses sync_all + atomic_secure_write (R13-04); .kyber seed reads gain O_NOFOLLOW + regular-file + size preflight (R13-05); hybrid sidecar 32 KiB cap (R13-06); MAX_FILE_SIZE = 1 << 44 cap on Vfs::write / truncate (R13-07); FUSE read capped at 16 MiB (R13-08); SecretBox::clone allocates fresh secret memory + direct copy_from_slice (R13-09). Regression coverage: 4 test files, 14 deterministic tests (0 ignored). Full report at docs/SECURITY_AUDIT_ROUND_13.md. |
What we still want to test (and how you can help)
External eyes find what internal eyes miss. We deliberately publish
the open gaps so external researchers know where to push first. The
honest list lives in
our internal ground-truth catalogue (available on request to [email protected]);
the highlights are below, with the contribution path for each.
Contribute a corpus seed
The fastest way to push fuzzing further is to add a real-world input
file to a target's corpus. Drop it under
fuzz/corpus/<target_name>/
and open a PR. Examples that would help today:
- Real headers from old vault layouts (V1 / V2) for
header_parse. - Authenticator-specific cred IDs (Google Titan, SoloKey
stateless, Trezor) for
keyslot_parse. - Edge-case Windows paths (UNC, network share, long path with
device-namespace prefix) for
winfsp_path_parse. - Truncated / extended
.hybridsidecars forhybrid_sidecar_parse.
Add a fuzz target
If a parser doesn't have a harness yet and you can imagine an
attacker shaping its input, please add one. See the harness template
in FUZZING.md.
Suggest a regression test
If you spot a code path where invariants aren't tested but feel like they should be, file an issue with the invariant in plain English. We write the test and credit the suggestion in the changelog.
Run the AFL++ campaign
The
scripts/fuzz_server.sh
script in the repository runs an AFL++ campaign indefinitely against
any target. If you have spare cycles and want to find something the
libFuzzer 5-minute PR run misses, this is the lever. Any crash you
find folds into the regression suite.
Audit the CLI / GUI / mount surface
Most audit rounds have focused on the cryptographic core. The CLI argument-parsing layer, GUI worker threads, and FUSE / WinFsp adapters have had less attention.
Where to send findings
| Type | Channel |
|---|---|
| Suspected vulnerabilities | Open a GitHub issue with reproduction steps. |
| Fuzz crashes that aren't security-sensitive | GitHub issue with the input file and the target name. |
| Test-suite suggestions, contributions to the corpus | Pull request against fuzz/corpus/ or an issue with the proposed invariant. |
| Audit findings of any depth | We credit external auditors on a dedicated changelog entry and (if you want it) in any advisory we publish. |