diff --git a/identity/README.md b/identity/README.md index bea502d..ef020fd 100644 --- a/identity/README.md +++ b/identity/README.md @@ -19,7 +19,11 @@ node identity-cli.mjs verify --presentation presentation.json --audience https:/ ``` `verify` prints one line per check (`ok`, `FAIL`, or `--` for not judged, with the reason) and `VALID` or `INVALID`; it exits 0 on -VALID, 1 on INVALID, 2 on a usage error. The claims it returns are the plain ones plus the ones the holder chose to show. +VALID, 1 on INVALID, 2 on a usage error (a broken `--trust-issuer` or `--max-age` included). The claims it returns are the plain ones +plus the ones the holder chose to show. `--json` prints the whole result as one object, with `subject` (type, credential, issuer, +holder, presenter, number of delegations) when no check failed. `--at ` judges on that clock instead of now: the +clock is the verifier's in any case, and a verdict given at one moment (by a service, for instance) can then be checked again later +with the same result. ## What is signed, and by whom @@ -94,6 +98,12 @@ node identity-cli.mjs comply --presentation p.json --policy policy.json --audien --status-list list.json --record record.json [--pseudonym-key-file key] ``` +`comply --json [--with-record] [--at T]` prints the result and, with `--with-record`, the record in one object; with `--at` the +record's time is T too, so the same command gives the same record byte for byte. It exits 0 compliant, 1 not compliant, 2 when the +policy or an option is refused (with the reason). Aere Cloud runs this command line, unmodified, behind `POST /v1/identity/verify` +and `POST /v1/compliance/check`, and each answer names the SHA-256 of the files that judged and the command, with `--at`, that +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 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 diff --git a/identity/identity-cli.mjs b/identity/identity-cli.mjs index 7ce4e2f..e00aff4 100644 --- a/identity/identity-cli.mjs +++ b/identity/identity-cli.mjs @@ -11,9 +11,11 @@ // revoke --keys k.json --delegation d.json [--reason R] --out r.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 ...] -// [--revocation r.json ...] [--max-age S] [--json] +// [--revocation r.json ...] [--max-age S] [--at T] [--json] --at: judge at time T (RFC 3339 UTC), not now // 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 +// [--json [--with-record]] [--at T] the whole result as one JSON object (with the record inside) +// Coduri de iesire: 0 VALID / COMPLIANT, 1 INVALID / NOT COMPLIANT, 2 intrare respinsa (motivul pe stderr, `error: ...`). // O valoare de --claim se citeste ca JSON daca e JSON (true, 42, {"a":1}), altfel ca text. import fs from 'node:fs'; import path from 'node:path'; @@ -34,6 +36,9 @@ const scrie = (f, o, privat = false) => { }; const lista = (v) => (v === '*' ? '*' : String(v).split(',').map((x) => x.trim()).filter(Boolean)); const zile = (n) => new Date(Date.now() + Number(n) * 86400000).toISOString(); +// 2026-09-30: --at judeca pe ceasul dat, nu pe al masinii: un verdict dat la un moment (de pilda de API-ul Cloud) se +// reface mai tarziu identic; fara --at, acum. Ceasul e al verificatorului oricum (cine ruleaza unealta il alege), deci nu slabeste nimic. +const momentul = () => { const a = get('--at'); if (a == null) return new Date(); if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(a) || Number.isNaN(Date.parse(a))) throw new Folosire('--at is an RFC 3339 UTC time (2026-09-30T08:00:00Z)'); return new Date(a); }; async function main() { if (cmd === 'keygen') { const k = I.generateKeys(); scrie(cere('--out'), I.exportKeys(k), true); console.log(k.id); return 0; } @@ -81,9 +86,15 @@ async function main() { } if (cmd === 'verify') { const p = citeste(cere('--presentation')); - const trusted = toate('--trust-issuer').map((t) => (/^aere-id:/.test(t) ? t : citeste(t))); + // 2026-09-30: un emitent de incredere stricat e o greseala a verificatorului (cod 2, cu motivul), nu o cadere fara verdict + const trusted = toate('--trust-issuer').map((t) => { + if (/^aere-id:/.test(t)) { if (!/^aere-id:[0-9a-f]{40}$/.test(t)) throw new Folosire(`--trust-issuer ${t} is not an aere-id (aere-id: + 40 hex)`); return t; } + const pub = citeste(t); try { I.idOf(pub); } catch { throw new Folosire(`--trust-issuer ${path.basename(t)} is not a public key file (the output of pub)`); } return pub; + }); + const maxAgeS = Number(get('--max-age') ?? 300); + if (!Number.isInteger(maxAgeS) || maxAgeS < 1 || maxAgeS > 3600) throw new Folosire('--max-age is a number of seconds, 1..3600'); const r = I.verifyPresentation(p, { audience: get('--audience'), nonce: get('--nonce'), trustedIssuers: trusted.length ? trusted : null, - statusLists: toate('--status-list').map(citeste), revocations: toate('--revocation').map(citeste), maxAgeS: Number(get('--max-age') ?? 300) }); + statusLists: toate('--status-list').map(citeste), revocations: toate('--revocation').map(citeste), maxAgeS, now: momentul() }); if (are('--json')) console.log(JSON.stringify(r, null, 1)); else { for (const x of r.rows) console.log(`${x.pass === true ? 'ok' : x.pass === false ? 'FAIL' : '--'} ${x.name}${x.detail ? ': ' + x.detail : ''}`); @@ -96,14 +107,21 @@ async function main() { // 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 pr = citeste(cere('--presentation')), po = citeste(cere('--policy')); + const audience = cere('--audience'), nonce = cere('--nonce'), statusLists = toate('--status-list').map(citeste), revocations = toate('--revocation').map(citeste); + // 2026-09-30: o politica stricata e o greseala a verificatorului (cod 2, cu motivul politicii), nu o cadere fara verdict + const acum = momentul(); + let r; try { r = checkCompliance(pr, po, { audience, nonce, statusLists, revocations, now: acum }); } catch (e) { throw new Folosire(e.message); } + let env = null; + if (get('--record') || are('--with-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 })); + env = complianceEnvelope(r, { audience, buildProof, pseudonymKey: k, createdAt: acum.toISOString() }); + if (get('--record')) scrie(get('--record'), env); } + // --json: rezultatul intreg (si inregistrarea, cu --record sau --with-record) intr-un singur obiect, pentru masini (API-ul Cloud) + if (are('--json')) console.log(JSON.stringify({ ...r, ...(env ? { record: env } : {}) }, null, 1)); + else console.log(r.compliant ? `COMPLIANT with ${r.policyId} (${r.policyHash})` : `NOT COMPLIANT with ${r.policyId}:\n ` + r.reasons.join('\n ')); 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)'); diff --git a/identity/identity.mjs b/identity/identity.mjs index af4b01d..92c68e5 100644 --- a/identity/identity.mjs +++ b/identity/identity.mjs @@ -285,7 +285,9 @@ export function verifyPresentation(p, { audience = null, nonce = null, now = new const ok = (name, pass, detail = '') => { rows.push({ name, pass: !!pass, detail: pass ? '' : detail }); return !!pass; }; const nejudecat = (name, detail) => rows.push({ name, pass: null, detail }); const acum = new Date(now).getTime(); - const gata = (claims = null) => { const valid = rows.every((r) => r.pass !== false); return { valid, rows, notJudged: rows.filter((r) => r.pass === null).length, claims: valid ? claims : null }; }; + // 2026-09-30 (API-ul Identity din Cloud): o prezentare VALIDA spune si despre cine e (tip, credential, emitent, detinator, cine a + // prezentat), ca verificatorul care nu a dat trustedIssuers sa vada pe cine ar trebui sa creada; numai pe drumul care ajunge la capat + const gata = (claims = null, subject = null) => { const valid = rows.every((r) => r.pass !== false); return { valid, rows, notJudged: rows.filter((r) => r.pass === null).length, claims: valid ? claims : null, subject: valid ? subject : null }; }; // configuratia verificatorului se valideaza inainte (o cheie de incredere stricata e o greseala a lui, nu o prezentare invalida) const idsIncredere = trustedIssuers == null ? null : trustedIssuers.map((x) => (typeof x === 'string' && ID.test(x) ? x : idOf(x))); try { @@ -397,7 +399,7 @@ export function verifyPresentation(p, { audience = null, nonce = null, now = new } if (lant.length && !revocations.length) nejudecat('delegation: revocations', 'none given; a revocation the verifier was not handed cannot be seen'); const claims = Object.fromEntries([...Object.entries(S.claims), ...dezvaluite.map((d) => [d.name, d.value])].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))); - return gata(claims); + return gata(claims, { type: S.type, credential: S.id, issuer: S.issuer.id, holder: S.holder.id, presenter: B.presenter.id, delegations: lant.length }); } catch (e) { ok('presentation: readable', false, String(e && e.message || e).slice(0, 200)); return gata();