The agent does not hold the payment key: the wallet holds it for the owner and signs an EIP-3009 authorization only for a payment the agent wrote into its signed ledger, verified without trusting the agent under the policy the owner pinned and against the ledger heads the wallet itself saw (a branch is refused with a proof of equivocation, a backdated entry is refused), naming exactly this purchase, written now, and within the limit judged also against what the wallet itself has signed. The authorization nonce is sha256(entry hash), so the on-chain payment names the ledger entry. The policy gains an optional `wallet` field. Also: an x402 v2 client, a minimal resource server, a local facilitator for tests, and verifica-plati.mjs, which proves from outside that a wallet's on-chain payments were allowed by the agent's policy (with --all-transfers, that no payment left the wallet without a ledger entry). Tests: wallet 25/25 and payment verifier 10/10 without a network (the verifier on chain responses recorded on testnet 28001), negative control 21/21; policy 27/27, agents control 26/26. On the public testnet 28001 through its x402 facilitator: 9/9, with the evidence in agents/x402/dovezi-28001/. Needs ethers (npm install in agents/x402).
186 lines
17 KiB
JavaScript
186 lines
17 KiB
JavaScript
// Proba portofelului de plati x402 al agentilor (wallet.mjs + client.mjs + resource-server.mjs), fara retea: un facilitator LOCAL cu
|
|
// aceleasi verificari ca cel de pe testnet (facilitator-local.mjs), semnaturi EIP-712 reale, chei ML-DSA-65 reale. Adversarul are cheia
|
|
// AGENTULUI (isi poate scrie si re-semna registrul oricum), nu cheia portofelului si nici cheile oamenilor. Fiecare afirmatie cu perechea
|
|
// ei negativa.
|
|
// node proba-wallet.mjs iesire 0 = toate cum trebuia
|
|
import crypto from 'node:crypto';
|
|
import { definePolicy } from '../agent-policy.mjs';
|
|
import { newAgentIdentity, openLedger, resumeLedger, verifyEquivocation, canonical } from '../agent-ledger.mjs';
|
|
import { newHumanIdentity, approve, revoke } from '../agent-aprobare.mjs';
|
|
import { createWallet, serve, x402Action, assetId, nonceForEntry, incarcaEthers } from './wallet.mjs';
|
|
import { payWithAgent, walletOverHttp, dinB64json } 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 arunca = (fn) => { try { fn(); return null; } catch (e) { return e.message; } };
|
|
const NET = 'eip155:28001';
|
|
const TOKEN = { address: '0x8215bA247a3574af8EBC36606eB437811E318FBd', name: 'AereTestUSD', version: '2' };
|
|
const ASSET = assetId(NET, TOKEN.address);
|
|
const cerinta = (payTo, amount = '10000') => ({ scheme: 'exact', network: NET, amount, asset: TOKEN.address, payTo, maxTimeoutSeconds: 60, extra: { name: TOKEN.name, version: TOKEN.version } });
|
|
const acum = () => Math.floor(Date.now() / 1000);
|
|
// o intrare scrisa si semnata de mana cu cheia agentului (adversarul ocoleste biblioteca registrului)
|
|
function intrareFalsa(exp, identity, body) {
|
|
const r = JSON.parse(JSON.stringify(exp)); const seq = r.entries.length; const prev = seq ? r.entries[seq - 1].hash : '0'.repeat(64);
|
|
const b = { v: 2, agentId: r.agentId, policyHash: r.policyHash, session: r.session, provenance: null, ...body, seq };
|
|
const signature = crypto.sign(null, Buffer.from(`${seq}|${prev}|${canonical(b)}`, 'utf8'), identity.privateKey).toString('base64');
|
|
r.entries.push({ seq, prev, body: b, signature, hash: crypto.createHash('sha256').update(`${seq}|${prev}|${canonical(b)}|${signature}`).digest('hex') });
|
|
return r;
|
|
}
|
|
// un agent cu portofelul lui: politica numeste portofelul, limita 50000 pe ora in activul x402
|
|
function agentCuPortofel({ limita = '50000', extra = {}, payee, state } = {}) {
|
|
const agent = newAgentIdentity(); const cheie = ethers.Wallet.createRandom();
|
|
const { policy, policyHash } = definePolicy({ agentId: agent.agentId, spend: { amount: limita, windowSeconds: 3600, asset: ASSET },
|
|
recipients: payee ? [payee] : undefined, wallet: cheie.address, ...extra });
|
|
const wallet = createWallet({ policy, policyHash, evmPrivateKey: cheie.privateKey, network: NET, token: TOKEN, state });
|
|
return { agent, cheie, policy, policyHash, wallet };
|
|
}
|
|
|
|
// serverele deschise se inchid in `finally`, si cand proba s-a oprit la jumatate: altfel procesul ramane viu dupa rezumat
|
|
// (masurat: controlul negativ astepta 240 s pe o copie oprita la jumatate)
|
|
const deschise = [];
|
|
const inchide = (s) => { if (!s) return; try { s.closeAllConnections && s.closeAllConnections(); s.close(); } catch { /* deja inchis */ } };
|
|
// un pas care arunca (o plantare care strica un raspuns) inroseste proba cu motivul, nu o opreste inainte de rezumat
|
|
try {
|
|
const payee = ethers.Wallet.createRandom().address.toLowerCase();
|
|
const A = agentCuPortofel({ payee });
|
|
const fac = createLocalFacilitator({ network: NET, token: TOKEN, balances: { [A.cheie.address]: '1000000' } });
|
|
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: A.agent, policy: A.policy, policyHash: A.policyHash });
|
|
|
|
// 1. cinci cumparaturi platite prin facilitator, fiecare scrisa intai in registru
|
|
const plati = [];
|
|
for (let i = 0; i < 5; i++) plati.push(await payWithAgent({ url, ledger: L, wallet: A.wallet }));
|
|
cer(plati.every((p) => p.paid && p.status === 200 && p.body === 'the paid content' && p.settlement.success), `1. cinci cumparaturi de 10000: platite si servite (${plati.map((p) => p.status).join(',')})`);
|
|
cer(fac.balanceOf(payee) === 50000n && fac.balanceOf(A.cheie.address) === 950000n, `1. soldurile la facilitator: vanzatorul 50000, portofelul 950000 (${fac.balanceOf(payee)})`);
|
|
cer(plati.every((p) => p.receipt.nonce === nonceForEntry(p.entry.hash) && fac.used.has(`${A.cheie.address.toLowerCase()}:${p.receipt.nonce}`)), '1. nonce-ul fiecarei autorizari EIP-3009 e sha256(hash-ul intrarii din registru), si chiar el e consumat');
|
|
cer(plati.every((p) => p.decision.statement.allowed === true && p.decision.statement.policyHash === A.policyHash), '1. fiecare plata vine cu plicul AIP-23 al deciziei, sub politica fixata');
|
|
|
|
// 2. a sasea depaseste limita: politica agentului o refuza, portofelul nici nu e chemat
|
|
const p6 = await payWithAgent({ url, ledger: L, wallet: A.wallet });
|
|
cer(!p6.paid && /policy refused.*over the limit/.test(p6.reason) && fac.balanceOf(payee) === 50000n && A.wallet.status().signed === 5, `2. CONTROL: a sasea cumparatura depaseste limita: refuzata inainte de portofel, soldul neschimbat (${p6.reason})`);
|
|
|
|
// 3. ATAC: agentul ocoleste biblioteca si scrie singur o plata "permisa" peste limita, re-semnata cu cheia lui
|
|
const r0 = await fetch(url); const pr = dinB64json(r0.headers.get('payment-required'));
|
|
const falsa = intrareFalsa(L.export(), A.agent, { at: acum(), action: { ...x402Action(A.cheie.address, pr.resource, pr.accepts[0]), at: acum() }, decision: { allowed: true, reason: 'under the limit' } });
|
|
const r3 = await A.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: falsa });
|
|
cer(!r3.ok && /false decision/.test(r3.error) && A.wallet.status().signed === 5, `3. ATAC: plata "permisa" peste limita, scrisa si semnata de agent -> portofelul re-ruleaza politica si refuza (${(r3.error || "").slice(0, 90)})`);
|
|
|
|
// 4. ATAC: intrarea numeste alta cumparatura decat cea platita (alta suma / alt destinatar)
|
|
const B = agentCuPortofel({ payee });
|
|
const LB = openLedger({ identity: B.agent, policy: B.policy, policyHash: B.policyHash });
|
|
LB.record({ ...x402Action(B.cheie.address, pr.resource, pr.accepts[0]), amount: '100' });
|
|
const r4 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB.export() });
|
|
cer(!r4.ok && /another amount/.test(r4.error), `4. ATAC: intrarea spune 100, cumparatura costa 10000 -> refuzat (${r4.error})`);
|
|
const LB2 = openLedger({ identity: B.agent, policy: B.policy, policyHash: B.policyHash }); // o intrare curata, pentru controlul pozitiv
|
|
LB2.record(x402Action(B.cheie.address, pr.resource, pr.accepts[0]));
|
|
const r4b = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() });
|
|
cer(r4b.ok, '4. CONTROL: aceeasi cumparatura, cu intrarea care o numeste exact -> semnata');
|
|
|
|
// 5. ATAC: acelasi registru trimis din nou (aceeasi ultima intrare)
|
|
const r5 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() });
|
|
cer(!r5.ok && /already seen/.test(r5.error), `5. ATAC: aceeasi intrare a doua oara -> refuzat (${r5.error})`);
|
|
|
|
// 6. ATAC: o ramura (registrul luat de la un prefix si continuat altfel) -> refuzat, cu dovada de echivocare
|
|
LB2.record(x402Action(B.cheie.address, pr.resource, pr.accepts[0]));
|
|
const r6a = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() });
|
|
const prefix = { ...LB2.export(), entries: LB2.export().entries.slice(0, 1) };
|
|
const ramura = resumeLedger({ identity: B.agent, policy: B.policy, ledger: prefix });
|
|
ramura.record(x402Action(B.cheie.address, { url: 'http://altundeva/x' }, pr.accepts[0]));
|
|
const r6 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: { url: 'http://altundeva/x' }, ledger: ramura.export() });
|
|
cer(r6a.ok && !r6.ok && /another branch/.test(r6.error), `6. ATAC: o ramura a registrului -> refuzata (${(r6.error || "").slice(0, 80)})`);
|
|
cer(!!r6.equivocation && verifyEquivocation(r6.equivocation).ok, '6. si portofelul da dovada de echivocare, verificabila de oricine cu cheia publica a agentului');
|
|
|
|
// 7. politica schimbata: registrul nou incepe de la zero, portofelul isi aminteste ce a semnat
|
|
const C = agentCuPortofel({ payee });
|
|
const LC = openLedger({ identity: C.agent, policy: C.policy, policyHash: C.policyHash });
|
|
let semnate = 0;
|
|
for (let i = 0; i < 5; i++) { LC.record(x402Action(C.cheie.address, { url: 'http://r/' + i }, cerinta(payee))); if ((await C.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://r/' + i }, ledger: LC.export() })).ok) semnate++; }
|
|
const polB = definePolicy({ ...C.policy, tools: ['search'] });
|
|
const Cb = createWallet({ policy: polB.policy, policyHash: polB.policyHash, evmPrivateKey: C.cheie.privateKey, network: NET, token: TOKEN, state: C.wallet.exportState() });
|
|
const LCb = openLedger({ identity: C.agent, policy: polB.policy, policyHash: polB.policyHash });
|
|
const rc = LCb.record(x402Action(C.cheie.address, { url: 'http://r/nou' }, cerinta(payee)));
|
|
const r7 = await Cb.authorize({ requirements: cerinta(payee), resource: { url: 'http://r/nou' }, ledger: LCb.export() });
|
|
cer(semnate === 5 && rc.allowed && !r7.ok && /by what this wallet has signed.*over the limit/.test(r7.error), `7. politica noua, registru nou (el permite: ${rc.allowed}), dar portofelul a semnat deja 50000 in ora asta -> refuzat (${(r7.error || '').slice(0, 70)})`);
|
|
|
|
// 8. revocarea data portofelului de proprietar, pe care agentul nu o scrie in registru
|
|
const O = newHumanIdentity();
|
|
const D = agentCuPortofel({ payee, extra: { owner: O.humanId } });
|
|
const LD = openLedger({ identity: D.agent, policy: D.policy, policyHash: D.policyHash });
|
|
LD.record(x402Action(D.cheie.address, { url: 'http://d/1' }, cerinta(payee)));
|
|
const r8a = await D.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://d/1' }, ledger: LD.export() });
|
|
cer(!D.wallet.addRevocation(revoke({ owner: newHumanIdentity(), agentId: D.agent.agentId, policyHash: D.policyHash, revokedAt: acum() - 5 })).ok, '8. CONTROL: o revocare semnata de altcineva decat proprietarul e refuzata de portofel');
|
|
cer(D.wallet.addRevocation(revoke({ owner: O, agentId: D.agent.agentId, policyHash: D.policyHash, revokedAt: acum() - 5 })).ok, '8. revocarea proprietarului e primita de portofel');
|
|
LD.record(x402Action(D.cheie.address, { url: 'http://d/2' }, cerinta(payee)));
|
|
const r8 = await D.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://d/2' }, ledger: LD.export() });
|
|
cer(r8a.ok && !r8.ok && /false decision.*revoked/.test(r8.error), `8. dupa revocare, plata urmatoare e refuzata desi agentul nu a scris revocarea (${(r8.error || '').slice(0, 80)})`);
|
|
|
|
// 9. aprobarea umana: peste 20000 cere un om
|
|
const H = newHumanIdentity();
|
|
const E = agentCuPortofel({ payee, limita: '100000', extra: { approval: { approvers: [H.humanId], threshold: 1, above: '20000' } } });
|
|
const LE = openLedger({ identity: E.agent, policy: E.policy, policyHash: E.policyHash });
|
|
const mare = cerinta(payee, '30000');
|
|
const fara = LE.record(x402Action(E.cheie.address, { url: 'http://e/1' }, mare));
|
|
const act = x402Action(E.cheie.address, { url: 'http://e/2' }, mare);
|
|
const apr = approve({ human: H, agentId: E.agent.agentId, policyHash: E.policyHash, action: act, issuedAt: acum() - 5, expiresAt: acum() + 300 });
|
|
LE.record(act, { approvals: [apr] });
|
|
const r9 = await E.wallet.authorize({ requirements: mare, resource: { url: 'http://e/2' }, ledger: LE.export() });
|
|
cer(!fara.allowed && r9.ok, `9. 30000 fara aprobare: refuzat de politica; cu aprobarea omului numit: semnat (${fara.reason})`);
|
|
|
|
// 10. ATAC: o plata antedatata (ceasul agentului dat inapoi)
|
|
const F = agentCuPortofel({ payee });
|
|
const LF = openLedger({ identity: F.agent, policy: F.policy, policyHash: F.policyHash, now: () => acum() - 600 });
|
|
LF.record(x402Action(F.cheie.address, { url: 'http://f/1' }, cerinta(payee)));
|
|
const r10 = await F.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://f/1' }, ledger: LF.export() });
|
|
cer(!r10.ok && /not now/.test(r10.error), `10. ATAC: plata datata cu 10 minute in urma -> refuzata (${(r10.error || '').slice(0, 70)})`);
|
|
|
|
// 11. serverul de resurse: plata pentru alta cerinta, si reluarea aceleiasi plati
|
|
const alta = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify({ ...r4b.paymentPayload, accepted: cerinta(payee, '1') })).toString('base64') } });
|
|
cer(alta.status === 402 && /another requirement/.test(await alta.text()), '11. CONTROL: o plata pentru alta cerinta decat a emis serverul -> 402');
|
|
fac.fund(B.cheie.address, '100000'); // portofelul lui B are acum sold la facilitator
|
|
const reluare = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(r4b.paymentPayload)).toString('base64') } });
|
|
const reluare2 = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(r4b.paymentPayload)).toString('base64') } });
|
|
cer(reluare.status === 200 && reluare2.status === 402 && /nonce_already_used/.test(await reluare2.text()), '11. aceeasi plata de doua ori: prima data servita, a doua oara 402 (nonce folosit la facilitator)');
|
|
|
|
// 12. doua ramuri la aceeasi pozitie, trimise DEODATA: portofelul semneaza cel mult una
|
|
const G = agentCuPortofel({ payee });
|
|
const LG1 = openLedger({ identity: G.agent, policy: G.policy, policyHash: G.policyHash });
|
|
const LG2 = openLedger({ identity: G.agent, policy: G.policy, policyHash: G.policyHash });
|
|
LG1.record(x402Action(G.cheie.address, { url: 'http://g/1' }, cerinta(payee, '50000')));
|
|
LG2.record(x402Action(G.cheie.address, { url: 'http://g/2' }, cerinta(payee, '50000')));
|
|
const [g1, g2] = await Promise.all([G.wallet.authorize({ requirements: cerinta(payee, '50000'), resource: { url: 'http://g/1' }, ledger: LG1.export() }),
|
|
G.wallet.authorize({ requirements: cerinta(payee, '50000'), resource: { url: 'http://g/2' }, ledger: LG2.export() })]);
|
|
cer([g1, g2].filter((x) => x.ok).length === 1 && G.wallet.status().signed === 1, `12. ATAC: doua ramuri de cate 50000 trimise deodata (limita 50000) -> exact una semnata (${[g1.ok, g2.ok]})`);
|
|
|
|
// 13. portofelul prin HTTP, cap la cap
|
|
const srv = await serve(A.wallet, { port: 0 }); deschise.push(srv);
|
|
const W = walletOverHttp(`http://127.0.0.1:${srv.address().port}`);
|
|
const st = await W.status();
|
|
const LA = resumeLedger({ identity: A.agent, policy: A.policy, ledger: L.export() });
|
|
const p13 = await payWithAgent({ url, ledger: LA, wallet: W });
|
|
cer(st.address === A.cheie.address && st.policyHash === A.policyHash && !p13.paid && /over the limit/.test(p13.reason), `13. prin HTTP: /status spune portofelul si politica; limita tine si dupa reluarea registrului (${(p13.reason || '').slice(0, 60)})`);
|
|
const H2 = agentCuPortofel({ payee }); const srv2 = await serve(H2.wallet, { port: 0 }); deschise.push(srv2);
|
|
const rs2 = createResourceServer({ requirement: cerinta(payee), facilitator: facUrl }); const url2 = await rs2.listen(); deschise.push(rs2.server);
|
|
const LH = openLedger({ identity: H2.agent, policy: H2.policy, policyHash: H2.policyHash });
|
|
const p13b = await payWithAgent({ url: url2, ledger: LH, wallet: walletOverHttp(`http://127.0.0.1:${srv2.address().port}`) });
|
|
cer(p13b.status === 402 && /wallet refused|insufficient_funds|not valid/.test(p13b.reason || ''), `13. CONTROL: un portofel fara sold -> plata nu trece la facilitator (${(p13b.reason || '').slice(0, 70)})`);
|
|
|
|
// 14. portofelul refuza sa porneasca pe o politica ce nu il numeste
|
|
const alt = ethers.Wallet.createRandom();
|
|
cer(/does not name this wallet/.test(arunca(() => createWallet({ policy: A.policy, evmPrivateKey: alt.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: o politica ce numeste alt portofel -> portofelul nu porneste');
|
|
cer(/pinned policyHash/.test(arunca(() => createWallet({ policy: A.policy, policyHash: '0x' + '00'.repeat(32), evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: un hash fixat care nu e al politicii -> portofelul nu porneste');
|
|
const faraLimita = definePolicy({ agentId: A.agent.agentId, wallet: A.cheie.address }).policy;
|
|
const altActiv = definePolicy({ agentId: A.agent.agentId, wallet: A.cheie.address, spend: { amount: '1', windowSeconds: 60 } }).policy;
|
|
cer(/spending limit in this wallet's asset/.test(arunca(() => createWallet({ policy: faraLimita, evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || '')
|
|
&& /spending limit in this wallet's asset/.test(arunca(() => createWallet({ policy: altActiv, evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || ''),
|
|
'14. CONTROL: o politica fara limita, sau cu limita in alt activ (AERE) -> portofelul nu porneste');
|
|
|
|
} 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-wallet: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
|
|
process.exitCode = rau ? 1 : 0;
|