AereAgentWallet2of2 holds the agent's tokens and accepts only the agent's signature followed by the policy service's, over the same digest. In the new cosign mode the wallet service signs only its half, after every check it already made, and the agent adds its half only after it recomputes the payment itself (payer, recipient, amount, the nonce of its own ledger entry, validity, digest, declared policy signer). verifica-plati.mjs requires the wallet's code on chain to be exactly the compiled contract with the two signers; recompileaza-contract.mjs recompiles the published artifact byte for byte with solc 0.8.23. Tests: co-signing 16/16, payment verifier 14/14, negative control 30/30, wallet 25/25. On the public testnet 28001 on 2026-09-29: 13/13 with the 2-of-2 wallet (the agent alone and the policy signer alone refused by the facilitator and by the token asked on chain) and 9/9 with the wallet key. Evidence in dovezi-28001/. Nothing here has been run on the Aere Network mainnet.
99 lines
11 KiB
JavaScript
99 lines
11 KiB
JavaScript
// 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;
|