Skip to content

Maintaining fuzz coverage

Conformance / contributor guide

Fuzzing is executable security reasoning. A useful target does more than feed random bytes to code: it identifies a trust boundary, gives the fuzzer a path to meaningful states, and asserts properties that must remain true for every input.

For Auths, the recurring properties are:

  • malformed input never panics;
  • work and allocation remain bounded by the applicable limits;
  • equal portable inputs and verifier configuration produce equal bytes;
  • accepted encodings are canonical and round-trip byte-for-byte;
  • result self-digests and other commitments recompute exactly;
  • cryptographic or policy mutations fail closed; and
  • composition and registry behavior agrees with a smaller, independently expressed oracle where one is practical.

A target that merely calls a function and discards the result can still detect panics, but it does not establish semantic correctness.

Extend an existing target or add a new one?

Section titled “Extend an existing target or add a new one?”

Start with the boundary, not the crate.

Extend an existing target when the new behavior shares its input grammar, execution path, limits, and useful oracle. Add a new target when at least one of these is materially different:

  • a new external byte grammar or parser;
  • a new state machine or transition system;
  • a separate trust boundary, such as the complete portable ABI;
  • a distinct resource profile that needs different maximum input sizes or timeouts;
  • an oracle that would make an existing target confused or mostly unreachable;
  • a valid-input generator or seed family that is incompatible with the existing corpus; or
  • a security-critical component that deserves isolated campaign results and crash artifacts.

Do not create one target per type or module by default. Too many shallow targets fragment campaign time. Conversely, do not put unrelated grammars into one byte-soup target merely to keep the target count small.

Target Primary responsibility Extend it when…
target_codec Total parsing of proof bundles and verifier contexts a top-level untrusted proof/context decoder changes
target_portable_codecs Canonical action, context, and result round-trips a portable wire object or canonical encoding rule changes
target_model_state Validated identifiers, digest collections, and model limits a bounded model constructor or invariant is added
target_composition Three-valued plan evaluation against a flat reference oracle plan operators, precedence, or branch-result semantics change
target_registry_handlers Built-in resource, profile, and budget policies against independent relations a built-in policy handler or registry dispatch rule changes
target_principal_parsers Every principal/evidence byte parser an adapter accepts a new evidence or public-key representation
target_portable_abi End-to-end proof/action/context verification, determinism, canonical results, and result digests a method, suite, verifier stage, portable input, or result commitment changes

The generated fuzz inventory is the current target list. The table above describes ownership; it does not replace the target source as the executable definition.

A new curve or post-quantum signature suite normally affects several existing boundaries. It does not automatically imply exactly one new fuzz target.

For example, adding PqSignatureSuite should trigger this review:

  1. Key and evidence parsing: add the new key/evidence decoder to target_principal_parsers if attacker-controlled bytes reach a new parser.

  2. Registry execution: construct the new suite in the relevant registry used by target_portable_abi.

  3. Reachability: add a valid proof fixture that actually selects the new suite. Merely placing a suite in this array:

    let suites: [&dyn SignatureSuite; 3] = [&ed25519, &p256, &pq];

    changes the configuration commitment, but does not prove that the fuzzer reaches PQ verification.

  4. Semantic mutations: exercise truncated keys/signatures, wrong suite IDs, cross-suite substitution, non-canonical encodings, boundary lengths, and any algorithm-specific malleability or domain-separation failures.

  5. Known-answer tests: keep authoritative positive and negative cryptographic vectors in ordinary tests or the canonical corpus. Fuzzing is complementary; it is not a replacement for published known-answer vectors.

  6. Resource behavior: account for larger PQ keys and signatures in limits, maximum fuzz input length, and hostile-resource measurements.

A dedicated target is warranted if the algorithm adds a complex independent parser, stateful verification, unusually large inputs, or an algorithm-specific oracle that the portable target cannot exercise efficiently. In that case, keep both layers: the dedicated algorithm target explores its internals, while target_portable_abi proves integration and dispatch.

Pure random bytes are useful for parser totality but rarely reach signatures, delegation, composition, or assurance. Seed semantic targets with a deeply valid object, then mutate one controlled dimension at a time.

target_portable_abi follows this pattern. It embeds a committed valid proof/action/context tuple, supports an APF1 framed tuple for structured inputs, and otherwise mutates one of the three inputs. This keeps both malformed and deep verifier paths reachable.

Good assertions include:

  • verification twice yields identical result bytes;
  • successful decode followed by encode yields the original canonical bytes;
  • a result digest recomputes to the embedded digest;
  • a small reference implementation agrees with the production evaluator;
  • changing a bound field cannot preserve authorization; and
  • an exact limit succeeds while the next value fails with the expected class.

An oracle should not call the same production helper on both sides of the assertion. That only proves a value equals itself.

The harness is part of the attack surface. Cap collection sizes, mutation counts, recursion, and generated string lengths. Avoid network access, wall clock dependence, nondeterministic randomness, and unbounded fixture construction.

All of these locations are required:

  1. create fuzz/fuzz_targets/target_<name>.rs;
  2. add any direct dependencies and a matching [[bin]] entry to fuzz/Cargo.toml;
  3. add the exact name to FUZZ_TARGETS in xtask/src/main.rs;
  4. add it to the matrix in .github/workflows/fuzz.yml;
  5. create fuzz/corpus/target_<name>/ with at least one useful seed; and
  6. confirm the generated docs inventory includes it.

The synchronization gate intentionally fails if the Cargo manifest, xtask inventory, workflow matrix, or seed directory disagree.

Run:

Terminal window
cargo xtask fuzz-inventory
cargo check --manifest-path fuzz/Cargo.toml --bins
cargo xtask fuzz-smoke
cargo fuzz run target_<name> --manifest-path fuzz/Cargo.toml

For a regression, copy the minimized crashing input into the target’s committed seed corpus with a descriptive name. First add an ordinary regression test when the invariant can be stated clearly; the seed then preserves the fuzzer’s route to the bug class.

Fuzz coverage must be reviewed when a change:

  • parses or canonicalizes attacker-controlled bytes;
  • adds a principal method, key representation, signature suite, registry handler, plan operator, or extension;
  • changes a verifier stage, dispatch rule, result code, or fail-closed precedence;
  • changes a limit or creates a new collection, loop, recursion path, or allocation driven by input;
  • changes the portable ABI or configuration commitment;
  • fixes a panic, hang, excessive allocation, nondeterminism, or semantic mismatch; or
  • makes an existing valid seed unreachable.

A rename or refactor with no changed boundary, invariant, or reachability normally needs no new target. Existing targets must still pass unchanged.