aere-quantum/identity/identity-cli.mjs

114 lines
8.9 KiB
JavaScript

#!/usr/bin/env node
// identity-cli.mjs: linia de comanda a lui AERE Identity (identity.mjs). Iesiri: 0 bun / VALID, 1 INVALID, 2 folosire gresita.
// keygen --out keys.json (0600; refuza sa suprascrie)
// pub --keys keys.json [--out pub.json] partea publica, de dat altora
// id --pub pub.json | --keys keys.json
// issue --issuer-keys k.json --holder-pub h.json --type T --claim name=value ... [--disclosable a,b | --all-disclosable]
// [--valid-days N] [--status-list ID --status-index I] [--decoys N] --out cred.json
// status-list --issuer-keys k.json --id ID [--size BITS] [--revoke i,j] [--valid-days N] --out list.json
// delegate --from-keys k.json --to-pub p.json [--parent d.json] [--credentials ids|*] [--claims names|*] [--audiences a|*]
// [--valid-minutes M] [--max-depth D] --out d.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
// verify --presentation p.json [--audience A --nonce N] [--trust-issuer id|pub.json ...] [--status-list l.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.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as I from './identity.mjs';
class Folosire extends Error {}
const argv = process.argv.slice(2);
const cmd = argv[0];
const get = (n) => { const i = argv.indexOf(n); return i === -1 ? null : (argv[i + 1] ?? null); };
const toate = (n) => argv.flatMap((a, i) => (a === n && argv[i + 1] != null ? [argv[i + 1]] : []));
const are = (n) => argv.includes(n);
const cere = (n) => { const v = get(n); if (v == null) throw new Folosire(`${cmd} needs ${n}`); return v; };
const citeste = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { throw new Folosire(`cannot read ${f}: ${e.message}`); } };
const scrie = (f, o, privat = false) => {
if (privat && fs.existsSync(f)) throw new Folosire(`${f} exists; a key file is never overwritten`);
fs.writeFileSync(f, JSON.stringify(o, null, 1) + '\n', privat ? { mode: 0o600, flag: 'wx' } : undefined);
};
const lista = (v) => (v === '*' ? '*' : String(v).split(',').map((x) => x.trim()).filter(Boolean));
const zile = (n) => new Date(Date.now() + Number(n) * 86400000).toISOString();
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 === '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 === 'issue') {
const issuer = I.importKeys(citeste(cere('--issuer-keys')));
const claims = {};
for (const c of toate('--claim')) {
const i = c.indexOf('='); if (i < 1) throw new Folosire('--claim is name=value');
const n = c.slice(0, i), v = c.slice(i + 1); let val; try { val = JSON.parse(v); } catch { val = v; }
if (Object.hasOwn(claims, n)) throw new Folosire('--claim ' + n + ' given twice');
claims[n] = val;
}
const disclosable = are('--all-disclosable') ? null : (get('--disclosable') ? lista(get('--disclosable')) : []);
const sl = get('--status-list');
const r = I.issueCredential({ issuer, holder: citeste(cere('--holder-pub')), type: cere('--type'), claims, disclosable,
validUntil: zile(get('--valid-days') ?? 365), status: sl ? { list: sl, index: Number(cere('--status-index')) } : null, decoys: Number(get('--decoys') ?? 0) });
scrie(cere('--out'), { v: 1, kind: 'aere-credential-with-disclosures', credential: r.credential, disclosures: r.disclosures });
console.log(`issued ${r.credential.statement.id} to ${r.credential.statement.holder.id}: ${Object.keys(r.credential.statement.claims).length} plain claim(s), ${r.disclosures.length} disclosable (the file holds the disclosures: give it to the holder only)`);
return 0;
}
if (cmd === 'status-list') {
const issuer = I.importKeys(citeste(cere('--issuer-keys')));
const l = I.createStatusList({ issuer, id: cere('--id'), size: Number(get('--size') ?? I.MIN_STATUS_BITS), revoked: get('--revoke') ? lista(get('--revoke')).map(Number) : [], validUntil: zile(get('--valid-days') ?? 7) });
scrie(cere('--out'), l); console.log(`status list ${l.statement.id}: ${l.statement.size} entries, valid until ${l.statement.validUntil}`); return 0;
}
if (cmd === 'delegate') {
const from = I.importKeys(citeste(cere('--from-keys')));
const d = I.delegate({ from, to: citeste(cere('--to-pub')), parent: get('--parent') ? citeste(get('--parent')) : null,
scope: { credentials: lista(get('--credentials') ?? '*'), claims: lista(get('--claims') ?? '*'), audiences: lista(get('--audiences') ?? '*') },
notAfter: new Date(Date.now() + Number(get('--valid-minutes') ?? 60) * 60000).toISOString(), maxDepth: Number(get('--max-depth') ?? 0) });
scrie(cere('--out'), d); console.log(`delegated ${I.delegationHash(d)}: ${d.statement.from.id} -> ${d.statement.to.id} until ${d.statement.notAfter}`); return 0;
}
if (cmd === 'revoke') {
const r = I.revokeDelegation({ by: I.importKeys(citeste(cere('--keys'))), delegation: citeste(cere('--delegation')), reason: get('--reason') });
scrie(cere('--out'), r); console.log(`revoked ${r.statement.target} at ${r.statement.at}`); return 0;
}
if (cmd === 'present') {
const c = citeste(cere('--cred'));
if (c.kind !== 'aere-credential-with-disclosures') throw new Folosire('--cred is the file written by issue');
const p = I.present({ credential: c.credential, disclosures: c.disclosures, reveal: get('--reveal') ? lista(get('--reveal')) : [],
presenter: I.importKeys(citeste(cere('--presenter-keys'))), delegations: toate('--delegation').map(citeste), audience: cere('--audience'), nonce: cere('--nonce') });
scrie(cere('--out'), p); console.log(`presentation for ${p.binding.audience}: ${p.disclosures.length} claim(s) disclosed`); return 0;
}
if (cmd === 'verify') {
const p = citeste(cere('--presentation'));
const trusted = toate('--trust-issuer').map((t) => (/^aere-id:/.test(t) ? t : citeste(t)));
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) });
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 : ''}`);
console.log(r.valid ? `VALID: every present claim holds${r.notJudged ? `; ${r.notJudged} not judged (the -- lines)` : ''}` : 'INVALID');
if (r.valid) console.log('claims: ' + JSON.stringify(r.claims));
}
return r.valid ? 0 : 1;
}
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)) {
main().then((c) => { process.exitCode = c; }, (e) => { console.error('error: ' + (e.message || e)); process.exitCode = e instanceof Folosire ? 2 : 1; });
}