// Proba modului cosign (portofel-contract 2-din-2, AereAgentWallet2of2.sol), fara retea: facilitatorul local emuleaza ERC-1271-ul // contractului (logica lui adevarata o proba hardhat si testnetul). Adversarii: agentul singur, serviciul de politica singur, si un // serviciu de politica COMPROMIS care incearca sa-l faca pe agent sa semneze alta plata decat cea din intrarea lui. // node proba-cosign.mjs iesire 0 = toate cum trebuia import { definePolicy } from '../agent-policy.mjs'; import { newAgentIdentity, openLedger } from '../agent-ledger.mjs'; import { createWallet, serve, assetId, x402Action, incarcaEthers, EIP3009_TYPES } from './wallet.mjs'; import { payWithAgent, completeazaCosemnarea, walletOverHttp } from './client.mjs'; import { createResourceServer } from './resource-server.mjs'; import { createLocalFacilitator } from './facilitator-local.mjs'; const ethers = incarcaEthers(); let ok = 0, rau = 0; const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; }; const NET = 'eip155:28001'; const TOKEN = { address: '0x8215bA247a3574af8EBC36606eB437811E318FBd', name: 'AereTestUSD', version: '2' }; const cerinta = (payTo, amount = '10000') => ({ scheme: 'exact', network: NET, amount, asset: TOKEN.address, payTo, maxTimeoutSeconds: 60, extra: { name: TOKEN.name, version: TOKEN.version } }); const deschise = []; const inchide = (s) => { try { s.closeAllConnections && s.closeAllConnections(); s.close(); } catch { /* inchis */ } }; try { const contract = ethers.Wallet.createRandom().address; // adresa portofelului-contract (emulat) const agentEvm = ethers.Wallet.createRandom(), politica = ethers.Wallet.createRandom(); const payee = ethers.Wallet.createRandom().address.toLowerCase(); const agent = newAgentIdentity(); const { policy, policyHash } = definePolicy({ agentId: agent.agentId, spend: { amount: '30000', windowSeconds: 3600, asset: assetId(NET, TOKEN.address) }, recipients: [payee], wallet: contract }); const wallet = createWallet({ policy, policyHash, evmPrivateKey: politica.privateKey, contractWallet: contract, network: NET, token: TOKEN }); const fac = createLocalFacilitator({ network: NET, token: TOKEN, balances: { [contract]: '100000' }, contracts: { [contract]: { agentSigner: agentEvm.address, policySigner: politica.address } } }); const facUrl = await fac.listen(); deschise.push(fac.server); const rs = createResourceServer({ requirement: cerinta(payee), facilitator: facUrl }); const url = await rs.listen(); deschise.push(rs.server); const L = openLedger({ identity: agent, policy, policyHash }); const st = wallet.status(); cer(st.mode === 'cosign-2of2' && st.address.toLowerCase() === contract.toLowerCase() && st.policySigner === politica.address, '1. status: modul cosign, portofelul e contractul, semnatarul de politica numit'); // 2. trei plati, cu jumatatea agentului adaugata de client const plati = []; for (let i = 0; i < 3; i++) plati.push(await payWithAgent({ url, ledger: L, wallet, agentEvmKey: agentEvm.privateKey })); cer(plati.every((p) => p.paid) && fac.balanceOf(payee) === 30000n && fac.balanceOf(contract) === 70000n, `2. trei plati din portofelul 2-din-2, fiecare cu ambele semnaturi (${plati.map((p) => p.status).join(',')})`); const s0 = rs.lastPayloads[0].payload.signature; const dig0 = ethers.TypedDataEncoder.hash({ name: TOKEN.name, version: TOKEN.version, chainId: 28001, verifyingContract: TOKEN.address }, EIP3009_TYPES, { ...rs.lastPayloads[0].payload.authorization, value: BigInt(rs.lastPayloads[0].payload.authorization.value), validAfter: BigInt(rs.lastPayloads[0].payload.authorization.validAfter), validBefore: BigInt(rs.lastPayloads[0].payload.authorization.validBefore) }); cer(ethers.dataLength(s0) === 130 && ethers.recoverAddress(dig0, ethers.dataSlice(s0, 0, 65)) === agentEvm.address && ethers.recoverAddress(dig0, ethers.dataSlice(s0, 65, 130)) === politica.address, '2. semnatura platii are 130 de octeti: intai agentul, apoi semnatarul de politica, peste acelasi digest'); const p4 = await payWithAgent({ url, ledger: L, wallet, agentEvmKey: agentEvm.privateKey }); cer(!p4.paid && /over the limit/.test(p4.reason || ''), `2. CONTROL: a patra depaseste limita -> refuzata (${(p4.reason || '').slice(0, 60)})`); // 3. agentul SINGUR: isi semneaza singur o autorizare catre vanzator, fara serviciu const acum = Math.floor(Date.now() / 1000); const a3 = { from: contract, to: ethers.getAddress(payee), value: '10000', validAfter: String(acum - 5), validBefore: String(acum + 50), nonce: ethers.hexlify(ethers.randomBytes(32)) }; const d3 = ethers.TypedDataEncoder.hash({ name: TOKEN.name, version: TOKEN.version, chainId: 28001, verifyingContract: TOKEN.address }, EIP3009_TYPES, { ...a3, value: 10000n, validAfter: BigInt(a3.validAfter), validBefore: BigInt(a3.validBefore) }); const trimite = async (sig) => { const pl = { x402Version: 2, accepted: cerinta(payee), payload: { signature: sig, authorization: a3 } }; const r = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(pl)).toString('base64') } }); return [r.status, await r.text()]; }; const [c3, t3] = await trimite(agentEvm.signingKey.sign(d3).serialized); const [c3b, t3b] = await trimite(ethers.concat([agentEvm.signingKey.sign(d3).serialized, agentEvm.signingKey.sign(d3).serialized])); cer(c3 === 402 && /invalid_signature/.test(t3) && c3b === 402 && /invalid_signature/.test(t3b), '3. ATAC: agentul singur (o jumatate, sau jumatatea lui de doua ori) nu poate plati din portofel'); // 4. serviciul de politica SINGUR const [c4, t4] = await trimite(ethers.concat([politica.signingKey.sign(d3).serialized, politica.signingKey.sign(d3).serialized])); cer(c4 === 402 && /invalid_signature/.test(t4) && fac.balanceOf(payee) === 30000n, '4. ATAC: serviciul de politica singur nu poate plati din portofel'); // 5. serviciul de politica COMPROMIS: raspunsuri falsificate, fiecare semnat consecvent cu cheia lui; agentul trebuie sa refuze const A = newAgentIdentity(); const pay2 = ethers.Wallet.createRandom().address.toLowerCase(); const atacator = ethers.Wallet.createRandom().address; const p5 = definePolicy({ agentId: A.agentId, spend: { amount: '1000000', windowSeconds: 3600, asset: assetId(NET, TOKEN.address) }, wallet: contract }); const W5 = createWallet({ policy: p5.policy, policyHash: p5.policyHash, evmPrivateKey: politica.privateKey, contractWallet: contract, network: NET, token: TOKEN }); const L5 = openLedger({ identity: A, policy: p5.policy, policyHash: p5.policyHash }); const req5 = cerinta(pay2); const e5 = L5.record(x402Action(contract, { url: 'http://x/5' }, req5)).entry; const bun = await W5.authorize({ requirements: req5, resource: { url: 'http://x/5' }, ledger: L5.export() }); const dom = { name: TOKEN.name, version: TOKEN.version, chainId: 28001, verifyingContract: TOKEN.address }; const refa = (schimba, cheie = politica) => { const pl = JSON.parse(JSON.stringify(bun.paymentPayload)); schimba(pl.payload.authorization); const au = pl.payload.authorization; const dg = ethers.TypedDataEncoder.hash(dom, EIP3009_TYPES, { ...au, value: BigInt(au.value), validAfter: BigInt(au.validAfter), validBefore: BigInt(au.validBefore) }); return { payload: pl, cosign: { digest: dg, policySignature: cheie.signingKey.sign(dg).serialized } }; }; const judeca = (x) => completeazaCosemnarea({ payload: x.payload, req: req5, status: W5.status(), entryHash: e5.hash, agentEvmKey: agentEvm.privateKey, cosign: x.cosign }); cer(bun.ok && judeca({ payload: bun.paymentPayload, cosign: bun.cosign }).ok, '5. CONTROL: raspunsul cinstit al serviciului e completat de agent'); cer(/another recipient or amount/.test(judeca(refa((a) => { a.to = atacator; })).error || ''), '5. ATAC: serviciul schimba destinatarul -> agentul refuza sa semneze'); cer(/not from the 2-of-2 wallet/.test(judeca(refa((a) => { a.from = atacator; })).error || ''), '5. ATAC: serviciul schimba platitorul (alt portofel) -> agentul refuza'); cer(/another recipient or amount/.test(judeca(refa((a) => { a.value = '999999'; })).error || ''), '5. ATAC: serviciul schimba suma -> agentul refuza'); cer(/nonce is not/.test(judeca(refa((a) => { a.nonce = ethers.hexlify(ethers.randomBytes(32)); })).error || ''), "5. ATAC: serviciul pune alt nonce decat intrarea agentului -> agentul refuza"); cer(/validity/.test(judeca(refa((a) => { a.validBefore = String(Number(a.validBefore) + 86400); })).error || ''), '5. ATAC: serviciul lungeste termenul autorizarii cu o zi -> agentul refuza'); cer(/digest the wallet signed/.test(judeca({ payload: bun.paymentPayload, cosign: { ...bun.cosign, digest: '0x' + 'ab'.repeat(32) } }).error || ''), '5. ATAC: serviciul declara alt digest decat cel pe care il calculeaza agentul -> agentul refuza'); cer(/not signed by the declared policy signer/.test(judeca(refa(() => {}, ethers.Wallet.createRandom())).error || ''), '5. ATAC: jumatatea serviciului semnata de alta cheie decat semnatarul declarat -> agentul refuza'); // 6. fara cheia agentului, clientul se opreste cu motivul const B = newAgentIdentity(); const p6 = definePolicy({ agentId: B.agentId, spend: { amount: '100000', windowSeconds: 3600, asset: assetId(NET, TOKEN.address) }, recipients: [payee], wallet: contract }); const W6 = createWallet({ policy: p6.policy, policyHash: p6.policyHash, evmPrivateKey: politica.privateKey, contractWallet: contract, network: NET, token: TOKEN }); const r6 = await payWithAgent({ url, ledger: openLedger({ identity: B, policy: p6.policy, policyHash: p6.policyHash }), wallet: W6 }); cer(!r6.paid && /agentEvmKey is missing/.test(r6.reason || ''), `6. CONTROL: fara cheia agentului, un portofel 2-din-2 nu poate plati, si clientul spune de ce (${(r6.reason || '').slice(0, 60)})`); // 7. serviciul cosign prin HTTP (wallet.mjs serve + walletOverHttp), cap la cap const C7 = newAgentIdentity(); const p7 = definePolicy({ agentId: C7.agentId, spend: { amount: '10000', windowSeconds: 3600, asset: assetId(NET, TOKEN.address) }, recipients: [payee], wallet: contract }); const W7 = createWallet({ policy: p7.policy, policyHash: p7.policyHash, evmPrivateKey: politica.privateKey, contractWallet: contract, network: NET, token: TOKEN }); const srv7 = await serve(W7, { port: 0 }); deschise.push(srv7); const H7 = walletOverHttp(`http://127.0.0.1:${srv7.address().port}`); const st7 = await H7.status(); const r7 = await payWithAgent({ url, ledger: openLedger({ identity: C7, policy: p7.policy, policyHash: p7.policyHash }), wallet: H7, agentEvmKey: agentEvm.privateKey }); cer(st7.mode === 'cosign-2of2' && st7.policySigner === politica.address && r7.paid && fac.balanceOf(payee) === 40000n, `7. prin HTTP: /status spune modul si semnatarul de politica, iar plata cu jumatatea agentului trece (${r7.status}${r7.reason ? ' ' + r7.reason.slice(0, 60) : ''})`); } catch (e) { cer(false, `proba s-a oprit: ${String(e.message || e).slice(0, 160)}`); } finally { for (const s of deschise) inchide(s); } console.log(`\nagent-cosign: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`); process.exitCode = rau ? 1 : 0;