Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

certinfo fuzzing

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.

How it works

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.

Targets

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).

Why fuzz the parser

The new in-tree DER parser takes untrusted bytes from the network on every TLS handshake. The risk classes fuzzing defends against:

  1. 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.
  2. 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.
  3. 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.

Easy mode (recommended)

From the repo root:

make fuzz           # 60-second smoke run
make fuzz-long      # 1-hour soak run

make 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.

Manual 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=3600

Drop -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.

Pre-release gate

Before tagging a release that touches rust_certinfo/src/{der,x509}/:

  1. Run make fuzz-long (1 hour minimum). Longer is better — 4 hours is a reasonable soak.
  2. Verify zero entries in fuzz/artifacts/.
  3. Note the #runs and cov numbers from the libfuzzer output in the release notes so future maintainers can see the gate was honored.

Why this is not in PR CI

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.

Adding new fuzz targets

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.