identity: verify and comply as a service can judge (--json, --at), subject in the result, refused input exits 2
- verify: --at <RFC 3339 UTC> judges on that clock (the verifier's clock in any case), so a verdict given at one moment can be checked again later with the same result; a broken --trust-issuer or --max-age exits 2 with the reason instead of failing without a verdict; the JSON result names the subject (type, credential, issuer, holder, presenter, delegations) when no check failed - comply: --json [--with-record] prints the result and the record in one object; with --at the record's time is the same, so the same command gives the same record byte for byte; a refused policy exits 2 with its reason - Aere Cloud runs this command line unmodified behind POST /v1/identity/verify and POST /v1/compliance/check Tests: identity 43/43, negative control 46/46.
This commit is contained in:
parent
8f5f0bd00d
commit
aec0ccbead
@ -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
|
`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 <RFC 3339 UTC time>` 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
|
## 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]
|
--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
|
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, 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
|
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
|
||||||
|
|||||||
@ -11,9 +11,11 @@
|
|||||||
// revoke --keys k.json --delegation d.json [--reason R] --out r.json
|
// 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
|
// 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] [--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 ...]
|
// 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
|
// [--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.
|
// 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';
|
||||||
@ -34,6 +36,9 @@ 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();
|
||||||
|
// 2026-09-30: --at <RFC 3339 UTC> 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() {
|
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; }
|
||||||
@ -81,9 +86,15 @@ async function main() {
|
|||||||
}
|
}
|
||||||
if (cmd === 'verify') {
|
if (cmd === 'verify') {
|
||||||
const p = citeste(cere('--presentation'));
|
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,
|
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));
|
if (are('--json')) console.log(JSON.stringify(r, null, 1));
|
||||||
else {
|
else {
|
||||||
for (const x of r.rows) console.log(`${x.pass === true ? 'ok' : x.pass === false ? 'FAIL' : '--'} ${x.name}${x.detail ? ': ' + x.detail : ''}`);
|
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
|
// 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)
|
// inregistrarea (plic AIP-23 compliance) fara date personale, cu proof-kinds de langa (../proof-kinds)
|
||||||
const { checkCompliance, complianceEnvelope } = await import('./conformitate.mjs');
|
const { checkCompliance, complianceEnvelope } = await import('./conformitate.mjs');
|
||||||
const r = checkCompliance(citeste(cere('--presentation')), citeste(cere('--policy')), { audience: cere('--audience'), nonce: cere('--nonce'),
|
const pr = citeste(cere('--presentation')), po = citeste(cere('--policy'));
|
||||||
statusLists: toate('--status-list').map(citeste), revocations: toate('--revocation').map(citeste) });
|
const 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 '));
|
// 2026-09-30: o politica stricata e o greseala a verificatorului (cod 2, cu motivul politicii), nu o cadere fara verdict
|
||||||
if (get('--record')) {
|
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 { buildProof } = await import('../proof-kinds/proof-kinds.mjs');
|
||||||
const k = get('--pseudonym-key-file') ? fs.readFileSync(get('--pseudonym-key-file')) : null;
|
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;
|
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)');
|
throw new Folosire('usage: identity-cli.mjs keygen|pub|id|issue|status-list|delegate|revoke|present|verify|comply ... (see README.md)');
|
||||||
|
|||||||
@ -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 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 nejudecat = (name, detail) => rows.push({ name, pass: null, detail });
|
||||||
const acum = new Date(now).getTime();
|
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)
|
// 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)));
|
const idsIncredere = trustedIssuers == null ? null : trustedIssuers.map((x) => (typeof x === 'string' && ID.test(x) ? x : idOf(x)));
|
||||||
try {
|
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');
|
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)));
|
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) {
|
} catch (e) {
|
||||||
ok('presentation: readable', false, String(e && e.message || e).slice(0, 200));
|
ok('presentation: readable', false, String(e && e.message || e).slice(0, 200));
|
||||||
return gata();
|
return gata();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user