Files
suaveplan/scripts/crypto-fuzz.ts
2026-05-27 23:47:53 +01:00

123 lines
4.1 KiB
TypeScript

/**
* Nightly differential-fuzz driver. For every registered primitive that has
* BOTH a noble and a native impl not stubbed out with `PrimitiveNotImplemented`,
* run `diffFuzz` native-vs-noble over N random inputs. Fails the run if any
* mismatch is observed.
*
* In Phase 0 every native is a stub, so this driver iterates every primitive,
* probes the native impl, catches `PrimitiveNotImplemented`, skips, and exits
* clean. The wiring fires the moment a real native lands.
*/
import { diffFuzz } from "../universal/crypto/crypto-utils/src/testing/diff-fuzz/diff-fuzz.ts";
/** Stable code emitted by `PrimitiveNotImplemented`. */
const NOT_IMPLEMENTED_CODE = "CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED";
interface PrimitiveJob {
readonly id: string;
readonly category: "hash" | "curve" | "cipher";
readonly run: () => Promise<void>;
}
const FUZZ_ITERATIONS = Number.parseInt(
process.env.CRYPTO_FUZZ_ITERATIONS ?? "500",
10
);
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
function randomBytes(len: number): Uint8Array {
const buf = new Uint8Array(len);
crypto.getRandomValues(buf);
return buf;
}
async function loadImpls<T>(
noblePath: string,
nativePath: string,
pick: (mod: Record<string, unknown>) => T
): Promise<{ noble: T; native: T } | "pending"> {
const [nobleMod, nativeMod] = await Promise.all([
import(noblePath),
import(nativePath),
]);
const noble = pick(nobleMod as Record<string, unknown>);
const native = pick(nativeMod as Record<string, unknown>);
// Probe the native with a throwaway call — if it raises
// PrimitiveNotImplemented, skip this primitive.
try {
(native as (x: Uint8Array) => unknown)(new Uint8Array(1));
} catch (err) {
if (
typeof err === "object" &&
err !== null &&
(err as { code?: unknown }).code === NOT_IMPLEMENTED_CODE
) {
return "pending";
}
// Unrelated failure — let it propagate so it's investigated.
}
return { noble, native };
}
async function fuzzHashOneShot(id: string): Promise<void> {
console.log(`[fuzz] ${id}: loading impls…`);
const noblePath = `../universal/crypto/crypto-hash/src/primitives/${id}/impl/${id}.noble.ts`;
const nativePath = `../universal/crypto/crypto-hash/src/primitives/${id}/impl/${id}.native.ts`;
const impls = await loadImpls<(data: Uint8Array) => Uint8Array>(
noblePath,
nativePath,
(m) => m[id] as (data: Uint8Array) => Uint8Array
);
if (impls === "pending") {
console.log(
`[fuzz] ${id}: native is PrimitiveNotImplemented, skipping`
);
return;
}
const result = diffFuzz<Uint8Array, Uint8Array>({
iterations: FUZZ_ITERATIONS,
sample: (_, i) => randomBytes((i % 256) + 1),
implA: impls.noble,
implB: impls.native,
equals: bytesEqual,
});
if (result.mismatches > 0) {
console.error(`[fuzz] ${id}: ${result.mismatches} mismatches`);
console.error(result.firstMismatch);
process.exit(1);
}
console.log(
`[fuzz] ${id}: ${result.iterations} iterations OK (0 mismatches)`
);
}
const HASH_ONE_SHOT_IDS = ["sha256", "sha384", "sha512"] as const;
async function main(): Promise<void> {
const jobs: PrimitiveJob[] = HASH_ONE_SHOT_IDS.map((id) => ({
id,
category: "hash",
run: () => fuzzHashOneShot(id),
}));
// Note: multi-algo primitives (hmac, hkdf, pbkdf2, argon2id), curves, and
// ciphers need richer sample builders. As their natives land the driver
// should be extended to call them. For now every native throws
// PrimitiveNotImplemented so the skip-on-probe path covers them via the
// per-impl loader — we simply do not register them here yet because the
// noble/native signatures differ from the one-shot hash shape.
for (const job of jobs) {
await job.run();
}
console.log("[fuzz] done");
}
if (import.meta.main) {
await main();
}