const { expect } = require("chai"); const { ethers } = require("hardhat"); // --------------------------------------------------------------------------- // AereCryptoRegistry: canonical scheme-ID constants (crypto-agility hardening). // // Covers the PURELY ADDITIVE SCHEME_* constants added per // docs/PQ-AGILITY-HARDENING-2026-07-18.md: // - every constant resolves to its canonical number (ids 1..14), // - the genesis constants (1..5) equal the ids seedLiveSchemes assigns, // - all 14 ids are distinct, // - a RESERVED id (ML-DSA-65) registered against a verifier resolves end to end // via precompileFor / resolveActive / isUsable, // - an ML-KEM discovery entry (isSignature=false) is refused by the signature-only // verify() path. // // These constants are reference values only: ids 6..14 name reserved parameter sets and // do NOT seed, activate, or route anything until governance adds a row. No precompile mock // is needed here (no cryptographic verify is exercised; the reserved verifier is a // placeholder address used only for routing/lookup). // --------------------------------------------------------------------------- const EXPECTED = { // genesis (live on chain 2800) SCHEME_FALCON512: 1, SCHEME_FALCON1024: 2, SCHEME_MLDSA44: 3, SCHEME_SLHDSA128S: 4, SCHEME_SHAKE256: 5, // reserved future schemes (no live verifier yet) SCHEME_MLDSA65: 6, SCHEME_MLDSA87: 7, SCHEME_FALCON512_PADDED: 8, SCHEME_FALCON1024_PADDED: 9, SCHEME_SLHDSA192S: 10, SCHEME_SLHDSA256S: 11, SCHEME_MLKEM512: 12, SCHEME_MLKEM768: 13, SCHEME_MLKEM1024: 14, }; // The five live precompile addresses seedLiveSchemes wires (scheme => verifier). const LIVE_PRECOMPILE = { 1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), 2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), 3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), 4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), }; // A stand-in verifier for a RESERVED scheme. Nonzero (addAlgorithm requires it) but never // staticcalled by this test: only registration/lookup/resolution are exercised, not verify. const PLACEHOLDER_VERIFIER = ethers.getAddress("0x00000000000000000000000000000000000c0de1"); const Status = { UNKNOWN: 0, ACTIVE: 1, DEPRECATED: 2, REVOKED: 3 }; describe("AereCryptoRegistry: canonical scheme-ID constants", function () { let reg; beforeEach(async function () { const R = await ethers.getContractFactory("AereCryptoRegistry"); reg = await R.deploy(); await reg.waitForDeployment(); }); it("every SCHEME_* constant resolves to its canonical number", async function () { for (const [name, value] of Object.entries(EXPECTED)) { const got = Number(await reg[name]()); expect(got, name).to.equal(value); } }); it("assigns 14 distinct ids across the whole canonical space", async function () { const seen = new Set(); for (const name of Object.keys(EXPECTED)) { const v = Number(await reg[name]()); expect(seen.has(v), `duplicate id ${v} at ${name}`).to.equal(false); seen.add(v); } expect(seen.size).to.equal(14); }); it("genesis constants equal the scheme ids seedLiveSchemes assigns", async function () { await reg.seedLiveSchemes(); // Row id == scheme number for the seeded set; assert the row for each genesis constant // carries that same scheme tag and (for signature schemes) routes to the live precompile. const genesis = [ { c: "SCHEME_FALCON512", id: 1, live: true }, { c: "SCHEME_FALCON1024", id: 2, live: true }, { c: "SCHEME_MLDSA44", id: 3, live: true }, { c: "SCHEME_SLHDSA128S", id: 4, live: true }, { c: "SCHEME_SHAKE256", id: 5, live: false }, // hash-only, not a routable signature ]; for (const g of genesis) { const scheme = Number(await reg[g.c]()); expect(scheme).to.equal(g.id); const a = await reg.getAlgorithm(g.id); expect(Number(a.scheme), g.c).to.equal(scheme); if (g.live) { expect(await reg.precompileFor(scheme)).to.equal(LIVE_PRECOMPILE[g.id]); expect(await reg.isUsable(g.id)).to.equal(true); } else { // SHAKE256 is ACTIVE but hash-only: not usable as a signature verifier. expect(await reg.isActive(g.id)).to.equal(true); expect(await reg.isUsable(g.id)).to.equal(false); } } }); it("a RESERVED signature id (ML-DSA-65) resolves end to end once a verifier is registered", async function () { await reg.seedLiveSchemes(); const scheme = Number(await reg.SCHEME_MLDSA65()); // 6 // Real FIPS 204 ML-DSA-65 lengths; fixed-concatenation family (esigHeader 0). const ML_DSA_65_PK = 1952; const ML_DSA_65_SIG = 3309; const tx = await reg.addAlgorithm( "ML-DSA-65", scheme, PLACEHOLDER_VERIFIER, ML_DSA_65_PK, ML_DSA_65_SIG, 0, 380000, true ); const rc = await tx.wait(); const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "AlgorithmAdded"); const id = Number(log.args.id); expect(id).to.equal(6); // first row after the 5 seeded // The reserved id now resolves through the registry's routing surface. const a = await reg.getAlgorithm(id); expect(Number(a.scheme)).to.equal(scheme); expect(a.verifier).to.equal(PLACEHOLDER_VERIFIER); expect(Number(a.pubKeyLen)).to.equal(ML_DSA_65_PK); expect(Number(a.sigLen)).to.equal(ML_DSA_65_SIG); expect(a.isSignature).to.equal(true); expect(Number(a.status)).to.equal(Status.ACTIVE); expect(await reg.precompileFor(scheme)).to.equal(PLACEHOLDER_VERIFIER); expect(Number(await reg.resolveActive(id))).to.equal(id); // ACTIVE resolves to itself expect(await reg.isUsable(id)).to.equal(true); }); it("an ML-KEM discovery entry (isSignature=false) is refused by the signature-only verify()", async function () { await reg.seedLiveSchemes(); const scheme = Number(await reg.SCHEME_MLKEM512()); // 12 // ML-KEM-512 (FIPS 203): encapsulation key 800 bytes; registered as a NON-signature // discovery entry, so verify() must refuse to route it (fail-closed, no revert). const ML_KEM_512_EK = 800; const tx = await reg.addAlgorithm( "ML-KEM-512", scheme, PLACEHOLDER_VERIFIER, ML_KEM_512_EK, 0, 0, 40000, false ); const rc = await tx.wait(); const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "AlgorithmAdded"); const id = Number(log.args.id); const a = await reg.getAlgorithm(id); expect(Number(a.scheme)).to.equal(scheme); expect(a.isSignature).to.equal(false); // Discovery entry: ACTIVE (discoverable) but never a usable signature verifier. expect(await reg.isActive(id)).to.equal(true); expect(await reg.isUsable(id)).to.equal(false); // verify() returns false for a non-signature row without ever touching a verifier. const anyPk = "0x" + "00".repeat(ML_KEM_512_EK); const anyMsg = ethers.keccak256(ethers.toUtf8Bytes("kem")); const anySig = "0x" + "11".repeat(64); expect(await reg.verify(id, anyPk, anyMsg, anySig)).to.equal(false); }); });