Maintaining fuzz coverage
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.
What the current targets own
Section titled “What the current targets own”| 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.
Adding a key type or signature algorithm
Section titled “Adding a key type or signature algorithm”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:
-
Key and evidence parsing: add the new key/evidence decoder to
target_principal_parsersif attacker-controlled bytes reach a new parser. -
Registry execution: construct the new suite in the relevant registry used by
target_portable_abi. -
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.
-
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.
-
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.
-
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.
Designing a target
Section titled “Designing a target”Begin from valid structure
Section titled “Begin from valid structure”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.
State an oracle
Section titled “State an oracle”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.
Bound the harness too
Section titled “Bound the harness too”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.
Adding a new target
Section titled “Adding a new target”All of these locations are required:
- create
fuzz/fuzz_targets/target_<name>.rs; - add any direct dependencies and a matching
[[bin]]entry tofuzz/Cargo.toml; - add the exact name to
FUZZ_TARGETSinxtask/src/main.rs; - add it to the matrix in
.github/workflows/fuzz.yml; - create
fuzz/corpus/target_<name>/with at least one useful seed; and - 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:
cargo xtask fuzz-inventorycargo check --manifest-path fuzz/Cargo.toml --binscargo xtask fuzz-smokecargo fuzz run target_<name> --manifest-path fuzz/Cargo.tomlFor 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.
When a change needs fuzz work
Section titled “When a change needs fuzz work”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.