identity: compliance without surveillance - a verifier's policy judged on a presentation, recorded as an AIP-23 envelope that carries no personal data
This commit is contained in:
parent
01c0c5e211
commit
154c424e94
@ -15,7 +15,7 @@ notarization command of the verification layer, and the agents' x402 wallet (EIP
|
|||||||
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
|
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
|
||||||
| [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again |
|
| [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again |
|
||||||
| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught) |
|
| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught) |
|
||||||
| [`identity/`](identity/) | post-quantum credentials: an issuer signs claims (hybrid Ed25519 + ML-DSA-65, both required), the holder shows only the claims it picks (selective disclosure in the manner of SD-JWT, RFC 9901, in a format of its own), bound to the verifier's audience and nonce; delegation to a phone, an agent or a ten-minute session key that can only narrow; revocation and an issuer status list in the manner of W3C Bitstring Status List; all checked offline from the files |
|
| [`identity/`](identity/) | post-quantum credentials: an issuer signs claims (hybrid Ed25519 + ML-DSA-65, both required), the holder shows only the claims it picks (selective disclosure in the manner of SD-JWT, RFC 9901, in a format of its own), bound to the verifier's audience and nonce; delegation to a phone, an agent or a ten-minute session key that can only narrow; revocation and an issuer status list in the manner of W3C Bitstring Status List; all checked offline from the files; and compliance without surveillance: a verifier's policy (issuers, required claims) judged on a presentation, recorded as an AIP-23 envelope that carries no personal data |
|
||||||
|
|
||||||
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
|
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
|
|||||||
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
|
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
|
||||||
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
|
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
|
||||||
| control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) |
|
| control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) |
|
||||||
| identity | 43/43 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), measured 2026-09-30 | 46/46 (`node control-negativ-identity.mjs`) |
|
| identity | 43/43 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 14/14 (`node proba-conformitate.mjs`), measured 2026-09-30 | 46/46 (`node control-negativ-identity.mjs`); compliance 13/13 (`node control-negativ-conformitate.mjs`) |
|
||||||
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`x402/proba-x402-testnet.mjs`, `x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
|
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`x402/proba-x402-testnet.mjs`, `x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
|
||||||
|
|
||||||
Code comments, most function and variable names (also many exported between the files of a component), test names and control
|
Code comments, most function and variable names (also many exported between the files of a component), test names and control
|
||||||
@ -45,4 +45,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ...
|
|||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
MIT, see [LICENSE](LICENSE). Files: 144 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 19, agents 39, identity 5, readiness 4).
|
MIT, see [LICENSE](LICENSE). Files: 147 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 19, agents 39, identity 8, readiness 4).
|
||||||
|
|||||||
@ -81,6 +81,27 @@ clock. For a time the issuer does not choose, notarize: `proofOfCredential` and
|
|||||||
envelopes (`identity` and `authorization` kinds of `../proof-kinds`) that the Aere Proof API notarizes and that the AIP-23 reference
|
envelopes (`identity` and `authorization` kinds of `../proof-kinds`) that the Aere Proof API notarizes and that the AIP-23 reference
|
||||||
verifier checks.
|
verifier checks.
|
||||||
|
|
||||||
|
## Compliance without surveillance (`conformitate.mjs`)
|
||||||
|
|
||||||
|
A verifier writes its compliance policy once: which issuers it believes, and which claims it needs (`equals`, `in`, `notIn`,
|
||||||
|
`atLeast`, `present`), for example "over 18, not in these jurisdictions, verification level at least 2". `checkCompliance` judges a
|
||||||
|
presentation against it: the presentation must be valid and bound to the verifier's audience and nonce, the issuer must be one of
|
||||||
|
the policy's, the credential status must be judged, not merely "not known to be revoked" (unless the policy says otherwise), and
|
||||||
|
every rule must hold. The policy's hash is recomputed from its normal form, so a looser policy cannot pass under a strict one's hash.
|
||||||
|
|
||||||
|
```
|
||||||
|
node identity-cli.mjs comply --presentation p.json --policy policy.json --audience https://exchange.example --nonce <nonce> \
|
||||||
|
--status-list list.json --record record.json [--pseudonym-key-file key]
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
"over 18" is a claim the issuer made), an AML or sanctions screening (it consults no list), or legal compliance with anything: it
|
||||||
|
says that one presentation met one policy at one time, as judged by the verifier.
|
||||||
|
|
||||||
## What it does not do
|
## What it does not do
|
||||||
|
|
||||||
It does not bind a key to hardware: a device key is a key like any other, and no TPM or secure-enclave attestation is checked here. It
|
It does not bind a key to hardware: a device key is a key like any other, and no TPM or secure-enclave attestation is checked here. It
|
||||||
@ -93,6 +114,8 @@ line is reported as not judged. The private key files are written with mode 0600
|
|||||||
```
|
```
|
||||||
node proba-identity.mjs # 43: the paths above, and each attack of the adversarial review as its own test
|
node proba-identity.mjs # 43: the paths above, and each attack of the adversarial review as its own test
|
||||||
node control-negativ-identity.mjs # on a copy, each of 46 guards removed -> its own named test turns red
|
node control-negativ-identity.mjs # on a copy, each of 46 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
|
||||||
```
|
```
|
||||||
|
|
||||||
The envelope test needs the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs>`); without it that test is reported as
|
The envelope test needs the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs>`); without it that test is reported as
|
||||||
|
|||||||
92
identity/conformitate.mjs
Normal file
92
identity/conformitate.mjs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// AERE Identity, conformitatea fara supraveghere (roadmap master punctul 13, pista B, 2026-09-30): o POLITICA de conformitate a
|
||||||
|
// verificatorului (ce emitenti crede, ce afirmatii cere: varsta, jurisdictie, nivel de verificare, acreditare), judecata pe o
|
||||||
|
// prezentare de credential (identity.mjs), si o INREGISTRARE a judecatii ca plic AIP-23 `compliance` care NU poarta date personale:
|
||||||
|
// numai hash-ul politicii, un pseudonim al detinatorului legat de verificator si digestul dovezii. Verificatorul pastreaza plicul
|
||||||
|
// (notarizabil: momentul il da lantul), nu datele; datele raman la detinator si se arata numai cat cere politica.
|
||||||
|
//
|
||||||
|
// Ce NU este: nu e o dovada cu cunoastere zero (ce se arata se arata intreg; "peste 18" e o afirmatie emisa de emitent, nu calculata
|
||||||
|
// din data nasterii fara s-o vada nimeni); nu e o verificare AML sau de sanctiuni (nu consulta nicio lista); nu face pe nimeni conform
|
||||||
|
// cu vreo lege: spune ca o prezentare anume a indeplinit o politica anume, la un moment anume, judecata de verificator.
|
||||||
|
//
|
||||||
|
// O politica e { v:1, kind:'aere-compliance-policy', id, trustedIssuers:[id], requireStatus, maxAgeS, require:[regula] }, cu regula
|
||||||
|
// { claim, equals } | { claim, in:[...] } | { claim, notIn:[...] } | { claim, atLeast:number } | { claim, present:true }. Hash-ul ei
|
||||||
|
// se recalculeaza din forma canonica, deci o politica mai laxa nu poate trece sub hash-ul uneia stricte.
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import { verifyPresentation, canonical, idOf } from './identity.mjs';
|
||||||
|
|
||||||
|
const sha = (s) => '0x' + crypto.createHash('sha256').update(Buffer.from(s, 'utf8')).digest('hex');
|
||||||
|
const ID = /^aere-id:[0-9a-f]{40}$/;
|
||||||
|
const OPERATORI = ['equals', 'in', 'notIn', 'atLeast', 'present'];
|
||||||
|
|
||||||
|
/** Forma normala a unei politici si hash-ul ei; refuza campuri necunoscute si reguli fara inteles. */
|
||||||
|
export function definePolicy(p) {
|
||||||
|
const chei = ['v', 'kind', 'id', 'trustedIssuers', 'requireStatus', 'maxAgeS', 'require'];
|
||||||
|
const necunoscute = Object.keys(p || {}).filter((k) => !chei.includes(k));
|
||||||
|
if (necunoscute.length) throw new Error('compliance policy: unknown field ' + necunoscute.join(', '));
|
||||||
|
if (typeof p.id !== 'string' || !p.id) throw new Error('compliance policy: id is required');
|
||||||
|
if (!Array.isArray(p.trustedIssuers) || !p.trustedIssuers.length) throw new Error('compliance policy: trustedIssuers is required (a policy that trusts anyone proves nothing)');
|
||||||
|
const emitenti = [...new Set(p.trustedIssuers.map((x) => (typeof x === 'string' && ID.test(x) ? x : idOf(x))))].sort();
|
||||||
|
if (!Array.isArray(p.require) || !p.require.length) throw new Error('compliance policy: require is a non-empty list of rules');
|
||||||
|
const reguli = p.require.map((r) => {
|
||||||
|
const op = OPERATORI.filter((o) => Object.hasOwn(r, o));
|
||||||
|
const extra = Object.keys(r).filter((k) => k !== 'claim' && !OPERATORI.includes(k));
|
||||||
|
if (typeof r.claim !== 'string' || !r.claim || op.length !== 1 || extra.length) throw new Error('compliance policy: a rule is { claim, <one of ' + OPERATORI.join('|') + '> }');
|
||||||
|
const o = op[0], v = r[o];
|
||||||
|
if ((o === 'in' || o === 'notIn') && (!Array.isArray(v) || !v.length)) throw new Error(`compliance policy: ${o} needs a non-empty list`);
|
||||||
|
if (o === 'atLeast' && !Number.isFinite(v)) throw new Error('compliance policy: atLeast needs a number');
|
||||||
|
if (o === 'present' && v !== true) throw new Error('compliance policy: present must be true');
|
||||||
|
canonical(v);
|
||||||
|
return { claim: r.claim, [o]: (o === 'in' || o === 'notIn') ? [...v].sort((a, b) => (canonical(a) < canonical(b) ? -1 : 1)) : v };
|
||||||
|
}).sort((a, b) => (canonical(a) < canonical(b) ? -1 : 1));
|
||||||
|
const maxAgeS = p.maxAgeS == null ? 300 : Number(p.maxAgeS);
|
||||||
|
if (!Number.isInteger(maxAgeS) || maxAgeS < 1 || maxAgeS > 3600) throw new Error('compliance policy: maxAgeS must be 1..3600');
|
||||||
|
const policy = { v: 1, kind: 'aere-compliance-policy', id: p.id, trustedIssuers: emitenti, requireStatus: p.requireStatus !== false, maxAgeS, require: reguli };
|
||||||
|
return { policy, policyHash: sha(canonical(policy)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function regula(r, claims) {
|
||||||
|
const are = Object.hasOwn(claims, r.claim), v = claims[r.claim];
|
||||||
|
if (Object.hasOwn(r, 'present')) return are;
|
||||||
|
if (!are) return false;
|
||||||
|
if (Object.hasOwn(r, 'equals')) return canonical(v) === canonical(r.equals);
|
||||||
|
if (Object.hasOwn(r, 'in')) return r.in.some((x) => canonical(x) === canonical(v));
|
||||||
|
if (Object.hasOwn(r, 'notIn')) return !r.notIn.some((x) => canonical(x) === canonical(v));
|
||||||
|
if (Object.hasOwn(r, 'atLeast')) return typeof v === 'number' && Number.isFinite(v) && v >= r.atLeast;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Judeca o prezentare dupa o politica. Cere: prezentarea VALIDA, legata de publicul si nonce-ul verificatorului, emitentul intre cei
|
||||||
|
* ai politicii, starea credentialului JUDECATA (nu doar "nerevocat pentru ca nu stiu") cand politica o cere, si fiecare regula.
|
||||||
|
* Intoarce { compliant, reasons, policyHash, presentation } fara afirmatiile detinatorului (le are verificatorul in `presentation.claims`
|
||||||
|
* numai daca le cere explicit, cu `keepClaims`).
|
||||||
|
*/
|
||||||
|
export function checkCompliance(p, politica, { audience, nonce, now = new Date(), statusLists = [], revocations = [], keepClaims = false } = {}) {
|
||||||
|
const { policy, policyHash } = definePolicy(politica);
|
||||||
|
if (typeof audience !== 'string' || !audience || typeof nonce !== 'string' || !nonce) throw new Error('compliance: the verifier\'s audience and nonce are required (without them a presentation can be replayed)');
|
||||||
|
const v = verifyPresentation(p, { audience, nonce, now, trustedIssuers: policy.trustedIssuers, statusLists, revocations, maxAgeS: policy.maxAgeS });
|
||||||
|
const motive = [];
|
||||||
|
if (!v.valid) motive.push('the presentation is not valid: ' + v.rows.filter((r) => r.pass === false).map((r) => r.name + (r.detail ? ' (' + r.detail + ')' : '')).join('; '));
|
||||||
|
const status = v.rows.find((r) => /^credential: (not revoked|status)/.test(r.name));
|
||||||
|
if (policy.requireStatus && !(status && status.pass === true)) motive.push('the policy requires the credential status to be judged: ' + (status ? status.detail || 'not judged' : 'no status row'));
|
||||||
|
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;
|
||||||
|
return { compliant: !motive.length, reasons: motive, policyHash, policyId: policy.id, holder, notJudged: v.notJudged,
|
||||||
|
presentationHash: sha(canonical(p || null)), ...(keepClaims && !motive.length ? { claims } : {}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inregistrarea judecatii ca plic AIP-23 `compliance` (proof-kinds), FARA date personale: subject = un pseudonim al detinatorului
|
||||||
|
* legat de verificator, policy = hash-ul politicii, result = compliant | not-compliant, evidence = digestul prezentarii (verificatorul
|
||||||
|
* o poate pastra sau sterge; digestul ramane). Pseudonimul: fara `pseudonymKey`, sha256 peste id-ul detinatorului si public, pe care il
|
||||||
|
* poate reface oricine stie id-ul si publicul (tine id-ul afara din inregistrare, nu il ascunde de ei); cu `pseudonymKey` (un secret al
|
||||||
|
* verificatorului), HMAC-SHA256, pe care numai verificatorul il poate reface.
|
||||||
|
*/
|
||||||
|
export function complianceEnvelope(rez, { audience, createdAt = new Date().toISOString(), buildProof, pseudonymKey = null }) {
|
||||||
|
if (!rez || !rez.policyHash || !rez.presentationHash) throw new Error('compliance: a result of checkCompliance is required');
|
||||||
|
if (typeof audience !== 'string' || !audience) throw new Error('compliance: the audience names the verifier the pseudonym is bound to');
|
||||||
|
const intrare = `aere-compliance-subject|${rez.holder}|${audience}`;
|
||||||
|
const pseudonim = 'pseudonym:' + (pseudonymKey ? crypto.createHmac('sha256', pseudonymKey).update(intrare).digest('hex') : sha(intrare).slice(2)).slice(0, 40);
|
||||||
|
return buildProof('compliance', { subject: pseudonim, policy: rez.policyHash, result: rez.compliant ? 'compliant' : 'not-compliant', evidenceHash: rez.presentationHash, createdAt });
|
||||||
|
}
|
||||||
65
identity/control-negativ-conformitate.mjs
Normal file
65
identity/control-negativ-conformitate.mjs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Controlul negativ al conformitatii (conformitate.mjs, proba-conformitate.mjs): fiecare paznic scos intr-o COPIE trebuie sa
|
||||||
|
// inroseasca proba NUMITA, cu proba chiar rulata; pe copia neatinsa, verde. Trei stari: un tipar care nu apare exact o data sau o
|
||||||
|
// proba care nu ajunge la rezumat e STRICAT si se numara esec.
|
||||||
|
// node control-negativ-conformitate.mjs iesire 0 = martorul verde si toate plantarile rosii pe proba lor
|
||||||
|
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path';
|
||||||
|
import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
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 = [
|
||||||
|
// [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'],
|
||||||
|
['atLeast nu mai compara', "if (Object.hasOwn(r, 'atLeast')) return typeof v === 'number' && Number.isFinite(v) && v >= r.atLeast;", "if (Object.hasOwn(r, 'atLeast')) return true;", 'neconform: jurisdictie interzisa'],
|
||||||
|
['o afirmatie ceruta si nearatata trece', 'if (!are) return false;', 'if (!are) return true;', 'neconform: o afirmatie ceruta nearatata'],
|
||||||
|
['starea nejudecata trece drept nerevocata', 'if (policy.requireStatus && !(status && status.pass === true))', 'if (false)', 'neconform: starea ceruta'],
|
||||||
|
['o prezentare invalida judecata conforma', 'if (!v.valid) motive.push(', 'if (false) motive.push(', 'neconform: emitent in afara'],
|
||||||
|
['emitentii politicii nu mai ajung la verificare', 'trustedIssuers: policy.trustedIssuers,', 'trustedIssuers: null,', 'neconform: emitent in afara'],
|
||||||
|
['judecata fara publicul si nonce-ul verificatorului', "if (typeof audience !== 'string' || !audience || typeof nonce !== 'string' || !nonce) throw new Error('compliance: the verifier", "if (false) throw new Error('compliance: the verifier", 'neconform: prezentare facuta pentru alt verificator'],
|
||||||
|
['regulile nu mai sunt in forma normala', '}).sort((a, b) => (canonical(a) < canonical(b) ? -1 : 1));', '});', 'politica: forma normala'],
|
||||||
|
['campuri necunoscute primite in politica', 'if (necunoscute.length) throw', 'if (false) throw', 'CONTROL: politica refuza'],
|
||||||
|
['id-ul detinatorului scris in inregistrare', 'subject: pseudonim,', 'subject: rez.holder,', 'inregistrarea (plic AIP-23 compliance) NU poarta date personale'],
|
||||||
|
['afirmatiile intoarse si pentru o judecata neconforma', '...(keepClaims && !motive.length ? { claims } : {})', '...(keepClaims ? { claims } : {})', 'rezultatul nu poarta afirmatiile'],
|
||||||
|
['cheia verificatorului ignorata la pseudonim', '(pseudonymKey ? crypto.createHmac(', '(false ? crypto.createHmac(', 'pseudonimul'],
|
||||||
|
];
|
||||||
|
const FISIERE = ['identity.mjs', 'identity-cli.mjs', C, 'proba-conformitate.mjs'];
|
||||||
|
function copie() {
|
||||||
|
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-conf-ctl-'));
|
||||||
|
fs.mkdirSync(path.join(t, 'aere-identity')); fs.mkdirSync(path.join(t, 'proof-kinds'));
|
||||||
|
for (const f of FISIERE) fs.copyFileSync(path.join(AICI, f), path.join(t, 'aere-identity', f));
|
||||||
|
fs.copyFileSync(path.join(AICI, '..', 'proof-kinds', 'proof-kinds.mjs'), path.join(t, 'proof-kinds', 'proof-kinds.mjs'));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
function ruleaza(t) {
|
||||||
|
const env = { ...process.env }; if (VERIFY) env.AERE_VERIFY_PROOF = VERIFY; else delete env.AERE_VERIFY_PROOF;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const c = spawn(process.execPath, [path.join(t, 'aere-identity', 'proba-conformitate.mjs')], { env }); let out = '';
|
||||||
|
const ceas = setTimeout(() => c.kill(), 180000);
|
||||||
|
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; });
|
||||||
|
c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /aere-compliance: \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => l.startsWith(' RAU ')) }); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function planteaza([nume, din, inl, tinta]) {
|
||||||
|
const t = copie();
|
||||||
|
try {
|
||||||
|
const f = path.join(t, 'aere-identity', C); const src = fs.readFileSync(f, 'utf8');
|
||||||
|
if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori`];
|
||||||
|
fs.writeFileSync(f, src.replace(din, inl));
|
||||||
|
const r = await ruleaza(t);
|
||||||
|
if (!r.rulat) return [false, ` STRICAT ${nume}: proba nu a ajuns la rezumat (cod ${r.cod})`];
|
||||||
|
if (r.rosii.some((l) => l.startsWith(' RAU ' + tinta))) return [true, ` ROSU cum trebuia ${nume} (proba '${tinta}' pica)`];
|
||||||
|
return [false, ` CONTROL CAZUT ${nume}: proba '${tinta}' a ramas verde (${r.rosii.length} rosii altundeva)`];
|
||||||
|
} finally { fs.rmSync(t, { recursive: true, force: true }); }
|
||||||
|
}
|
||||||
|
let rele = 0;
|
||||||
|
const t0 = copie(); const m = await ruleaza(t0); fs.rmSync(t0, { recursive: true, force: true });
|
||||||
|
if (m.rulat && !m.rosii.length && (m.cod === 0 || (m.cod === 2 && !VERIFY))) console.log(' OK martorul: copia neatinsa verde' + (m.cod === 2 ? ' (plicul AIP-23 NEMASURAT: fara verificator)' : ''));
|
||||||
|
else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii)`); }
|
||||||
|
const rez = new Array(PLANTARI.length); let i = 0;
|
||||||
|
await Promise.all(Array.from({ length: 4 }, async () => { while (i < PLANTARI.length) { const k = i++; rez[k] = await planteaza(PLANTARI[k]); } }));
|
||||||
|
for (const [bun, linie] of rez) { console.log(linie); if (!bun) rele++; }
|
||||||
|
console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `DOVEDIT: martorul verde, ${PLANTARI.length} din ${PLANTARI.length} paznici scosi -> proba lor rosie`);
|
||||||
|
process.exitCode = rele ? 1 : 0;
|
||||||
@ -12,6 +12,8 @@
|
|||||||
// present --cred cred.json --reveal a,b --presenter-keys k.json [--delegation d1.json ...] --audience A --nonce N --out p.json
|
// present --cred cred.json --reveal a,b --presenter-keys k.json [--delegation d1.json ...] --audience A --nonce N --out p.json
|
||||||
// verify --presentation p.json [--audience A --nonce N] [--trust-issuer id|pub.json ...] [--status-list l.json ...]
|
// verify --presentation p.json [--audience A --nonce N] [--trust-issuer id|pub.json ...] [--status-list l.json ...]
|
||||||
// [--revocation r.json ...] [--max-age S] [--json]
|
// [--revocation r.json ...] [--max-age S] [--json]
|
||||||
|
// comply --presentation p.json --policy policy.json --audience A --nonce N [--status-list l.json ...] [--revocation r.json ...]
|
||||||
|
// [--record record.json] [--pseudonym-key-file k] a compliance policy judged; the record carries no personal data
|
||||||
// O valoare de --claim se citeste ca JSON daca e JSON (true, 42, {"a":1}), altfel ca text.
|
// O valoare de --claim se citeste ca JSON daca e JSON (true, 42, {"a":1}), altfel ca text.
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@ -33,7 +35,7 @@ const scrie = (f, o, privat = false) => {
|
|||||||
const lista = (v) => (v === '*' ? '*' : String(v).split(',').map((x) => x.trim()).filter(Boolean));
|
const lista = (v) => (v === '*' ? '*' : String(v).split(',').map((x) => x.trim()).filter(Boolean));
|
||||||
const zile = (n) => new Date(Date.now() + Number(n) * 86400000).toISOString();
|
const zile = (n) => new Date(Date.now() + Number(n) * 86400000).toISOString();
|
||||||
|
|
||||||
function main() {
|
async function main() {
|
||||||
if (cmd === 'keygen') { const k = I.generateKeys(); scrie(cere('--out'), I.exportKeys(k), true); console.log(k.id); return 0; }
|
if (cmd === 'keygen') { const k = I.generateKeys(); scrie(cere('--out'), I.exportKeys(k), true); console.log(k.id); return 0; }
|
||||||
if (cmd === 'pub') { const k = I.importKeys(citeste(cere('--keys'))); const o = get('--out'); if (o) scrie(o, k.public); else console.log(JSON.stringify(k.public)); return 0; }
|
if (cmd === 'pub') { const k = I.importKeys(citeste(cere('--keys'))); const o = get('--out'); if (o) scrie(o, k.public); else console.log(JSON.stringify(k.public)); return 0; }
|
||||||
if (cmd === 'id') { const p = get('--pub') ? citeste(get('--pub')) : I.importKeys(citeste(cere('--keys'))).public; console.log(I.idOf(p)); return 0; }
|
if (cmd === 'id') { const p = get('--pub') ? citeste(get('--pub')) : I.importKeys(citeste(cere('--keys'))).public; console.log(I.idOf(p)); return 0; }
|
||||||
@ -90,8 +92,22 @@ function main() {
|
|||||||
}
|
}
|
||||||
return r.valid ? 0 : 1;
|
return r.valid ? 0 : 1;
|
||||||
}
|
}
|
||||||
throw new Folosire('usage: identity-cli.mjs keygen|pub|id|issue|status-list|delegate|revoke|present|verify ... (see README.md)');
|
if (cmd === 'comply') {
|
||||||
|
// conformitatea fara supraveghere (conformitate.mjs): politica verificatorului judecata pe o prezentare; --record scrie
|
||||||
|
// inregistrarea (plic AIP-23 compliance) fara date personale, cu proof-kinds de langa (../proof-kinds)
|
||||||
|
const { checkCompliance, complianceEnvelope } = await import('./conformitate.mjs');
|
||||||
|
const r = checkCompliance(citeste(cere('--presentation')), citeste(cere('--policy')), { audience: cere('--audience'), nonce: cere('--nonce'),
|
||||||
|
statusLists: toate('--status-list').map(citeste), revocations: toate('--revocation').map(citeste) });
|
||||||
|
console.log(r.compliant ? `COMPLIANT with ${r.policyId} (${r.policyHash})` : `NOT COMPLIANT with ${r.policyId}:\n ` + r.reasons.join('\n '));
|
||||||
|
if (get('--record')) {
|
||||||
|
const { buildProof } = await import('../proof-kinds/proof-kinds.mjs');
|
||||||
|
const k = get('--pseudonym-key-file') ? fs.readFileSync(get('--pseudonym-key-file')) : null;
|
||||||
|
scrie(get('--record'), complianceEnvelope(r, { audience: get('--audience'), buildProof, pseudonymKey: k }));
|
||||||
|
}
|
||||||
|
return r.compliant ? 0 : 1;
|
||||||
|
}
|
||||||
|
throw new Folosire('usage: identity-cli.mjs keygen|pub|id|issue|status-list|delegate|revoke|present|verify|comply ... (see README.md)');
|
||||||
}
|
}
|
||||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
try { process.exitCode = main(); } catch (e) { console.error('error: ' + (e.message || e)); process.exitCode = e instanceof Folosire ? 2 : 1; }
|
main().then((c) => { process.exitCode = c; }, (e) => { console.error('error: ' + (e.message || e)); process.exitCode = e instanceof Folosire ? 2 : 1; });
|
||||||
}
|
}
|
||||||
|
|||||||
120
identity/proba-conformitate.mjs
Normal file
120
identity/proba-conformitate.mjs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
// Proba conformitatii fara supraveghere (conformitate.mjs): politica, judecata pe prezentari reale (identity.mjs), si inregistrarea
|
||||||
|
// fara date personale. Fiecare afirmatie cu perechea ei negativa. Offline. Plicul AIP-23 se judeca si cu verificatorul de referinta
|
||||||
|
// (AERE_VERIFY_PROOF sau, in depozitul de dezvoltare, ../aere-proof-protocol/verify.mjs); fara el, acea proba iese NEMASURATA (cod 2).
|
||||||
|
// node proba-conformitate.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 { definePolicy, checkCompliance, complianceEnvelope } from './conformitate.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 arunca = (f) => { try { f(); return null; } catch (e) { return e.message; } };
|
||||||
|
|
||||||
|
const NOW = new Date('2026-09-30T08:00:00Z'), AUD = 'https://exchange.example', NONCE = 'c-1';
|
||||||
|
const iss = I.generateKeys(), hol = I.generateKeys(), alt = I.generateKeys();
|
||||||
|
const CLAIMS = { name: 'Ana Pop', birthdate: '1990-01-01', age_over_18: true, jurisdiction: 'RO', kyc_level: 2 };
|
||||||
|
const emite = (emitent = iss, index = 3) => I.issueCredential({ issuer: emitent, holder: hol.public, type: 'KycCredential', claims: CLAIMS,
|
||||||
|
disclosable: ['name', 'birthdate', 'age_over_18', 'jurisdiction', 'kyc_level'], validUntil: '2027-09-30T00:00:00Z', status: { list: 'urn:s:kyc', index }, now: NOW });
|
||||||
|
const { credential, disclosures } = emite();
|
||||||
|
const LISTA = I.createStatusList({ issuer: iss, id: 'urn:s:kyc', revoked: [9], validUntil: '2026-10-07T00:00:00Z', now: NOW });
|
||||||
|
const POL = { id: 'exchange-onboarding-v1', trustedIssuers: [iss.id], require: [{ claim: 'age_over_18', equals: true }, { claim: 'jurisdiction', notIn: ['KP', 'IR'] }, { claim: 'kyc_level', atLeast: 2 }] };
|
||||||
|
const prez = (reveal = ['age_over_18', 'jurisdiction', 'kyc_level'], o = {}) => I.present({ credential: o.credential || credential, disclosures: o.disclosures || disclosures, reveal, presenter: hol, audience: o.audience || AUD, nonce: NONCE, now: NOW });
|
||||||
|
const judeca = (p, pol = POL, o = {}) => checkCompliance(p, pol, { audience: AUD, nonce: NONCE, now: NOW, statusLists: [LISTA], ...o });
|
||||||
|
|
||||||
|
test('politica: forma normala; aceeasi politica cu regulile si emitentii in alta ordine are acelasi hash; alta regula, alt hash', () => {
|
||||||
|
const a = definePolicy(POL), b = definePolicy({ ...POL, require: [...POL.require].reverse(), trustedIssuers: [iss.public] });
|
||||||
|
cere(a.policyHash === b.policyHash && /^0x[0-9a-f]{64}$/.test(a.policyHash), `${a.policyHash} ${b.policyHash}`);
|
||||||
|
cere(definePolicy({ ...POL, require: [{ claim: 'kyc_level', atLeast: 1 }] }).policyHash !== a.policyHash, 'o politica mai laxa are acelasi hash');
|
||||||
|
});
|
||||||
|
test('CONTROL: politica refuza campuri necunoscute, lista de emitenti goala, o regula cu doi operatori sau fara niciunul', () => {
|
||||||
|
const m = [arunca(() => definePolicy({ ...POL, allowAll: true })), arunca(() => definePolicy({ ...POL, trustedIssuers: [] })),
|
||||||
|
arunca(() => definePolicy({ ...POL, require: [{ claim: 'x', equals: 1, in: [1] }] })), arunca(() => definePolicy({ ...POL, require: [{ claim: 'x' }] }))];
|
||||||
|
cere(/unknown field/.test(m[0]) && /trustedIssuers is required/.test(m[1]) && /a rule is/.test(m[2]) && /a rule is/.test(m[3]), m.join(' | '));
|
||||||
|
});
|
||||||
|
test('conform: varsta, jurisdictia in afara celor interzise, nivelul de verificare; starea judecata', () => {
|
||||||
|
const r = judeca(prez()); cere(r.compliant && !r.reasons.length && r.notJudged === 0, JSON.stringify(r.reasons));
|
||||||
|
});
|
||||||
|
test('neconform: o afirmatie ceruta nearatata (kyc_level) -> motivul numeste regula', () => {
|
||||||
|
const r = judeca(prez(['age_over_18', 'jurisdiction'])); cere(!r.compliant && r.reasons.some((x) => /kyc_level/.test(x)), JSON.stringify(r.reasons));
|
||||||
|
});
|
||||||
|
test('neconform: jurisdictie interzisa (notIn); nivel sub prag (atLeast); valoare falsa (equals)', () => {
|
||||||
|
const c2 = I.issueCredential({ issuer: iss, holder: hol.public, type: 'KycCredential', claims: { ...CLAIMS, jurisdiction: 'KP', kyc_level: 1, age_over_18: false },
|
||||||
|
disclosable: ['age_over_18', 'jurisdiction', 'kyc_level'], validUntil: '2027-09-30T00:00:00Z', status: { list: 'urn:s:kyc', index: 4 }, now: NOW });
|
||||||
|
const r = judeca(prez(undefined, c2));
|
||||||
|
cere(!r.compliant && ['jurisdiction', 'kyc_level', 'age_over_18'].every((n) => r.reasons.some((x) => x.includes(n))), JSON.stringify(r.reasons));
|
||||||
|
});
|
||||||
|
test('neconform: emitent in afara celor ai politicii', () => {
|
||||||
|
const c3 = emite(alt); const r = judeca(prez(undefined, c3)); cere(!r.compliant && r.reasons.some((x) => /issuer trusted/.test(x)), JSON.stringify(r.reasons));
|
||||||
|
});
|
||||||
|
test('neconform: starea ceruta si lista nedata (nejudecat NU e "nerevocat"); fara cerinta de stare, conform cu randul nejudecat numarat', () => {
|
||||||
|
const r = judeca(prez(), POL, { statusLists: [] }); cere(!r.compliant && r.reasons.some((x) => /status to be judged/.test(x)), JSON.stringify(r.reasons));
|
||||||
|
const r2 = judeca(prez(), { ...POL, requireStatus: false }, { statusLists: [] }); cere(r2.compliant && r2.notJudged === 1, JSON.stringify(r2));
|
||||||
|
});
|
||||||
|
test('neconform: credentialul revocat de emitent', () => {
|
||||||
|
const c4 = emite(iss, 9); const r = judeca(prez(undefined, c4)); cere(!r.compliant && r.reasons.some((x) => /revoked/.test(x)), JSON.stringify(r.reasons));
|
||||||
|
});
|
||||||
|
test('neconform: prezentare facuta pentru alt verificator (reluare); fara public si nonce, judecata refuza sa inceapa', () => {
|
||||||
|
const r = judeca(prez(undefined, { audience: 'https://other.example' })); cere(!r.compliant && r.reasons.some((x) => /made for/.test(x)), JSON.stringify(r.reasons));
|
||||||
|
cere(/audience and nonce are required/.test(arunca(() => checkCompliance(prez(), POL, { now: NOW, statusLists: [LISTA] })) || ''), 'a judecat fara public');
|
||||||
|
});
|
||||||
|
test('rezultatul nu poarta afirmatiile detinatorului decat la cerere (keepClaims), si numai pentru o judecata conforma', () => {
|
||||||
|
const r = judeca(prez()); cere(!('claims' in r) && !JSON.stringify(r).includes('"RO"'), JSON.stringify(r));
|
||||||
|
const k = judeca(prez(), POL, { keepClaims: true }); cere(k.claims && k.claims.jurisdiction === 'RO', JSON.stringify(k.claims));
|
||||||
|
const kn = judeca(prez(['age_over_18']), POL, { keepClaims: true }); cere(!kn.compliant && !('claims' in kn), 'afirmatiile intoarse pentru o judecata neconforma');
|
||||||
|
});
|
||||||
|
test('inregistrarea (plic AIP-23 compliance) NU poarta date personale: nici valori, nici id-ul detinatorului', () => {
|
||||||
|
const e = complianceEnvelope(judeca(prez()), { audience: AUD, createdAt: NOW.toISOString(), buildProof });
|
||||||
|
const s = JSON.stringify(e);
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
const k = complianceEnvelope(r, { audience: AUD, buildProof, pseudonymKey: crypto.randomBytes(32) }).statement.subject;
|
||||||
|
const refacut = 'pseudonym:' + crypto.createHash('sha256').update(`aere-compliance-subject|${hol.id}|${AUD}`).digest('hex').slice(0, 40);
|
||||||
|
cere(a !== b && a === refacut && k !== refacut, `${a} ${b} ${k}`);
|
||||||
|
});
|
||||||
|
test('plicul verifica la verificatorul AIP-23 de referinta; unul cu rezultatul rescris nu', () => {
|
||||||
|
if (!fs.existsSync(VERIFY)) { sarite++; console.log(` SARIT plicul AIP-23: verificatorul nu e la ${VERIFY}; AERE_VERIFY_PROOF=<verify-proof.mjs>`); return 'SARIT'; }
|
||||||
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-conf-'));
|
||||||
|
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 e = complianceEnvelope(judeca(prez(['age_over_18'])), { audience: AUD, buildProof });
|
||||||
|
const rescris = JSON.parse(JSON.stringify(e)); rescris.statement.result = 'compliant';
|
||||||
|
cere(e.statement.result === 'not-compliant' && v(e) === 'VALID' && v(rescris) !== 'VALID', `${v(e)} ${v(rescris)}`);
|
||||||
|
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linia de comanda: comply -> COMPLIANT (0) si inregistrarea scrisa fara date personale; o prezentare care nu arata destul -> NOT COMPLIANT (1)', () => {
|
||||||
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-conf-cli-')); const CLI = path.join(AICI, 'identity-cli.mjs');
|
||||||
|
const run = (...a) => { const r = spawnSync(process.execPath, [CLI, ...a], { cwd: T, encoding: 'utf8' }); return { cod: r.status, out: (r.stdout || '') + (r.stderr || '') }; };
|
||||||
|
try {
|
||||||
|
for (const n of ['iss', 'hol']) cere(run('keygen', '--out', n + '.keys.json').cod === 0, 'keygen');
|
||||||
|
cere(run('pub', '--keys', 'hol.keys.json', '--out', 'hol.pub.json').cod === 0, 'pub');
|
||||||
|
cere(run('issue', '--issuer-keys', 'iss.keys.json', '--holder-pub', 'hol.pub.json', '--type', 'KycCredential', '--claim', 'age_over_18=true', '--claim', 'jurisdiction=RO',
|
||||||
|
'--claim', 'name=Ana Pop', '--all-disclosable', '--status-list', 'urn:s:1', '--status-index', '2', '--out', 'cred.json').cod === 0, 'issue');
|
||||||
|
cere(run('status-list', '--issuer-keys', 'iss.keys.json', '--id', 'urn:s:1', '--out', 'list.json').cod === 0, 'status-list');
|
||||||
|
const issId = run('id', '--keys', 'iss.keys.json').out.trim();
|
||||||
|
fs.writeFileSync(path.join(T, 'pol.json'), JSON.stringify({ id: 'p1', trustedIssuers: [issId], require: [{ claim: 'age_over_18', equals: true }, { claim: 'jurisdiction', notIn: ['KP'] }] }));
|
||||||
|
cere(run('present', '--cred', 'cred.json', '--reveal', 'age_over_18,jurisdiction', '--presenter-keys', 'hol.keys.json', '--audience', AUD, '--nonce', 'n1', '--out', 'p.json').cod === 0, 'present');
|
||||||
|
const a = run('comply', '--presentation', 'p.json', '--policy', 'pol.json', '--audience', AUD, '--nonce', 'n1', '--status-list', 'list.json', '--record', 'rec.json');
|
||||||
|
const rec = fs.readFileSync(path.join(T, 'rec.json'), 'utf8');
|
||||||
|
cere(a.cod === 0 && /COMPLIANT/.test(a.out) && /"result": "compliant"/.test(rec) && !/Ana Pop|"RO"|aere-id:/.test(rec), a.out + rec);
|
||||||
|
cere(run('present', '--cred', 'cred.json', '--reveal', 'jurisdiction', '--presenter-keys', 'hol.keys.json', '--audience', AUD, '--nonce', 'n2', '--out', 'p2.json').cod === 0, 'present 2');
|
||||||
|
const b = run('comply', '--presentation', 'p2.json', '--policy', 'pol.json', '--audience', AUD, '--nonce', 'n2', '--status-list', 'list.json');
|
||||||
|
cere(b.cod === 1 && /NOT COMPLIANT/.test(b.out) && /age_over_18/.test(b.out), b.out);
|
||||||
|
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\naere-compliance: ${treceri}/${treceri + esecuri.length} cum trebuia${sarite ? `, ${sarite} NEMASURATE (fara verificatorul AIP-23)` : ''}`);
|
||||||
|
process.exitCode = esecuri.length ? 1 : (sarite ? 2 : 0);
|
||||||
Loading…
Reference in New Issue
Block a user