// Proba Travel Rule post-cuantic (travel-rule.mjs): doi VASP reali (chei hibride, credentiale de VASP emise de un registru, lista de // stare), drumul intreg si fiecare atac al revizuirii ca proba numita. Offline. Plicul AIP-23 al inregistrarii se judeca si cu // verificatorul de referinta (AERE_VERIFY_PROOF sau ../aere-proof-protocol/verify.mjs); fara el, acea proba iese NEMASURATA (cod 2). // node proba-travel-rule.mjs iesire 0 = toate cum trebuia import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import * as I from './identity.mjs'; import * as TR from './travel-rule.mjs'; import { buildProof } from '../proof-kinds/proof-kinds.mjs'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const VERIFY = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs'); let treceri = 0, sarite = 0; const esecuri = []; function test(nume, fn) { try { if (fn() === 'SARIT') return; treceri++; console.log(' OK ' + nume); } catch (e) { esecuri.push(nume); console.log(' RAU ' + nume + ' -- ' + (e.message || e)); } } const cere = (c, m) => { if (!c) throw new Error(m); }; const clon = (o) => JSON.parse(JSON.stringify(o)); const NOW = new Date('2026-09-30T09:00:00Z'); const reg = I.generateKeys(), regStrain = I.generateKeys(); const A = I.generateKeys(), B = I.generateKeys(), C = I.generateKeys(); // A plateste, B primeste, C e un alt VASP (sau un strain) const kemB = TR.generateKemKeys(), kemC = TR.generateKemKeys(); const vaspCred = (vasp, idx, emitent = reg, claims = { vasp: true, name: 'VASP', lei: '5493001KJTIIGC8Y1R12' }) => I.issueCredential({ issuer: emitent, holder: vasp.public, type: 'VaspCredential', claims, disclosable: [], validUntil: '2027-09-30T00:00:00Z', status: { list: 'urn:reg:vasps', index: idx }, now: NOW }); const credA = vaspCred(A, 1), credB = vaspCred(B, 2), credC = vaspCred(C, 3); const LISTA = I.createStatusList({ issuer: reg, id: 'urn:reg:vasps', revoked: [3], validUntil: '2026-10-07T00:00:00Z', now: NOW }); // C revocat const pres = (cred, vasp, audience, nonce, now = NOW) => I.present({ credential: cred.credential, disclosures: cred.disclosures, presenter: vasp, audience, nonce, now }); const IVMS = { originator: { originatorPersons: [{ naturalPerson: { name: { nameIdentifier: [{ primaryIdentifier: 'Pop', secondaryIdentifier: 'Ana' }] } } }], accountNumber: ['0xAAAA000000000000000000000000000000000001'] }, beneficiary: { beneficiaryPersons: [{ naturalPerson: { name: { nameIdentifier: [{ primaryIdentifier: 'Ionescu', secondaryIdentifier: 'Dan' }] } } }], accountNumber: ['0xBBBB000000000000000000000000000000000002'] } }; const TRANSFER = { chainId: 2800, asset: 'AERE', amount: '1500000000000000000000', beneficiaryAddress: '0xBBBB000000000000000000000000000000000002' }; const bindB = TR.bindKemKeys({ vasp: B, kem: kemB, validUntil: '2026-12-31T00:00:00Z', now: NOW }); const accB = TR.acceptBeneficiary({ binding: bindB, presentation: pres(credB, B, A.id, 'n-acc'), registries: [reg.id], originatorId: A.id, nonce: 'n-acc', now: NOW, statusLists: [LISTA] }); const MID = 'urn:uuid:11111111-2222-4333-8444-555555555555'; const sigileaza = (o = {}) => TR.sealMessage({ from: A, beneficiary: accB.beneficiary, ivms101: IVMS, transfer: TRANSFER, presentation: pres(credA, A, B.id, MID), messageId: MID, now: NOW, ...o }); const deschide = (m, o = {}) => TR.openMessage(m, { me: B.id, kem: kemB, registries: [reg.id], now: NOW, statusLists: [LISTA], ...o }); const are = (r, re) => !r.ok && r.motive.some((x) => re.test(x)); // ---------------------------------------------------------------- beneficiarul acceptat test('initiatorul accepta beneficiarul: cheile KEM legate semnat de identitatea lui, credential de VASP de la registru, pentru initiator, nerevocat', () => { cere(accB.ok && accB.beneficiary.id === B.id && accB.beneficiary.claims.vasp === true, accB.motive.join(' | ')); }); test('ATAC: legarea KEM semnata de alt VASP in numele lui B -> refuzata', () => { const b = clon(bindB); b.statement.kem = kemC.public; b.signature = I.signText('kem-binding', I.canonical(b.statement), C); const r = TR.acceptBeneficiary({ binding: b, presentation: pres(credB, B, A.id, 'n'), registries: [reg.id], originatorId: A.id, nonce: 'n', now: NOW, statusLists: [LISTA] }); cere(are(r, /not signed by the VASP it names/), r.motive.join(' | ')); }); test('ATAC: legarea KEM a lui C cu prezentarea de VASP a lui B (alta identitate) -> refuzata', () => { const bC = TR.bindKemKeys({ vasp: C, kem: kemC, validUntil: '2026-12-31T00:00:00Z', now: NOW }); const r = TR.acceptBeneficiary({ binding: bC, presentation: pres(credB, B, A.id, 'n'), registries: [reg.id], originatorId: A.id, nonce: 'n', now: NOW, statusLists: [LISTA] }); cere(are(r, /belongs to/), r.motive.join(' | ')); }); test('ATAC: credential de VASP de la un registru in care initiatorul nu are incredere -> refuzat; credential fara vasp:true -> refuzat', () => { const cS = vaspCred(B, 2, regStrain); const r = TR.acceptBeneficiary({ binding: bindB, presentation: pres(cS, B, A.id, 'n'), registries: [reg.id], originatorId: A.id, nonce: 'n', now: NOW, statusLists: [LISTA] }); cere(are(r, /issuer trusted/), r.motive.join(' | ')); const cF = vaspCred(B, 2, reg, { vasp: false, name: 'X' }); const r2 = TR.acceptBeneficiary({ binding: bindB, presentation: pres(cF, B, A.id, 'n'), registries: [reg.id], originatorId: A.id, nonce: 'n', now: NOW, statusLists: [LISTA] }); cere(are(r2, /vasp: true/), r2.motive.join(' | ')); }); test('ATAC: un VASP revocat de registru nu mai primeste date; fara lista de stare, starea nejudecata e refuz, nu "nerevocat"', () => { const bC = TR.bindKemKeys({ vasp: C, kem: kemC, validUntil: '2026-12-31T00:00:00Z', now: NOW }); const r = TR.acceptBeneficiary({ binding: bC, presentation: pres(credC, C, A.id, 'n'), registries: [reg.id], originatorId: A.id, nonce: 'n', now: NOW, statusLists: [LISTA] }); cere(are(r, /revoked/), r.motive.join(' | ')); const r2 = TR.acceptBeneficiary({ binding: bindB, presentation: pres(credB, B, A.id, 'n'), registries: [reg.id], originatorId: A.id, nonce: 'n', now: NOW, statusLists: [] }); cere(are(r2, /status is not judged/), r2.motive.join(' | ')); }); test('ATAC: prezentarea beneficiarului facuta pentru alt initiator (reluata) -> refuzata; legarea expirata -> refuzata', () => { const r = TR.acceptBeneficiary({ binding: bindB, presentation: pres(credB, B, C.id, 'n'), registries: [reg.id], originatorId: A.id, nonce: 'n', now: NOW, statusLists: [LISTA] }); cere(are(r, /made for/), r.motive.join(' | ')); const tarziu = new Date('2027-01-02T00:00:00Z'); const r2 = TR.acceptBeneficiary({ binding: bindB, presentation: pres(credB, B, A.id, 'n', tarziu), registries: [reg.id], originatorId: A.id, nonce: 'n', now: tarziu, statusLists: [] }); cere(are(r2, /binding is valid from/), r2.motive.join(' | ')); }); // ---------------------------------------------------------------- mesajul const M = sigileaza(); test('drumul intreg: beneficiarul deschide mesajul; datele IVMS101 si transferul sunt cele trimise; nimic din ele nu e in clar in mesaj', () => { const o = deschide(M); cere(o.ok && I.canonical(o.ivms101) === I.canonical(IVMS) && o.transfer.amount === TRANSFER.amount && o.from.id === A.id, o.motive.join(' | ')); const s = JSON.stringify(M); cere(!s.includes('Ionescu') && !s.includes('Pop') && !s.includes('0xAAAA000000000000000000000000000000000001'), 'date personale in clar in mesaj'); }); test('ATAC: un octet al cifrului schimbat -> refuzat (semnatura; si fara ea, GCM)', () => { const m = clon(M); const b = Buffer.from(m.ciphertext, 'base64'); b[5] ^= 1; m.ciphertext = b.toString('base64'); cere(are(deschide(m), /not signed by the originator/), 'trecut'); m.signature = I.signText('travel-rule', I.canonical({ ...(({ ciphertext, signature, fromPresentation, ...h }) => h)(m), ciphertext: m.ciphertext }), A); cere(are(deschide(m), /modified/), 'cifrul atins, resemnat, a trecut de GCM'); }); test('ATAC: suma din antet schimbata si mesajul resemnat de initiator (cifrul facut pentru alta suma) -> GCM il refuza (antetul e AAD)', () => { const m = clon(M); m.transfer.amount = '1'; m.signature = I.signText('travel-rule', I.canonical({ ...(({ ciphertext, signature, fromPresentation, ...h }) => h)(m), ciphertext: m.ciphertext }), A); cere(are(deschide(m), /modified/), 'antetul schimbat a trecut'); }); test('ATAC: mesaj semnat de un strain in numele lui A -> refuzat; semnatura lui A de alt scop (kem-binding) peste acelasi text -> refuzat', () => { const m = clon(M); const text = I.canonical({ ...(({ ciphertext, signature, fromPresentation, ...h }) => h)(m), ciphertext: m.ciphertext }); m.signature = I.signText('travel-rule', text, C); cere(are(deschide(m), /not signed by the originator/), 'strainul a trecut'); const m2 = clon(M); m2.signature = I.signText('kem-binding', text, A); cere(are(deschide(m2), /not signed by the originator/), 'alt scop a trecut'); }); test('ATAC: alt VASP (C) incearca sa deschida mesajul lui B -> refuzat; cu cheile lui KEM in numele lui B -> refuzat', () => { cere(are(TR.openMessage(M, { me: C.id, kem: kemC, registries: [reg.id], now: NOW, statusLists: [LISTA] }), /is for/), 'C l-a deschis'); cere(are(TR.openMessage(M, { me: B.id, kem: kemC, registries: [reg.id], now: NOW, statusLists: [LISTA] }), /other KEM keys/), 'cheile lui C au trecut'); }); test('KEM hibrid: fara secretul ML-KEM corect (cheia X25519 buna) sau fara cel X25519 (ML-KEM bun), mesajul nu se descifreaza', () => { const k1 = { public: kemB.public }; Object.defineProperty(k1, 'privat', { value: { x25519: kemB.privat.x25519, mlkem768: kemC.privat.mlkem768 } }); const k2 = { public: kemB.public }; Object.defineProperty(k2, 'privat', { value: { x25519: kemC.privat.x25519, mlkem768: kemB.privat.mlkem768 } }); const r1 = deschide(M, { kem: k1 }), r2 = deschide(M, { kem: k2 }); cere(!r1.ok && !r2.ok && r1.motive.concat(r2.motive).every((x) => /modified|does not open/.test(x)), r1.motive.concat(r2.motive).join(' | ')); }); test('ATAC: acelasi mesaj primit a doua oara (reluare) -> refuzat; un mesaj vechi (301 s) -> refuzat', () => { const seen = new Set(); cere(deschide(M, { seen }).ok, 'prima deschidere'); cere(are(deschide(M, { seen }), /replay/), 'reluarea a trecut'); cere(are(deschide(M, { now: new Date(NOW.getTime() + 301000) }), /outside 300 s/), 'mesajul vechi a trecut'); }); test('ATAC: initiatorul nedovedit ca VASP (prezentare pentru alt beneficiar, sau a altui VASP, sau cu alt nonce decat id-ul mesajului) -> refuzat', () => { const m1 = sigileaza({ presentation: pres(credA, A, C.id, MID) }); cere(are(deschide(m1), /originator VASP: .*made for/), 'alt beneficiar a trecut'); const m2 = sigileaza({ presentation: pres(credC, C, B.id, MID) }); cere(are(deschide(m2), /originator VASP: .*belongs to/), 'alt VASP a trecut'); const m3 = sigileaza({ presentation: pres(credA, A, B.id, 'alt-nonce') }); cere(are(deschide(m3), /originator VASP: .*nonce/), 'alt nonce a trecut'); }); // ---------------------------------------------------------------- confirmarea si inregistrarea const O = deschide(M); const R = TR.acknowledge({ opened: O, message: M, me: B, now: NOW }); test('confirmarea: semnata de B, pentru acest mesaj, verificata de initiator', () => { const v = TR.verifyReceipt(R, { message: M, beneficiaryId: B.id }); cere(v.ok && v.accepted, v.motive.join(' | ')); }); test('ATAC: confirmare semnata de C in numele lui B -> refuzata; confirmarea altui mesaj -> refuzata', () => { const r = clon(R); r.signature = I.signText('travel-rule-receipt', I.canonical(r.statement), C); cere(!TR.verifyReceipt(r, { message: M, beneficiaryId: B.id }).ok, 'semnatura lui C a trecut'); const rc = TR.acknowledge({ opened: O, message: M, me: C, now: NOW }); // o confirmare valida, dar a lui C, nu a beneficiarului acceptat cere(!TR.verifyReceipt(rc, { message: M, beneficiaryId: B.id }).ok, 'confirmarea proprie a lui C a trecut drept a lui B'); const M2 = sigileaza({ messageId: 'urn:uuid:99999999-2222-4333-8444-555555555555', presentation: pres(credA, A, B.id, 'urn:uuid:99999999-2222-4333-8444-555555555555') }); cere(!TR.verifyReceipt(R, { message: M2, beneficiaryId: B.id }).ok, 'confirmarea altui mesaj a trecut'); }); test('inregistrarea (plic AIP-23 compliance) nu poarta date personale nici suma; verifica la verificatorul AIP-23, rescrisa nu', () => { const e = TR.travelRuleRecord({ message: M, receipt: R, buildProof, createdAt: NOW.toISOString() }); const s = JSON.stringify(e); for (const x of ['Ionescu', 'Pop', '0xBBBB000000000000000000000000000000000002', TRANSFER.amount, A.id, B.id]) cere(!s.includes(x), 'inregistrarea contine ' + x.slice(0, 24)); cere(e.statement.result === 'delivered-and-acknowledged' && e.statement.policy === 'travel-rule/fatf-r16', JSON.stringify(e.statement)); if (!fs.existsSync(VERIFY)) { sarite++; console.log(` SARIT plicul AIP-23: verificatorul nu e la ${VERIFY}`); return 'SARIT'; } const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-tr-')); try { const v = (o) => { const f = path.join(T, crypto.randomUUID() + '.json'); fs.writeFileSync(f, JSON.stringify(o)); try { return JSON.parse(spawnSync(process.execPath, [VERIFY, f, '--json'], { encoding: 'utf8' }).stdout).verdict; } catch { return '?'; } }; const x = clon(e); x.statement.result = 'delivered'; cere(v(e) === 'VALID' && v(x) !== 'VALID', `${v(e)} ${v(x)}`); } finally { fs.rmSync(T, { recursive: true, force: true }); } }); test('cheile KEM: exportKemKeys -> importKemKeys deschide acelasi mesaj; un fisier cu partea publica a altcuiva e refuzat', () => { const k = TR.importKemKeys(clon(TR.exportKemKeys(kemB))); cere(deschide(M, { kem: k }).ok, 'cheile importate nu deschid'); const j = clon(TR.exportKemKeys(kemB)); j.public = kemC.public; let m = null; try { TR.importKemKeys(j); } catch (e) { m = e.message; } cere(/not those of its private keys/.test(m || ''), 'acceptat'); }); console.log(`\naere-travel-rule: ${treceri}/${treceri + esecuri.length} cum trebuia${sarite ? `, ${sarite} NEMASURATE (fara verificatorul AIP-23)` : ''}`); process.exitCode = esecuri.length ? 1 : (sarite ? 2 : 0);