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