/ Documentation / Security / Security tests & how to help

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:

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.