aere-quantum/conformance/implementare-aere.mjs

184 lines
21 KiB
JavaScript

#!/usr/bin/env node
// implementare-aere.mjs: the published verifiers behind the one verdict form (verdict.mjs). This file holds NO verification rule of
// its own: an AIP-23 envelope is judged by the reference verifier (verify-proof.mjs, run as the program it is), a payment by the
// risk module, a receipt by the receipt verifier, a market timeline by the market judge, a dataset record, answer, consistency proof
// or sample by the dataset module, a presentation of a credential by the identity verifier. What it adds is the translation: the rule ids those verifiers name -> the public rule ids and
// the level of each (reguli.mjs), and the composition (a receipt or a timeline that carries an AIP-23 envelope gets that envelope
// judged too, so one verdict covers the artifact).
//
// node implementare-aere.mjs --capabilities what this implementation judges (artifacts, rules, online)
// node implementare-aere.mjs <file.json> [--online] one artifact: { artifact, input } (a vector file is one); verdict on stdout
// node implementare-aere.mjs --batch <list.json> [{ file, online }]; one verdict per line, in order
//
// Offline (no --online) nothing here opens a connection: an envelope that claims a notarization is judged without the claim and the
// verdict says so (PARTIAL: claim-not-checked). With --online the envelope goes to the reference verifier with the chain's endpoint.
// Exit: 0 VALID, 1 INVALID, 2 PARTIAL, 3 the artifact could not be judged at all (not a verdict).
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process';
import { VERIFICATOARE, FINALITY_SITES, VERIFY_PROOF, NEJUDECATE_IDENTITY, incarca } from './reguli.mjs';
import { acumulator, pica, trece, nemasurat, verdictUnic } from './verdict.mjs';
const RPC = { 2800: process.env.AERE_CONFORMANCE_RPC_2800 || 'https://rpc.aere.network', 28001: process.env.AERE_CONFORMANCE_RPC_28001 || 'https://testnet-rpc.aere.network' };
// a verifier that takes the reference verifier as a DEPENDENCY (the trust chain judges its envelopes with it) gets the real one even
// while the planting control substitutes a planted copy for the envelopes this file judges itself
const VERIFY_PROOF_DEP = process.env.AERE_CONF_VERIFY_PROOF_DEP || VERIFY_PROOF;
const LANT = (n) => (n === 28001 ? 'chain 28001, a testnet' : 'chain ' + n);
const ARTIFACTE = Object.values(VERIFICATOARE).flatMap((v) => v.artifacts);
const toateRegulile = Object.values(VERIFICATOARE).flatMap((v) => Object.values(v.reguli).map((r) => r[0]));
function ruleaza(cmd, args, timeout) {
return new Promise((gata) => {
const c = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }); let out = '', err = ''; const t = setTimeout(() => c.kill(), timeout);
c.stdout.on('data', (d) => { out += d; }); c.stderr.on('data', (d) => { err += d; });
c.on('error', (e) => { clearTimeout(t); gata({ code: null, out, err: String(e.message) }); }); c.on('close', (code) => { clearTimeout(t); gata({ code, out, err }); });
});
}
/** The envelope, judged by the reference verifier; its levels land in the accumulator under the public rule ids. */
async function plic(acc, env, { online, chain, raw }) {
if (!fs.existsSync(VERIFY_PROOF)) throw new Error('the AIP-23 reference verifier is not at ' + VERIFY_PROOF + ' (AERE_VERIFY_PROOF=<verify-proof.mjs>)');
const claim = env && typeof env === 'object' ? env.notarization : null; let judecat = env;
if (!online && claim) { judecat = { ...env }; delete judecat.notarization; }
const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aere-conf-')), 'envelope.json'); fs.writeFileSync(f, typeof raw === 'string' ? raw : JSON.stringify(judecat === undefined ? null : judecat)); // `raw`: the text of a file that is not JSON
const lant = Number(chain || (claim && claim.chainId) || 28001);
const args = [VERIFY_PROOF, f, '--json']; if (online) { if (!RPC[lant]) throw new Error('no endpoint known for chain ' + lant); args.push('--chain', String(lant), '--rpc', RPC[lant]); }
let r; try { r = await ruleaza(process.execPath, args, online ? 240000 : 60000); } finally { fs.rmSync(path.dirname(f), { recursive: true, force: true }); }
let j; try { j = JSON.parse(r.out); } catch { throw new Error('the reference verifier gave no verdict (exit ' + r.code + '): ' + (r.err || r.out).trim().split('\n').slice(-1)[0].slice(0, 160)); }
const nivel = (n) => (j.levels || []).find((x) => x.level === n); const R = VERIFICATOARE['aip23-envelope'].reguli;
for (const n of ['form', 'integrity']) { const x = nivel(n); if (!x) continue; if (x.state === 'FAILED') pica(acc, 'integrity', R[n][0], x.detail); else trece(acc, 'integrity', 'envelope ' + n); }
const s = nivel('signature');
if (!s) nemasurat(acc, 'authenticity', 'not-reached', 'the envelope failed before its signature was looked at');
else if (s.state === 'PASSED') trece(acc, 'authenticity', s.detail);
else if (s.state === 'FAILED') pica(acc, 'authenticity', R.signature[0], s.detail);
else nemasurat(acc, 'authenticity', s.state === 'ABSENT' ? 'absent' : 'unmeasurable', s.detail);
const fin = nivel('finality'); const ambele = (cause, reason) => { nemasurat(acc, 'anchoring', cause, reason); nemasurat(acc, 'finality', cause, reason); };
if (!online && claim) ambele('claim-not-checked', 'the envelope claims a notarization on chain ' + (claim.chainId || '?') + '; this run was offline, the claim was not checked');
else if (!fin) ambele('not-reached', 'the envelope failed before its notarization was looked at');
else if (fin.state === 'PASSED') { trece(acc, 'anchoring', fin.detail); trece(acc, 'finality', 'the post-quantum certificate of the covering anchor verified (' + LANT(lant) + '; the finality is that chain\'s)'); }
else if (fin.state === 'FAILED') {
const loc = FINALITY_SITES.filter((x) => x.re.test(fin.detail));
if (loc.length !== 1) throw new Error('the reference verifier refused finality at a site this suite does not classify: ' + fin.detail.slice(0, 120));
pica(acc, loc[0].level, R.finality[0], fin.detail);
if (loc[0].level === 'anchoring') nemasurat(acc, 'finality', 'not-applicable', 'nothing is anchored, so no certificate covers it');
else nemasurat(acc, 'anchoring', 'not-reached', 'the anchor certificate did not verify, the notary state under it was not read');
} else ambele(fin.state === 'ABSENT' ? 'absent' : fin.state === 'PENDING' ? 'pending' : 'unmeasurable', fin.detail);
return j;
}
const faraPlic = (acc) => { for (const l of ['authenticity', 'anchoring', 'finality']) nemasurat(acc, l, 'not-judged', 'this artifact carries no AIP-23 envelope; its commitment is signed or anchored through one, judged as the artifact `envelope`'); };
/** The rules a module named, as public ids at their levels; `text` is the module's own reasons. */
function reguli(acc, verifier, surse, motive, titlu) {
const R = VERIFICATOARE[verifier].reguli; const text = (motive || []).filter(Boolean).join('; ');
for (const s of surse) { if (!R[s]) throw new Error(`${verifier} named a rule this suite does not map: ${s}`); pica(acc, R[s][1], R[s][0], text); }
if (!surse.length) trece(acc, 'integrity', titlu);
}
async function judeca(artifact, input, online) {
const acc = acumulator();
if (artifact === 'envelope') await plic(acc, input.envelope, { online, chain: input.chain, raw: input.raw });
else if (artifact === 'payment-risk') {
const M = await incarca('risc'); const v = M.judeca(input.policy, input.inputs);
if ((v.decision === 'ALLOW') !== (v.rules.length === 0)) throw new Error('the risk module refused without naming a rule');
reguli(acc, 'payment-risk', v.rules, v.reasons, 'the declared policy allows this payment'); faraPlic(acc);
} else if (artifact === 'payment-receipt') {
const M = await incarca('chitanta'); const r = M.verificaChitanta(input.receipt, { policy: input.policy, inputs: input.inputs, tx: input.tx, receipt: input.txReceipt });
if ((r.verdict === 'VALID') !== (r.reguli.length === 0)) throw new Error('the receipt verifier refused without naming a rule');
reguli(acc, 'payment-receipt', r.reguli, r.motive, 'no rule of the receipt verifier refused' + (r.chainChecked ? ' (transaction facts supplied)' : ' (no transaction facts supplied)'));
await plic(acc, input.receipt && input.receipt.envelope, { online, chain: input.chain });
} else if (artifact === 'market-timeline') {
// the envelope first: WHEN it was notarized is the reading of the reference verifier under the anchor (its finality level's
// `certified`), handed to the judge. A `certified` the artifact brings itself is dropped: that reading is never the artifact's.
const M = await incarca('piata'); const { certified: adus, ...cronologie } = input; let certified = null;
if (input.envelope) { const j = await plic(acc, input.envelope, { online, chain: input.chainId }); const f = (j.levels || []).find((x) => x.level === 'finality'); if (f && f.state === 'PASSED' && f.certified) certified = { ...f.certified, statementHash: input.envelope.statementHash }; } else faraPlic(acc);
const r = M.judeca(certified ? { ...cronologie, certified } : cronologie);
if (r.verdict === 'INCOMPLETE') nemasurat(acc, 'integrity', 'incomplete', 'the timeline is not terminal (' + (r.status || 'no job or no events') + ')');
else { if ((r.verdict === 'VALID') !== (r.reguli.length === 0)) throw new Error('the market judge refused without naming a rule'); reguli(acc, 'market-timeline', r.reguli, r.motive, 'no rule of the market judge refused (status ' + r.status + ')'); }
} else if (artifact === 'audit-log' || artifact === 'audit-inclusion' || artifact === 'audit-consistency') {
// an audit log against its own chain and, when given, an attested head; an inclusion or a consistency proof against tree heads.
// The checks of a head's signature are the sidecar's whichever file calls them: their rules are mapped there.
const S = VERIFICATOARE['audit-log'].reguli, T = VERIFICATOARE['audit-tree'].reguli; let r, semnat = null;
if (artifact === 'audit-log') { const M = await incarca('sidecar'); r = input.head !== undefined ? M.verificaFataDeCap(input.log, input.head, { semnatar: input.signer || null }) : M.verificaJurnal(input.log); if (r.ok && input.head !== undefined) semnat = r.semnatDe; }
else { const M = await incarca('arbore'); r = artifact === 'audit-inclusion' ? M.verificaIncludere(input.proof, { cap: input.head || null, semnatar: input.signer || null }) : M.verificaConsistenta(input.proof, { vechi: input.older, nou: input.newer, semnatar: input.signer || null }); if (r.ok) semnat = r.semnatDe || r.semnatNou || null; }
if (r.ok) { trece(acc, 'integrity', 'no rule of the audit log verifier refused'); if (semnat) trece(acc, 'authenticity', 'the head is signed with ML-DSA-65 by key ' + String(semnat).slice(0, 18) + (input.signer ? '.., the expected signer' : '.. (not compared with an expected signer)')); else nemasurat(acc, 'authenticity', 'absent', 'no signed head'); }
else { const m = (artifact === 'audit-log' ? S[r.regula] : T[r.regula] || S[r.regula]); if (!m) throw new Error('the audit log verifier refused without a rule this suite maps: ' + r.regula); pica(acc, m[1], m[0], r.motiv);
nemasurat(acc, m[1] === 'integrity' ? 'authenticity' : 'integrity', 'not-reached', 'the verifier stopped at its first refusal'); }
for (const l of ['anchoring', 'finality']) nemasurat(acc, l, 'not-judged', 'an audit head is anchored by notarizing its envelope, judged as the artifact `envelope`');
} else if (artifact === 'agent-ledger') {
// an agent's signed ledger, verified as a stranger would: the policy re-hashed, the identity bound to the key, every signature, the
// chain, and every decision re-derived by replaying the policy
const M = await incarca('ledger'); const R = VERIFICATOARE['agent-ledger'].reguli; const o = { policy: input.policy, policyHash: input.policyHash, anchors: input.anchors || [], notBefore: input.notBefore === undefined ? null : input.notBefore, revocations: input.revocations || [] };
if (input.maxTime !== undefined) o.maxTime = input.maxTime;
const r = M.verifyLedger(input.ledger, o);
if (r.ok) { trece(acc, 'integrity', `the ledger verifies under its policy: ${r.seq} entries, ${r.allowedPayments} allowed payments, time ${r.time.lowerBound}`); trece(acc, 'authenticity', 'the agent id is derived from its ML-DSA-65 key and every entry is signed by it'); }
else { const m = R[r.rule]; if (!m) throw new Error('the ledger verifier refused without a rule this suite maps: ' + r.rule); pica(acc, m[1], m[0], r.error); nemasurat(acc, m[1] === 'integrity' ? 'authenticity' : 'integrity', 'not-reached', 'the verifier stopped at its first refusal'); }
for (const l of ['anchoring', 'finality']) nemasurat(acc, l, 'not-judged', 'a ledger head is anchored by notarizing its envelope, judged as the artifact `envelope`');
} else if (artifact === 'identity-presentation') {
// a presentation of a credential, judged by the identity verifier with what THIS verifier brings: its clock (required: a verdict
// on the clock of the machine that runs the vector would not reproduce), its audience, its nonce, the issuers it trusts, the
// status lists and the revocations it was handed. The verifier makes every check it can reach and names every rule that refused.
const M = await incarca('identity'); const R = VERIFICATOARE.identity.reguli;
if (typeof input.now !== 'string') throw new Error('an identity presentation is judged on the verifier\'s clock: input.now (an RFC 3339 time) is required');
const o = { now: input.now }; for (const k of ['audience', 'nonce', 'trustedIssuers', 'statusLists', 'revocations', 'maxAgeS']) if (input[k] !== undefined) o[k] = input[k];
const r = M.verifyPresentation(input.presentation, o);
if (r.valid !== (r.rules.length === 0)) throw new Error('the identity verifier refused without naming a rule');
for (const s of r.rules) { if (!R[s]) throw new Error('the identity verifier named a rule this suite does not map: ' + s); pica(acc, R[s][1], R[s][0], r.rows.filter((x) => x.pass === false && x.rule === s).map((x) => x.name + ': ' + x.detail).join('; ')); }
const oprit = r.rules.find((s) => M.OPRIRI.includes(s));
if (!r.rules.some((s) => R[s][1] === 'integrity')) trece(acc, 'integrity', 'no integrity rule of the identity verifier refused (' + r.rows.filter((x) => x.pass === true).length + ' checks held)');
if (!r.rules.some((s) => R[s][1] === 'authenticity')) { if (oprit) nemasurat(acc, 'authenticity', 'not-reached', 'the verifier stopped at ' + R[oprit][0] + ' before every signature was looked at'); else trece(acc, 'authenticity', 'the ids are derived from the keys; the credential, the presentation and every delegation link are signed with Ed25519 and ML-DSA-65, both'); }
// what the verifier did not judge, and why: a check that needs something the verifier did not bring leaves the verdict PARTIAL
const NEJ = NEJUDECATE_IDENTITY; if (M.NEJUDECATE.some((c) => !(c in NEJ))) throw new Error('the identity verifier declares a cause of not judging that this suite does not map');
for (const c of r.unjudged) { if (!(c in NEJ)) throw new Error('the identity verifier left unjudged for a cause this suite does not map: ' + c); if (NEJ[c]) nemasurat(acc, NEJ[c][0], NEJ[c][1], r.rows.filter((x) => x.unjudged === c).map((x) => x.name + ': ' + x.detail).join('; ')); }
for (const l of ['anchoring', 'finality']) nemasurat(acc, l, 'not-judged', 'a credential or a delegation is anchored by notarizing its AIP-23 envelope, judged as the artifact `envelope`');
} else if (artifact === 'ai-trust-bundle') {
// the trust chain of an AI result, judged by its own verifier (which runs the reference verifier on each envelope and the agents'
// and the audit log's verifiers on theirs). Offline, a notarization claimed by the attestation, the runtime record or the audit
// head is taken out of the copy judged (those envelopes are not hashed into the ledger) and reported as not checked.
const M = await incarca('aitrust'); const b = JSON.parse(JSON.stringify(input.bundle === undefined ? null : input.bundle)); let claim = false; const ob = (x) => !!x && typeof x === 'object';
const lant = Number((ob(b) && ob(b.witness) && b.witness.chainId) || input.chain || 28001);
if (!online && ob(b)) {
for (const e of [ob(b.version) && b.version.attestation, ob(b.runtime) && b.runtime.record, ob(b.runtime) && b.runtime.head]) if (ob(e) && e.notarization) { delete e.notarization; claim = true; }
if (ob(b.provenance) && ob(b.provenance.proofOfAi) && b.provenance.proofOfAi.notarization) { nemasurat(acc, 'integrity', 'claim-not-checked', 'the Proof of AI envelope claims a notarization: this bundle is judged only with the chain (--online)'); return verdictUnic(artifact, acc); }
}
if (online && !RPC[lant]) throw new Error('no endpoint known for chain ' + lant);
const v = await M.verifica(b, { verifyProof: VERIFY_PROOF_DEP, rpc: online ? RPC[lant] : null, chain: online ? lant : null });
const R = VERIFICATOARE['ai-trust'].reguli; const st = v.levels, det = v.details;
for (const l of Object.keys(st)) if (st[l] === 'FAILED') { const r = R[det[l].reason]; if (!r) throw new Error('ai-trust named a rule this suite does not map: ' + det[l].reason); pica(acc, r[1], r[0], det[l].detail); }
const INT = ['version', 'runtime', 'permissions', 'provenance'];
const nem = INT.find((l) => st[l] === 'UNMEASURED');
if (nem) nemasurat(acc, 'integrity', 'unmeasurable', nem + ': ' + det[nem].reason);
else if (st.form === 'PASSED' && st.permissions === 'PASSED' && st.provenance === 'PASSED') trece(acc, 'integrity', 'the links hold: ' + INT.map((l) => l + ' ' + st[l]).join(', '));
if (st.identity === 'PASSED') trece(acc, 'authenticity', det.identity.detail); else if (st.identity !== 'FAILED') nemasurat(acc, 'authenticity', 'not-reached', 'the identity level was not reached');
const ambele = (cause, reason) => { nemasurat(acc, 'anchoring', cause, reason); nemasurat(acc, 'finality', cause, reason); };
if (st.witness === 'PASSED') { trece(acc, 'anchoring', det.witness.detail); trece(acc, 'finality', 'the post-quantum certificate of the covering anchor verified (' + LANT(lant) + '; the finality is that chain\'s)'); }
else if (st.witness === 'FAILED') nemasurat(acc, 'finality', 'not-applicable', 'no head of this ledger is anchored');
else if (st.witness === 'UNMEASURED') ambele(online ? 'unmeasurable' : 'claim-not-checked', online ? String(det.witness.detail).slice(0, 200) : 'the bundle brings notarized heads; this run was offline, they were not read');
else if (st.form === 'PASSED') ambele('unwitnessed', 'no witness: every time in this bundle is the statement of whoever holds the keys');
if (claim) nemasurat(acc, 'anchoring', 'claim-not-checked', 'an envelope of the bundle claims a notarization; this run was offline, the claim was not checked');
} else if (artifact.startsWith('dataset-')) {
const M = await incarca('baza'); let r;
if (artifact === 'dataset-record') r = M.verificaInregistrare(input.record, input.commitment);
else if (artifact === 'dataset-answer') { r = M.verificaRaspuns(input.answer, input.commitment); r.ok = r.verdict === 'VALID'; }
else if (artifact === 'dataset-consistency') r = M.verificaConsistentaVersiuni(input.proof, input.older, input.newer);
else if (artifact === 'dataset-sample') r = await M.esantioneaza({ url: 'vector://served', commitment: input.commitment, k: input.k, seed: input.seed, ia: async (u) => (input.served || {})[u.split('/').pop()] || null });
else throw new Error('unknown artifact ' + artifact);
if (r.ok !== (r.reguli.length === 0)) throw new Error('the dataset module refused without naming a rule');
reguli(acc, 'dataset', r.reguli, r.motive, 'no rule of the dataset module refused'); faraPlic(acc);
} else throw new Error('unknown artifact ' + artifact);
return verdictUnic(artifact, acc);
}
async function unul(file, online) {
let j; try { j = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { return { error: 'the file cannot be read: ' + e.message }; }
if (!j || !ARTIFACTE.includes(j.artifact) || !j.input) return { error: 'not { artifact, input } with a known artifact (' + ARTIFACTE.join(', ') + ')' };
try { return await judeca(j.artifact, j.input, online); } catch (e) { return { error: String(e.message).split('\n')[0].slice(0, 300) }; }
}
const a = process.argv.slice(2);
if (a.includes('--capabilities')) console.log(JSON.stringify({ v: 1, kind: 'aere-conformance-capabilities', name: 'aere reference verifiers (JavaScript)', artifacts: ARTIFACTE, rules: toateRegulile.sort(), online: true, batch: true }));
else if (a.includes('--batch')) {
const lista = JSON.parse(fs.readFileSync(a[a.indexOf('--batch') + 1], 'utf8')); const rez = new Array(lista.length); let urm = 0;
const lucrator = async () => { while (urm < lista.length) { const k = urm++; rez[k] = await unul(lista[k].file, !!lista[k].online); } };
await Promise.all(Array.from({ length: Math.min(6, lista.length) }, lucrator));
for (const r of rez) console.log(JSON.stringify(r));
} else {
const file = a.find((x) => !x.startsWith('--'));
if (!file) { console.error('usage: node implementare-aere.mjs --capabilities | <file.json> [--online] | --batch <list.json>'); process.exitCode = 3; }
else { const r = await unul(file, a.includes('--online')); console.log(JSON.stringify(r, null, 1)); process.exitCode = r.error ? 3 : r.verdict === 'VALID' ? 0 : r.verdict === 'INVALID' ? 1 : 2; }
}