/** * 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; } 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( noblePath: string, nativePath: string, pick: (mod: Record) => T ): Promise<{ noble: T; native: T } | "pending"> { const [nobleMod, nativeMod] = await Promise.all([ import(noblePath), import(nativePath), ]); const noble = pick(nobleMod as Record); const native = pick(nativeMod as Record); // 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 { 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({ 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 { 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(); }