Skip to content

feat: feature-gated Arbitrary derives for structure-aware fuzzing - #45

Closed
stormer78 wants to merge 1 commit into
mainfrom
feat/arbitrary-support
Closed

feat: feature-gated Arbitrary derives for structure-aware fuzzing#45
stormer78 wants to merge 1 commit into
mainfrom
feat/arbitrary-support

Conversation

@stormer78

Copy link
Copy Markdown
Collaborator

What

Adds an off-by-default arbitrary feature that derives arbitrary::Arbitrary on the public log-entry / parameters graph, so downstream consumers can run structure-aware, coverage-guided fuzzing (cargo-fuzz) of the chain verifier.

Closes #44. Requested by the affinidi-webvh-service hosting service, which fuzzes the publish path that bottoms out in LogEntry::deserialize_string + DIDWebVHState::validate() (their tracking issue: affinidi/affinidi-webvh-service#45).

Why

Raw byte/string mutation almost never produces input that survives LogEntry deserialization, let alone a multi-entry chain whose hashes link — so a fuzzer spends ~all its budget bouncing off the JSON parser and rarely reaches validate(), where the interesting bugs live (SCID derivation, entry-hash linkage, parameter transitions, pre-rotation, deactivation tamper detection). With Arbitrary, a harness turns a &[u8] into a structurally-valid-but-mutated chain and drives the verifier directly.

Types covered

LogEntry, LogEntry1_0, LogEntry1_0Pre, Parameters, Parameters1_0, Parameters1_0Pre, Version, Multibase, Witnesses, Witness.

Foreign-type fields

Three fields on the log-entry structs have types from foreign crates with no Arbitrary impl, handled via #[arbitrary(with = ...)] generators in a new gated arbitrary_support module:

  • version_time: DateTime<FixedOffset> — bounded Unix timestamp ([0, 2100)).
  • state: serde_json::Value — depth-bounded recursive JSON.
  • proof: Vec<DataIntegrityProof>emitted empty. A valid proof must be signed over the canonical log-entry bytes, which can only happen after the rest of the entry exists; and DataIntegrityProof lives in affinidi-data-integrity. This still exercises everything up to the "is a proof present / does it authorize this update" gate plus all of SCID/hash-chain/parameter validation. This is the main design point I'd like your call on (see below).

Safety / impact

  • Fully gated behind #[cfg_attr(feature = "arbitrary", ...)]; no change to default builds and no new always-on dependency.
  • examples/fuzz_validate.rs (gated on the feature) is a ready-to-copy libfuzzer target body that builds a chain and asserts validate() never panics.

Verification

  • cargo build --features arbitrary
  • cargo build (default, example excluded via required-features) ✅
  • cargo test --features arbitrary435 passed, 0 failed
  • cargo clippy --features arbitrary --lib --examples — clean ✅
  • cargo clippy (default) — clean ✅
  • cargo run --example fuzz_validate --features arbitrary -- <random bytes> — no panic ✅

Open questions for maintainers

  1. Proof generation — keep proofs empty (current), or add a feature-gated Arbitrary to DataIntegrityProof upstream in affinidi-data-integrity so mutated/invalid proofs can be generated too? Empty keeps this PR self-contained; the latter widens signature-path coverage.
  2. DIDWebVHState — I left it deriving-free and assemble it from a Vec<LogEntry> in the example. Happy to derive it directly if you'd prefer a one-call entry point.
  3. Want the actual fuzz/ cargo-fuzz workspace + a CI gate upstreamed here, or kept downstream?

Marking as draft pending your steer on (1)–(3).

Adds an off-by-default `arbitrary` feature that derives
`arbitrary::Arbitrary` on the public log-entry / parameters graph
(`LogEntry`, `LogEntry1_0`, `LogEntry1_0Pre`, `Parameters`,
`Parameters1_0`, `Parameters1_0Pre`, `Version`, `Multibase`,
`Witnesses`, `Witness`). This lets downstream consumers run
structure-aware, coverage-guided fuzzing (cargo-fuzz) of the chain
verifier instead of byte mutation, which almost never survives JSON
parsing and so rarely reaches `DIDWebVHState::validate()`.

Three fields on the log-entry structs have types from foreign crates
that can't derive `Arbitrary`, so they use `#[arbitrary(with = ...)]`
generators in the new `arbitrary_support` module:

- `version_time: DateTime<FixedOffset>` — bounded Unix timestamp.
- `state: serde_json::Value` — bounded recursive JSON.
- `proof: Vec<DataIntegrityProof>` — emitted empty (a valid proof must
  be signed over the canonical bytes after the entry is built; harnesses
  that want to exercise signature verification should sign the generated
  entry or mutate a seed-corpus entry that already carries proofs).

The feature adds no always-on dependency and is fully gated via
`#[cfg_attr(feature = "arbitrary", ...)]`, so default builds are
unchanged. `examples/fuzz_validate.rs` (gated on the feature) shows a
ready-to-copy libfuzzer target body driving the verifier.

Closes #44
@stormer78

Copy link
Copy Markdown
Collaborator Author

Superseded by #46 (shipped in v0.5.5), which is more complete than this PR — it generates real DataIntegrityProof values via the public constructor (fuzzing the signature-verification path, which this PR left as empty proofs / an open question) and ships a full fuzz/ cargo-fuzz crate. Closing in favour of the published version; downstream (affinidi-webvh-service) is now on 0.5.5. Thanks!

@stormer78 stormer78 closed this Jun 14, 2026
@stormer78
stormer78 deleted the feat/arbitrary-support branch June 14, 2026 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature-gated Arbitrary derives on LogEntry/Parameters for structure-aware fuzzing

1 participant