diff --git a/identity/README.md b/identity/README.md index d0a5a14..9c1142d 100644 --- a/identity/README.md +++ b/identity/README.md @@ -108,7 +108,9 @@ and `POST /v1/compliance/check`, and each answer names the SHA-256 of the files reproduces it. The record (`complianceEnvelope`) is an AIP-23 `compliance` envelope that carries no personal data: the policy's hash, the result, the -digest of the presentation, and a pseudonym of the holder bound to this verifier. Without `--pseudonym-key-file` the pseudonym is a +digest of the presentation's signed binding (which names, by hash, the credential, the disclosures, the delegation chain, the +audience, the nonce and the time; an unsigned field added to the presentation does not change it), and a pseudonym of the holder +bound to this verifier. Without `--pseudonym-key-file` the pseudonym is a SHA-256 over the holder's id and the audience, which anyone who knows both can recompute (it keeps the id out of the record, it does not hide it from them); with a key it is an HMAC only the verifier can recompute. The verifier keeps the record, and can notarize it for a time it does not choose, instead of keeping the data. What this is not: a zero-knowledge proof (a shown claim is shown whole; @@ -152,8 +154,8 @@ line is reported as not judged. The private key files are written with mode 0600 ``` node proba-identity.mjs # 44: the paths above, and each attack of the adversarial review as its own test node control-negativ-identity.mjs # on a copy, each of 49 guards removed -> its own named test turns red -node proba-conformitate.mjs # 14: compliance policies judged on real presentations, the record without personal data, the command line -node control-negativ-conformitate.mjs # on a copy, each of 13 guards removed -> its own named test turns red +node proba-conformitate.mjs # 15: compliance policies judged on real presentations, the record without personal data, the command line +node control-negativ-conformitate.mjs # on a copy, each of 14 guards removed -> its own named test turns red node proba-travel-rule.mjs # 19: two VASPs with registry credentials, the whole exchange, and each attack of the review node control-negativ-travel-rule.mjs # on a copy, each of 19 guards removed -> its own named test turns red ``` diff --git a/identity/conformitate.mjs b/identity/conformitate.mjs index 70e8d96..1523965 100644 --- a/identity/conformitate.mjs +++ b/identity/conformitate.mjs @@ -72,8 +72,13 @@ export function checkCompliance(p, politica, { audience, nonce, now = new Date() const claims = v.claims || {}; for (const r of policy.require) if (!regula(r, claims)) motive.push(`rule not met: ${canonical(r)}`); const holder = p && p.credential && p.credential.statement && p.credential.statement.holder ? p.credential.statement.holder.id : null; + // B-29 (2026-09-30, revizuirea API-ului): digestul e al LEGATURII semnate de cel care prezinta (ea numeste prin hash credentialul intreg, + // dezvaluirile, lantul de delegare, publicul, nonce-ul si momentul), nu al obiectului intreg: un camp de sus nesemnat, adaugat de + // oricine, schimba obiectul fara sa schimbe nimic semnat, deci digestul vechi nu numea unic prezentarea. Semnatura nu intra: ML-DSA + // semneaza aleator, deci aceeasi legatura poate avea oricate semnaturi valide. Fara legatura (prezentare stricata), obiectul intreg. + const legatura = p && typeof p === 'object' && p.binding && typeof p.binding === 'object' && !Array.isArray(p.binding) ? p.binding : null; return { compliant: !motive.length, reasons: motive, policyHash, policyId: policy.id, holder, notJudged: v.notJudged, - presentationHash: sha(canonical(p || null)), ...(keepClaims && !motive.length ? { claims } : {}) }; + presentationHash: sha(canonical(legatura || p || null)), ...(keepClaims && !motive.length ? { claims } : {}) }; } /** diff --git a/identity/control-negativ-conformitate.mjs b/identity/control-negativ-conformitate.mjs index 8ed7907..e4b724d 100644 --- a/identity/control-negativ-conformitate.mjs +++ b/identity/control-negativ-conformitate.mjs @@ -10,6 +10,8 @@ const DEV_VERIFY = path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs') const VERIFY = process.env.AERE_VERIFY_PROOF || (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : ''); const C = 'conformitate.mjs'; const PLANTARI = [ + // B-29: forma publicata in 154c424 (digestul obiectului intreg) + ['digestul prezentarii peste obiectul intreg (B-29)', 'presentationHash: sha(canonical(legatura || p || null)),', 'presentationHash: sha(canonical(p || null)),', 'B-29'], // [nume, tipar, inlocuire, proba (inceputul numelui ei)] ['equals nu mai compara', "if (Object.hasOwn(r, 'equals')) return canonical(v) === canonical(r.equals);", "if (Object.hasOwn(r, 'equals')) return true;", 'neconform: jurisdictie interzisa'], ['notIn nu mai compara', "if (Object.hasOwn(r, 'notIn')) return !r.notIn.some((x) => canonical(x) === canonical(v));", "if (Object.hasOwn(r, 'notIn')) return true;", 'neconform: jurisdictie interzisa'], diff --git a/identity/proba-conformitate.mjs b/identity/proba-conformitate.mjs index 0753aaa..5a0cd01 100644 --- a/identity/proba-conformitate.mjs +++ b/identity/proba-conformitate.mjs @@ -77,6 +77,16 @@ test('inregistrarea (plic AIP-23 compliance) NU poarta date personale: nici valo for (const x of ['Ana Pop', '1990-01-01', '"RO"', hol.id, hol.public.mldsa65.slice(0, 40)]) cere(!s.includes(x), 'plicul contine ' + x.slice(0, 30)); cere(e.statement.result === 'compliant' && /^pseudonym:[0-9a-f]{40}$/.test(e.statement.subject) && e.statement.policy === definePolicy(POL).policyHash, JSON.stringify(e.statement)); }); +// B-29 (2026-09-30): digestul din inregistrare numea obiectul intreg, deci un camp de sus nesemnat il schimba fara sa schimbe nimic semnat +test('B-29: digestul prezentarii e al legaturii semnate: un camp de sus nesemnat nu il schimba; alta prezentare (alt nonce) il schimba', () => { + const p = prez(), r1 = judeca(p); + const p2 = { ...p, nota: 'adaugat dupa semnare' }; const r2 = judeca(p2); + cere(r1.compliant && r2.compliant && r1.presentationHash === r2.presentationHash, 'un camp nesemnat a schimbat digestul: ' + r1.presentationHash + ' ' + r2.presentationHash); + const e1 = complianceEnvelope(r1, { audience: AUD, buildProof, createdAt: '2026-09-30T08:00:00Z' }), e2 = complianceEnvelope(r2, { audience: AUD, buildProof, createdAt: '2026-09-30T08:00:00Z' }); + cere(e1.statementHash === e2.statementHash, 'inregistrarile difera pentru aceeasi prezentare semnata'); + const p3 = I.present({ credential, disclosures, reveal: ['age_over_18', 'jurisdiction', 'kyc_level'], presenter: hol, audience: AUD, nonce: 'alt-nonce', now: NOW }); + cere(checkCompliance(p3, POL, { audience: AUD, nonce: 'alt-nonce', now: NOW, statusLists: [LISTA] }).presentationHash !== r1.presentationHash, 'alta prezentare are acelasi digest'); +}); test('pseudonimul: alt verificator (alt public) vede alt pseudonim; cu o cheie a verificatorului, altul decat forma publica refacuta de oricine', () => { const r = judeca(prez()); const a = complianceEnvelope(r, { audience: AUD, buildProof }).statement.subject, b = complianceEnvelope(r, { audience: 'https://bank.example', buildProof }).statement.subject;