156 lines
5.2 KiB
TypeScript
156 lines
5.2 KiB
TypeScript
/**
|
|
* Spec-conformance tests for `openspec/specs/crypto-cipher/spec.md`.
|
|
*
|
|
* Pins structural scenarios that the per-primitive unit suites do not
|
|
* cover directly: every AEAD has both `noble.ts` and `native.ts`,
|
|
* every `*.select.ts` carries the GENERATED banner, every
|
|
* `*.native.ts` is currently a `PrimitiveNotImplemented` stub
|
|
* (runtime reflection of the dangerous-tier flip gate), the package
|
|
* is in a consistent pre/post-Phase-0.5 vendoring state, and no
|
|
* primitive source file consumes a CSPRNG (nonce-is-caller-supplied).
|
|
*/
|
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
|
|
const ROOT = join(__dirname, "..");
|
|
const SRC = join(ROOT, "src");
|
|
const PRIMITIVES = join(SRC, "primitives");
|
|
|
|
const AEAD_IDS = [
|
|
"aes-256-gcm",
|
|
"chacha20-poly1305",
|
|
"xchacha20-poly1305",
|
|
] as const;
|
|
|
|
function* walk(dir: string): Generator<string> {
|
|
for (const entry of readdirSync(dir)) {
|
|
const full = join(dir, entry);
|
|
const st = statSync(full);
|
|
if (
|
|
st.isDirectory() &&
|
|
!full.includes("node_modules") &&
|
|
!full.includes("dist")
|
|
) {
|
|
yield* walk(full);
|
|
} else if (full.endsWith(".ts") && !full.endsWith(".test.ts")) {
|
|
yield full;
|
|
}
|
|
}
|
|
}
|
|
|
|
describe("crypto-cipher — universal AEAD primitive surface", () => {
|
|
it("every AEAD id has its primitive directory", { timeout: 5000 }, () => {
|
|
for (const id of AEAD_IDS) {
|
|
expect(existsSync(join(PRIMITIVES, id))).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("crypto-cipher — per-primitive replaceability layout", () => {
|
|
for (const id of AEAD_IDS) {
|
|
it(`${id} has both noble.ts and native.ts impl files`, {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
existsSync(
|
|
join(
|
|
PRIMITIVES,
|
|
id,
|
|
"impl",
|
|
`${id}.noble`,
|
|
`${id}.noble.ts`
|
|
)
|
|
)
|
|
).toBe(true);
|
|
expect(
|
|
existsSync(
|
|
join(
|
|
PRIMITIVES,
|
|
id,
|
|
"impl",
|
|
`${id}.native`,
|
|
`${id}.native.ts`
|
|
)
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
it(`${id}.select.ts carries the GENERATED banner`, {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const sel = join(PRIMITIVES, id, `${id}.select`, `${id}.select.ts`);
|
|
expect(existsSync(sel)).toBe(true);
|
|
const c = readFileSync(sel, "utf8");
|
|
expect(c).toMatch(/GENERATED/);
|
|
expect(c).toMatch(/DO NOT EDIT/);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("crypto-cipher — vendored noble source (Phase 0.5)", () => {
|
|
it("is a Phase-0.5 forward gate; current state is pre-0.5 (noble dep + no vendored/)", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const pkg = JSON.parse(
|
|
readFileSync(join(ROOT, "package.json"), "utf8")
|
|
);
|
|
const noblePresent =
|
|
"@noble/ciphers" in (pkg.dependencies ?? {}) ||
|
|
"@noble/ciphers" in (pkg.peerDependencies ?? {});
|
|
const vendoredPresent = existsSync(join(SRC, "vendored"));
|
|
const upstreamFile = existsSync(join(ROOT, "NOBLE_UPSTREAM.md"));
|
|
const pre05 = noblePresent && !vendoredPresent && !upstreamFile;
|
|
const post05 = !noblePresent && vendoredPresent && upstreamFile;
|
|
expect(pre05 || post05).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("crypto-cipher — constant-time and dangerous-tier reflection", () => {
|
|
for (const id of AEAD_IDS) {
|
|
it(`${id}.native.ts is currently a PrimitiveNotImplemented stub`, {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const src = readFileSync(
|
|
join(PRIMITIVES, id, "impl", `${id}.native`, `${id}.native.ts`),
|
|
"utf8"
|
|
);
|
|
expect(src).toMatch(/PrimitiveNotImplemented/);
|
|
});
|
|
}
|
|
|
|
it("every native is a stub today, so no AEAD has been flipped without an audit", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const offenders: string[] = [];
|
|
for (const id of AEAD_IDS) {
|
|
const src = readFileSync(
|
|
join(PRIMITIVES, id, "impl", `${id}.native`, `${id}.native.ts`),
|
|
"utf8"
|
|
);
|
|
if (!/PrimitiveNotImplemented/.test(src)) offenders.push(id);
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("crypto-cipher — nonce is caller-supplied", () => {
|
|
it("no primitive source file consumes a CSPRNG (no Math.random / crypto.getRandomValues / randomBytes)", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const banned = [
|
|
/\bMath\.random\b/,
|
|
/\bcrypto\.getRandomValues\b/,
|
|
/\brandomBytes\b/,
|
|
];
|
|
const hits: string[] = [];
|
|
for (const file of walk(SRC)) {
|
|
const c = readFileSync(file, "utf8");
|
|
for (const re of banned) {
|
|
if (re.test(c)) hits.push(`${file}: ${re}`);
|
|
}
|
|
}
|
|
expect(hits).toEqual([]);
|
|
});
|
|
});
|