aere-quantum/conformance/reguli.mjs

221 lines
21 KiB
JavaScript

// reguli.mjs: which verifiers the conformance suite enrols, the rules each of them NAMES (derived from its code, never typed here as
// a list of its own), the public id and the level of each rule.
//
// A verifier that lives in this repository as a module exports `REGULI` (the rules it can name; its helper throws on any other) and
// carries a /*PAZA:<rule>*/ marker on every check of a rule. The derivation imports the module, reads its source, and requires:
// declared == marked == mapped below. A rule added to a verifier and not mapped here, or mapped here and gone from the verifier, is a
// refusal of the derivation, not a silent drift. The AIP-23 reference verifier (verify-proof.mjs) is a published program, not edited
// for this suite: its rules are its level names, read from its `level('<name>', ... 'FAILED'` sites, and every FAILED site of its
// `finality` level must be classified below (anchoring or finality) or the derivation refuses.
import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
export const LEVELS = ['integrity', 'authenticity', 'anchoring', 'finality'];
/** The AIP-23 reference verifier: AERE_VERIFY_PROOF=<verify-proof.mjs> (published with aere-node), or the development repository. */
export const VERIFY_PROOF = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs');
/** The modules judged in-process. `env` names the variable through which the planting control substitutes a planted copy. */
export const MODULE = {
risc: { url: new URL('../payments/risc.mjs', import.meta.url), env: 'AERE_CONF_MODUL_RISC' },
chitanta: { url: new URL('../payments/chitanta.mjs', import.meta.url), env: 'AERE_CONF_MODUL_CHITANTA' },
piata: { url: new URL('../market-settlement/piata.mjs', import.meta.url), env: 'AERE_CONF_MODUL_PIATA' },
baza: { url: new URL('../verifiable-data/baza.mjs', import.meta.url), env: 'AERE_CONF_MODUL_BAZA' },
aitrust: { url: new URL('../ai-trust/lant-de-incredere.mjs', import.meta.url), env: 'AERE_CONF_MODUL_AITRUST' },
sidecar: { url: new URL('../verify-layer/sidecar.mjs', import.meta.url), env: 'AERE_CONF_MODUL_SIDECAR' },
arbore: { url: new URL('../verify-layer/jurnal-arbore.mjs', import.meta.url), env: 'AERE_CONF_MODUL_ARBORE' },
ledger: { url: new URL('../agents/agent-ledger.mjs', import.meta.url), env: 'AERE_CONF_MODUL_LEDGER' },
};
export const caleModul = (k) => process.env[MODULE[k].env] || fileURLToPath(MODULE[k].url);
export const incarca = (k) => import(pathToFileURL(caleModul(k)).href);
const I = 'integrity', AU = 'authenticity', AN = 'anchoring';
/**
* How a verifier MARKS its rules in code. `paza`: a comment marker on every check of a rule (the planting makes the marked check true).
* `apel`: every refusal names its rule where it calls the verifier's one refusal helper (`apel.re` finds those calls; `apel.planteaza`
* rewrites that helper, in a copy, so that it drops the refusals of one rule). Either way a rule is planted out WHOLE, in a copy.
*/
const APEL = {
aitrust: { re: /(?:\bfail|fail : unm\))\('[a-z]+', '([a-z-]+)'/g,
planteaza: (src, r) => { const a = "const fail = (l, reason, detail) => { if (!REGULI.includes(reason)) throw new Error('ai-trust: undeclared rule ' + reason); "; return src.split(a).length === 2 ? src.replace(a, () => a + `if (reason === '${r}') return; `) : null; } },
};
export const marcajul = (sursa) => APEL[sursa] || null;
/** verifier -> { artifacts it judges, where its rules come from, source rule -> [public id, level, what the rule says] } */
export const VERIFICATOARE = {
'aip23-envelope': { artifacts: ['envelope'], sursa: 'verify-proof', reguli: {
form: ['envelope.form', I, 'the envelope has a statement (object) and a statementHash (0x + 64 hex)'],
integrity: ['envelope.integrity', I, 'sha256 of the canonical statement text is the statementHash'],
signature: ['envelope.signature', 'authenticity', 'the ML-DSA signature the envelope carries verifies over the canonical statement text'],
finality: ['envelope.finality', 'anchoring', 'the statementHash is recorded in the notary under a state root that a verified post-quantum anchor certificate covers (level by site: see finalitySites)'] } },
'payment-risk': { artifacts: ['payment-risk'], sursa: 'risc', reguli: {
forma: ['risk.form', I, 'payer and payee are addresses; chain, asset, a positive integer amount and the time are given'],
activ: ['risk.asset', I, 'the asset on that chain is allowed by the declared policy'],
contrapartida: ['risk.counterparty', I, 'the payee is not the payer and is not a refused counterparty of the policy'],
curs: ['risk.rate', I, 'the USD rate is declared (value, source, time) when the policy requires it'],
limita: ['risk.limit', I, 'the amount in USD is within the per-payment limit of the policy'],
fereastra: ['risk.window', I, 'the amount and the count stay within the sliding window of the policy'] } },
'payment-receipt': { artifacts: ['payment-receipt'], sursa: 'chitanta', reguli: {
legatura: ['receipt.binding', I, 'the settlement envelope commits to this instruction and says the same thing'],
suma: ['receipt.amount', I, 'the USD amount is the amount at the declared rate, exactly'],
politica: ['receipt.policy', I, 'the policy shown hashes to the policy the risk verdict names'],
risc: ['receipt.risk', I, 'the risk verdict reproduces from the inputs and the policy, and it allowed this payment'],
lant: ['receipt.chain', I, 'the transaction supplied is this payment: payer, payee, value, block, mined'] } },
'market-timeline': { artifacts: ['market-timeline'], sursa: 'piata', reguli: {
forma: ['market.form', I, 'the timeline is well formed: events with name, arguments, sender, integer time and block; the job and the constants with their integer fields'],
intreg: ['market.complete', I, 'the timeline carries every event the job state implies: posted; claimed when a provider is set; submitted and disputed when their times are set'],
postare: ['market.posting', I, 'the JobPosted event matches the job (requester, spec, deadline, reward)'],
termen: ['market.deadline', I, 'claim and submission are within the deadline; an expiry refund only after it'],
fereastra: ['market.window', I, 'a dispute is raised inside the challenge window; a third party finalizes only after it'],
arbitru: ['market.arbiter', I, 'a dispute is resolved by the arbiter, or timed out only after the arbitration timeout'],
plata: ['market.payout', I, 'the terminal payout matches the final state (who, how much, which bond)'],
rezultat: ['market.result', I, 'the result bytes hash to the commitment on chain'],
replay: ['market.replay', I, 'the public task recomputed agrees with who was paid or slashed'],
dovada: ['market.proof', I, 'the execution envelope names this job and existed before the window closed'] } },
dataset: { artifacts: ['dataset-record', 'dataset-answer', 'dataset-consistency', 'dataset-sample'], sursa: 'baza', reguli: {
forma: ['dataset.form', I, 'a row carries its field commitments and its disclosed fields'],
angajament: ['dataset.commitment', I, 'the record, answer or proof names the commitment it is checked against'],
camp: ['dataset.field', I, 'a disclosed value opens its salted field commitment'],
includere: ['dataset.inclusion', I, 'the record is in the log of that version (RFC 9162 inclusion)'],
index: ['dataset.index', I, 'the row is at its stated position of the sorted index'],
interval: ['dataset.range', I, 'every row returned is inside the queried range'],
ordine: ['dataset.order', I, 'consecutive rows are in index order'],
completitudine: ['dataset.completeness', I, 'the rows are the whole contiguous run; the neighbours on both sides are outside the range'],
consistenta: ['dataset.consistency', I, 'the older log is a prefix of the newer (RFC 9162 consistency) and the versions are linked'],
esantion: ['dataset.sample', I, 'every sampled position is served and verifies'] } },
'ai-trust': { artifacts: ['ai-trust-bundle'], sursa: 'aitrust', reguli: {
form: ['aitrust.form', I, 'the bundle has the format, an identity, a ledger with its policy, and a Proof of AI envelope'],
'identity-mismatch': ['aitrust.identity-mismatch', AU, 'the id derived from the key is the one the bundle, the ledger and the output name'],
'identity-key': ['aitrust.identity-key', AU, 'the public key of the identity can be read'],
'version-attestation': ['aitrust.version-attestation', I, 'the Proof of Software attestation of the model artifact verifies as an envelope'],
'version-artifact-mismatch': ['aitrust.version-artifact-mismatch', I, 'the output names model weights that the attestation covers'],
'version-label-mismatch': ['aitrust.version-label-mismatch', I, 'the output and the attestation name the same model version'],
'runtime-record': ['aitrust.runtime-record', I, 'the runtime record is a verified runtime attestation'],
'runtime-artifact-mismatch': ['aitrust.runtime-artifact-mismatch', I, 'the host ran the artifact the output names'],
'runtime-after-output': ['aitrust.runtime-after-output', I, 'the runtime record is not dated after the output'],
'audit-log-broken': ['aitrust.audit-log-broken', I, 'the hash chain of the audit log given is intact'],
'runtime-not-in-audit-log': ['aitrust.runtime-not-in-audit-log', I, 'the runtime record is an entry of the audit log given'],
'audit-head': ['aitrust.audit-head', I, 'the audit log continues the attested head given'],
'witness-head': ['aitrust.witness-head', AN, 'every notarized head given is a head of this ledger, recorded on the chain'],
'policy-hash': ['aitrust.policy-hash', I, 'the policy given hashes to the policy hash the bundle pins'],
'ledger-invalid': ['aitrust.ledger-invalid', I, 'the ledger verifies under its policy (signatures, chain, every decision re-derived)'],
'output-not-in-ledger': ['aitrust.output-not-in-ledger', I, 'a ledger entry links this Proof of AI envelope'],
'action-refused': ['aitrust.action-refused', I, 'the ledger entry of the output was allowed by the policy'],
'ledger-time-mismatch': ['aitrust.ledger-time-mismatch', I, 'the ledger entry and the output are dated within the tolerance'],
policy: ['aitrust.policy', I, 'the policy given is a policy'],
'provenance-envelope': ['aitrust.provenance-envelope', I, 'the Proof of AI envelope verifies as an envelope of its kind'],
'provenance-finality': ['aitrust.provenance-finality', AN, 'a notarization the Proof of AI envelope claims is on the chain'] } },
'audit-log': { artifacts: ['audit-log'], sursa: 'sidecar', reguli: {
intrare: ['auditlog.entry', I, 'the log is a list, and each entry is a log entry'],
seq: ['auditlog.seq', I, 'the entries are numbered from zero, without a gap'],
lant: ['auditlog.chain', I, 'each entry links the hash of the one before it'],
hash: ['auditlog.hash', I, 'the hash of each entry is the hash of its content'],
'cap-forma': ['auditlog.head-form', I, 'the attested head is an audit head attestation with a count and a head hash'],
'cap-hash': ['auditlog.head-hash', I, 'the statementHash of the attested head is the hash of its statement'],
'cap-nesemnat': ['auditlog.head-unsigned', AU, 'the head is signed when a signer is required'],
'cap-schema': ['auditlog.head-scheme', AU, 'the head signature is an ml-dsa-65 signature, made with an ML-DSA-65 key'],
'cap-cheie': ['auditlog.head-key', AU, 'the public key of the head signature can be read'],
'cap-semnatura': ['auditlog.head-signature', AU, 'the head signature verifies over the statement'],
'cap-semnatar': ['auditlog.head-signer', AU, 'the head is signed by the expected signer'],
'cap-scurt': ['auditlog.head-short', I, 'the log has at least as many entries as the attested head covers'],
'cap-rescris': ['auditlog.head-rewritten', I, 'the entry at the attested point is the attested head (the history before it was not rewritten)'] } },
'audit-tree': { artifacts: ['audit-inclusion', 'audit-consistency'], sursa: 'arbore', reguli: {
'dovada-forma': ['audittree.proof-form', I, 'the proof is an inclusion or a consistency proof that can be read'],
'arbore-forma': ['audittree.head-form', I, 'the tree head is a tree head attestation with a size and a root'],
'arbore-hash': ['audittree.head-hash', I, 'the statementHash of the tree head is the hash of its statement'],
'includere-index': ['audittree.inclusion-index', I, 'the entry shown is the entry of the index proven'],
'includere-intrare': ['audittree.inclusion-entry', I, 'the hash of the entry shown is the hash of its content'],
'includere-plic': ['audittree.inclusion-envelope', I, 'the envelope inside the entry hashes to its statementHash'],
'includere-frunza': ['audittree.inclusion-leaf', I, 'the leaf is the leaf of this entry'],
'includere-drum': ['audittree.inclusion-path', I, 'the path leads from the leaf to the root (RFC 9162 inclusion)'],
'includere-cap': ['audittree.inclusion-head', I, 'the proof is for the tree of the head given (size and root)'],
'consistenta-inceput': ['audittree.consistency-start', I, 'the consistency proof starts at the old head'],
'consistenta-sfarsit': ['audittree.consistency-end', I, 'the consistency proof ends at the new head'],
consistenta: ['audittree.consistency', I, 'the new tree extends the old one (RFC 9162 consistency)'] } },
'agent-ledger': { artifacts: ['agent-ledger'], sursa: 'ledger', reguli: {
registru: ['agents.ledger-form', I, 'a ledger with a list of entries is given'],
politica: ['agents.policy', I, 'the policy given is a policy, and hashes to the hash the verifier pins'],
'alta-politica': ['agents.other-policy', I, 'the ledger and every entry name the policy given, and the policy is this agent\'s'],
sesiune: ['agents.session', I, 'the session is the one derived from the agent and the policy'],
cheie: ['agents.key', AU, 'the agent key can be read and is an ML-DSA-65 key'],
identitate: ['agents.identity', AU, 'the agent id is derived from the key'],
ancora: ['agents.anchor', I, 'every anchor given names an entry of this ledger, with a time'],
seq: ['agents.seq', I, 'the entries are numbered from zero, without a gap'],
lant: ['agents.chain', I, 'each entry links the hash of the one before it'],
corp: ['agents.body', I, 'the body of an entry names the agent, the session and the sequence number of its header'],
semnatura: ['agents.signature', AU, 'every entry is signed by the agent key'],
hash: ['agents.hash', I, 'the hash of each entry reproduces'],
timp: ['agents.time', I, 'entry times do not go backwards and stay within the bounds the verifier has (its clock, a witnessed start, the anchors)'],
revocare: ['agents.revocation', I, 'a revocation entry carries a revocation that verifies, and says what it carries'],
aprobari: ['agents.approvals', I, 'an approvals field, when present, is a list that is not empty'],
decizie: ['agents.decision', I, 'every decision written is the decision the policy gives when replayed'] } },
};
/**
* The FAILED sites of verify-proof's `finality` level, classified. `fragment` must appear on exactly one such source line; `re`
* matches the detail the verifier prints there; `level` is where the suite reports it. Only the last one is reachable by an honest
* endpoint (a claim that is not on chain); the others need an endpoint that lies or a chain whose anchor does not verify.
*/
export const FINALITY_SITES = [
{ fragment: 'most recent anchor did not verify', re: /^the chain's most recent anchor did not verify/, level: 'finality' },
{ fragment: 'the header the endpoint served for block', re: /^the header the endpoint served for block/, level: 'anchoring' },
{ fragment: 'has no account at', re: /has no account at/, level: 'anchoring' },
{ fragment: 'is not the AereNotary contract', re: /is not the AereNotary contract/, level: 'anchoring' },
{ fragment: 'the Merkle proof for block', re: /^the Merkle proof for block/, level: 'anchoring' },
{ fragment: 'not notarized:', re: /^not notarized:/, level: 'anchoring', reachable: true },
];
/** Verifiers published beside these that the suite does NOT enrol yet, with the pattern that counts their refusal sites. */
export const NEINROLATE = [
{ name: 'identity', files: [new URL('../identity/identity.mjs', import.meta.url)], re: /\bok\(['`]/g, why: 'its checks are labelled rows, not rule ids (counted: the check rows of its presentation verifier; its SD-JWT, Travel Rule and compliance verifiers are not counted)' },
];
/** public rule id -> { verifier, source, level, says } for every mapped rule */
export function hartaPublica() {
const h = {};
for (const [v, d] of Object.entries(VERIFICATOARE)) for (const [src, [id, level, says]] of Object.entries(d.reguli)) h[id] = { verifier: v, source: src, level, says };
return h;
}
/** source rule ids of a verifier -> public ids */
export const public_ = (verifier, surse) => [...new Set(surse.map((s) => { const r = VERIFICATOARE[verifier].reguli[s]; if (!r) throw new Error(`${verifier}: the verifier named a rule this suite does not map: ${s}`); return r[0]; }))].sort();
/**
* Derives the rule table from the code. Returns { tabel, probleme, nemasurat }: `probleme` are refusals (a rule declared and not
* marked, marked and not declared, declared and not mapped, mapped and not declared, an unclassified finality site); `nemasurat`
* names a verifier whose source could not be read here. `cai` substitutes the file of a module (the probe's control derives from a
* planted copy to show that a rule added to a verifier and not mapped here is refused).
*/
export async function deriva({ cai = {} } = {}) {
const probleme = [], nemasurat = []; const tabel = { v: 1, kind: 'aere-conformance-rules', levels: LEVELS, verifiers: {}, notEnrolled: [] };
for (const [v, d] of Object.entries(VERIFICATOARE)) {
const rules = []; let declared = [], sites = {}, extra = {};
if (d.sursa === 'verify-proof') {
if (!fs.existsSync(VERIFY_PROOF)) { nemasurat.push(`${v}: the reference verifier is not at ${VERIFY_PROOF} (AERE_VERIFY_PROOF=<verify-proof.mjs>)`); continue; }
const linii = fs.readFileSync(VERIFY_PROOF, 'utf8').split('\n').filter((l) => /level\('[a-z]+',[^;]*'FAILED'/.test(l) && !/^\s*\/\//.test(l));
for (const l of linii) for (const m of l.matchAll(/level\('([a-z]+)',[^;]*?'FAILED'/g)) sites[m[1]] = (sites[m[1]] || 0) + 1;
declared = Object.keys(sites);
const fin = linii.filter((l) => /level\('finality'/.test(l));
for (const s of FINALITY_SITES) { const n = fin.filter((l) => l.includes(s.fragment)).length; if (n !== 1) probleme.push(`${v}: the finality site "${s.fragment}" appears on ${n} FAILED lines of the verifier (expected 1)`); }
if (fin.length !== FINALITY_SITES.length) probleme.push(`${v}: the verifier has ${fin.length} FAILED sites at its finality level, the suite classifies ${FINALITY_SITES.length}`);
extra = { finalitySites: FINALITY_SITES.map((s) => ({ site: s.fragment, level: s.level, reachableWithAnHonestEndpoint: !!s.reachable })) };
} else {
const p = cai[d.sursa] || fileURLToPath(MODULE[d.sursa].url); const src = fs.readFileSync(p, 'utf8'); const M = await import(pathToFileURL(p).href);
if (!Array.isArray(M.REGULI)) { probleme.push(`${v}: the module does not export REGULI`); continue; }
declared = [...M.REGULI];
const ap = APEL[d.sursa];
for (const m of src.matchAll(ap ? ap.re : /\/\*PAZA:([a-z-]+)\*\//g)) sites[m[1]] = (sites[m[1]] || 0) + 1;
for (const r of declared) if (!sites[r]) probleme.push(`${v}: rule ${r} is declared and no ${ap ? 'refusal names it' : 'check carries its marker'}`);
for (const r of Object.keys(sites)) if (!declared.includes(r)) probleme.push(`${v}: a ${ap ? 'refusal names' : 'check is marked'} ${r}, a rule the module does not declare`);
if (ap) for (const r of declared) if (ap.planteaza(src, r) === null) probleme.push(`${v}: the refusal helper is not where the planting expects it`);
extra = { marked: ap ? 'at the refusal helper' : 'by a marker on every check' };
}
for (const r of declared) if (!d.reguli[r]) probleme.push(`${v}: the verifier names rule ${r} and the suite does not map it (a new rule needs a public id, a level and a vector)`);
for (const r of Object.keys(d.reguli)) if (!declared.includes(r)) probleme.push(`${v}: the suite maps rule ${r}, which the verifier no longer names`);
for (const [src, [id, level, says]] of Object.entries(d.reguli)) rules.push({ id, source: src, level, sites: sites[src] || 0, says });
tabel.verifiers[v] = { artifacts: d.artifacts, rules, ...extra };
}
for (const n of NEINROLATE) {
let sitesN = 0, citit = true;
for (const u of n.files) { const p = fileURLToPath(u); if (!fs.existsSync(p)) { citit = false; continue; } sitesN += (fs.readFileSync(p, 'utf8').match(n.re) || []).length; }
tabel.notEnrolled.push({ verifier: n.name, refusalSites: citit ? sitesN : null, why: n.why });
if (citit && sitesN === 0) probleme.push(`not enrolled ${n.name}: the pattern that counts its refusal sites finds none (the pattern is stale)`);
}
return { tabel, probleme, nemasurat };
}