aere-contracts/test/aere-pq-screen.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

419 lines
20 KiB
JavaScript

// AerePQ-Screen: institution-grade post-quantum compliance passport.
//
// Exercises the three new compliance contracts end to end over Aere's LIVE PQC primitives:
// - AereTrustRegistry federated accredited-issuer trust list (eIDAS-2.0-in-spirit)
// - AereVerifiableCredential W3C VC 2.0 anchor + verify, POST-QUANTUM (Falcon-512) issuer signature
// - AereBitstringStatusList W3C Bitstring Status List revocation + zk non-revocation stub
//
// Aere has no PQC precompile in the local Hardhat EVM, so we install the repo's MockPQCPrecompile at
// 0x0AE1 (Falcon-512) via hardhat_setCode, exactly as the erc8004-adapters / AereRecoveryRegistry
// tests do. The mock parses the input at the LIVE precompile's spec offsets and accepts iff
// commitment == keccak256(pk || message), so a positive result proves the Trust Registry routed the
// correct input to the correct precompile and enforces fail-closed semantics. The REAL precompile
// path (0x0AE1 accepting a genuine Falcon-512 signature) is proven separately against live mainnet
// 2800 via eth_call, as AerePQCKeyRegistry / AerePQCAttestation already are.
//
// Run: npx hardhat test test/aere-pq-screen.test.js
const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const FALCON = ethers.getAddress("0x0000000000000000000000000000000000000ae1"); // Falcon-512 precompile
const SCHEME_FALCON512 = 1;
const REG_STATUS_ACTIVE = 1;
const NO_KEY = (1n << 256n) - 1n; // type(uint256).max sentinel
const TYPE_KYC = ethers.id("KYCCredential");
const TYPE_ADDRESS = ethers.id("ProofOfAddressCredential");
// ---- deterministic seeded RNG (keccak hash-chain) so the test is fully reproducible ----
function makeRng(seedText) {
let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
return {
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 makeFalconPubKey(rng) {
const raw = rng.bytes(897);
raw[0] = 0x09; // Falcon-512 public-key header byte
return ethers.hexlify(raw);
}
function commitmentFor(pubKey, message) {
return ethers.keccak256(ethers.concat([pubKey, message]));
}
// A genuine Falcon-512 signature envelope under the mock's semantics: nonce(40) || esig(0x29 || commit).
function genuineFalcon(pubKey, message, rng) {
const nonce = rng.bytes(40);
const esig = ethers.concat([new Uint8Array([0x29]), commitmentFor(pubKey, message)]);
return ethers.hexlify(ethers.concat([nonce, esig]));
}
// Flip one byte inside the commitment region (a genuine forgery the precompile must reject).
function tamperFalcon(sigHex) {
const b = ethers.getBytes(sigHex);
b[41] ^= 0x01; // Falcon commitment starts at offset 41 (40 nonce + 1 header)
return ethers.hexlify(b);
}
async function futureTs(secs) {
const b = await ethers.provider.getBlock("latest");
return b.timestamp + secs;
}
describe("AerePQ-Screen (post-quantum compliance passport)", function () {
let owner, issuerA, issuerB, holder, relayer;
let mockCode;
let reg, trust, statusList, vc, sp1;
before(async function () {
[owner, issuerA, issuerB, holder, relayer] = await ethers.getSigners();
const F = await ethers.getContractFactory("MockPQCPrecompile");
const mock = await F.deploy(SCHEME_FALCON512);
await mock.waitForDeployment();
mockCode = await ethers.provider.getCode(await mock.getAddress());
});
async function installFalconMock() {
await network.provider.send("hardhat_setCode", [FALCON, mockCode]);
}
// Register a Falcon-512 key under `signer` in the live AerePQCKeyRegistry (proof-of-possession).
async function registerFalconKey(signer, rng) {
const pk = makeFalconPubKey(rng);
const nonce = await reg.identityNonce(signer.address);
const ch = await reg.popChallenge(signer.address, SCHEME_FALCON512, pk, nonce);
const tx = await reg.connect(signer).registerKey(SCHEME_FALCON512, pk, genuineFalcon(pk, ch, rng));
const rc = await tx.wait();
const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered");
return { keyId: Number(log.args.keyId), pk };
}
beforeEach(async function () {
await installFalconMock();
reg = await (await ethers.getContractFactory("AerePQCKeyRegistry")).deploy();
await reg.waitForDeployment();
sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
await sp1.waitForDeployment();
trust = await (await ethers.getContractFactory("AereTrustRegistry")).deploy(await reg.getAddress());
await trust.waitForDeployment();
statusList = await (await ethers.getContractFactory("AereBitstringStatusList")).deploy(await sp1.getAddress());
await statusList.waitForDeployment();
vc = await (await ethers.getContractFactory("AereVerifiableCredential")).deploy(
await trust.getAddress(),
await statusList.getAddress()
);
await vc.waitForDeployment();
});
// Accredit issuerA for KYC with a Falcon key; create a Revocation list it controls.
async function accreditAndList(rngSeed = "issuerA") {
const rng = makeRng(rngSeed);
const key = await registerFalconKey(issuerA, rng);
const tx = await trust.accreditIssuer(issuerA.address, "did:aere:2800:issuerA", [TYPE_KYC], key.keyId);
const rc = await tx.wait();
const issuerId = Number(
rc.logs.map((l) => trust.interface.parseLog(l)).find((p) => p && p.name === "IssuerAccredited").args.issuerId
);
// issuerA creates its revocation status list (capacity 1024).
const lt = await statusList.connect(issuerA).createList(0 /* Revocation */, 1024);
const lrc = await lt.wait();
const listId = Number(
lrc.logs.map((l) => statusList.interface.parseLog(l)).find((p) => p && p.name === "ListCreated").args.listId
);
return { rng, issuerId, listId, pk: key.pk, keyId: key.keyId };
}
async function makeCredential(ctx, overrides = {}) {
const c = {
issuerId: overrides.issuerId ?? ctx.issuerId,
subjectDidHash: overrides.subjectDidHash ?? ethers.keccak256(ethers.toUtf8Bytes("did:aere:2800:holder-1")),
credentialType: overrides.credentialType ?? TYPE_KYC,
validFrom: overrides.validFrom ?? (await futureTs(-60)),
validUntil: overrides.validUntil ?? (await futureTs(365 * 24 * 3600)),
statusListId: overrides.statusListId ?? ctx.listId,
statusIndex: overrides.statusIndex ?? 42,
claimsHash: overrides.claimsHash ?? ethers.id("kyc-claims-blob"),
};
const digest = await vc.credentialDigest(c);
const sig = genuineFalcon(ctx.pk, digest, ctx.rng);
return { c, digest, sig };
}
// =========================================================================
// 1. Wiring + accreditation lifecycle (append-only, fail-closed)
// =========================================================================
describe("AereTrustRegistry (federated accredited issuers)", function () {
it("accredits an issuer with a scope + Falcon key, and reports accreditation fail-closed", async function () {
const ctx = await accreditAndList();
expect(await trust.issuerCount()).to.equal(1n);
expect(await trust.issuerStatus(ctx.issuerId)).to.equal(1); // Active
expect(await trust.isAccredited(ctx.issuerId, TYPE_KYC)).to.equal(true);
// Fail-closed: out-of-scope type, and unknown issuer id.
expect(await trust.isAccredited(ctx.issuerId, TYPE_ADDRESS)).to.equal(false);
expect(await trust.isAccredited(9999, TYPE_KYC)).to.equal(false);
expect(await trust.pqcKeyOf(ctx.issuerId)).to.equal(BigInt(ctx.keyId));
});
it("only the owner may accredit / suspend / revoke", async function () {
const rng = makeRng("noauth");
const key = await registerFalconKey(issuerA, rng);
// OZ v4 Ownable reverts with the string "Ownable: caller is not the owner".
await expect(
trust.connect(issuerB).accreditIssuer(issuerA.address, "did:aere:2800:x", [TYPE_KYC], key.keyId)
).to.be.reverted;
});
it("keeps an append-only status history across suspend / reinstate / revoke", async function () {
const ctx = await accreditAndList();
expect(await trust.statusHistoryLength(ctx.issuerId)).to.equal(1n); // Active at accreditation
await trust.suspendIssuer(ctx.issuerId, ethers.id("audit-lapsed"));
await trust.reinstateIssuer(ctx.issuerId, ethers.id("audit-restored"));
await trust.revokeIssuer(ctx.issuerId, ethers.id("accreditation-withdrawn"));
expect(await trust.statusHistoryLength(ctx.issuerId)).to.equal(4n);
expect((await trust.statusHistoryAt(ctx.issuerId, 3)).status).to.equal(3); // Revoked
// Terminal: revoked cannot be reinstated or re-revoked.
await expect(trust.reinstateIssuer(ctx.issuerId, ethers.ZeroHash)).to.be.revertedWithCustomError(
trust,
"NotSuspended"
);
await expect(trust.revokeIssuer(ctx.issuerId, ethers.ZeroHash)).to.be.revertedWithCustomError(
trust,
"AlreadyRevoked"
);
});
});
// =========================================================================
// 2. Issue + verify a credential with a POST-QUANTUM issuer signature
// =========================================================================
describe("AereVerifiableCredential (issue / verify / revoke)", function () {
it("verifies and anchors a valid PQC-signed credential (0x0AE1), relayable by anyone", async function () {
const ctx = await accreditAndList();
const { c, digest, sig } = await makeCredential(ctx);
// Full W3C verification of the presented credential passes.
expect(await vc.verifyPresented(c, sig)).to.equal(true);
expect(await vc.verifyCredentialSignature(c, sig)).to.equal(true);
// Anchor it (relayed by a third party; authorship is the Falcon signature).
await expect(vc.connect(relayer).anchorCredential(c, sig))
.to.emit(vc, "CredentialAnchored")
.withArgs(digest, ctx.issuerId, TYPE_KYC, c.subjectDidHash, ctx.listId, 42);
expect(await vc.isAnchored(digest)).to.equal(true);
expect(await vc.isValid(digest)).to.equal(true);
// Cannot double-anchor.
await expect(vc.anchorCredential(c, sig)).to.be.revertedWithCustomError(vc, "AlreadyAnchored");
});
it("REVOKES via the Bitstring Status List: verification flips to false", async function () {
const ctx = await accreditAndList();
const { c, digest, sig } = await makeCredential(ctx);
await vc.anchorCredential(c, sig);
expect(await vc.isValid(digest)).to.equal(true);
expect(await vc.isRevoked(c)).to.equal(false);
// The issuer flips the credential's status bit (revocation is terminal).
await statusList.connect(issuerA).revoke(ctx.listId, 42);
expect(await statusList.statusOf(ctx.listId, 42)).to.equal(true);
expect(await vc.isRevoked(c)).to.equal(true);
expect(await vc.verifyPresented(c, sig)).to.equal(false);
expect(await vc.isValid(digest)).to.equal(false);
// Revocation cannot be undone on a Revocation-purpose list.
await expect(statusList.connect(issuerA).setStatus(ctx.listId, 42, false)).to.be.revertedWithCustomError(
statusList,
"RevocationIsTerminal"
);
});
it("a non-accredited issuer (wrong scope) cannot issue that credential type", async function () {
const ctx = await accreditAndList();
// issuerB is accredited only for ProofOfAddress, not KYC.
const rngB = makeRng("issuerB");
const keyB = await registerFalconKey(issuerB, rngB);
const txB = await trust.accreditIssuer(issuerB.address, "did:aere:2800:issuerB", [TYPE_ADDRESS], keyB.keyId);
const rcB = await txB.wait();
const issuerBId = Number(
rcB.logs.map((l) => trust.interface.parseLog(l)).find((p) => p && p.name === "IssuerAccredited").args.issuerId
);
// A KYC credential from issuerB fails accreditation, even with a genuine signature.
const ctxB = { rng: rngB, issuerId: issuerBId, listId: ctx.listId, pk: keyB.pk, keyId: keyB.keyId };
const { c, sig } = await makeCredential(ctxB, { statusIndex: 7 });
expect(await vc.verifyPresented(c, sig)).to.equal(false);
await expect(vc.anchorCredential(c, sig)).to.be.revertedWithCustomError(vc, "NotAccredited");
});
it("REJECTS a tampered PQC signature fail-closed (nothing anchored)", async function () {
const ctx = await accreditAndList();
const { c, sig } = await makeCredential(ctx, { statusIndex: 11 });
const bad = tamperFalcon(sig);
expect(await vc.verifyCredentialSignature(c, bad)).to.equal(false);
expect(await vc.verifyPresented(c, bad)).to.equal(false);
await expect(vc.anchorCredential(c, bad)).to.be.revertedWithCustomError(vc, "SignatureInvalid");
expect(await vc.isAnchored(await vc.credentialDigest(c))).to.equal(false);
});
it("fails closed when the issuer is SUSPENDED after anchoring, and recovers on reinstatement", async function () {
const ctx = await accreditAndList();
const { c, digest, sig } = await makeCredential(ctx, { statusIndex: 21 });
await vc.anchorCredential(c, sig);
expect(await vc.isValid(digest)).to.equal(true);
await trust.suspendIssuer(ctx.issuerId, ethers.id("under-review"));
expect(await vc.isValid(digest)).to.equal(false);
expect(await vc.verifyPresented(c, sig)).to.equal(false);
await trust.reinstateIssuer(ctx.issuerId, ethers.id("cleared"));
expect(await vc.isValid(digest)).to.equal(true);
});
it("rejects an expired credential and binds the did:aere subject DID on anchor", async function () {
const ctx = await accreditAndList();
// Expired window.
const { c: expiredC, sig: expiredSig } = await makeCredential(ctx, {
validFrom: await futureTs(-100),
validUntil: await futureTs(-10),
statusIndex: 31,
});
expect(await vc.verifyPresented(expiredC, expiredSig)).to.equal(false);
await expect(vc.anchorCredential(expiredC, expiredSig)).to.be.revertedWithCustomError(vc, "Expired");
// did:aere binding on anchor.
const subjectDid = "did:aere:2800:holder-42";
const { c, digest, sig } = await makeCredential(ctx, {
subjectDidHash: ethers.keccak256(ethers.toUtf8Bytes(subjectDid)),
statusIndex: 33,
});
await expect(vc.anchorCredentialWithDid(c, subjectDid, sig))
.to.emit(vc, "CredentialAnchoredWithDid")
.withArgs(digest, subjectDid);
// A DID that does not hash to the bound subject is rejected.
const { c: c2, sig: sig2 } = await makeCredential(ctx, {
subjectDidHash: ethers.keccak256(ethers.toUtf8Bytes("did:aere:2800:holder-99")),
statusIndex: 34,
});
await expect(vc.anchorCredentialWithDid(c2, "did:aere:2800:someone-else", sig2)).to.be.revertedWithCustomError(
vc,
"DidMismatch"
);
// A non-did:aere subject is rejected even when the hash matches.
const nonAere = "did:web:example.com";
const { c: c3, sig: sig3 } = await makeCredential(ctx, {
subjectDidHash: ethers.keccak256(ethers.toUtf8Bytes(nonAere)),
statusIndex: 35,
});
await expect(vc.anchorCredentialWithDid(c3, nonAere, sig3)).to.be.revertedWithCustomError(vc, "NotAereDid");
});
});
// =========================================================================
// 3. Bitstring Status List + zk non-revocation stub (shape + freshness binding)
// =========================================================================
describe("AereBitstringStatusList (bitset + zk non-revocation stub)", function () {
// L1 (LOW) fix: verifyProof returns void, so a NON-ZERO but code-less verifier makes the
// try/verifyProof in verifyNonRevocation succeed silently (a revoked credential proves "not
// revoked" = fail-OPEN). The constructor must reject a code-less verifier, while still allowing
// the zero address (which keeps the zk path fail-closed OFF until configured).
it("rejects a non-zero code-less non-revocation verifier at construction, but allows zero (L1 guard)", async function () {
const F = await ethers.getContractFactory("AereBitstringStatusList");
// A non-zero EOA verifier has no code.
await expect(F.deploy(holder.address)).to.be.revertedWithCustomError(statusList, "VerifierHasNoCode");
// The zero address is allowed: the zk non-revocation path stays fail-closed OFF.
const off = await F.deploy(ethers.ZeroAddress);
await off.waitForDeployment();
expect(await off.nonRevocationConfigured()).to.equal(false);
// Control: the real (code-bearing) mock SP1 gateway deploys fine.
const ok = await F.deploy(await sp1.getAddress());
await ok.waitForDeployment();
expect(await ok.NON_REVOCATION_VERIFIER()).to.equal(await sp1.getAddress());
});
it("packs status bits and enforces controller-only mutation", async function () {
await statusList.connect(issuerA).createList(0, 1024); // listId 0
expect(await statusList.statusOf(0, 5)).to.equal(false);
await statusList.connect(issuerA).setStatus(0, 5, true);
expect(await statusList.statusOf(0, 5)).to.equal(true);
// A non-controller cannot mutate.
await expect(statusList.connect(issuerB).setStatus(0, 6, true)).to.be.revertedWithCustomError(
statusList,
"NotController"
);
// No silent no-op: re-setting the same value reverts.
await expect(statusList.connect(issuerA).setStatus(0, 5, true)).to.be.revertedWithCustomError(
statusList,
"NoStatusChange"
);
});
it("verifies a zk non-revocation proof against a FRESH committed root (mock SP1), fail-closed on staleness", async function () {
// Configure the network non-revocation program (vkey). Verifier is the MockSp1Verifier.
const vkey = ethers.id("aere-nonrevocation-v1-PLACEHOLDER"); // [MEASURE] real vkey once circuit is built
await statusList.setNonRevocationVKey(vkey);
expect(await statusList.nonRevocationConfigured()).to.equal(true);
await statusList.connect(issuerA).createList(0, 1024); // listId 0
// Controller publishes a commitment to the current bitstring at the current epoch.
const root = ethers.id("bitstring-root-epoch-0");
await statusList.connect(issuerA).publishStatusRoot(0, root);
// Build public values in the exact ABI shape the contract decodes.
const publicValues = ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "address", "uint256", "bytes32", "bytes32", "uint8"],
[
(await ethers.provider.getNetwork()).chainId,
await statusList.getAddress(),
0, // listId
root, // statusRoot must equal the published root
ethers.id("holder-credential-commitment"), // credentialCommitment (bound in-circuit)
0, // purpose = Revocation
]
);
const proof = "0x1234"; // opaque; the mock accepts by (vkey, publicValues, proof) marker
const marker = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
["bytes32", "bytes", "bytes"],
[vkey, publicValues, proof]
));
await sp1.setValid(marker, true);
expect(await statusList.verifyNonRevocation(0, publicValues, proof)).to.equal(true);
// Mutate a bit after publishing the root: now rootEpoch != epoch, so the proof is STALE -> false.
await statusList.connect(issuerA).setStatus(0, 3, true);
expect(await statusList.verifyNonRevocation(0, publicValues, proof)).to.equal(false);
});
it("reverts NonRevocationNotConfigured before a vkey is set (never mistaken for not-revoked)", async function () {
await statusList.connect(issuerA).createList(0, 1024);
const pv = ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "address", "uint256", "bytes32", "bytes32", "uint8"],
[(await ethers.provider.getNetwork()).chainId, await statusList.getAddress(), 0, ethers.ZeroHash, ethers.ZeroHash, 0]
);
await expect(statusList.verifyNonRevocation(0, pv, "0x")).to.be.revertedWithCustomError(
statusList,
"NonRevocationNotConfigured"
);
});
});
});