aere-contracts/test/AereCryptoRegistry.agility.test.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

166 lines
7.0 KiB
JavaScript

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);
});
});