123 lines
4.1 KiB
TypeScript
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();
|
|
}
|