88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
/**
|
|
* Nightly dudect-style timing driver. For every native impl that is not a
|
|
* `PrimitiveNotImplemented` stub, sample its runtime across fixed vs. random
|
|
* inputs and flag t-statistics above the threshold as potential leaks.
|
|
*
|
|
* In Phase 0 every native is a stub — the driver probes each, catches
|
|
* `PrimitiveNotImplemented`, skips, and exits clean. The wiring fires the
|
|
* moment a real native lands.
|
|
*/
|
|
|
|
import { dudect } from "../universal/crypto/crypto-utils/src/testing/dudect/dudect.ts";
|
|
|
|
/** Stable code emitted by `PrimitiveNotImplemented`. */
|
|
const NOT_IMPLEMENTED_CODE = "CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED";
|
|
|
|
const MEASUREMENTS = Number.parseInt(
|
|
process.env.CRYPTO_TIMING_MEASUREMENTS ?? "20000",
|
|
10
|
|
);
|
|
const T_THRESHOLD = Number.parseFloat(
|
|
process.env.CRYPTO_TIMING_T_THRESHOLD ?? "4.5"
|
|
);
|
|
|
|
function randomBytes(len: number): Uint8Array {
|
|
const buf = new Uint8Array(len);
|
|
crypto.getRandomValues(buf);
|
|
return buf;
|
|
}
|
|
|
|
async function probeAndRun(
|
|
id: string,
|
|
category: "hash",
|
|
sampleFixed: () => unknown,
|
|
sampleRandom: () => unknown,
|
|
pickFn: (mod: Record<string, unknown>) => (input: unknown) => unknown
|
|
): Promise<void> {
|
|
const nativePath = `../universal/crypto/crypto-${category}/src/primitives/${id}/impl/${id}.native.ts`;
|
|
const mod = (await import(nativePath)) as Record<string, unknown>;
|
|
const fn = pickFn(mod);
|
|
try {
|
|
fn(sampleFixed());
|
|
} catch (err) {
|
|
if (
|
|
typeof err === "object" &&
|
|
err !== null &&
|
|
(err as { code?: unknown }).code === NOT_IMPLEMENTED_CODE
|
|
) {
|
|
console.log(
|
|
`[timing] ${id}: native is PrimitiveNotImplemented, skipping`
|
|
);
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
const result = dudect({
|
|
fn,
|
|
sampleFixed,
|
|
sampleRandom,
|
|
measurements: MEASUREMENTS,
|
|
tThreshold: T_THRESHOLD,
|
|
});
|
|
console.log(
|
|
`[timing] ${id}: t=${result.tStatistic.toFixed(3)} (threshold ${result.tThreshold}) → ${
|
|
result.constantTime ? "OK" : "LEAK"
|
|
}`
|
|
);
|
|
if (!result.constantTime) process.exit(1);
|
|
}
|
|
|
|
const HASH_ONE_SHOT_IDS = ["sha256", "sha384", "sha512"] as const;
|
|
|
|
async function main(): Promise<void> {
|
|
for (const id of HASH_ONE_SHOT_IDS) {
|
|
await probeAndRun(
|
|
id,
|
|
"hash",
|
|
() => new Uint8Array(64), // class A: all zeros, fixed length
|
|
() => randomBytes(64), // class B: random bytes, same length
|
|
(mod) => mod[id] as (input: unknown) => unknown
|
|
);
|
|
}
|
|
console.log("[timing] done");
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
await main();
|
|
}
|