Files
skills/docs/security-stack/crypto/crypto-utils/src/testing/diff-fuzz/diff-fuzz.md
2026-05-29 08:20:56 +01:00

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 byteSource injection 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 mismatches count 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) => E
    • opts.equals: (a: E, b: E) => boolean
    • opts.byteSource?: ByteSource — optional deterministic source; defaults to defaultByteSource.
  • 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.