3.2 KiB
diffFuzz — differential-fuzz harness
(no submodules)
Purpose
Run N iterations of random input against two implementations of the same primitive and report the first diverging reproducer, plus the total number of mismatches. Designed specifically for the "noble vs native" gating in crypto-primitive-replaceability Phase 1+: before a native implementation is allowed to flip to 'native' in crypto.config.ts, it must run through diffFuzz against the reference noble impl without a single mismatch across millions of random inputs.
Features
- Deterministic
byteSourceinjection for reproducible fuzz campaigns. - Generic over input and output types — works for hashes, ciphers, MACs, curve ops.
- Captures first reproducer with the exact input, both outputs, and iteration index for CI log forensics.
- Continues running after the first mismatch so the
mismatchescount approximates the rate, not just the existence of a bug.
Usage: Basic Example
import { diffFuzz } from "@suaveplan/crypto-utils/testing";
const result = diffFuzz({
iterations: 1000,
sample: (src) => src(32),
implA: (buf: Uint8Array) => buf.reduce((a, b) => a + b, 0),
implB: (buf: Uint8Array) => buf.reduce((a, b) => a + b, 0),
equals: (a, b) => a === b,
});
console.log(result.mismatches); // 0
Usage: Advanced Example
import { diffFuzz } from "@suaveplan/crypto-utils/testing";
import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";
// Differential fuzz SHA-256 noble vs native
declare function sha256Noble(msg: Uint8Array): Uint8Array;
declare function sha256Native(msg: Uint8Array): Uint8Array;
const result = diffFuzz({
iterations: 100_000,
sample: (src, iter) => src(iter % 4096), // variable-length messages
implA: sha256Noble,
implB: sha256Native,
equals: constantTimeEqual,
});
if (result.firstMismatch !== undefined) {
const { iter, input, outputA, outputB } = result.firstMismatch;
throw new Error(`SHA-256 diff at iter=${iter} len=${input.length} noble=${outputA.length}B native=${outputB.length}B`);
}
API Reference
diffFuzz<V, E>(opts: DiffFuzzOptions<V, E>): DiffFuzzResult<V, E>opts.iterations: number— how many trials to run.opts.sample: (byteSource, iter) => V— builds a fresh input each iteration. Receives the iteration index so callers can vary input shape by iteration.opts.implA,opts.implB: (input: V) => Eopts.equals: (a: E, b: E) => booleanopts.byteSource?: ByteSource— optional deterministic source; defaults todefaultByteSource.
- Returns
{ iterations, mismatches, firstMismatch? }.
Implementation Notes
The loop is intentionally unrolled to a single pair of calls per iteration — no async, no microtasks, no chunking. For 100 000 iterations of SHA-256 this completes in a few hundred milliseconds in Node 20+. Callers that need structured output (e.g. DifferentialFuzzCorpus for CI regression pinning) should compose on top of this primitive rather than complicate its API. The byteSource injection is how reproducibility is achieved: when a CI job reports a fail, the caller can replay the run with a fixed source seeded from the failing iteration's context.