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.
166 lines
7.0 KiB
JavaScript
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);
|
|
});
|
|
});
|