Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# diffFuzz — differential-fuzz harness
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Run N iterations of random input against two implementations of the same primitive and report the first diverging reproducer, plus the total number of mismatches. Designed specifically for the "noble vs native" gating in `crypto-primitive-replaceability` Phase 1+: before a native implementation is allowed to flip to `'native'` in `crypto.config.ts`, it must run through `diffFuzz` against the reference `noble` impl without a single mismatch across millions of random inputs.
|
||||
|
||||
## Features
|
||||
|
||||
- Deterministic `byteSource` injection for reproducible fuzz campaigns.
|
||||
- Generic over input and output types — works for hashes, ciphers, MACs, curve ops.
|
||||
- Captures first reproducer with the exact input, both outputs, and iteration index for CI log forensics.
|
||||
- Continues running after the first mismatch so the `mismatches` count approximates the rate, not just the existence of a bug.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { diffFuzz } from "@suaveplan/crypto-utils/testing";
|
||||
|
||||
const result = diffFuzz({
|
||||
iterations: 1000,
|
||||
sample: (src) => src(32),
|
||||
implA: (buf: Uint8Array) => buf.reduce((a, b) => a + b, 0),
|
||||
implB: (buf: Uint8Array) => buf.reduce((a, b) => a + b, 0),
|
||||
equals: (a, b) => a === b,
|
||||
});
|
||||
console.log(result.mismatches); // 0
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { diffFuzz } from "@suaveplan/crypto-utils/testing";
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";
|
||||
|
||||
// Differential fuzz SHA-256 noble vs native
|
||||
declare function sha256Noble(msg: Uint8Array): Uint8Array;
|
||||
declare function sha256Native(msg: Uint8Array): Uint8Array;
|
||||
|
||||
const result = diffFuzz({
|
||||
iterations: 100_000,
|
||||
sample: (src, iter) => src(iter % 4096), // variable-length messages
|
||||
implA: sha256Noble,
|
||||
implB: sha256Native,
|
||||
equals: constantTimeEqual,
|
||||
});
|
||||
|
||||
if (result.firstMismatch !== undefined) {
|
||||
const { iter, input, outputA, outputB } = result.firstMismatch;
|
||||
throw new Error(`SHA-256 diff at iter=${iter} len=${input.length} noble=${outputA.length}B native=${outputB.length}B`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `diffFuzz<V, E>(opts: DiffFuzzOptions<V, E>): DiffFuzzResult<V, E>`
|
||||
- `opts.iterations: number` — how many trials to run.
|
||||
- `opts.sample: (byteSource, iter) => V` — builds a fresh input each iteration. Receives the iteration index so callers can vary input shape by iteration.
|
||||
- `opts.implA`, `opts.implB: (input: V) => E`
|
||||
- `opts.equals: (a: E, b: E) => boolean`
|
||||
- `opts.byteSource?: ByteSource` — optional deterministic source; defaults to `defaultByteSource`.
|
||||
- Returns `{ iterations, mismatches, firstMismatch? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The loop is intentionally unrolled to a single pair of calls per iteration — no async, no microtasks, no chunking. For 100 000 iterations of SHA-256 this completes in a few hundred milliseconds in Node 20+. Callers that need structured output (e.g. `DifferentialFuzzCorpus` for CI regression pinning) should compose on top of this primitive rather than complicate its API. The `byteSource` injection is how reproducibility is achieved: when a CI job reports a fail, the caller can replay the run with a fixed source seeded from the failing iteration's context.
|
||||
@@ -0,0 +1,118 @@
|
||||
# dudect
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`dudect` is a minimal constant-time smoke check for secret-handling code.
|
||||
It runs a function many times against two input classes — a fixed / low-entropy
|
||||
class and a random / high-entropy class — and applies Welch's t-test to the
|
||||
per-call latencies. A large absolute t-statistic suggests the function's
|
||||
runtime depends on its input, i.e. a potential timing side channel.
|
||||
|
||||
The harness is a statistical signal, not cryptographic analysis. It catches
|
||||
the obvious class of leaks (secret-indexed table lookups, early returns on
|
||||
secret byte comparisons) on hardware where the noise floor is low enough. It
|
||||
does NOT prove constant-time behaviour in a formal sense; production primitives
|
||||
still require code review and, ideally, formally-verified implementations.
|
||||
|
||||
The `crypto:timing` turbo task wires this harness up to run nightly against
|
||||
any `impl/*.native.ts` in `crypto-hash`, `crypto-curve`, or `crypto-cipher`
|
||||
that isn't still a `PrimitiveNotImplemented` stub. In Phase 0 every native
|
||||
is a stub, so the nightly runs a no-op and exits clean — the wiring is live
|
||||
and fires the moment a real native lands.
|
||||
|
||||
## Features
|
||||
|
||||
- Welch's t-test on interleaved class-A / class-B latency samples
|
||||
- Configurable total measurement count, warmup, and t-threshold
|
||||
- Deterministic control-flow — no I/O, no network, no filesystem
|
||||
- Returns numeric result (mean, variance, t-statistic) plus boolean verdict
|
||||
- Reference implementation of the test from Reparaz et al., DATE 2017
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { dudect } from "@suaveplan/crypto-utils/testing/dudect/dudect";
|
||||
|
||||
const result = dudect<Uint8Array>({
|
||||
fn: (buf) => nativeSha256(buf),
|
||||
sampleFixed: () => new Uint8Array(64),
|
||||
sampleRandom: () => crypto.getRandomValues(new Uint8Array(64)),
|
||||
});
|
||||
|
||||
if (!result.constantTime) {
|
||||
throw new Error(`sha256 native leaked timing: t=${result.tStatistic}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { dudect } from "@suaveplan/crypto-utils/testing/dudect/dudect";
|
||||
import { hmac as nativeHmac } from "@suaveplan/crypto-hash/primitives/hmac/impl/hmac.native";
|
||||
|
||||
const key = new Uint8Array(32);
|
||||
|
||||
const result = dudect<Uint8Array>({
|
||||
fn: (msg) => nativeHmac("SHA-256", key, msg),
|
||||
sampleFixed: () => new Uint8Array(128), // all zeros
|
||||
sampleRandom: () => {
|
||||
const buf = new Uint8Array(128);
|
||||
crypto.getRandomValues(buf);
|
||||
return buf;
|
||||
},
|
||||
measurements: 100_000,
|
||||
warmup: 5_000,
|
||||
tThreshold: 4.5,
|
||||
});
|
||||
|
||||
console.log({
|
||||
meanFixed: result.meanA,
|
||||
meanRandom: result.meanB,
|
||||
t: result.tStatistic,
|
||||
verdict: result.constantTime ? "PASS" : "LEAK",
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `dudect<V>(opts: DudectOptions<V>): DudectResult`
|
||||
|
||||
Run the dudect t-test.
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `opts.fn` — function under test. Must do real work; a short-circuiting
|
||||
`fn` produces meaningless statistics.
|
||||
- `opts.sampleFixed` — returns a class-A (fixed / low-entropy) input.
|
||||
- `opts.sampleRandom` — returns a class-B (random / high-entropy) input of
|
||||
the same shape as class A.
|
||||
- `opts.measurements` — total timed calls, split evenly between the two
|
||||
classes. Default `20_000`.
|
||||
- `opts.warmup` — pre-measurement iterations discarded. Default `1_000`.
|
||||
- `opts.tThreshold` — absolute-value threshold for the leak verdict.
|
||||
Default `4.5` (per Reparaz et al.).
|
||||
|
||||
**Returns** `DudectResult` with per-class counts, means, variances, the
|
||||
Welch t-statistic, the configured threshold, and a boolean `constantTime`
|
||||
verdict (`true` when `|t| < threshold`).
|
||||
|
||||
**Throws** never; degenerate inputs (zero variance in both classes) yield
|
||||
`tStatistic === 0` which registers as constant-time.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `performance.now()` provides sub-ms resolution under Bun and modern Node.
|
||||
Precision degrades in sandboxed environments; interpret tight verdicts
|
||||
under VM / container load with care.
|
||||
- Class A and class B measurements are interleaved one-for-one. That smooths
|
||||
out GC pauses and clock drift that would otherwise bias the comparison.
|
||||
- Welch's t-test is the right tool because class-A and class-B variances
|
||||
are rarely equal in JavaScript — we cannot rely on Student's t.
|
||||
- The harness is intentionally small (~130 lines). It is not a replacement
|
||||
for formal constant-time analysis or `valgrind --track-origins` on a
|
||||
native binary; it is a cheap continuous-integration signal.
|
||||
- Hardware variability matters. CI-runner results should be interpreted
|
||||
relative to a historical baseline, not as an absolute pass/fail.
|
||||
@@ -0,0 +1,70 @@
|
||||
# runVectors — KAT harness
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Generic Known-Answer-Test runner. Given a list of `TestVector`s (input + expected output), a list of `Impl`s (functions that map input → output), and an equality predicate, it executes the cross product and returns structured pass / fail / pending counts per implementation. Every `impl/<id>.noble.ts` and `impl/<id>.native.ts` pair across the `@suaveplan/crypto-*` family drives its own test suite through this harness so the pass/pending/fail matrix is reported uniformly.
|
||||
|
||||
## Features
|
||||
|
||||
- Generic over input and output types.
|
||||
- Catches `PrimitiveNotImplemented` and marks those vectors "pending" so unimplemented native tiers don't colour red.
|
||||
- Per-impl summary: `pass`, `fail`, `pending` counts.
|
||||
- Preserves vector order in the per-impl result list so failing vectors can be located by name.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { runVectors } from "@suaveplan/crypto-utils/testing";
|
||||
|
||||
const report = runVectors({
|
||||
vectors: [{ name: "v1", input: 2, expected: 4 }],
|
||||
impls: [{ name: "square", run: (n: number) => n * n }],
|
||||
equals: (a, b) => a === b,
|
||||
});
|
||||
console.log(report[0].summary); // { pass: 1, fail: 0, pending: 0 }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { runVectors } from "@suaveplan/crypto-utils/testing";
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";
|
||||
import { fromHex } from "@suaveplan/crypto-utils/encoding";
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
type V = { msg: Uint8Array };
|
||||
type E = Uint8Array;
|
||||
|
||||
const vectors = [
|
||||
{ name: "empty", input: { msg: new Uint8Array() }, expected: fromHex("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") },
|
||||
];
|
||||
|
||||
const report = runVectors<V, E>({
|
||||
vectors,
|
||||
impls: [
|
||||
{ name: "noble", run: ({ msg }) => sha256Noble(msg) },
|
||||
{ name: "native", run: () => { throw new PrimitiveNotImplemented("sha256"); } },
|
||||
],
|
||||
equals: constantTimeEqual,
|
||||
});
|
||||
|
||||
declare function sha256Noble(m: Uint8Array): Uint8Array;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runVectors<V, E>(opts: RunVectorsOptions<V, E>): readonly ImplReport[]`
|
||||
- `opts.vectors: readonly TestVector<V, E>[]`
|
||||
- `opts.impls: readonly Impl<V, E>[]`
|
||||
- `opts.equals: (a: E, b: E) => boolean` — for byte outputs, pass `constantTimeEqual`; for structural outputs, pass a deep-equal.
|
||||
- `type TestVector<V, E> = { name: string; input: V; expected: E }`
|
||||
- `type Impl<V, E> = { name: string; run: (input: V) => E }`
|
||||
- `type VectorResult = { name: string; status: 'pass' | 'fail' | 'pending'; reason?: string }`
|
||||
- `type ImplReport = { impl: string; results: readonly VectorResult[]; summary: { pass; fail; pending } }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The harness detects `PrimitiveNotImplemented` by checking `err.constructor.name === 'PrimitiveNotImplemented'` rather than `instanceof`. This decouples the harness from the error class's module identity (which would fail across bundler boundaries and `vite-node` hot reloads) and matches the convention used by `@noble/hashes`' own test helpers. Any other exception is captured as `fail` with its `message` as the `reason`. The harness is intentionally synchronous — KAT vectors are always fast — which keeps Vitest reporting tight.
|
||||
Reference in New Issue
Block a user