Initial Commit
This commit is contained in:
122
scripts/crypto-fuzz.ts
Normal file
122
scripts/crypto-fuzz.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user