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.
603 lines
27 KiB
JavaScript
603 lines
27 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers, network } = require("hardhat");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AereHybridAuthorizer — a crypto-agility CONSUMER over AereCryptoRegistry.
|
|
//
|
|
// The authorizer never pins a scheme: every check calls registry.resolveActive(id) to
|
|
// follow the successor chain to the live ACTIVE row, then verifies through the registry's
|
|
// routed verifier. So a Foundation-governed scheme swap on the REGISTRY (deprecate old ->
|
|
// setSuccessor -> new ACTIVE) migrates this consumer with NO redeploy.
|
|
//
|
|
// Like the registry test, Hardhat has no PQC precompile, so we install MockPQCPrecompile
|
|
// at 0x0AE1..0x0AE4 (hardhat_setCode). The mock parses the input at the precompiles' SPEC
|
|
// offsets and accepts iff commitment == keccak256(pk || message); the registry builds the
|
|
// SAME wire format, so a positive authorize through the registry proves correct encoding.
|
|
// The REAL encoding is separately proven against the LIVE mainnet precompiles via eth_call
|
|
// (AerePQCAttestation suite / conformance runner). Mock-vs-live caveat: these tests prove
|
|
// the ROUTING/POLICY/AGILITY logic, not the cryptography of the native precompiles.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const PRECOMPILE = {
|
|
1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), // Falcon-512
|
|
2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), // Falcon-1024
|
|
3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), // ML-DSA-44
|
|
4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), // SLH-DSA-128s
|
|
};
|
|
|
|
const META = {
|
|
1: { name: "Falcon-512", pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, sigLen: 0, falcon: true, gas: 120000 },
|
|
2: { name: "Falcon-1024", pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, sigLen: 0, falcon: true, gas: 145000 },
|
|
3: { name: "ML-DSA-44", pkLen: 1312, esigHeader: 0x00, sigLen: 2420, falcon: false, gas: 351000 },
|
|
4: { name: "SLH-DSA-128s", pkLen: 32, esigHeader: 0x00, sigLen: 7856, falcon: false, gas: 400000 },
|
|
};
|
|
|
|
const Status = { UNKNOWN: 0, ACTIVE: 1, DEPRECATED: 2, REVOKED: 3 };
|
|
|
|
// Deterministic seeded RNG (keccak hash-chain) — identical to AereCryptoRegistry.test.js.
|
|
function makeRng(seedText) {
|
|
let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
|
|
function bytes(n) {
|
|
const out = new Uint8Array(n);
|
|
let i = 0;
|
|
while (i < n) {
|
|
seed = ethers.keccak256(seed);
|
|
const chunk = ethers.getBytes(seed);
|
|
for (let j = 0; j < chunk.length && i < n; j++, i++) out[i] = chunk[j];
|
|
}
|
|
return out;
|
|
}
|
|
function int(n) {
|
|
const b = bytes(4);
|
|
return (((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]) >>> 0) % n;
|
|
}
|
|
return { bytes, int };
|
|
}
|
|
|
|
function makePubKey(scheme, rng) {
|
|
const m = META[scheme];
|
|
const raw = rng.bytes(m.pkLen);
|
|
if (m.falcon) raw[0] = m.pkHeader;
|
|
return ethers.hexlify(raw);
|
|
}
|
|
|
|
function commitmentFor(pubKey, message) {
|
|
return ethers.keccak256(ethers.concat([pubKey, message]));
|
|
}
|
|
|
|
function envelope(scheme, commitment, rng) {
|
|
const m = META[scheme];
|
|
if (m.falcon) {
|
|
const nonce = rng.bytes(40);
|
|
const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]); // 33 bytes
|
|
return ethers.hexlify(ethers.concat([nonce, esig]));
|
|
} else {
|
|
const sig = new Uint8Array(m.sigLen);
|
|
sig.set(ethers.getBytes(commitment), 0);
|
|
return ethers.hexlify(sig);
|
|
}
|
|
}
|
|
|
|
function genuine(scheme, pubKey, message, rng) {
|
|
return envelope(scheme, commitmentFor(pubKey, message), rng);
|
|
}
|
|
|
|
function tamperCommitment(scheme, sigHex) {
|
|
const b = ethers.getBytes(sigHex);
|
|
const idx = META[scheme].falcon ? 41 : 0;
|
|
b[idx] ^= 0x01;
|
|
return ethers.hexlify(b);
|
|
}
|
|
|
|
// Convenience: registry.addAlgorithm args for a scheme's live shape, pointing at its mock.
|
|
function algoArgs(scheme, name) {
|
|
const m = META[scheme];
|
|
return [name, scheme, PRECOMPILE[scheme], m.pkLen, m.sigLen, m.esigHeader, m.gas, true];
|
|
}
|
|
|
|
describe("AereHybridAuthorizer", function () {
|
|
let reg, auth, owner, other, mockCode;
|
|
|
|
before(async function () {
|
|
[owner, other] = await ethers.getSigners();
|
|
mockCode = {};
|
|
const F = await ethers.getContractFactory("MockPQCPrecompile");
|
|
for (const scheme of [1, 2, 3, 4]) {
|
|
const mock = await F.deploy(scheme);
|
|
await mock.waitForDeployment();
|
|
mockCode[scheme] = await ethers.provider.getCode(await mock.getAddress());
|
|
}
|
|
});
|
|
|
|
async function installMocks() {
|
|
for (const scheme of [1, 2, 3, 4]) {
|
|
await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]);
|
|
}
|
|
}
|
|
|
|
// Add an algorithm row to the registry, return its id.
|
|
async function addAlgo(scheme, name, signer = owner) {
|
|
const tx = await reg.connect(signer).addAlgorithm(...algoArgs(scheme, name));
|
|
const rc = await tx.wait();
|
|
const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "AlgorithmAdded");
|
|
return Number(log.args.id);
|
|
}
|
|
|
|
async function deployAuthorizer(defaultId) {
|
|
const A = await ethers.getContractFactory("AereHybridAuthorizer");
|
|
const a = await A.deploy(await reg.getAddress(), defaultId);
|
|
await a.waitForDeployment();
|
|
return a;
|
|
}
|
|
|
|
beforeEach(async function () {
|
|
await installMocks();
|
|
const R = await ethers.getContractFactory("AereCryptoRegistry");
|
|
reg = await R.deploy();
|
|
await reg.waitForDeployment();
|
|
await reg.seedLiveSchemes(); // ids 1..5 = Falcon-512/1024, ML-DSA-44, SLH-DSA-128s, SHAKE256
|
|
auth = await deployAuthorizer(1); // default = Falcon-512
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Construction / wiring
|
|
// -----------------------------------------------------------------------
|
|
describe("construction", function () {
|
|
it("wires the registry, sets the deployer as owner and a usable default", async function () {
|
|
expect(await auth.registry()).to.equal(await reg.getAddress());
|
|
expect(await auth.owner()).to.equal(owner.address);
|
|
expect(Number(await auth.defaultAlgorithmId())).to.equal(1);
|
|
});
|
|
|
|
it("reverts ZeroRegistry for a zero registry", async function () {
|
|
const A = await ethers.getContractFactory("AereHybridAuthorizer");
|
|
await expect(A.deploy(ethers.ZeroAddress, 1)).to.be.revertedWithCustomError(auth, "ZeroRegistry");
|
|
});
|
|
|
|
it("reverts UnusableAlgorithm for a hash-only default (SHAKE256 id 5)", async function () {
|
|
const A = await ethers.getContractFactory("AereHybridAuthorizer");
|
|
await expect(A.deploy(await reg.getAddress(), 5)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
|
|
});
|
|
|
|
it("reverts UnusableAlgorithm for a nonexistent default id", async function () {
|
|
const A = await ethers.getContractFactory("AereHybridAuthorizer");
|
|
await expect(A.deploy(await reg.getAddress(), 999)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
|
|
});
|
|
|
|
it("reverts UnusableAlgorithm when the default id is REVOKED with no successor", async function () {
|
|
await reg.setStatus(2, Status.REVOKED); // Falcon-1024 revoked, no successor
|
|
const A = await ethers.getContractFactory("AereHybridAuthorizer");
|
|
await expect(A.deploy(await reg.getAddress(), 2)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// checkAuthorized (fail-closed routed happy path)
|
|
// -----------------------------------------------------------------------
|
|
describe("checkAuthorized (routed, fail-closed)", function () {
|
|
for (const scheme of [1, 2, 3, 4]) {
|
|
it(`authorizes a genuine ${META[scheme].name} signature and rejects a tampered one`, async function () {
|
|
const rng = makeRng("auth-" + scheme);
|
|
const pk = makePubKey(scheme, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("msg-" + scheme));
|
|
const good = genuine(scheme, pk, msg, rng);
|
|
// requested id == scheme id (seeded 1:1)
|
|
expect(await auth.checkAuthorized(scheme, pk, msg, good)).to.equal(true);
|
|
expect(await auth.checkAuthorized(scheme, pk, msg, tamperCommitment(scheme, good))).to.equal(false);
|
|
});
|
|
}
|
|
|
|
it("returns false (no revert) for an unknown requested id", async function () {
|
|
const rng = makeRng("unknown");
|
|
const pk = makePubKey(1, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
|
|
const sig = genuine(1, pk, msg, rng);
|
|
expect(await auth.checkAuthorized(0, pk, msg, sig)).to.equal(false);
|
|
expect(await auth.checkAuthorized(999, pk, msg, sig)).to.equal(false);
|
|
});
|
|
|
|
it("returns false when the message differs from the signed one", async function () {
|
|
const rng = makeRng("wrong-msg");
|
|
const pk = makePubKey(1, rng);
|
|
const signed = ethers.keccak256(ethers.toUtf8Bytes("signed"));
|
|
const otherMsg = ethers.keccak256(ethers.toUtf8Bytes("other"));
|
|
const sig = genuine(1, pk, signed, rng);
|
|
expect(await auth.checkAuthorized(1, pk, otherMsg, sig)).to.equal(false);
|
|
});
|
|
|
|
it("routes to a hash-only entry as false (SHAKE256 id 5 is not usable)", async function () {
|
|
const rng = makeRng("hash");
|
|
const pk = makePubKey(1, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
|
|
const sig = genuine(1, pk, msg, rng);
|
|
// resolveActive(5) returns 5 (ACTIVE) but registry.verify refuses a hash-only row.
|
|
expect(await auth.checkAuthorized(5, pk, msg, sig)).to.equal(false);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Per-account preference with a safe default
|
|
// -----------------------------------------------------------------------
|
|
describe("per-account preference", function () {
|
|
it("uses the app default for an account with no preference", async function () {
|
|
expect(Number(await auth.algorithmFor(other.address))).to.equal(1);
|
|
expect(Number(await auth.preferredAlgorithmOf(other.address))).to.equal(0);
|
|
expect(Number(await auth.resolvedAlgorithmFor(other.address))).to.equal(1);
|
|
});
|
|
|
|
it("lets an account pick a usable scheme and route checks under it", async function () {
|
|
await auth.connect(other).setPreferredAlgorithm(3); // ML-DSA-44
|
|
expect(Number(await auth.algorithmFor(other.address))).to.equal(3);
|
|
|
|
const rng = makeRng("pref-mldsa");
|
|
const pk = makePubKey(3, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("p"));
|
|
const sig = genuine(3, pk, msg, rng);
|
|
expect(await auth.checkAuthorizedFor(other.address, pk, msg, sig)).to.equal(true);
|
|
|
|
// A Falcon-512 signature (the default scheme) is NOT accepted for this account.
|
|
const fpk = makePubKey(1, rng);
|
|
const fsig = genuine(1, fpk, msg, rng);
|
|
expect(await auth.checkAuthorizedFor(other.address, fpk, msg, fsig)).to.equal(false);
|
|
});
|
|
|
|
it("clears a preference with id 0 and falls back to the safe default", async function () {
|
|
await auth.connect(other).setPreferredAlgorithm(3);
|
|
await auth.connect(other).setPreferredAlgorithm(0);
|
|
expect(Number(await auth.preferredAlgorithmOf(other.address))).to.equal(0);
|
|
expect(Number(await auth.algorithmFor(other.address))).to.equal(1);
|
|
});
|
|
|
|
it("rejects a preference that is unusable (hash-only / unknown / revoked-no-successor)", async function () {
|
|
await expect(auth.connect(other).setPreferredAlgorithm(5)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
|
|
await expect(auth.connect(other).setPreferredAlgorithm(999)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
|
|
await reg.setStatus(4, Status.REVOKED);
|
|
await expect(auth.connect(other).setPreferredAlgorithm(4)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
|
|
});
|
|
|
|
it("setPreferredAlgorithm is permissionless (any account, not owner-gated)", async function () {
|
|
await expect(auth.connect(other).setPreferredAlgorithm(2)).to.not.be.reverted;
|
|
expect(Number(await auth.algorithmFor(other.address))).to.equal(2);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// THE HEADLINE: a successor swap transparently changes the accepted scheme,
|
|
// with NO redeploy of the authorizer and the SAME requested id.
|
|
// -----------------------------------------------------------------------
|
|
describe("zero-redeploy scheme swap (deprecate -> successor -> new)", function () {
|
|
it("the same requested id starts accepting the NEW scheme and rejecting the OLD one", async function () {
|
|
const rng = makeRng("swap");
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("swap-msg"));
|
|
|
|
// Old world: id 1 == Falcon-512.
|
|
const fpk = makePubKey(1, rng);
|
|
const fsig = genuine(1, fpk, msg, rng);
|
|
// New world signature: an ML-DSA-44 key/sig.
|
|
const mpk = makePubKey(3, rng);
|
|
const msig = genuine(3, mpk, msg, rng);
|
|
|
|
// BEFORE the swap: id 1 accepts Falcon-512, rejects the ML-DSA-44 signature.
|
|
expect(await auth.checkAuthorized(1, fpk, msg, fsig)).to.equal(true);
|
|
expect(await auth.checkAuthorized(1, mpk, msg, msig)).to.equal(false);
|
|
|
|
// Foundation-governed swap on the REGISTRY (no authorizer redeploy):
|
|
// 1. register the successor (an ML-DSA-44-shaped row), ACTIVE
|
|
// 2. point id 1 -> successor
|
|
// 3. deprecate id 1
|
|
const newId = await addAlgo(3, "ML-DSA-44-succ"); // id 6, routes to the scheme-3 mock
|
|
await reg.setSuccessor(1, newId);
|
|
await reg.setStatus(1, Status.DEPRECATED);
|
|
|
|
// AFTER the swap: SAME requested id 1 now routes to the successor.
|
|
expect(Number(await auth.resolvedAlgorithmFor(owner.address))).to.equal(newId);
|
|
// The old Falcon-512 signature no longer authorizes; the new ML-DSA-44 one does.
|
|
expect(await auth.checkAuthorized(1, fpk, msg, fsig)).to.equal(false);
|
|
expect(await auth.checkAuthorized(1, mpk, msg, msig)).to.equal(true);
|
|
});
|
|
|
|
it("a multi-hop successor chain resolves to the final ACTIVE scheme", async function () {
|
|
const rng = makeRng("multi-hop");
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
|
|
|
|
const id6 = await addAlgo(2, "F1024-v2"); // ACTIVE
|
|
const id7 = await addAlgo(4, "SLH-v3"); // ACTIVE
|
|
await reg.setSuccessor(1, id6);
|
|
await reg.setSuccessor(id6, id7);
|
|
await reg.setStatus(1, Status.DEPRECATED);
|
|
await reg.setStatus(id6, Status.DEPRECATED);
|
|
|
|
expect(Number(await auth.resolvedAlgorithmFor(owner.address))).to.equal(id7);
|
|
// A genuine SLH-DSA-128s (scheme 4) signature authorizes under requested id 1.
|
|
const pk = makePubKey(4, rng);
|
|
const sig = genuine(4, pk, msg, rng);
|
|
expect(await auth.checkAuthorized(1, pk, msg, sig)).to.equal(true);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// A REVOKED scheme is NEVER authorized
|
|
// -----------------------------------------------------------------------
|
|
describe("REVOKED is never authorized", function () {
|
|
it("a revoked id with no successor authorizes nothing (fail-closed view, revert on authorize)", async function () {
|
|
const rng = makeRng("revoked");
|
|
const pk = makePubKey(1, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
|
|
const sig = genuine(1, pk, msg, rng);
|
|
|
|
expect(await auth.checkAuthorized(1, pk, msg, sig)).to.equal(true);
|
|
|
|
await reg.setStatus(1, Status.REVOKED); // no successor
|
|
// View fail-closes to false (resolveActive reverts NoActiveSuccessor, caught).
|
|
expect(await auth.checkAuthorized(1, pk, msg, sig)).to.equal(false);
|
|
// State-changing authorize surfaces the registry revert (NoActiveSuccessor).
|
|
await expect(auth.authorize(1, pk, msg, sig)).to.be.revertedWithCustomError(reg, "NoActiveSuccessor");
|
|
});
|
|
|
|
it("a revoked id is skipped for its ACTIVE successor; a signature under the revoked scheme still fails", async function () {
|
|
const rng = makeRng("revoked-succ");
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
|
|
const fpk = makePubKey(1, rng); // revoked-scheme (Falcon-512) key
|
|
const fsig = genuine(1, fpk, msg, rng);
|
|
const mpk = makePubKey(3, rng); // successor-scheme (ML-DSA-44) key
|
|
const msig = genuine(3, mpk, msg, rng);
|
|
|
|
const succ = await addAlgo(3, "ML-DSA-succ"); // ACTIVE
|
|
await reg.setSuccessor(1, succ);
|
|
await reg.setStatus(1, Status.REVOKED);
|
|
|
|
// The revoked scheme's own signature can NEVER authorize (routes to successor).
|
|
expect(await auth.checkAuthorized(1, fpk, msg, fsig)).to.equal(false);
|
|
// Only a signature valid under the ACTIVE successor authorizes.
|
|
expect(await auth.checkAuthorized(1, mpk, msg, msig)).to.equal(true);
|
|
expect(Number(await auth.resolvedAlgorithmFor(owner.address))).to.equal(succ);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Owner governance: setDefaultAlgorithm / migrate — only owner
|
|
// -----------------------------------------------------------------------
|
|
describe("owner governance (only-owner)", function () {
|
|
const OWNABLE_REVERT = "Ownable: caller is not the owner";
|
|
|
|
it("reverts for a non-owner on setDefaultAlgorithm / migrate", async function () {
|
|
await expect(auth.connect(other).setDefaultAlgorithm(2)).to.be.revertedWith(OWNABLE_REVERT);
|
|
await expect(auth.connect(other).migrate(1)).to.be.revertedWith(OWNABLE_REVERT);
|
|
});
|
|
|
|
it("owner can retarget the default to another usable scheme", async function () {
|
|
await expect(auth.setDefaultAlgorithm(3)).to.emit(auth, "DefaultAlgorithmSet").withArgs(3);
|
|
expect(Number(await auth.defaultAlgorithmId())).to.equal(3);
|
|
});
|
|
|
|
it("setDefaultAlgorithm rejects an unusable target (hash-only / unknown)", async function () {
|
|
await expect(auth.setDefaultAlgorithm(5)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
|
|
await expect(auth.setDefaultAlgorithm(999)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
|
|
});
|
|
|
|
it("migrate crystallizes the default onto the ACTIVE successor after a swap", async function () {
|
|
const newId = await addAlgo(2, "F1024-succ");
|
|
await reg.setSuccessor(1, newId);
|
|
await reg.setStatus(1, Status.DEPRECATED);
|
|
|
|
await expect(auth.migrate(1)).to.emit(auth, "DefaultMigrated").withArgs(1, newId);
|
|
expect(Number(await auth.defaultAlgorithmId())).to.equal(newId);
|
|
});
|
|
|
|
it("migrate is a no-op (no event, default unchanged) when the id is still ACTIVE", async function () {
|
|
await expect(auth.migrate(1)).to.not.emit(auth, "DefaultMigrated");
|
|
expect(Number(await auth.defaultAlgorithmId())).to.equal(1);
|
|
});
|
|
|
|
it("migrate reverts NotCurrentDefault when oldId != the current default", async function () {
|
|
await expect(auth.migrate(2)).to.be.revertedWithCustomError(auth, "NotCurrentDefault").withArgs(2, 1);
|
|
});
|
|
|
|
it("migrate reverts (from resolveActive) if the default dead-ends with no ACTIVE successor", async function () {
|
|
// Make the default (1) DEPRECATED with no successor -> resolveActive reverts.
|
|
await reg.setStatus(1, Status.DEPRECATED);
|
|
await expect(auth.migrate(1)).to.be.revertedWithCustomError(reg, "NoActiveSuccessor");
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// State-changing authorize: ledger + events
|
|
// -----------------------------------------------------------------------
|
|
describe("authorize (state-changing, recorded)", function () {
|
|
it("records a successful authorization and emits Authorized", async function () {
|
|
const rng = makeRng("authorize");
|
|
const pk = makePubKey(1, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("do-it"));
|
|
const sig = genuine(1, pk, msg, rng);
|
|
|
|
expect(await auth.isProofAuthorized(pk, msg)).to.equal(false);
|
|
await expect(auth.authorize(1, pk, msg, sig))
|
|
.to.emit(auth, "Authorized")
|
|
.withArgs(owner.address, 1, 1, msg);
|
|
|
|
expect(Number(await auth.authorizationCount())).to.equal(1);
|
|
expect(await auth.isProofAuthorized(pk, msg)).to.equal(true);
|
|
expect(Number(await auth.lastResolvedId())).to.equal(1);
|
|
expect(await auth.lastMessageHash()).to.equal(msg);
|
|
});
|
|
|
|
it("reverts AuthorizationFailed on a tampered signature (verifier returns false)", async function () {
|
|
const rng = makeRng("authorize-bad");
|
|
const pk = makePubKey(1, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("nope"));
|
|
const bad = tamperCommitment(1, genuine(1, pk, msg, rng));
|
|
await expect(auth.authorize(1, pk, msg, bad))
|
|
.to.be.revertedWithCustomError(auth, "AuthorizationFailed")
|
|
.withArgs(1, 1);
|
|
expect(Number(await auth.authorizationCount())).to.equal(0);
|
|
});
|
|
|
|
it("authorizeWithPreferred uses the caller's effective scheme and records the resolved id", async function () {
|
|
await auth.connect(other).setPreferredAlgorithm(4); // SLH-DSA-128s
|
|
const rng = makeRng("authorize-pref");
|
|
const pk = makePubKey(4, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("pref-do"));
|
|
const sig = genuine(4, pk, msg, rng);
|
|
|
|
await expect(auth.connect(other).authorizeWithPreferred(pk, msg, sig))
|
|
.to.emit(auth, "Authorized")
|
|
.withArgs(other.address, 4, 4, msg);
|
|
expect(Number(await auth.lastResolvedId())).to.equal(4);
|
|
});
|
|
|
|
it("authorize routes through a live successor swap without a redeploy", async function () {
|
|
const rng = makeRng("authorize-swap");
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("swap-do"));
|
|
const newId = await addAlgo(3, "ML-DSA-do");
|
|
await reg.setSuccessor(1, newId);
|
|
await reg.setStatus(1, Status.DEPRECATED);
|
|
|
|
const pk = makePubKey(3, rng);
|
|
const sig = genuine(3, pk, msg, rng);
|
|
await expect(auth.authorize(1, pk, msg, sig))
|
|
.to.emit(auth, "Authorized")
|
|
.withArgs(owner.address, 1, newId, msg); // requestedId 1, resolvedId = successor
|
|
expect(Number(await auth.lastResolvedId())).to.equal(newId);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Seeded invariant fuzz
|
|
// -----------------------------------------------------------------------
|
|
describe("seeded invariant fuzz", function () {
|
|
it("a REVOKED scheme is NEVER authorized, across many rows and random statuses", async function () {
|
|
const rng = makeRng("fuzz-authz-revoked-v1");
|
|
const N = 16;
|
|
const rows = [];
|
|
for (let i = 0; i < N; i++) {
|
|
const scheme = 1 + rng.int(4); // 1..4
|
|
const id = await addAlgo(scheme, "R-" + i);
|
|
const pk = makePubKey(scheme, rng);
|
|
const msg = ethers.hexlify(rng.bytes(32));
|
|
const sig = genuine(scheme, pk, msg, rng);
|
|
const pick = rng.int(3); // 0 ACTIVE, 1 DEPRECATED, 2 REVOKED
|
|
let status = Status.ACTIVE;
|
|
if (pick === 1) {
|
|
await reg.setStatus(id, Status.DEPRECATED);
|
|
status = Status.DEPRECATED;
|
|
} else if (pick === 2) {
|
|
await reg.setStatus(id, Status.REVOKED);
|
|
status = Status.REVOKED;
|
|
}
|
|
rows.push({ id, scheme, pk, msg, sig, status });
|
|
}
|
|
|
|
for (const r of rows) {
|
|
// No successor is ever set on these rows. So:
|
|
// ACTIVE -> authorizes a genuine sig (true)
|
|
// DEPRECATED / REVOKED (no ACTIVE successor) -> resolveActive reverts -> false
|
|
const expected = r.status === Status.ACTIVE;
|
|
expect(await auth.checkAuthorized(r.id, r.pk, r.msg, r.sig)).to.equal(expected);
|
|
// A tampered signature is always false.
|
|
expect(await auth.checkAuthorized(r.id, r.pk, r.msg, tamperCommitment(r.scheme, r.sig))).to.equal(false);
|
|
// A REVOKED row can never be authorized, and authorize() must revert.
|
|
if (r.status === Status.REVOKED) {
|
|
let reverted = false;
|
|
try {
|
|
await auth.authorize.staticCall(r.id, r.pk, r.msg, r.sig);
|
|
} catch (e) {
|
|
reverted = true;
|
|
}
|
|
expect(reverted).to.equal(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("checkAuthorized always terminates (never hangs) over random successor graphs", async function () {
|
|
const rng = makeRng("fuzz-authz-resolve-v1");
|
|
const M = 10;
|
|
const ids = [];
|
|
for (let i = 0; i < M; i++) ids.push(await addAlgo(1, "G-" + i));
|
|
|
|
const status = {};
|
|
for (const id of ids) {
|
|
if (rng.int(3) !== 0) {
|
|
await reg.setStatus(id, Status.DEPRECATED);
|
|
status[id] = Status.DEPRECATED;
|
|
} else {
|
|
status[id] = Status.ACTIVE;
|
|
}
|
|
}
|
|
const succ = {};
|
|
for (const id of ids) {
|
|
if (rng.int(2) === 0) {
|
|
const t = ids[rng.int(M)];
|
|
if (t !== id) {
|
|
await reg.setSuccessor(id, t);
|
|
succ[id] = t;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Off-chain reference walk mirroring resolveActive.
|
|
function refResolve(start) {
|
|
let cur = start;
|
|
const seen = new Set();
|
|
while (true) {
|
|
if (status[cur] === Status.ACTIVE) return cur;
|
|
if (seen.has(cur)) return "cycle";
|
|
seen.add(cur);
|
|
const n = succ[cur];
|
|
if (n === undefined) return "dead";
|
|
cur = n;
|
|
}
|
|
}
|
|
|
|
for (const id of ids) {
|
|
const rng2 = makeRng("g-" + id);
|
|
const pk = makePubKey(1, rng2);
|
|
const msg = ethers.hexlify(rng2.bytes(32));
|
|
const sig = genuine(1, pk, msg, rng2);
|
|
const ref = refResolve(id);
|
|
const got = await auth.checkAuthorized(id, pk, msg, sig);
|
|
if (typeof ref === "number") {
|
|
// Resolves to an ACTIVE Falcon-512 row -> the genuine Falcon-512 sig authorizes.
|
|
expect(got).to.equal(true);
|
|
} else {
|
|
// Dead-end or cycle -> fail-closed false (terminated, did not hang).
|
|
expect(got).to.equal(false);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Gas (informational)
|
|
// -----------------------------------------------------------------------
|
|
describe("gas", function () {
|
|
it("reports deploy / authorize / admin gas", async function () {
|
|
const A = await ethers.getContractFactory("AereHybridAuthorizer");
|
|
const fresh = await A.deploy(await reg.getAddress(), 1);
|
|
const deployRc = await fresh.deploymentTransaction().wait();
|
|
|
|
const rng = makeRng("gas");
|
|
const pkF = makePubKey(2, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("gas"));
|
|
const sigF = genuine(2, pkF, msg, rng);
|
|
const pkM = makePubKey(3, rng);
|
|
const sigM = genuine(3, pkM, msg, rng);
|
|
|
|
const gCheckFalcon = await fresh.checkAuthorized.estimateGas(2, pkF, msg, sigF);
|
|
const gCheckMldsa = await fresh.checkAuthorized.estimateGas(3, pkM, msg, sigM);
|
|
const gAuth = await fresh.authorize.estimateGas(2, pkF, msg, sigF);
|
|
const authRc = await (await fresh.authorize(2, pkF, msg, sigF)).wait();
|
|
const gSetDefault = await fresh.setDefaultAlgorithm.estimateGas(3);
|
|
const gSetPref = await fresh.connect(other).setPreferredAlgorithm.estimateGas(3);
|
|
|
|
console.log(" gas — deploy :", deployRc.gasUsed.toString());
|
|
console.log(" gas — checkAuthorized(F1024):", gCheckFalcon.toString());
|
|
console.log(" gas — checkAuthorized(MLDSA):", gCheckMldsa.toString());
|
|
console.log(" gas — authorize (F1024) :", gAuth.toString(), "(actual", authRc.gasUsed.toString() + ")");
|
|
console.log(" gas — setDefaultAlgorithm :", gSetDefault.toString());
|
|
console.log(" gas — setPreferredAlgorithm :", gSetPref.toString());
|
|
|
|
expect(deployRc.gasUsed).to.be.greaterThan(0n);
|
|
});
|
|
});
|
|
});
|