const { expect } = require("chai"); const { ethers, network } = require("hardhat"); // --------------------------------------------------------------------------- // AereAgentDID — Falcon-root DID for AI agents issuing short-lived secp256k1 // session keys with scope + spend-policy + on-chain revocation. // // The ROOT authority is a Falcon key held in AerePQCKeyRegistry (proof-of-possession // enforced there). We install MockPQCPrecompile at 0x0AE1..0x0AE4 (hardhat_setCode) so // the registry's Falcon verification runs; the real precompile encoding is proven // separately against the LIVE mainnet precompile via eth_call. Session ISSUANCE is // gated by a Falcon PoP over an issuance challenge; session ACTIONS are cheap secp256k1 // (ecrecover). A genuine Falcon envelope has commitment == keccak256(pk || challenge). // --------------------------------------------------------------------------- const PRECOMPILE = { 1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), 2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), 3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), 4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), }; 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 }, }; 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]); 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); } // secp256k1 signature (r||s||v, low-s, v in {27,28}) by `wallet` over the 32-byte digest. function ecdsaSign(wallet, digest) { return wallet.signingKey.sign(digest).serialized; } const AGENT_BOND = ethers.getAddress("0x32E0015F622a8719d1C380A87CE1a09bcd0cB86A"); // AereAgentBond (mainnet) const AI_REPUTATION = ethers.getAddress("0x781ef746c08760aa854cDa4621d54db6734bfeBF"); // AereAIReputation describe("AereAgentDID (Falcon-root DID + secp256k1 sessions + seeded fuzz)", function () { let reg, did, owner, other, relayer, mockCode; before(async function () { [owner, other, relayer] = 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(); const D = await ethers.getContractFactory("AereAgentDID"); did = await D.deploy(await reg.getAddress(), AGENT_BOND, AI_REPUTATION); await did.waitForDeployment(); }); // Register a Falcon root key under `signer`, return { rootKeyId, rootPk, scheme }. async function registerFalconRoot(scheme, signer = owner, rng = makeRng("root-" + scheme)) { const pk = makePubKey(scheme, rng); const nonce = await reg.identityNonce(signer.address); const ch = await reg.popChallenge(signer.address, scheme, pk, nonce); const tx = await reg.connect(signer).registerKey(scheme, pk, genuine(scheme, 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 { rootKeyId: Number(log.args.keyId), rootPk: pk, scheme }; } // Issue a session with a genuine Falcon PoP (relayed by `caller`). async function issueSession( root, { sessionAddr, scopeHash, spendCap, expiry, rng, caller = relayer, signer = owner } ) { const nonce = (await did.getAgent(root.rootKeyId)).sessionNonce; const challenge = await did.sessionChallenge(root.rootKeyId, nonce, sessionAddr, scopeHash, spendCap, expiry); const pop = genuine(root.scheme, root.rootPk, challenge, rng); const tx = await did.connect(caller).issueSession(root.rootKeyId, sessionAddr, scopeHash, spendCap, expiry, pop); const rc = await tx.wait(); const log = rc.logs.map((l) => did.interface.parseLog(l)).find((p) => p && p.name === "SessionIssued"); return Number(log.args.sessionId); } // Base expiry on the ON-CHAIN latest block timestamp (evm_increaseTime in earlier // tests advances the node clock past wall time, so Date.now() would be stale). async function futureTs(secs = 3600) { const b = await ethers.provider.getBlock("latest"); return b.timestamp + secs; } // ----------------------------------------------------------------------- // Deployment & cross-links // ----------------------------------------------------------------------- describe("deployment", function () { it("wires the registry and exposes the read-only cross-links", async function () { expect(await did.keyRegistry()).to.equal(await reg.getAddress()); expect(await did.agentBond()).to.equal(AGENT_BOND); expect(await did.aiReputation()).to.equal(AI_REPUTATION); }); it("reverts on a zero registry", async function () { const D = await ethers.getContractFactory("AereAgentDID"); await expect(D.deploy(ethers.ZeroAddress, AGENT_BOND, AI_REPUTATION)).to.be.revertedWithCustomError( D, "ZeroRegistry" ); }); }); // ----------------------------------------------------------------------- // createAgent // ----------------------------------------------------------------------- describe("createAgent", function () { it("creates a DID for an ACTIVE Falcon root, callable only by the root controller", async function () { const rng = makeRng("create"); const root = await registerFalconRoot(1, owner, rng); // non-controller cannot create. await expect(did.connect(other).createAgent(root.rootKeyId)).to.be.revertedWithCustomError( did, "NotRootController" ); await expect(did.createAgent(root.rootKeyId)).to.emit(did, "AgentCreated"); const a = await did.getAgent(root.rootKeyId); expect(a.controller).to.equal(owner.address); expect(Number(a.sessionNonce)).to.equal(0); expect(await did.agentExists(root.rootKeyId)).to.equal(true); }); it("rejects a non-Falcon root key", async function () { const rng = makeRng("nonfalcon"); // register an ML-DSA-44 key and try to use it as a root. const pk = makePubKey(3, rng); const nonce = await reg.identityNonce(owner.address); const ch = await reg.popChallenge(owner.address, 3, pk, nonce); const tx = await reg.registerKey(3, pk, genuine(3, pk, ch, rng)); const rc = await tx.wait(); const id = Number( rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered").args.keyId ); await expect(did.createAgent(id)).to.be.revertedWithCustomError(did, "RootKeyNotFalcon"); }); it("rejects a rotated-away (non-ACTIVE) root and a duplicate DID", async function () { const rng = makeRng("dup"); const root = await registerFalconRoot(2, owner, rng); await did.createAgent(root.rootKeyId); await expect(did.createAgent(root.rootKeyId)).to.be.revertedWithCustomError(did, "AgentExists"); // A separate root that we rotate away is no longer ACTIVE -> cannot create. const root2 = await registerFalconRoot(1, owner, rng); const np = makePubKey(1, rng); const n = await reg.identityNonce(owner.address); const ch = await reg.popChallenge(owner.address, 1, np, n); await reg.rotateKey(root2.rootKeyId, 1, np, genuine(1, np, ch, rng)); await expect(did.createAgent(root2.rootKeyId)).to.be.revertedWithCustomError(did, "RootKeyNotActive"); }); }); // ----------------------------------------------------------------------- // issueSession (Falcon-PoP gated, relayable) // ----------------------------------------------------------------------- describe("issueSession", function () { it("issues a session with a genuine Falcon PoP, relayable by anyone", async function () { const rng = makeRng("issue"); const root = await registerFalconRoot(1, owner, rng); await did.createAgent(root.rootKeyId); const session = ethers.Wallet.createRandom(); const scopeHash = ethers.keccak256(ethers.toUtf8Bytes("swap:read")); const expiry = await futureTs(); const sid = await issueSession(root, { sessionAddr: session.address, scopeHash, spendCap: 1000n, expiry, rng, caller: relayer, // relayed, not the controller }); expect(sid).to.equal(0); const s = await did.getSession(sid); expect(s.sessionAddr).to.equal(session.address); expect(s.scopeHash).to.equal(scopeHash); expect(s.spendCap).to.equal(1000n); expect(s.revoked).to.equal(false); expect(await did.isSessionValid(sid)).to.equal(true); expect(Number((await did.getAgent(root.rootKeyId)).sessionNonce)).to.equal(1); }); it("rejects a forged PoP and a stale/replayed PoP", async function () { const rng = makeRng("issue-bad"); const root = await registerFalconRoot(2, owner, rng); await did.createAgent(root.rootKeyId); const session = ethers.Wallet.createRandom(); const scopeHash = ethers.ZeroHash; const expiry = await futureTs(); const nonce = (await did.getAgent(root.rootKeyId)).sessionNonce; const challenge = await did.sessionChallenge(root.rootKeyId, nonce, session.address, scopeHash, 500n, expiry); const forged = tamperCommitment(2, genuine(2, root.rootPk, challenge, rng)); await expect( did.issueSession(root.rootKeyId, session.address, scopeHash, 500n, expiry, forged) ).to.be.revertedWithCustomError(did, "SessionAuthFailed"); // A genuine PoP for the CURRENT nonce works once... const good = genuine(2, root.rootPk, challenge, rng); await did.issueSession(root.rootKeyId, session.address, scopeHash, 500n, expiry, good); // ...and replaying it fails (nonce advanced -> different challenge). await expect( did.issueSession(root.rootKeyId, session.address, scopeHash, 500n, expiry, good) ).to.be.revertedWithCustomError(did, "SessionAuthFailed"); }); it("rejects a past expiry, a zero session address, and an unknown agent", async function () { const rng = makeRng("issue-params"); const root = await registerFalconRoot(1, owner, rng); await did.createAgent(root.rootKeyId); const session = ethers.Wallet.createRandom(); const pastExpiry = 1; // in the past const nonce = (await did.getAgent(root.rootKeyId)).sessionNonce; const ch = await did.sessionChallenge(root.rootKeyId, nonce, session.address, ethers.ZeroHash, 1n, pastExpiry); const pop = genuine(1, root.rootPk, ch, rng); await expect( did.issueSession(root.rootKeyId, session.address, ethers.ZeroHash, 1n, pastExpiry, pop) ).to.be.revertedWithCustomError(did, "InvalidSessionParams"); // unknown agent. await expect( did.issueSession(999, session.address, ethers.ZeroHash, 1n, await futureTs(), "0x") ).to.be.revertedWithCustomError(did, "UnknownAgent"); }); }); // ----------------------------------------------------------------------- // authorize (secp256k1 hot path: scope + spend cap + anti-replay) // ----------------------------------------------------------------------- describe("authorize", function () { let root, session, sid, scopeHash; beforeEach(async function () { const rng = makeRng("auth-setup"); root = await registerFalconRoot(1, owner, rng); await did.createAgent(root.rootKeyId); session = ethers.Wallet.createRandom(); scopeHash = ethers.keccak256(ethers.toUtf8Bytes("trade")); sid = await issueSession(root, { sessionAddr: session.address, scopeHash, spendCap: 100n, expiry: await futureTs(), rng, }); }); async function authorize(amount, actionLabel, opts = {}) { const scope = opts.scope ?? scopeHash; const actionHash = ethers.keccak256(ethers.toUtf8Bytes(actionLabel)); const nonce = opts.nonce ?? (await did.getSession(sid)).actionNonce; const digest = await did.actionDigest(sid, scope, actionHash, amount, nonce); const wallet = opts.wallet ?? session; const sig = ecdsaSign(wallet, digest); return did.connect(relayer).authorize(sid, scope, actionHash, amount, sig); } it("authorizes a scoped action signed by the session key and records spend", async function () { await expect(authorize(30n, "a1")).to.emit(did, "ActionAuthorized"); expect((await did.getSession(sid)).spent).to.equal(30n); expect(Number((await did.getSession(sid)).actionNonce)).to.equal(1); expect(await did.remainingSpend(sid)).to.equal(70n); await authorize(70n, "a2"); expect((await did.getSession(sid)).spent).to.equal(100n); expect(await did.remainingSpend(sid)).to.equal(0n); }); it("enforces the spend cap (cumulative)", async function () { await authorize(60n, "a1"); await expect(authorize(41n, "a2")).to.be.revertedWithCustomError(did, "SessionSpendExceeded"); // exactly hitting the cap is allowed. await authorize(40n, "a3"); expect((await did.getSession(sid)).spent).to.equal(100n); }); it("enforces scope: a non-matching scope tag reverts", async function () { const wrongScope = ethers.keccak256(ethers.toUtf8Bytes("withdraw")); await expect(authorize(1n, "x", { scope: wrongScope })).to.be.revertedWithCustomError( did, "SessionScopeViolation" ); }); it("rejects a signature by a non-session key", async function () { const stranger = ethers.Wallet.createRandom(); await expect(authorize(1n, "x", { wallet: stranger })).to.be.revertedWithCustomError(did, "SessionSigInvalid"); }); it("is anti-replay: reusing a consumed action nonce reverts", async function () { // Sign at nonce 0, submit -> nonce becomes 1. Re-submitting the SAME signature // (still nonce 0) now recovers a wrong digest -> SessionSigInvalid. const actionHash = ethers.keccak256(ethers.toUtf8Bytes("replay")); const digest0 = await did.actionDigest(sid, scopeHash, actionHash, 10n, 0); const sig0 = ecdsaSign(session, digest0); await did.authorize(sid, scopeHash, actionHash, 10n, sig0); expect(Number((await did.getSession(sid)).actionNonce)).to.equal(1); await expect(did.authorize(sid, scopeHash, actionHash, 10n, sig0)).to.be.revertedWithCustomError( did, "SessionSigInvalid" ); }); it("rejects a malformed (wrong-length) signature", async function () { const actionHash = ethers.keccak256(ethers.toUtf8Bytes("m")); await expect(did.authorize(sid, scopeHash, actionHash, 1n, "0x1234")).to.be.revertedWithCustomError( did, "SessionSigInvalid" ); }); it("reverts on an unknown sessionId", async function () { await expect(did.authorize(999, scopeHash, ethers.ZeroHash, 1n, "0x" + "00".repeat(65))) .to.be.revertedWithCustomError(did, "UnknownSession"); }); }); // ----------------------------------------------------------------------- // revocation & expiry & root-lifecycle cross-check // ----------------------------------------------------------------------- describe("session validity: revoke / expiry / root lifecycle", function () { let root, session, sid, scopeHash; beforeEach(async function () { const rng = makeRng("valid-setup"); root = await registerFalconRoot(1, owner, rng); await did.createAgent(root.rootKeyId); session = ethers.Wallet.createRandom(); scopeHash = ethers.keccak256(ethers.toUtf8Bytes("s")); sid = await issueSession(root, { sessionAddr: session.address, scopeHash, spendCap: 100n, expiry: await futureTs(1000), rng, }); this.rng = rng; }); async function authorizeOnce(amount) { const actionHash = ethers.keccak256(ethers.toUtf8Bytes("act")); const nonce = (await did.getSession(sid)).actionNonce; const digest = await did.actionDigest(sid, scopeHash, actionHash, amount, nonce); return did.authorize(sid, scopeHash, actionHash, amount, ecdsaSign(session, digest)); } it("controller can fast-revoke (cheap ECDSA); a stranger cannot", async function () { await expect(did.connect(other).revokeSession(sid)).to.be.revertedWithCustomError(did, "NotSessionRevoker"); await expect(did.connect(owner).revokeSession(sid)).to.emit(did, "SessionRevoked"); expect(await did.isSessionValid(sid)).to.equal(false); await expect(authorizeOnce(1n)).to.be.revertedWithCustomError(did, "SessionExpiredOrInvalid"); // revoke is idempotent (no-op success on a second call). await did.connect(owner).revokeSession(sid); }); it("a session past its expiry is invalid", async function () { expect(await did.isSessionValid(sid)).to.equal(true); await network.provider.send("evm_increaseTime", [2000]); await network.provider.send("evm_mine", []); expect(await did.isSessionValid(sid)).to.equal(false); await expect(authorizeOnce(1n)).to.be.revertedWithCustomError(did, "SessionExpiredOrInvalid"); }); it("REVOKING the Falcon root invalidates all its sessions", async function () { expect(await did.isSessionValid(sid)).to.equal(true); await reg.connect(owner).revokeKey(root.rootKeyId); expect(await did.isSessionValid(sid)).to.equal(false); await expect(authorizeOnce(1n)).to.be.revertedWithCustomError(did, "SessionExpiredOrInvalid"); }); it("ROTATING the Falcon root (root no longer ACTIVE) invalidates its sessions", async function () { const rng = this.rng; const np = makePubKey(1, rng); const n = await reg.identityNonce(owner.address); const ch = await reg.popChallenge(owner.address, 1, np, n); await reg.rotateKey(root.rootKeyId, 1, np, genuine(1, np, ch, rng)); expect(await did.isSessionValid(sid)).to.equal(false); }); }); // ----------------------------------------------------------------------- // Seeded invariant fuzz // ----------------------------------------------------------------------- describe("seeded invariant fuzz", function () { it("scope + spend-cap + anti-replay + revocation hold across random rounds", async function () { const rng = makeRng("fuzz-did-v1"); const scheme = 2; const root = await registerFalconRoot(scheme, owner, rng); await did.createAgent(root.rootKeyId); const ROUNDS = 24; for (let r = 0; r < ROUNDS; r++) { const session = ethers.Wallet.createRandom(); const scopeHash = ethers.hexlify(rng.bytes(32)); const cap = BigInt(50 + rng.int(200)); const sid = await issueSession(root, { sessionAddr: session.address, scopeHash, spendCap: cap, expiry: await futureTs(5000), rng, }); let spent = 0n; // A few genuine scoped actions within cap succeed and accumulate. const k = 1 + rng.int(3); for (let i = 0; i < k; i++) { const remaining = cap - spent; if (remaining === 0n) break; const amount = BigInt(1 + rng.int(Number(remaining > 40n ? 40n : remaining))); const actionHash = ethers.hexlify(rng.bytes(32)); const nonce = (await did.getSession(sid)).actionNonce; const digest = await did.actionDigest(sid, scopeHash, actionHash, amount, nonce); await did.authorize(sid, scopeHash, actionHash, amount, ecdsaSign(session, digest)); spent += amount; expect((await did.getSession(sid)).spent).to.equal(spent); } // (a) wrong scope always reverts. { const badScope = ethers.hexlify(rng.bytes(32)); const actionHash = ethers.hexlify(rng.bytes(32)); const nonce = (await did.getSession(sid)).actionNonce; const digest = await did.actionDigest(sid, badScope, actionHash, 1n, nonce); await expect( did.authorize(sid, badScope, actionHash, 1n, ecdsaSign(session, digest)) ).to.be.revertedWithCustomError(did, "SessionScopeViolation"); } // (b) exceeding the cap always reverts. { const over = cap - spent + 1n; const actionHash = ethers.hexlify(rng.bytes(32)); const nonce = (await did.getSession(sid)).actionNonce; const digest = await did.actionDigest(sid, scopeHash, actionHash, over, nonce); await expect( did.authorize(sid, scopeHash, actionHash, over, ecdsaSign(session, digest)) ).to.be.revertedWithCustomError(did, "SessionSpendExceeded"); } // (c) a stranger key never authorizes. Use amount 0 so the spend check (which // runs before the signature check) always passes, isolating the SIG failure — // the session's cap may already be exhausted this round. { const stranger = ethers.Wallet.createRandom(); const actionHash = ethers.hexlify(rng.bytes(32)); const nonce = (await did.getSession(sid)).actionNonce; const digest = await did.actionDigest(sid, scopeHash, actionHash, 0n, nonce); await expect( did.authorize(sid, scopeHash, actionHash, 0n, ecdsaSign(stranger, digest)) ).to.be.revertedWithCustomError(did, "SessionSigInvalid"); } // Randomly revoke ~1/3 of sessions; a revoked session never authorizes again. if (rng.int(3) === 0) { await did.connect(owner).revokeSession(sid); expect(await did.isSessionValid(sid)).to.equal(false); const actionHash = ethers.hexlify(rng.bytes(32)); const nonce = (await did.getSession(sid)).actionNonce; const digest = await did.actionDigest(sid, scopeHash, actionHash, 1n, nonce); await expect( did.authorize(sid, scopeHash, actionHash, 1n, ecdsaSign(session, digest)) ).to.be.revertedWithCustomError(did, "SessionExpiredOrInvalid"); } } // sessionsOf reflects everything issued. const list = await did.sessionsOf(root.rootKeyId); expect(list.length).to.equal(ROUNDS); expect(Number(await did.sessionCount())).to.equal(ROUNDS); }); }); });