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.
530 lines
24 KiB
JavaScript
530 lines
24 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers, network } = require("hardhat");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AerePQCKeyRegistry — on-chain proof-of-possession, rotation, revocation,
|
|
// owner-only mutation, and seeded invariant fuzz.
|
|
//
|
|
// Hardhat has no PQC precompile, so we install MockPQCPrecompile at 0x0AE1..0x0AE4
|
|
// (hardhat_setCode), exactly as the AerePQCAttestation / AereCryptoRegistry suites do.
|
|
// The mock parses the input at the precompiles' SPEC offsets and accepts iff
|
|
// commitment == keccak256(pk || message). A "genuine signature" is one whose commitment
|
|
// equals keccak256(pk || challenge). Because the registry builds the SAME wire format as
|
|
// AerePQCAttestation (proven separately against the LIVE mainnet precompile via eth_call),
|
|
// a positive proof-of-possession here proves the registry encoded the input correctly.
|
|
// A registrant who does not hold the key cannot produce a matching commitment.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true },
|
|
2: { pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, falcon: true },
|
|
3: { pkLen: 1312, sigLen: 2420, falcon: false },
|
|
4: { pkLen: 32, sigLen: 7856, falcon: false },
|
|
};
|
|
|
|
const Status = { NONE: 0, ACTIVE: 1, ROTATED: 2, REVOKED: 3 };
|
|
const NO_KEY = (1n << 256n) - 1n; // type(uint256).max sentinel
|
|
|
|
// Deterministic seeded RNG (keccak hash-chain), so the fuzz is fully reproducible.
|
|
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; // Falcon requires the header byte
|
|
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);
|
|
}
|
|
}
|
|
|
|
// A genuine signature envelope binding to `message` (bytes32) under `pubKey`.
|
|
function genuine(scheme, pubKey, message, rng) {
|
|
return envelope(scheme, commitmentFor(pubKey, message), rng);
|
|
}
|
|
|
|
// Flip one byte inside the commitment region -> a genuine forgery (mock rejects it).
|
|
function tamperCommitment(scheme, sigHex) {
|
|
const b = ethers.getBytes(sigHex);
|
|
const idx = META[scheme].falcon ? 41 : 0;
|
|
b[idx] ^= 0x01;
|
|
return ethers.hexlify(b);
|
|
}
|
|
|
|
describe("AerePQCKeyRegistry (proof-of-possession + lifecycle + seeded fuzz)", function () {
|
|
let reg, 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]]);
|
|
}
|
|
}
|
|
|
|
beforeEach(async function () {
|
|
await installMocks();
|
|
const R = await ethers.getContractFactory("AerePQCKeyRegistry");
|
|
reg = await R.deploy();
|
|
await reg.waitForDeployment();
|
|
});
|
|
|
|
// Build a genuine PoP signature for `signer` registering `pk` under `scheme` at the
|
|
// signer's current identity nonce, then send registerKey and return the new keyId.
|
|
async function registerGenuine(scheme, pk, signer = owner, rng = makeRng("pop-" + scheme)) {
|
|
const nonce = await reg.identityNonce(signer.address);
|
|
const challenge = await reg.popChallenge(signer.address, scheme, pk, nonce);
|
|
const sig = genuine(scheme, pk, challenge, rng);
|
|
const tx = await reg.connect(signer).registerKey(scheme, pk, sig);
|
|
const rc = await tx.wait();
|
|
const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered");
|
|
return Number(log.args.keyId);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Registration: proof-of-possession is required and per-scheme validated
|
|
// -----------------------------------------------------------------------
|
|
describe("registerKey (proof-of-possession)", function () {
|
|
it("registers a well-formed key for every scheme with a genuine PoP, assigning ids", async function () {
|
|
const rng = makeRng("reg-all");
|
|
let expectedId = 0;
|
|
for (const scheme of [1, 2, 3, 4]) {
|
|
const pk = makePubKey(scheme, rng);
|
|
const id = await registerGenuine(scheme, pk, owner, rng);
|
|
expect(id).to.equal(expectedId++);
|
|
const k = await reg.getKey(id);
|
|
expect(k.owner).to.equal(owner.address);
|
|
expect(Number(k.scheme)).to.equal(scheme);
|
|
expect(Number(k.status)).to.equal(Status.ACTIVE);
|
|
expect(k.pubKey).to.equal(pk);
|
|
expect(k.rotatedFromId).to.equal(NO_KEY);
|
|
expect(k.rotatedToId).to.equal(NO_KEY);
|
|
expect(Number(k.registeredBlock)).to.be.greaterThan(0);
|
|
}
|
|
expect(await reg.keyCount()).to.equal(4n);
|
|
expect(Number(await reg.identityNonce(owner.address))).to.equal(4);
|
|
});
|
|
|
|
it("REVERTS when the PoP signature is a forgery (tampered commitment) — key not stored", async function () {
|
|
const rng = makeRng("forge");
|
|
const scheme = 2;
|
|
const pk = makePubKey(scheme, rng);
|
|
const nonce = await reg.identityNonce(owner.address);
|
|
const challenge = await reg.popChallenge(owner.address, scheme, pk, nonce);
|
|
const forged = tamperCommitment(scheme, genuine(scheme, pk, challenge, rng));
|
|
await expect(reg.registerKey(scheme, pk, forged)).to.be.revertedWithCustomError(reg, "ProofOfPossessionFailed");
|
|
expect(await reg.keyCount()).to.equal(0n);
|
|
expect(Number(await reg.identityNonce(owner.address))).to.equal(0);
|
|
});
|
|
|
|
it("REVERTS when the PoP proves possession of a DIFFERENT key", async function () {
|
|
const rng = makeRng("wrongkey");
|
|
const scheme = 3;
|
|
const pk = makePubKey(scheme, rng);
|
|
const otherPk = makePubKey(scheme, rng);
|
|
const nonce = await reg.identityNonce(owner.address);
|
|
const challenge = await reg.popChallenge(owner.address, scheme, pk, nonce);
|
|
// Signature is genuine for otherPk over the challenge, but we register pk -> reject.
|
|
const sigForOther = genuine(scheme, otherPk, challenge, rng);
|
|
await expect(reg.registerKey(scheme, pk, sigForOther)).to.be.revertedWithCustomError(
|
|
reg,
|
|
"ProofOfPossessionFailed"
|
|
);
|
|
});
|
|
|
|
it("PoP is non-transferable: a proof made for owner cannot be replayed by `other`", async function () {
|
|
const rng = makeRng("nontransfer");
|
|
const scheme = 1;
|
|
const pk = makePubKey(scheme, rng);
|
|
// Build a genuine proof for OWNER's challenge (owner address + owner nonce).
|
|
const ownerNonce = await reg.identityNonce(owner.address);
|
|
const ownerChallenge = await reg.popChallenge(owner.address, scheme, pk, ownerNonce);
|
|
const proofForOwner = genuine(scheme, pk, ownerChallenge, rng);
|
|
// owner can use it.
|
|
// ...but `other` cannot: their challenge binds `other`'s address, so it fails.
|
|
await expect(reg.connect(other).registerKey(scheme, pk, proofForOwner)).to.be.revertedWithCustomError(
|
|
reg,
|
|
"ProofOfPossessionFailed"
|
|
);
|
|
// owner still can (proof is valid for owner).
|
|
await expect(reg.connect(owner).registerKey(scheme, pk, proofForOwner)).to.emit(reg, "KeyRegistered");
|
|
});
|
|
|
|
it("PoP cannot be replayed by the same identity after the nonce advances", async function () {
|
|
const rng = makeRng("replay");
|
|
const scheme = 2;
|
|
const pk = makePubKey(scheme, rng);
|
|
const nonce0 = await reg.identityNonce(owner.address);
|
|
const challenge0 = await reg.popChallenge(owner.address, scheme, pk, nonce0);
|
|
const proof0 = genuine(scheme, pk, challenge0, rng);
|
|
|
|
await reg.registerKey(scheme, pk, proof0); // consumes nonce 0
|
|
expect(Number(await reg.identityNonce(owner.address))).to.equal(1);
|
|
// Replaying the exact same (pk, proof) now fails: the challenge uses nonce 1.
|
|
await expect(reg.registerKey(scheme, pk, proof0)).to.be.revertedWithCustomError(reg, "ProofOfPossessionFailed");
|
|
expect(await reg.keyCount()).to.equal(1n);
|
|
});
|
|
|
|
it("rejects an unknown scheme or malformed public key BEFORE touching the precompile", async function () {
|
|
await expect(reg.registerKey(5, "0x" + "00".repeat(897), "0x")).to.be.revertedWithCustomError(reg, "InvalidScheme");
|
|
// Falcon-1024 correct length, wrong header.
|
|
await expect(reg.registerKey(2, "0x09" + "00".repeat(1792), "0x")).to.be.revertedWithCustomError(
|
|
reg,
|
|
"InvalidPubKey"
|
|
);
|
|
// ML-DSA-44 wrong length.
|
|
await expect(reg.registerKey(3, "0x" + "00".repeat(1311), "0x")).to.be.revertedWithCustomError(
|
|
reg,
|
|
"InvalidPubKey"
|
|
);
|
|
});
|
|
|
|
it("records keysOf and allows multiple keys per identity", async function () {
|
|
const rng = makeRng("multi");
|
|
const a = await registerGenuine(1, makePubKey(1, rng), owner, rng);
|
|
const b = await registerGenuine(3, makePubKey(3, rng), owner, rng);
|
|
const list = (await reg.keysOf(owner.address)).map((x) => Number(x));
|
|
expect(list).to.deep.equal([a, b]);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Rotation
|
|
// -----------------------------------------------------------------------
|
|
describe("rotateKey", function () {
|
|
it("rotates an ACTIVE key to a fresh successor, retiring the old one and linking both", async function () {
|
|
const rng = makeRng("rotate");
|
|
const oldId = await registerGenuine(1, makePubKey(1, rng), owner, rng);
|
|
|
|
const newPk = makePubKey(2, rng); // rotate Falcon-512 -> Falcon-1024
|
|
const nonce = await reg.identityNonce(owner.address);
|
|
const challenge = await reg.popChallenge(owner.address, 2, newPk, nonce);
|
|
const sig = genuine(2, newPk, challenge, rng);
|
|
|
|
await expect(reg.rotateKey(oldId, 2, newPk, sig)).to.emit(reg, "KeyRotated");
|
|
const newId = oldId + 1;
|
|
|
|
const oldK = await reg.getKey(oldId);
|
|
const newK = await reg.getKey(newId);
|
|
expect(Number(oldK.status)).to.equal(Status.ROTATED);
|
|
expect(oldK.rotatedToId).to.equal(BigInt(newId));
|
|
expect(Number(newK.status)).to.equal(Status.ACTIVE);
|
|
expect(newK.rotatedFromId).to.equal(BigInt(oldId));
|
|
expect(await reg.isActiveKey(oldId)).to.equal(false);
|
|
expect(await reg.isActiveKey(newId)).to.equal(true);
|
|
});
|
|
|
|
it("only the key owner may rotate", async function () {
|
|
const rng = makeRng("rot-owner");
|
|
const oldId = await registerGenuine(1, makePubKey(1, rng), owner, rng);
|
|
const newPk = makePubKey(1, rng);
|
|
const nonce = await reg.identityNonce(other.address);
|
|
const challenge = await reg.popChallenge(other.address, 1, newPk, nonce);
|
|
const sig = genuine(1, newPk, challenge, rng);
|
|
await expect(reg.connect(other).rotateKey(oldId, 1, newPk, sig)).to.be.revertedWithCustomError(
|
|
reg,
|
|
"NotKeyOwner"
|
|
);
|
|
});
|
|
|
|
it("rotation requires a genuine PoP of the NEW key", async function () {
|
|
const rng = makeRng("rot-pop");
|
|
const oldId = await registerGenuine(2, makePubKey(2, rng), owner, rng);
|
|
const newPk = makePubKey(2, rng);
|
|
const nonce = await reg.identityNonce(owner.address);
|
|
const challenge = await reg.popChallenge(owner.address, 2, newPk, nonce);
|
|
const forged = tamperCommitment(2, genuine(2, newPk, challenge, rng));
|
|
await expect(reg.rotateKey(oldId, 2, newPk, forged)).to.be.revertedWithCustomError(
|
|
reg,
|
|
"ProofOfPossessionFailed"
|
|
);
|
|
// old key untouched.
|
|
expect(Number((await reg.getKey(oldId)).status)).to.equal(Status.ACTIVE);
|
|
});
|
|
|
|
it("cannot rotate a non-ACTIVE (already rotated or revoked) key", async function () {
|
|
const rng = makeRng("rot-twice");
|
|
const oldId = await registerGenuine(1, makePubKey(1, rng), owner, rng);
|
|
// First rotation.
|
|
const p1 = makePubKey(1, rng);
|
|
let nonce = await reg.identityNonce(owner.address);
|
|
let ch = await reg.popChallenge(owner.address, 1, p1, nonce);
|
|
await reg.rotateKey(oldId, 1, p1, genuine(1, p1, ch, rng));
|
|
// Second rotation of the SAME (now ROTATED) old key must fail.
|
|
const p2 = makePubKey(1, rng);
|
|
nonce = await reg.identityNonce(owner.address);
|
|
ch = await reg.popChallenge(owner.address, 1, p2, nonce);
|
|
await expect(reg.rotateKey(oldId, 1, p2, genuine(1, p2, ch, rng))).to.be.revertedWithCustomError(
|
|
reg,
|
|
"KeyNotActive"
|
|
);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Revocation
|
|
// -----------------------------------------------------------------------
|
|
describe("revokeKey", function () {
|
|
it("owner revokes an ACTIVE key; it becomes terminal and stops verifying", async function () {
|
|
const rng = makeRng("rev");
|
|
const scheme = 2;
|
|
const pk = makePubKey(scheme, rng);
|
|
const id = await registerGenuine(scheme, pk, owner, rng);
|
|
|
|
// verifyWithKey works before revocation.
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
|
|
const sig = genuine(scheme, pk, msg, rng);
|
|
expect(await reg.verifyWithKey(id, msg, sig)).to.equal(true);
|
|
|
|
await expect(reg.revokeKey(id)).to.emit(reg, "KeyRevoked");
|
|
expect(Number(await reg.statusOf(id))).to.equal(Status.REVOKED);
|
|
expect(await reg.isActiveKey(id)).to.equal(false);
|
|
// verifyWithKey now fail-closes to false even for a genuine signature.
|
|
expect(await reg.verifyWithKey(id, msg, sig)).to.equal(false);
|
|
});
|
|
|
|
it("only the owner may revoke, and revoke is idempotent-terminal", async function () {
|
|
const rng = makeRng("rev-owner");
|
|
const id = await registerGenuine(1, makePubKey(1, rng), owner, rng);
|
|
await expect(reg.connect(other).revokeKey(id)).to.be.revertedWithCustomError(reg, "NotKeyOwner");
|
|
await reg.revokeKey(id);
|
|
await expect(reg.revokeKey(id)).to.be.revertedWithCustomError(reg, "KeyAlreadyRevoked");
|
|
});
|
|
|
|
it("reverts on unknown keyId for revoke / rotate / getKey", async function () {
|
|
await expect(reg.revokeKey(999)).to.be.revertedWithCustomError(reg, "UnknownKey");
|
|
await expect(reg.rotateKey(999, 1, "0x", "0x")).to.be.revertedWithCustomError(reg, "UnknownKey");
|
|
await expect(reg.getKey(999)).to.be.revertedWithCustomError(reg, "UnknownKey");
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// verifyWithKey / verify semantics
|
|
// -----------------------------------------------------------------------
|
|
describe("verifyWithKey & stateless verify", function () {
|
|
it("verifyWithKey returns false (never reverts) for unknown/malformed inputs", async function () {
|
|
const rng = makeRng("vwk");
|
|
const scheme = 1;
|
|
const pk = makePubKey(scheme, rng);
|
|
const id = await registerGenuine(scheme, pk, owner, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("x"));
|
|
// unknown key id.
|
|
expect(await reg.verifyWithKey(999, msg, "0x" + "00".repeat(73))).to.equal(false);
|
|
// malformed (too-short Falcon) signature -> fail-closed false, no revert.
|
|
expect(await reg.verifyWithKey(id, msg, "0x" + "00".repeat(40))).to.equal(false);
|
|
// genuine works.
|
|
expect(await reg.verifyWithKey(id, msg, genuine(scheme, pk, msg, rng))).to.equal(true);
|
|
});
|
|
|
|
it("a ROTATED key still verifies historically via verifyWithKey", async function () {
|
|
const rng = makeRng("vwk-rot");
|
|
const oldId = await registerGenuine(1, makePubKey(1, rng), owner, rng);
|
|
const oldPk = (await reg.getKey(oldId)).pubKey;
|
|
const newPk = makePubKey(1, rng);
|
|
const nonce = await reg.identityNonce(owner.address);
|
|
const ch = await reg.popChallenge(owner.address, 1, newPk, nonce);
|
|
await reg.rotateKey(oldId, 1, newPk, genuine(1, newPk, ch, rng));
|
|
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("hist"));
|
|
const sig = genuine(1, oldPk, msg, rng);
|
|
expect(Number(await reg.statusOf(oldId))).to.equal(Status.ROTATED);
|
|
expect(await reg.verifyWithKey(oldId, msg, sig)).to.equal(true); // historical still valid
|
|
});
|
|
|
|
it("stateless verify() checks any scheme over a raw message; false when precompile wiped", async function () {
|
|
const rng = makeRng("stateless");
|
|
const scheme = 3;
|
|
const pk = makePubKey(scheme, rng);
|
|
const msg = ethers.keccak256(ethers.toUtf8Bytes("s"));
|
|
const sig = genuine(scheme, pk, msg, rng);
|
|
expect(await reg.verify(scheme, pk, msg, sig)).to.equal(true);
|
|
await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], "0x"]);
|
|
expect(await reg.verify(scheme, pk, msg, sig)).to.equal(false);
|
|
});
|
|
|
|
it("precompileFor returns the canonical address and reverts on an unknown scheme", async function () {
|
|
expect(await reg.precompileFor(1)).to.equal(PRECOMPILE[1]);
|
|
expect(await reg.precompileFor(4)).to.equal(PRECOMPILE[4]);
|
|
await expect(reg.precompileFor(9)).to.be.revertedWithCustomError(reg, "InvalidScheme");
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Seeded invariant fuzz
|
|
// -----------------------------------------------------------------------
|
|
describe("seeded invariant fuzz", function () {
|
|
it("proof-of-possession can never be forged/replayed across many random rounds", async function () {
|
|
const rng = makeRng("fuzz-pop-v1");
|
|
const ROUNDS = 40;
|
|
let expectedKeys = 0n;
|
|
|
|
for (let r = 0; r < ROUNDS; r++) {
|
|
const scheme = [1, 2, 3, 4][rng.int(4)];
|
|
const pk = makePubKey(scheme, rng);
|
|
const signer = rng.int(2) === 0 ? owner : other;
|
|
|
|
const nonce = await reg.identityNonce(signer.address);
|
|
const challenge = await reg.popChallenge(signer.address, scheme, pk, nonce);
|
|
|
|
// --- a battery of BAD proofs: all revert, nothing stored, nonce frozen ---
|
|
const bad = [];
|
|
bad.push(tamperCommitment(scheme, genuine(scheme, pk, challenge, rng))); // forgery
|
|
{
|
|
// genuine proof but for a different key
|
|
const otherPk = makePubKey(scheme, rng);
|
|
bad.push(genuine(scheme, otherPk, challenge, rng));
|
|
}
|
|
{
|
|
// genuine proof but for a different (wrong) challenge/nonce
|
|
const wrongCh = await reg.popChallenge(signer.address, scheme, pk, nonce + 7n);
|
|
bad.push(genuine(scheme, pk, wrongCh, rng));
|
|
}
|
|
for (const f of bad) {
|
|
await expect(reg.connect(signer).registerKey(scheme, pk, f)).to.be.revertedWithCustomError(
|
|
reg,
|
|
"ProofOfPossessionFailed"
|
|
);
|
|
}
|
|
expect(await reg.keyCount()).to.equal(expectedKeys);
|
|
expect(await reg.identityNonce(signer.address)).to.equal(nonce);
|
|
|
|
// --- the genuine proof succeeds exactly once; state advances by one ---
|
|
const good = genuine(scheme, pk, challenge, rng);
|
|
await reg.connect(signer).registerKey(scheme, pk, good);
|
|
expectedKeys += 1n;
|
|
expect(await reg.keyCount()).to.equal(expectedKeys);
|
|
expect(await reg.identityNonce(signer.address)).to.equal(nonce + 1n);
|
|
|
|
// --- replay of that exact proof now fails (nonce moved on) ---
|
|
await expect(reg.connect(signer).registerKey(scheme, pk, good)).to.be.revertedWithCustomError(
|
|
reg,
|
|
"ProofOfPossessionFailed"
|
|
);
|
|
expect(await reg.keyCount()).to.equal(expectedKeys);
|
|
}
|
|
});
|
|
|
|
it("revoked keys never verify and are never active, across random lifecycles", async function () {
|
|
const rng = makeRng("fuzz-life-v1");
|
|
const N = 18;
|
|
const rows = [];
|
|
for (let i = 0; i < N; i++) {
|
|
const scheme = [1, 2, 3, 4][rng.int(4)];
|
|
const pk = makePubKey(scheme, rng);
|
|
const id = await registerGenuine(scheme, pk, owner, rng);
|
|
const msg = ethers.hexlify(rng.bytes(32));
|
|
const sig = genuine(scheme, pk, msg, rng);
|
|
// random action: 0 leave ACTIVE, 1 rotate away, 2 revoke
|
|
const pick = rng.int(3);
|
|
let status = Status.ACTIVE;
|
|
if (pick === 1) {
|
|
const np = makePubKey(scheme, rng);
|
|
const n = await reg.identityNonce(owner.address);
|
|
const ch = await reg.popChallenge(owner.address, scheme, np, n);
|
|
await reg.rotateKey(id, scheme, np, genuine(scheme, np, ch, rng));
|
|
status = Status.ROTATED;
|
|
} else if (pick === 2) {
|
|
await reg.revokeKey(id);
|
|
status = Status.REVOKED;
|
|
}
|
|
rows.push({ id, scheme, pk, msg, sig, status });
|
|
}
|
|
|
|
for (const row of rows) {
|
|
const expected = row.status !== Status.REVOKED; // ACTIVE or ROTATED verify; REVOKED never
|
|
expect(await reg.verifyWithKey(row.id, row.msg, row.sig)).to.equal(expected);
|
|
expect(await reg.isActiveKey(row.id)).to.equal(row.status === Status.ACTIVE);
|
|
if (row.status === Status.REVOKED) {
|
|
expect(Number(await reg.statusOf(row.id))).to.equal(Status.REVOKED);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Gas (informational)
|
|
// -----------------------------------------------------------------------
|
|
describe("gas", function () {
|
|
it("reports deploy / register / rotate / revoke / verify gas (mock precompile)", async function () {
|
|
const R = await ethers.getContractFactory("AerePQCKeyRegistry");
|
|
const fresh = await R.deploy();
|
|
const deployRc = await fresh.deploymentTransaction().wait();
|
|
|
|
const rng = makeRng("gas");
|
|
// register Falcon-512
|
|
const pk1 = makePubKey(1, rng);
|
|
let n = await fresh.identityNonce(owner.address);
|
|
let ch = await fresh.popChallenge(owner.address, 1, pk1, n);
|
|
const regRc = await (await fresh.registerKey(1, pk1, genuine(1, pk1, ch, rng))).wait();
|
|
|
|
// rotate to Falcon-512
|
|
const pk2 = makePubKey(1, rng);
|
|
n = await fresh.identityNonce(owner.address);
|
|
ch = await fresh.popChallenge(owner.address, 1, pk2, n);
|
|
const rotRc = await (await fresh.rotateKey(0, 1, pk2, genuine(1, pk2, ch, rng))).wait();
|
|
|
|
// register ML-DSA-44
|
|
const pk3 = makePubKey(3, rng);
|
|
n = await fresh.identityNonce(owner.address);
|
|
ch = await fresh.popChallenge(owner.address, 3, pk3, n);
|
|
const regMldsaRc = await (await fresh.registerKey(3, pk3, genuine(3, pk3, ch, rng))).wait();
|
|
|
|
const revRc = await (await fresh.revokeKey(1)).wait();
|
|
|
|
console.log(" gas — deploy :", deployRc.gasUsed.toString());
|
|
console.log(" gas — registerKey Falcon-512:", regRc.gasUsed.toString());
|
|
console.log(" gas — rotateKey Falcon-512 :", rotRc.gasUsed.toString());
|
|
console.log(" gas — registerKey ML-DSA-44 :", regMldsaRc.gasUsed.toString());
|
|
console.log(" gas — revokeKey :", revRc.gasUsed.toString());
|
|
expect(deployRc.gasUsed).to.be.greaterThan(0n);
|
|
});
|
|
});
|
|
});
|