cargo fuzz target for the in-tree X.509 parser. Looks for inputs that
cause Certificate::from_der to panic. A clean run is evidence, not a proof
that every possible input is safe. This is a
manual pre-release gate, not a CI check — cargo fuzz requires the
nightly Rust toolchain and runs for arbitrary durations.
The fuzz crate (fuzz/) depends on certinfo with default-features = false, which disables the python feature and drops the entire PyO3
layer. What's left is the pure-Rust DER / X.509 parser core under
rust_certinfo/src/{der,x509}/ plus error.rs — exactly the code we
want to fuzz. No Python interpreter is needed; the fuzz binary runs as
a standalone executable.
This means the wheel build is unaffected. make develop, the
release wheel, and cargo test of the main crate all build with the
default python feature on and get the full PyO3 surface.
| Target | What it feeds | What a finding means |
|---|---|---|
parse_certificate |
Certificate::from_der |
a panic in the X.509 parser |
parse_server_hello |
the TLS ServerHello / HelloRetryRequest parser | a panic in the PQ probe's parser |
parse_ocsp_response |
OcspResponse::from_der |
a panic parsing an OCSP responder's answer |
parse_crl |
Crl::from_der, revoked_count, lookup |
a panic parsing or searching a CRL |
parse_pkcs7 |
pkcs7_certificates, the certs-only SignedData walker |
a panic unpacking a .p7b bundle or a caIssuers response |
verify_signature |
arbitrary signatures against real keys, and arbitrary keys against a real signature | a panic in signature or key parsing, or in the big-integer arithmetic |
bigint_divrem |
two arbitrary integers | a wrong answer: the division identity or a modular power failed, which would mean a wrong signature verdict |
parse_pss_parameters |
PssParameters::parse, the RSASSA-PSS-params decoder |
a panic parsing the PSS hash, MGF1, and salt length parameters |
eddsa_decode_point |
arbitrary bytes as an Ed25519 or Ed448 public key | a panic in the Edwards point decoder or the group arithmetic |
Run one with make fuzz FUZZ_TARGET=verify_signature, or all of them
with make fuzz-all (60 seconds each by default).
The new in-tree DER parser takes untrusted bytes from the network on every TLS handshake. The risk classes fuzzing defends against:
- Panic on malformed input.
#![forbid(unsafe_code)]at the certinfo crate root prevents memory-safety bugs, but it does not prevent Rust panics (which unwind or abort depending on the build). Every bounds-check in the parser is a potential panic if I got the math wrong. Fuzzing tries millions of adversarial byte sequences and reports any that crash. - Pathological CPU on malformed input (denial of service). A length-parsing bug that loops on a particular byte sequence is a useful target for coverage-guided fuzzing and explicit resource-limit tests.
- Bounds bugs in length fields. A recurring source of bugs in DER/ASN.1 parsers. Hand-written tests cover the cases I thought of; libfuzzer covers the cases I didn't.
Existing tests in tests/test_certinfo_corpus.py already run every
public certinfo entry point against 130 real-world certs on every CI
run. Fuzzing is the deeper, slower defense against the bytes a real
cert never contains. It's worth running before tagging a release;
not worth running on every commit.
From the repo root:
make fuzz # 60-second smoke run
make fuzz-long # 1-hour soak runmake fuzz handles toolchain checks (nightly, cargo-fuzz), seeds the
corpus from tests/fixtures/diff_corpus/, and runs the parser fuzz
target. Acceptance: zero crashes during the run.
If you want to run for a specific duration or with custom libfuzzer flags:
rustup toolchain install nightly # one-time
cargo install cargo-fuzz # one-time
cargo +nightly fuzz run parse_certificate -- -max_total_time=3600Drop -max_total_time to run until you Ctrl-C. Crashes (if any) land
under fuzz/artifacts/parse_certificate/ — each file is
the exact byte sequence that triggered the crash, ready to drop into a
unit test for regression coverage.
Before tagging a release that touches rust_certinfo/src/{der,x509}/:
- Run
make fuzz-long(1 hour minimum). Longer is better — 4 hours is a reasonable soak. - Verify zero entries in
fuzz/artifacts/. - Note the
#runsandcovnumbers from the libfuzzer output in the release notes so future maintainers can see the gate was honored.
cargo fuzz needs nightly Rust, takes orders of magnitude longer than
a unit test, and brings in libfuzzer-sys. None of those belong in PR
CI. The corpus snapshot test at tests/test_certinfo_corpus.py covers
the day-to-day regression check against real-world certs, the Wycheproof
vectors and the OpenSSL differential check cover the signature verifier,
and this fuzzing is the deeper, slower defense against malformed input we
haven't seen yet. A weekly fuzz-smoke job in the CI workflow runs every
target for a minute so regressions surface without anyone remembering to
run it.
To fuzz another entry point (e.g. analyze_chain), add a new file
under fuzz_targets/ and a matching [[bin]] block in Cargo.toml.
The DER reader, OID decoder, and time decoders are good candidates for
isolated targets if you want narrower fuzzing of specific layers.