From cf8c56792f488a6fc09b72add819f4ab09c1bb37 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Fri, 2 Oct 2026 11:28:14 +0300 Subject: [PATCH] ai-trust, proof-of-ai, verifiable-data: the trust chain of an AI result, the AIP-23 envelope of an agent action, and a dataset whose answers are checkable without the data ai-trust/ links six existing proofs (agent identity, model artifact attestation, host runtime record, policy and signed ledger, Proof of AI envelope, on-chain witness) into one bundle judged by one verifier that checks only the links; 13/13 in the lab, 16/16 live on testnet 28001 (ledger head notarized). proof-of-ai/ is the AIP-23 envelope builder it uses. verifiable-data/ publishes append-only versions with an RFC 9162 root and a sorted index per field, range and equality queries proven correct and complete, selective disclosure per field, availability by sampling, and the AIP-23 data envelope notarized (version 2 on testnet 28001). agents/agent-ancora.mjs gains notarizeEnvelope({kind}), the general form of notarizeHead. Each README says what is not built: zero-knowledge queries need a prover and are not here. No third party has reviewed any of this. --- README.md | 8 +- agents/agent-ancora.mjs | 10 +- ai-trust/README.md | 49 ++++ .../dovezi/lant-28001-2026-10-02T0519Z.json | 269 +++++++++++++++++ ai-trust/fixturi/atestare-model.json | 45 +++ ai-trust/fixturi/model-weights.bin | Bin 0 -> 4096 bytes ai-trust/lant-de-incredere.mjs | 213 ++++++++++++++ ai-trust/proba-lant-de-incredere.mjs | 162 +++++++++++ proof-of-ai/README.md | 18 ++ proof-of-ai/proba-proof-of-ai.mjs | 61 ++++ proof-of-ai/proof-of-ai.mjs | 52 ++++ verifiable-data/README.md | 63 ++++ verifiable-data/baza.mjs | 272 ++++++++++++++++++ .../notarizare-28001-2026-10-02T0820Z.json | 67 +++++ verifiable-data/proba-verifiable-data.mjs | 171 +++++++++++ verifiable-data/serveste.mjs | 52 ++++ verifiable-data/vd.mjs | 61 ++++ verifiable-data/verifica.mjs | 54 ++++ 18 files changed, 1624 insertions(+), 3 deletions(-) create mode 100644 ai-trust/README.md create mode 100644 ai-trust/dovezi/lant-28001-2026-10-02T0519Z.json create mode 100644 ai-trust/fixturi/atestare-model.json create mode 100644 ai-trust/fixturi/model-weights.bin create mode 100644 ai-trust/lant-de-incredere.mjs create mode 100644 ai-trust/proba-lant-de-incredere.mjs create mode 100644 proof-of-ai/README.md create mode 100644 proof-of-ai/proba-proof-of-ai.mjs create mode 100644 proof-of-ai/proof-of-ai.mjs create mode 100644 verifiable-data/README.md create mode 100644 verifiable-data/baza.mjs create mode 100644 verifiable-data/dovezi/notarizare-28001-2026-10-02T0820Z.json create mode 100644 verifiable-data/proba-verifiable-data.mjs create mode 100644 verifiable-data/serveste.mjs create mode 100644 verifiable-data/vd.mjs create mode 100644 verifiable-data/verifica.mjs diff --git a/README.md b/README.md index 5b1df2b..ac4bce7 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ notarization command of the verification layer, and the agents' x402 wallet (EIP | [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass | | [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again | | [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, either holding the payment key for the owner or co-signing from a 2-of-2 contract wallet that neither the agent nor the owner can spend alone; both run on the public testnet with their evidence; and the chain as the witness of a ledger: a notarized head, read through the AIP-23 verifier under a post-quantum certified anchor, bounds entry times from below (a backdated entry is caught) | +| [`proof-of-ai/`](proof-of-ai/) | the AIP-23 envelope of an AI agent's action: model, version, weights digest, prompt, input, output, tools, agent, requester, as digests; verified by the same AIP-23 verifier as every other proof | +| [`verifiable-data/`](verifiable-data/) | a dataset whose every answer is checkable against a short commitment without the data: RFC 9162 commitments per version (append-only, consistency proofs), equality and range queries proven correct AND complete (contiguous run of a sorted index plus its two neighbours), hash-based selective disclosure of fields, availability sampling against the serving endpoint, and the version's AIP-23 envelope notarized and read back under a post-quantum certified anchor; not zero knowledge, and says so | +| [`ai-trust/`](ai-trust/) | the trust chain of an AI result: the agent's identity (agents/), the model artifact's attestation, the host's runtime record (verify-layer/), the policy and signed ledger, the Proof of AI envelope and the chain as witness, joined in one bundle whose verifier checks only the links between them | | [`identity/`](identity/) | post-quantum credentials: an issuer signs claims (hybrid Ed25519 + ML-DSA-65, both required), the holder shows only the claims it picks (selective disclosure in the manner of SD-JWT, RFC 9901, in a format of its own), bound to the verifier's audience and nonce; delegation to a phone, an agent or a ten-minute session key that can only narrow; revocation and an issuer status list in the manner of W3C Bitstring Status List; all checked offline from the files; and compliance without surveillance: a verifier's policy (issuers, required claims) judged on a presentation, recorded as an AIP-23 envelope that carries no personal data; and the Travel Rule between VASPs, the IVMS101 data sealed for the receiving VASP with a hybrid X25519 + ML-KEM-768 key encapsulation, signed, bound to the transfer and acknowledged; and the same credentials as standard SD-JWT (IETF RFC 9901): issued with the issuer's Ed25519 key (EdDSA) or its ML-DSA-65 key, presented with a key binding JWT, and verified against the RFC's own example vectors (`identity-cli.mjs verify-sdjwt`, behind Aere Cloud's POST /v1/identity/sd-jwt/verify) | Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them. @@ -37,6 +40,9 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j | control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) | | identity | 44/44 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-09-30 | 49/49 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 28/28 (`node control-negativ-sdjwt.mjs`) | | agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`x402/proba-x402-testnet.mjs`, `x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc `) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository | +| proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) | +| verifiable-data | 22/22 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-verifiable-data.mjs`: honest answers VALID, nine forged ones INVALID for their named reason, availability sampling on a local server; the envelope row is NEMASURAT without the verifier, exit 2); 24/24 with `--viu` on testnet 28001 on 2026-10-02 (a version notarized with a test key and read back under a post-quantum certified anchor; evidence in `dovezi/`) | 5/5 (`node proba-verifiable-data.mjs --control-negativ`: field commitment, index order, completeness, consistency, sampling, each planted out on a copy and each forged fixture then accepted) | +| ai-trust | 13/13 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-lant-de-incredere.mjs`: the whole chain on a committed fixture, then ten links broken on purpose, each refused for its named reason); without the verifier it exits 2 | 8/8 (`node proba-lant-de-incredere.mjs --control-negativ`) | Code comments, most function and variable names (also many exported between the files of a component), test names and control messages are in Romanian, and so are the two command words of the KMS HSM tool (explained in its README). Error codes, error @@ -45,4 +51,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## Licence -MIT, see [LICENSE](LICENSE). Files: 158 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 12, proof-kinds 3, control-plane 19, agents 39, identity 15, readiness 4). +MIT, see [LICENSE](LICENSE). Files: 174 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 12, proof-kinds 3, control-plane 19, agents 39, identity 15, proof-of-ai 3, ai-trust 6, verifiable-data 7, readiness 4). diff --git a/agents/agent-ancora.mjs b/agents/agent-ancora.mjs index e6bd47b..5d30c71 100644 --- a/agents/agent-ancora.mjs +++ b/agents/agent-ancora.mjs @@ -67,10 +67,16 @@ export function cheiaDinFisier(f) { } /** Pune statementHash-ul capului pe AereNotary. Lantul se citeste de pe RPC, nu se crede din argument; cheia din fisier, netiparita. */ -export async function notarizeHead({ envelope, rpc, keyFile, ethers, notary = null, confirmMainnet = process.env.AERE_CONFIRM_MAINNET === 'yes' }) { +export function notarizeHead(o) { return notarizeEnvelope({ ...o, kind: HEAD_KIND }); } +/** + * Pune statementHash-ul unui plic AIP-23 de felul `kind` pe AereNotary (2026-10-02: forma generala a lui notarizeHead, ca datele + * verificabile sa notarizeze acelasi fel, din acelasi loc; fara `kind`, numai capul de registru). Aceleasi garzi: lantul de pe RPC, + * refuz pe 2800 fara AERE_CONFIRM_MAINNET, cheia din fisier si niciodata tiparita. + */ +export async function notarizeEnvelope({ envelope, kind = HEAD_KIND, rpc, keyFile, ethers, notary = null, confirmMainnet = process.env.AERE_CONFIRM_MAINNET === 'yes' }) { const taie = (s) => String(s ?? '').replace(/(0x)?[0-9a-fA-F]{40,}/g, '').slice(0, 300); const st = envelope && envelope.statement; - if (!st || envelope.kind !== HEAD_KIND + '-attestation' || st.kind !== HEAD_KIND || !H32.test(String(envelope.statementHash))) return { ok: false, cod: 2, motiv: `REFUSED: not an ${HEAD_KIND} attestation with a 32-byte statementHash` }; + if (!st || envelope.kind !== kind + '-attestation' || st.kind !== kind || !H32.test(String(envelope.statementHash))) return { ok: false, cod: 2, motiv: `REFUSED: not an ${kind} attestation with a 32-byte statementHash` }; if (sha(JSON.stringify(st)) !== envelope.statementHash) return { ok: false, cod: 1, motiv: 'REFUSED: statementHash does not match the statement (modified envelope)' }; try { const provider = new ethers.JsonRpcProvider(rpc); diff --git a/ai-trust/README.md b/ai-trust/README.md new file mode 100644 index 0000000..91ad11b --- /dev/null +++ b/ai-trust/README.md @@ -0,0 +1,49 @@ +# AERE AI Trust Chain + +Trust infrastructure for an AI result, built only from pieces that already exist and are verified on their own: the agent's +post-quantum identity, the attestation of the model artifact, the host's runtime record, the agent's policy and signed ledger, the +Proof of AI envelope of the output, and the chain as the witness of time. This component adds **no new proof**: it adds the +**links** between those proofs, in one bundle, judged by one verifier that calls the existing verifiers and checks only that the +digests, the identities and the times agree. + +| facet | what proves it | the link checked here | +|---|---|---| +| identity | `agents/` (agentId derived from an ML-DSA-65 key) | the ledger, the bundle and the output name the id the key derives | +| version | Proof of Software attestation of the model artifact (hybrid signature) | the output's `model.weightsSha256` is an artifact of the attestation; the version labels agree | +| runtime | the verify-layer sidecar's `runtime` record in its audit log (chain, head) | the host ran that same artifact (`matches`), before the output; the record is an entry of an intact log that continues the attested head | +| permissions | the agent's policy and ledger (`agents/`) | the ledger verifies; an **allowed** entry links the output by its provenance hash; the entry's time matches the output's | +| provenance | the Proof of AI envelope (AIP-23; digests only) | the envelope verifies with the AIP-23 reference verifier; notarized, its finality is read under a post-quantum certified anchor | +| witness | the ledger head notarized on Aere (`agents/agent-ancora.mjs`) | the heads read back through the reference verifier become the ledger's time anchors; without them every time is the key holder's statement | + +Verdict: `VALID` when every present level PASSED **and** the witness PASSED; `INVALID` when a level FAILED; `PARTIAL` otherwise +(an UNMEASURED level, or no witness). Every level reports what it rests on. A verdict says that the bytes link and who vouches for +each link. It does not say the model is good, the host is honest, or the output is true. + +``` +import { construieste, verifica } from './lant-de-incredere.mjs'; +const bundle = construieste({ ledger, policy: { policy, policyHash }, proofOfAi, software: { attestation }, runtime: { record, auditLog, head }, witness: { heads: [notarizedHead], chainId: 28001 } }); +const v = await verifica(bundle, { rpc: 'https://testnet-rpc.aere.network', chain: 28001 }); // rpc: only for notarized pieces +``` + +## What it is not + +- Not an attestation of execution in a TEE: the runtime record is the **host's declaration**, as the sidecar says; what is checkable + without trusting the host is that the declaration was made before the output and sits in a history published outside the host. +- Not a judgement of the signer of the model attestation: who holds the signing keys is `pos.mjs verify --signer`'s question. +- Not a judgement of the content: prompts, inputs and outputs are digests; the verifier proves that bytes with these hashes were + named, not what they say. + +## Probes + +``` +AERE_VERIFY_PROOF= node proba-lant-de-incredere.mjs # no network: 13 rows (the chain, then every link broken on purpose) +AERE_VERIFY_PROOF=... node proba-lant-de-incredere.mjs --control-negativ # 8 plantings in a copy of the verifier; each must turn its fixture red +AERE_VERIFY_PROOF=... AERE_NOTARY_KEY_FILE= AERE_ETHERS_DIR= node proba-lant-de-incredere.mjs --viu + # plus the witness on testnet 28001 (a notarization paid with a test key) +``` + +The probes use the real children beside this component: the verify-layer sidecar (record, attest-head), the agents (ledger, policy, +anchor), proof-of-ai, and the AIP-23 reference verifier named by `AERE_VERIFY_PROOF` (without it the probe exits 2, NEMASURAT). The +model artifact and its Proof of Software attestation are a committed fixture (`fixturi/`: 4 KB of random bytes attested with +laboratory keys by `pos.mjs`, which is not part of this component; the digest is recomputed from the bytes, never read from the +attestation). `--viu` writes its evidence in `dovezi/`. diff --git a/ai-trust/dovezi/lant-28001-2026-10-02T0519Z.json b/ai-trust/dovezi/lant-28001-2026-10-02T0519Z.json new file mode 100644 index 0000000..4b62871 --- /dev/null +++ b/ai-trust/dovezi/lant-28001-2026-10-02T0519Z.json @@ -0,0 +1,269 @@ +{ + "bundle": { + "format": "aere-ai-trust-chain-1", + "createdAt": "2026-10-02T05:18:59.202Z", + "identity": { + "agentId": "aere-agent:965ac8dd3578bd4c32289394319a516b5f513947", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIHsjALBglghkgBZQMEAxIDggehAM549FJ8JaP9xfUWtr8m03ddBI0WBDTNZvgx\ndgFFZD5kVj+eB895lzRDN+naS0PmRsTKjqhH7F2FhissOBbrvvAyfzjqkGCWi3Jm\nqSPM5154djoN6Xh1jLUr0VYk50HGRMOHZfwAhYXoOYHpBswRs35Ld6k4UbsWhBiw\nqR02+xyNFLv5cW9QBBJdZOMh33rpqzNJV7Wwzl33qjpRS2je4VciAivx92ELI/72\nHFMFfVCpWuUl+3PPoFM1YMTsjgaUypYvK2gB3jw2pB3g+/KSRR08zLCD3SILdwYW\nlnp7V+f9ko5IT+1eTDGmZCqGRf2vlMnZiXOO/WqPMryPfC7erVbq8DsQimviURc6\nQxB5TM2+qQRRbB7MWJvkLuc5/2qs0kzHQ+NF57MTNXDsPBOGsSKvvGtwUlqiIYnO\nu2f1yhNni4HNcdC0uy7ohwnJAzxuZKuHQaNeTl9PgPoJ16Uu61ebFE7vOBOCvPvC\n6rtnmx0q8HqwduWfLKRSuUx82a2bZRr5Z31Jg0onLZS57sQzn6P3Q9wXSoq9VOzq\nDdFmg5ydH2h9wFBrw9Lm8hkabPjZt39cI3nGW3sn9NoJuPcFWfRsSTo1ndC25bHF\nPcoBz1WEkfw7hU3uMuhVuO1F2vw1NvFsnozgUXqcdf3KRcRRL/nInYkt+HFC8VDX\ni7q4J8V32EU3jdRvcgXlrf7g0PPkw5nCACpVUwUfb/PXs9+FunlRppR59146Unpm\naQlZgdN/9Ax5dKK7grcp7puz617D7l3wYg8RjNgRloEWkZ2Qt1g8l3MWYftvB3jF\nROC/gz8uPp8hMd7qtBSL9mMKDYvQ7yAdiYkJBzLXAyzKCP5oJkutUQHHqT5tkyrb\n6Qwr5PjFE/j0nbR2agqYLyKW/13dwUXV9GCHT58N5VpsorFds+6eXwd8CwW7zEgc\nXZNEDEUHVbFHKeh5ZLDlrBam1Y7xsLnxV/FSfjD54Wam/cvhcgGrQeUpc9xsoTY8\nm6aBtAPiAJoLAsdGn4shc5HhAXQzt5kD6o0NIKaU/zO3KRHu3580Jz65k9DRycJj\n2qQHR0RWCrsr7fSUaCT/mGCAmULsjJafVYz+VIRQ8RLvTwy9du8c7WCbcPqNo5Ro\nBJVT1Mfdvh78CVTWzjjPOoZiMaUUJdxsqDp0qvi5MrJLiPBYtlibLkSN5/DRqkOZ\nj7VT8wUd1vcCSwVNiRFb9YN/TV+X6cFJJQrlS8nEQm5Sv809036K+kTU2ejcSEnq\nmQgpX/6h01OJ6P19W6gj+eL1tMyrpc5dfPW1Yt+BSz+KPATHWKws+8fzhUuNDRz7\nKlu1cMkQoXiMY43q6ld6hEbOqjA/Pab7rKqm69fNO25ih/+Zujk1KysBOowBbiLo\nuf0lbHRzVPF5qv7pOFto31LSnQpuEoz8UeScjMAYJJ2bf9F/xg4O16Awiv4L4DDP\nN13jZpOHYWJFaZaF4ku39tR7QVFjap8ZKx3lmg2KuxznrEcD6sHAjwZmR1OT24D/\nSM3Bs+kiWEv62Y2zyU7NBnIBdzVju9JeXslna0YlSfm/tOZMIBb7LsDu7gVJC9yG\n5dmruzBO9eHUF4IBAbk9+G9rgrj5f4s166kxLzXx4O8Po+A3FVNqIeLWc/wBgOKw\n72fCdrhQsgomClBaGpSrQZate6uE8xeN1Pudnyj0NsZD51bv8bXd3r/DdfDjtV5I\nXkl36VCqMG7yW3N+k3B4H2SoEkGGQPPgugqj1OgA4UTgh+e3vdXu+SgwBBpXeJWC\n7EEsVL6thDE7riJq2xBHDetfndIUh7C46Munqy4qcaA8tlsuIVL8nTpL/zkAV/Jh\n2NCvtEVc0rv+ix1R8Yzk+i/n1laJingzkR+hWv4kLvb5cA3OzwkAXZ0CBEXbYE2m\ndxiakopyBt4Tt1ALsUD0cpT0El03X/sbMaKygW1RouU3iJOVs2vZOiKfOOq+cXtI\nl4I5VlWDCh/H33jaHYsnX0HMDbNF0R7K7rCWQzAoR7rC4a8EDAvabyR3m4koiatz\nthHUVYMbpUD3m2MBFBBV7WObShXy4xkaJ37+LDQTsJ6MNpiv4Pbi/wrDHiPSNXWl\ne4E8ZzSXGyZJYSKu7jWQUdZR9v7IsykngtEGTR9stWiMlgyPZrTRO35aH3thVWti\nvm6Dgu+/FMSRAcuU0vUuD0UG/q4Wk6o3T4mQsu5DdAvwPc/IdZ/bwow0cWlnbE7H\n/IEGNe5RpI6EPtI189ADE1JTVnNZGGz9Px66bbcjHcG+B8QtLX6OH3IroHo6vfsT\nz3ae6+V9BdmrtKlKoxjAB5MCuZaluafFe2nJHAalS6XRfRD/YRr+8O52kI/h19lm\nWbwzBg0zgUtcpcAxyyVp8fvWznqjizWKIfTTqM1lWWMFVIkA0sIa5cm7I9mjOk6J\nXqwQ1kpDRc7pm4X0r1LE189V0iNpCzaquUMgObXXfnuLeQdM7Nl9iwEhnGLwuK3B\nCk+rsWyw8vOOpk5i7SU5186RsnNJZhtQATeZZnr8v1HYB2rTWzPmM6Kv9ArvbUWX\nzQp/vDDyO6CZV8G9uRVIfMph1dlo+KRB1/bCaOx7VZ4Ychx8R9+0CpwmSj2uJSLb\nr4bN7MGD\n-----END PUBLIC KEY-----\n" + }, + "permissions": { + "policy": { + "v": 1, + "kind": "aere-agent-policy", + "agentId": "aere-agent:965ac8dd3578bd4c32289394319a516b5f513947", + "spend": null, + "tools": [ + "model:aere-model-proba" + ], + "recipients": null + }, + "policyHash": "0x5040392a96c4f93295031ecc408d40b2ab717df5d6a8031d915377900bf06b76", + "ledger": { + "version": "aere-agent-ledger/2 (2026-09-29)", + "agentId": "aere-agent:965ac8dd3578bd4c32289394319a516b5f513947", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n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\nr4bN7MGD\n-----END PUBLIC KEY-----\n", + "policyHash": "0x5040392a96c4f93295031ecc408d40b2ab717df5d6a8031d915377900bf06b76", + "session": "08bdf8fc04fba02a829bc11220a86e40", + "entries": [ + { + "seq": 0, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "body": { + "v": 2, + "agentId": "aere-agent:965ac8dd3578bd4c32289394319a516b5f513947", + "policyHash": "0x5040392a96c4f93295031ecc408d40b2ab717df5d6a8031d915377900bf06b76", + "session": "08bdf8fc04fba02a829bc11220a86e40", + "at": 1790918267, + "action": { + "kind": "tool", + "tool": "model:aere-model-proba", + "args": { + "promptHash": "0x1ed19f00c605cc860b3f375197b2c94d03d99a81c15d949294f8417d9edd1f93" + }, + "at": 1790918267 + }, + "decision": { + "allowed": true, + "reason": "tool allowed" + }, + "provenance": "b31b4582c8ee4caab3587b80378f8e7ef0345328b9616b046b5ac0c7af35bcee", + "seq": 0 + }, + "signature": "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", + "hash": "bd9095330bb9b9bec67463d842b9eaec595f108ec221bcfff3cf14104a0e4dcb" + } + ] + }, + "revocations": [] + }, + "provenance": { + "proofOfAi": { + "v": 1, + "kind": "aere-proof-of-ai-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-ai", + "model": { + "name": "aere-model-proba", + "version": "1.0.0", + "weightsSha256": "0xd0ce419be4152cc92aaf23799d6551044e286057bf9583032e4277317c742d1b" + }, + "promptHash": "0x1ed19f00c605cc860b3f375197b2c94d03d99a81c15d949294f8417d9edd1f93", + "inputHash": "0x101246d82b1883367c721f286b66a06272464e9de4a6b3c76818ea4f22dad2cb", + "outputHash": "0xd49920d0712d4c9ead9845329b4c921c5423fa71330c464363690d352b273e3c", + "tools": [ + "model:aere-model-proba" + ], + "agentId": "aere-agent:965ac8dd3578bd4c32289394319a516b5f513947", + "requesterHash": "0x9bba5c53a0545e0c80184b946153c9f58387e3bd1d4ee35740f29ac2e718b019", + "createdAt": "2026-10-02T05:17:47.000Z" + }, + "statementHash": "0xbf61d868ca971d865fd0ee807439b77f2456ef7a4cd0b56e6abf4bdb4185c196" + } + }, + "version": { + "attestation": { + "v": 1, + "kind": "aere-proof-of-software-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-software", + "tool": "aere-proof-of-software/1.5.0", + "subject": { + "name": "aere-model-proba", + "version": "1.0.0" + }, + "artifacts": [ + { + "name": "model-weights.bin", + "sha256": "0xd0ce419be4152cc92aaf23799d6551044e286057bf9583032e4277317c742d1b", + "bytes": 4096 + } + ], + "sbom": null, + "source": null, + "builder": { + "platform": "win32/x64", + "node": "v24.14.1" + }, + "createdAt": "2026-10-02T05:18:47.385Z" + }, + "statementHash": "0x2054e2817854b8b600079cde2041b07f459b332784a56dfc744ea09f041507c7", + "signature": { + "v": 1, + "kind": "aere-hybrid-signature", + "alg": "secp256k1+ml-dsa-65", + "hash": "sha256", + "messageHash": "0x2054e2817854b8b600079cde2041b07f459b332784a56dfc744ea09f041507c7", + "toSign": "0xf29da105e3cb554c05d79eeb5b710ac763cf91b7cbd7ab719cf35ea0bb812a70", + "classicalPublicKey": "0x02fb216d6425b1639575de6b318a2208735e855017d69e2d0ce8b19651adc0b4a3", + "classicalSignature": "0x2f97f740add016e61a5a4bd25221922beb3cf00ca2660506a865a4cc5a91538175dcf4c1cf9162f3bd78b0b84fd4f9f8d2817c9cc4a80d8026991003441dcf94", + "pqPublicKey": "0x72b4db6f419f2ae35f60b12d22252ddc49f5746d5187a91a27f1553043324844cef20b04a78d2612e77e2c4663215851e34b5a9142bafa9f459eae8795d2ca7b103c956ae1e6b87547b32fcd0e741daef9f4dd878a1a2ecb04624d499900b9718eb5b91d1d295f53e8ee793e21962fcd13eada511cb75fe5c42d3a1d72ea1ba99d727f26502b3259b9d8c29d487b47f3dc9711adc425897c01be4f20c01d99da3b36bffa28ef5dec0984b052cbb1db728686669313e02e42c3b844a2eba0bbbca79faaccab686014c8bfebaf37b1ee2285db9111be069d858ca5e14198c42067380add6f282a4b4701a1390f7ca5b69e62817f41c44961a70496ed1b8fb4283d4f05d59e8ce0293237d115e1269d3b73815b7bc67d52e7c55a550722eba20ee63c4c04256080c915f9a4f7e0e0a790322b371e50c9992deeb22a329e25986fd2ecd61125465f04d23f9a13aa786e453d932d0339bf3f0951d652d1d652de4b2ac3aed28d22a36371a65a27ad254f9435519c15e4b63a28b1f73eb4810cbf83a575d23aae1470c6ccb37b1095033d06ce050a200aead03bd5b36157755cfb33ee104bea6e1118ad730f817038f0abd78b460b922f6ff508f0ec84035988bdc3de5c7b5e3f29986d750ae9c07dd74aae47b48cd9c2cd875d0a9d36b0caa857ad3e74928fd6ecc57fc3a98edc9aa68fcf902f8b6d28b631b79f7cd24efb0f351072027d0bcd0018f93d84e7460e42e6cd1d7b9f4859b8030dd55774271bfccd38372db7a8181ca60a14d1f998e7ee2e190a0bd95a3251712460adc915175dce10c3399df069c05f74a7ae1d6e009bff2810598506d09df508517a0c800f135b3a06d96c7a2bd94c77aa44d6e5d7f74768cf6730f046b8ecf60ef499738e7acd676d6122968cd18956185dbdd965ed199650de4fc5bf96a2cd5f0bd1e0e721223d4def12241714961c806c131b283d0697ca4966d4460e5eeb0a92b3373a8efba2ed303db7f107c597cfa97c1ce0535a4278e09dd365c82e8587573c94eb3a66c057a5ed2751dfc4f9c68823992aed6543e4abb25b6ba423b5bc423cd1068be6149467ef03a3d156c0838aa6584c07620a7d9a0b9c2c3bc4365873e3051009d8e0b3159c9eb437d13c757eb3f77ed2e82eb1fbc2530ffe09d0e57d4f3c720f46517512a12a2fe7185f3d7ad4416ff1f115c39e6da0653850f285294d8175f2f13964c8e03086d8cdedfd264b12dceb3a7695781bfb1a9102cb9abf1a367abfcf3024e923eba458bbc72e5928bd71d4afa1768698617c552ba034c5deee00e76f7af3d9a8e06b2c60e4562a83dec816ce902bcc70a7cd6b35d7ddc68d4e322a215ecf0df3930107a2c7cd334a2ea14e7ebe73b685973998852ee4f7d6781fbe49fa3a27353b15a0380db2f3252f2c57de907b2454211dc7a6d364429c0737a05c53911f7d8af8852e1a289f02229ad2906d1758a65002fc0d31e2f46b177297bbedaa8d00d74f66a6689d958adb5aa2f816f7f785d1b0037303ba4d9c14eb358465d3c280a078af64df6bd5dfb5d61c4cad4ed1be8eb42589e6854f23a8f9d58825438001cfded789076fd108e7aecc6f334beb1b0cad4d091bc09c69ed312bf1abd55666b9e6e7fce1b308539a8f3ae4c59cd706fad2c2c533892028a105c266ae0ed1da5f96e1aa6a95f3b88f9a5a141d6c2c7d0fa89f0fd4c2da1e96cc9cd9a53135abb1f2aeb1fff061664e5c22338272c3d8e66b67cec4de3ccda1baabcd1639dc188d914cce1eac8c81989477de3cb5a31a38ac8276ffe5ca3f1aa91db55fd3da2a1e679735ade7f542bf5d9f6277b9ef34f7176bdd6713e2fe2b7b8d58bbd4a59f7c7a20936b54b9ea720a3d3f0bfb6cfd7d67520157d865e51fe2e9bbe53717b4e429198459fa2c4f6aa1ee95eaf2a52841df1bee67481871757864466832c4ecfa2e52efbd62371d4756b940abc396f1d15efcf0eb74e82469496d39d56b3db071bf5ae517a2699db269c18f703aa461f08cf7166497eca142c6ec64013381cdaff628e540f384f416d0707bc372de592dbf9365676f747a3a0410627e6fc135f595012c5409ab002160eb61d3b9fafd044f16e05e583341bf672b5c62ab4747a2c9ac9d4637c6e35cf8308d50958ca329da06d2e2b761efa916cf79b57b2d9017d2f4e434b0420989f4742aaa44006a9cab3c7e78144d26f73b5f2a3b72853bf5163d90e53ab59e7692a642ace65e24013d568248cf3a4a1b2fdb2fc5b9c8f2633ae625ff05d2a95f59dfbab30a07000fed589e81d5c9570ec00e2c710a5f6c2fef9a41e833b314366bd8d40c2838c46a2554bfcbbccd244a97df36020989d79769bebc0121764a1a2a9e148815fa724776daeb1e09dc60305df192870c7d30f37c07529fad5340b00c31207fe3ccc2bf988cef1626195ea50c9edcf533ab3ae6cf6c3dfb06e021936319bee8d95555bbee413fbd923990d77877de4eb310b7e4fe448202e0ac532b24db5da05f404a61b885a12d9701db80facf42beaa06dfbe2fbd44d86ae772cb34595b21d4fdae659a61344b38decd2c949a837923fd7708c14236210abed998b06e013163a1c505f6fa0d3d883277c19d7eacb199bb81ceccab2c1c984fae7c408238f54d03dff3c27db2b44c02d3a96a0cc880c4e54321af7b8ea7b787b3657a3a8b40d851614f1ab9dcff17b561303181eb1a03d879977eecd0b2d8dfe519d8fc31c746294ad979689102818084732b7d37c1a1", + "pqSignature": "0x7a0f9915556471cc7b2719794367459c24f3b00329d43be64d57f9bedca5cda49ae10b45f36de74799d78e20215765615410be1b5373f740b2311ae26ecec0286ef62c84bcb6d3f3e228f1f4914dfb4d0454aafc09c27f95b8d44c4906523e11ca484f97af0d0060cf3470141b887b0c24186e1dd9973779de79ec9c28f47ca370cb4726a5577b55e548f5f99812c29efba78160dcc7444d268fa41ed2499ca787dff8587868b13aa95d6b3ff48d74dcfd8a81d78c21a012cb2a9e79505f4653a5d604c3e48998a73a979bfd4b3da266844905abfa74b5525c32f46d08aa88489c3f84fb2b3fc9bf6a95a5b3ad61719f027abcfa55b6f9b17a49db540b44fe4cf4a9f46e275e218da4d456e64cfff8cc40a893e5c312cf9d7f91cbfbced1f8d1d5f732a3dce8e045dd3fac19a3f009050a59ccf5ae0fea669f40f2115d1089e78b30fa21cd457f9d5008f4803074da5724e5d7e888706d8d8e638f1552c6d93d5b1ede02f603b1d7a4c25d980e88ea3d1183fcd4af233ad51ad42ac20eceb2e579a28eb31ef43cb3ef37433e645c2621380a0fd731065f36fadb53850e7708bcc4bb700eadedbf2665dbfeb7fa6f182db18058b3887dc78cbde72cfea5e0fbc69571fdb4f1a81e83f601fc11f34b2378dd7a5d39325654fd202b890adf8b172e997a575a07454f95a7612b026e93c223b2aba2ea88d491a8cb5701b3688a46696b994b3ad665771c208be8d58646f28be2aaaadfb4c577dd7dde53620aab4d19676f5bee1634bd658af388f7b1b20e5fe0fca241395b14adfca0254f936c1807e9032af3718d4490b6d54beaea77f5e78b0509f15a659cbc5cfd84970fbd02d6342ed4047abe21a030af11558566b830f13540ff68f36bc85868ec33e86022e8fd5f8512386ae17d7bbd4d27c27ab2fe9ed160e802fa36db6d5f2e84a02b3c874cef27c2aae972406a79de6763ce5d04fbdf7712da16ecf5c20427fe5bee1be3d70ec276af0ef43c7fe13c64b5531ad79e556cc4bf1d64b74e5f87d3d05c04fd17f8e5a6d6b29277778a73c7c42e9d0ce73e8dc3fc865ace955b6d9f694ae4c3a7e051e9ab68c7b91e5214d71fdd1956ea2fe8136f40e0a2849d222014c14c22b83f1cc4a2deb00904ddb6483a278f8d8e8a8edd3d12451da4098a44d8e4c79155d8818895478d3ee7a0857b2971a56266265f6b38b5affb9d878c75ecb7c4541ac7c9bb01d8030c9b057ad3ed5877137c34fbcb23cce4c7bca5aa7ce10ac3a92416b972dea33c21cc49439f2b50650576fd3bca128da6053c5f5703d7107106e05a87e97a08a03cf28ea81dcbd713e71f0ef4b4285ec88029f8937e84607598e45a518d5bb95ba47a9e48ac0139fc76c7f6d9f0134c86749c5458ed59cebbe2fbad50a60a890eec6b3ab449eb17e2d02a3308633a9310f9aa25c54bb181bb298d25367851794dad6c4c25efe8598b1e718371d16e9b89a23b8ba8f31b2da54ae7f9cdfde9328e045f85a395950d24a4d48841d317564d9c06df9a70e95efae8f218c3b63ce42510caa93605b0e63b7434b1eb96db22df88fccb40da7652382ab0f24eec598d96b0915acfaaa0a6be242da6bc242f934920de333251e1a1c217734435879e24271fcd162e317a6f926434ed2a121793eb704c208fcfe0cc165d5a00e81a526cb25dbc0e65984bd74ec6a94911ca94e1903abc638879ed006928a47f36dcfcef6857e938c65906f2d6949208ebbfd351c3142d47f8ccf31b988a6735df449e30c38788c55fa274a59cb045cd99592ea4c9e17f175db2d84e9582dd40cbd81ef7ca510eed3b40da7ce62a2269bfbd3a9d3ff6dd379dcd73eccffcb1061d038b3e9807002e51cb32d912f8e7e3d5b9dc050584c2f61e8257daa0abc88c732ac8cd0081cf14f9f21041e4bd593d0b13d63aa5d1af95c8a421303d5b89b5af1c8d3b274f188b0530a398f09fd2048852295b115a0bd58c0c996cfb62dd8874566e5e3c252743a842111d3adc3d49d9c3d381fc0ecccc19e8429d96f02244c79be7171bda8ed836331657794beba1f863c0818eb8468e4e0864fd17b2ec4c910df9e016e75e589881684bbd5aedb309ca0e67d4f5902201625b6407eae56599c61c4c18fda2f1debff684ffb26e0a88808891a09943bc96a93ce540dfceed9969af0316beeb75084a790e69be50a046daa03ffdf69f05ed07296da1325293341d987d455eb60b2067dc4e264b4bf7fa47e7f081cb10e97f13d5f3ecae20812ac4f6c83f15dffb4d011335073fb6bf3c936d27dc6c789b6f4c3881f88365b402cf110eb20d664ed5fa66e8d520113e1b320f17068017694cd6cd85ad6075d6aa9e95f8a9b5aef03295425a2ffeeb852458f26536597da2b003824011054662896649496305f0c1e99f1c08948c70c7083dabacb94d57c587906c15cd03e52916a235fd515ba5ad325e8290148926aec6a40a6b132c2de7f02b81527fadb8de5bd4390e83fe36e0f09b7072380cf05d93508c877ea95760d405e925ac54ba239140f74edc4debd28bc6eb20d310153a17baac946db25776dafe491bac594d1732a5c339e0a0e2a2bed5e77fb536807ace1abf2d60cc005ac0a8927f3824e58a33730b5b3c144788d1c3b89f2296dbb2bbca4f9150b3b13652f0d170017e312f995d86c717782f0120a8d31df1c9da16a8dff419676e91ea5bc22065e30a9794657fc2f107f4cda2af965dea3bf40f06fc9d2e97a9f3d825ff58696e9209ecbb773ccee9c284c839bbeb5f2d10a2284c17b4cc1c3c9160a73bef0fe1ce38d93218237653f14c3a761c491162a2a3351fde6068e5d94f35f7effed952f2a9cdba13cb1894154ae22ee59ed584eebf0898bc01d8d67de0d02a4e8a3507c36e1ba3f0e2014b9d707a5862dd44af253ecb3b26f79c0e8be04ca65fccf6351633618c43cf1e857e3e846e4479c6b49029e86d152d70b190bb3f2cb870526020f1f326d128cfe6dfe3912172f5db6347181b0b0a7ec1d9ae62cdbd04aaa8d98f5c59fc6c81f9cba363eb3f0d4fcf1139406173c065aebb77ab06862a1cf989c6e7e01f3e0686cf0ab683e5e5c5fd28c35a631b60655764efd1c4900e0ce154bfca4d2da9c2c0fbf1e9b3d3a8c631873789ae0828e227d872a501b63f83b8a9717d3954bc69262423046fc6a711cacab0df12fb5ddfb3f5579fd6c9949ce6b63f209bdcb669ae73d9f8f09f314029d76b223bd15e0120b6d14e26b8a5c74f69fe8908d51a0958a4d3692d95459d5384c773a21880eb6d4dc90fcab84b62b2cc66deade1b1a26e8d239c94e75781ee5e866181b2579a9b16186e67079bc35ed2bdb6c57e82e578fe3b37ad7f7983d47a49a5f0f82240fe92afdc5a7d17fe9062fe3454946952765b1cf8337691d715a9b4ce5c78be8fb0179eae49ea9fc488308d549bacd1b0d0577a2032117f02c62f12c3db1b67f8e0b0dc53eaa182676540f785267d44253acf60abd0be18b91972815b7aa80be6e20789dad365ba0a610050f081aebc1b20d80b3aa532be0b2dbbffe32359bf0cf7a97ecd0054d986a03dcdf3e8fe9b796077c6d12ece9312fb4fa38b57998ea31f2d78851520a85779ed9cf5712c7278ee9d2be1f8d8e3759cd1a49143e90edc9a6dc7fcc7d7693733693df0649a67758c55bd0acf3368534d84efdf560e8221edcee71591ea8c203146cc9d8aaf97b0b756ff54b7f826775638f3b6a2d65a99d5f27a3acd503d3a19ac732092c7d73d4dca4bf7ac546e5ffab1304f77e6d6724aade54eeaaa920eaaceacd1b78d70b787773ef4b7448cbcc485b55fadd1ca22430a824360da74d910d09aa130e1565673b1532d0231ecbaf7d3ca79ab464dea076b5dcd29b6bb9acee554788c0c8d82d145b78fe6dbafbfff729273aa7f943d6c49b7f77f0b214cb35a203598366fb8ed9e6a9e1da7203beca6b86286aab0e0608199c00c466c2ca247b7d3bf5c5255485a6507ee3959bdbefd95f98d00966925eeca3e738f6b8469c6c73a112a6fa28d448b1cc2287ea7afcc5277d417285f14eecdc9e3e5ba82215dec3f8ec22b65f8672f22385b41fcb89a96dcfcb18800b2b1461f39986eab15a8caa5a954f2ec19dd9e6d505b4fbc87478bb2178231cc214175036245f8cea200415a36b29b0bf0171840b838dde166d21bf1abe6cb45ae1d7a3c5c4831e7dd1e2c02e9cc67d5648a18330c841f00b3d9102e219e39929a300810e33e2bf0fb83949d6c220ad34b50931864810b41576f11e0c303cfb6f8a29e1429f20356f82d094714df497bc8878a8a2f965f9cc40c13c21fd7d880b7f13cbd29b894db469b5a1711b039ac1eec76a72763170c40e90e78623f73dbcba9b000c280092a8b8f6d98954c70c96f00f31ed69c47d0682cccfa6d9b11709f43573ebe9e3b54277eb961b52c5768ac4b53253021af69a646e71135eaef611e6df5a9c2f775b3f67a405b3d810eb4c1f136644105a552326e891db73149822731fcf45c37bf273449652b7c597706dbcd9f518d07c5c67b12fb5d6ba65240e7d6b07d2a70377a9dea017d518d809db75311cb410f0d518ad95c7a4d78a0bb0942ccd63b407f8699eff2175e829dd6177ce3e7fd617eabf1000000000000000000000000000000000000000000000000000004080f14191d" + }, + "notarization": null + }, + "artifactName": null + }, + "runtime": { + "record": { + "v": 1, + "kind": "aere-proof-of-runtime-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-runtime", + "artifactSha256": "0xd0ce419be4152cc92aaf23799d6551044e286057bf9583032e4277317c742d1b", + "attestedSha256": "0xd0ce419be4152cc92aaf23799d6551044e286057bf9583032e4277317c742d1b", + "host": "lab-probe", + "matches": true, + "createdAt": "2026-10-02T05:16:47.000Z" + }, + "statementHash": "0x43be8a42d60c487d5f31d4d45f05122aaab8aad7ef851e8a3df32d8469dc9fd8" + }, + "auditLog": [ + { + "seq": 0, + "prev": "0x0000000000000000000000000000000000000000000000000000000000000000", + "proof": { + "v": 1, + "kind": "aere-proof-of-runtime-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-runtime", + "artifactSha256": "0xd0ce419be4152cc92aaf23799d6551044e286057bf9583032e4277317c742d1b", + "attestedSha256": "0xd0ce419be4152cc92aaf23799d6551044e286057bf9583032e4277317c742d1b", + "host": "lab-probe", + "matches": true, + "createdAt": "2026-10-02T05:16:47.000Z" + }, + "statementHash": "0x43be8a42d60c487d5f31d4d45f05122aaab8aad7ef851e8a3df32d8469dc9fd8" + }, + "hash": "0x191e053dd8a9362c1f7be1f9fd2ec91a6a3b39c49d1255e01c55f21f14ba52d5" + } + ], + "head": { + "v": 1, + "kind": "aere-audit-head-attestation", + "statement": { + "v": 1, + "kind": "aere-audit-head", + "host": "lab-probe", + "count": 1, + "head": "0x191e053dd8a9362c1f7be1f9fd2ec91a6a3b39c49d1255e01c55f21f14ba52d5", + "createdAt": "2026-10-02T05:18:47.712Z" + }, + "statementHash": "0x3df17b7cdc1197d45291c50b27ffe66c9b7317d66c7fa94589673886d07605fa" + } + }, + "witness": { + "heads": [ + { + "v": 1, + "kind": "aere-agent-ledger-head-attestation", + "statement": { + "v": 1, + "kind": "aere-agent-ledger-head", + "agentId": "aere-agent:965ac8dd3578bd4c32289394319a516b5f513947", + "session": "08bdf8fc04fba02a829bc11220a86e40", + "policyHash": "0x5040392a96c4f93295031ecc408d40b2ab717df5d6a8031d915377900bf06b76", + "seq": 0, + "hash": "bd9095330bb9b9bec67463d842b9eaec595f108ec221bcfff3cf14104a0e4dcb", + "createdAt": "2026-10-02T05:18:52.410Z" + }, + "statementHash": "0x596f0fbc420a50a9289332a2c28a379f66b8a2d58fe4e5a458a98fb16db61e1a", + "notarization": { + "chainId": 28001, + "notary": "0x70099E62735500AA2F85B60C518551a57B202d54", + "firstSeen": 1790918334, + "txHash": "0x03a9151c4e6257f40548bb57b9173cc45677529302ce8666390f386e5100e9ca", + "block": 4383359 + } + } + ], + "chainId": 28001 + } + }, + "verdict": { + "verdict": "VALID", + "reason": null, + "levels": { + "form": "PASSED", + "identity": "PASSED", + "version": "PASSED", + "runtime": "PASSED", + "permissions": "PASSED", + "provenance": "PASSED", + "witness": "PASSED" + }, + "details": { + "form": { + "state": "PASSED" + }, + "identity": { + "state": "PASSED", + "detail": "aere-agent:965ac8dd3578bd4c32289394319a516b5f513947 (ML-DSA-65 key; the id is derived from the key, nobody can claim it without the key)" + }, + "version": { + "state": "PASSED", + "detail": "model-weights.bin 0xd0ce419be4152cc9.. attested as aere-model-proba@1.0.0; signature UNMEASURED" + }, + "runtime": { + "state": "PASSED", + "detail": "host lab-probe declared the artifact running at 2026-10-02T05:16:47.000Z; in the host's audit log (1 entries, chain intact); continues the attested head (count 1)" + }, + "permissions": { + "state": "PASSED", + "detail": "entry seq 0 (tool model:aere-model-proba) allowed under policy 0x5040392a96c4..; 1 entries verified, revocations 0, time witnessed" + }, + "provenance": { + "state": "PASSED", + "detail": "model aere-model-proba@1.0.0, prompt 0x1ed19f00c6.., output 0xd49920d071.., tools [model:aere-model-proba], requester 0x9bba5c53a0..; signature ABSENT" + }, + "witness": { + "state": "PASSED", + "detail": "1 anchor(s) from notarized heads under a post-quantum certified state: seq 0 first seen on chain 28001 at 2026-10-02T05:18:54.000Z" + } + }, + "assumptions": { + "identity": [ + "ML-DSA-65 unbroken; the agent key holder is the agent" + ], + "version": [ + "the attestation proves what its signing keys attested (who holds them is a separate judgement: pos.mjs verify --signer); the artifact digest is what links it to the output" + ], + "runtime": [ + "the runtime record is the HOST's declaration (the sidecar reads the export the operator makes); it proves what the host said ran, not that it did; the audit log has no key and is judged against a head published outside the host" + ], + "permissions": [ + "the policy given is the one pinned (its hash is recomputed); revocations come from the owner, outside the ledger, and only those given are judged" + ], + "provenance": [ + "digests only: the prompt, input, output and requester are proven to exist as the bytes whose hashes these are, not shown; the envelope's time is its producer's statement unless notarized" + ], + "witness": [ + "the time is the chain's (the notary records the block time; on 28001 and 2800 every validator is run by the Foundation): whoever notarizes can only notarize late" + ] + }, + "notice": "A verdict says that the bytes link and who vouches for each link. It does not say the model is good, the host is honest, or the output is true." + } +} \ No newline at end of file diff --git a/ai-trust/fixturi/atestare-model.json b/ai-trust/fixturi/atestare-model.json new file mode 100644 index 0000000..aa646ef --- /dev/null +++ b/ai-trust/fixturi/atestare-model.json @@ -0,0 +1,45 @@ +{ + "v": 1, + "kind": "aere-proof-of-software-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-software", + "tool": "aere-proof-of-software/1.5.0", + "subject": { + "name": "aere-model-proba", + "version": "1.0.0" + }, + "artifacts": [ + { + "name": "model-weights.bin", + "sha256": "0xa78f10b9b2926338085b0be951ed16ac1400b843e32c78fa5f844f5333afe647", + "bytes": 4096 + } + ], + "sbom": null, + "source": { + "commit": "03fdb26469b7353b002c490d5737195fa9e5d983", + "remote": "https://git.aere.network/aere-network/aerenew.git", + "dirty": true + }, + "builder": { + "platform": "win32/x64", + "node": "v24.14.1" + }, + "createdAt": "2026-10-02T05:28:15.365Z" + }, + "statementHash": "0x8ca7931c4635405886e7ade1743bd188599517ba78af17991560aaaf516d6daa", + "signature": { + "v": 1, + "kind": "aere-hybrid-signature", + "alg": "secp256k1+ml-dsa-65", + "hash": "sha256", + "messageHash": "0x8ca7931c4635405886e7ade1743bd188599517ba78af17991560aaaf516d6daa", + "toSign": "0xf3d92e34ce3bd43e4ba1ade10d149a2756730ed9278832f7325febd18599b184", + "classicalPublicKey": "0x0342abaa1cf81e492f81802f03171a0bf7b01fe7b9a1b38c416f32419ee1138bca", + "classicalSignature": "0xc895ef477c760bdae2371b62e7134d5fbf190a7b13c60a4ca83f97d50c4dcfb95f51dee1e7f5073d216d1a11f40eb3d13e217676be804b9f44a8c1ab77beb0e1", + "pqPublicKey": "0xfce0947486089663afb7dd72b1edbb6874430065a43ba950b770b0cabf36d36ef394d32d2a49e487222639d5d30fe1254ab08736a64b91a86f6e888e603d3019657a20e99c4e139bc34373793956ac01c4c713b9eb899d16ee0de4ee08da5f1b45c5d3bf00cc593779310756dfa4f7268d08a5f05ef256eec821fbebb90b70eb15a309f04512aa281e8d1e0cb200a93eb911cc4f46737565d052df6e76469806488596ca9ecc51b4d5ce54c331e270071c9871f735de2438ee8cc190a5066c78dabba9e4aa8d44d271640f0d7a610ffba5051c7f9a0aa7ca8d3f485e4a2d7ccf3687844ef41be11c2f649fb35068910ce3eb676807d1d26fbe6a27710bbcd15e41bbff1e71353e1893a6107d5bb3011670cfea811db8cb9e59137a08344542c24e2bdeb8f90276ed31a5714ecee07d2d613e977a85ca610a57cbd4c3504e72d00e255de9b77947b9bd6ae2f937959a792e5b20fe3b7a72e26cee3c9603239f318213b4ab1c3346ae48501b1e903f3f7c8b95c0c6d93370060b6df69e785582af19201605bbb19102c08cbd534f08d6d2f443fa6ff820f9f8ffab836eb04bd35bc5ddb5690732abb483112e0d9a35ec69256ced270c3ae9a8a28239eadfa439f2753e8c838869dc9d79e47b1653aaecb99462befb0683f9d3ef321432af27bea85380044f20aa7f5e6b00b6b64ebba742b43ca903d2244955abe0c9fc99e23928de48ff2b3e822e9a93d4d39e4675fa3b516b166e6545ae4981ee3f957612a506105be9215c5c589083a96cec17521f84417ce26ab72c1f16c8165937f54393d6edaeda200fd55c2617d8fbbd7e35045ea810bba58bae0fe52d04b7ae1284b75e2c93fe2d62f181073dc1aff2100581fb8b6e6a8a485d5a063f1568f738e8e807275e1f13666839d6093cc06028d3345b8e50e680dd8ba52b024ebd3178590267aab5df06ec558bce697f1ef2e9721cb495b114e8fc8e181bd9cad47056d8af998d74c128bdb8a638621e1b0865e24a5fbfa02a43cecf07bda043e023b85db414d041ec743e9eb94d8ef9423cc66e9cf11d85ee48e9ec95738ddc285463ad07934a44e6a3562362344798c575652b127b9b1c8b653ea424621e4d2d37406f5ae4c27e076a775f86f67e89eb08e93f34037a74cb1e991b458505106b4188af8e9a4b453dadfee2efcae46b2df38cf72d0685a9676f8f047dadf9f53e66f634e4a51f6958afe736f258bbf2aaf33b4e337a2fdfacf74531a9ecd2d24b8892e42e23ad00ae9695f4d5437cb4b6f75ba17a90b25646d43e5e2a5b2abc3ebf09edd889c07e40c42d5d62cc449eefc2f093e05e9b9a14fa8b75cd6ffa50c367640a2f3aa4c196ec567ff4e087f2e784d4f1ccb9073dd73c34a531afd11de3621f0e65c131987dabe6a77005bd75ab9943f4abd3527b58fc1764520936a47432fae890885f68b0fd5ce18d6348453b4d06e9fb3058d4ed16dc160e8b3b2afb38f3c212d8f2b371cea3a6b3a112876430def86e37bfcd624a0ff01cab238be38be77bdb2944579a18b0ba4dfa13af740b6ef23c0a5a7052122694c503b6c4952d3f09322fd6bf5a5c3793d0a6a9ad6d8b041c120a05b5c7fbbac2abe70747e65dced88fc0797bef0543b3cbcbbc53663cef39a1c7f492a7082a41f767d3d36fd25feec9de8297f71b021b67d2c57790c4e288af7a34e960dc5cf917d85f671a2c8d47208bf9b2b268c80d90413c3b28148e63bd4ca8246e17759c268abd0053afa6a01bbae2f7fc08c4ca57c500e5d761ddd3e35c14409f8a9d382f135872c9871fb6dc2e7e2baa18e6e26c17ace046460a5bdc0c1a87b2056eaeff857b3e524100ee9439a92303574cedb13b094b7772a4c8d2c72c5f8eb89285846c63fdcb0d3ba36e31e515103a69be267e492207b1d4a53be3a6b34c1a5e5ecac4bf18a8a0790f02c834ebcf55da573f1fdb1e83db1870eae67280e58c6bf431e17b1977e07dec3fdcdf2823fd5dd81e739b3cf9da8d08d726735aa9e9ad817acf36576bf43abeacae23505327cb6e34147731c9e7c5d4ee12113acaceb5e34fc3c49191a4c14e01b6814345d54cea8243c6cc04c4a49777644c7ed0ec4bf6f0a1fee239678b680bdbcc261465bb44f6994e97600be5ea97a12dcfb5c52601895b93444f3d4e564eddad951095d01a282f17a75a66db88a920b4a52da15187ab84f7f7113031c1760496bdd0c719ac1fe5e87dd6013dc4342277219cca7eb3571ed7cecec4213e8365da3c7e7eba51bef04710430558096fa7dbb10d694d24a9fe5ce7f959dbded9563e52f1da362f242e4142a6c560fa4d270ca3e24c530920ef297b3fd447a985a51f61395ca908509cb184d16f41bbc0d505e5feab44636a6158a3457583ab146beadc0965ed9ed375f8d1145a36e1e38fd8f15638a8d86386cef4d4f4d69497a59474b2a3633b2dea0f325bb63c3da63dcbae9d0ba938bc96e7d0a52aab6a5380162d20a105c762c5e8ae7c876428a0662e5327f6f91780da728534e2df792dae13b78564b2128c7e40ceb1a302c93606510009e52e475898d4e6e738d248d202da1adeabfd50ad382423492301ad06827e1df730330391a0bb707883a86dc543c3f743da3d5aebff073e39fc4d1f60d298ab29bb45686a3101276c7745ae558b71e653fde828f0748b0f56b28f3a5b6ccc08548d23805d8177f7e50f4ea007223de88d685474231e2610cb822feea3dc711f0f18a28eae60c284f37a5598982b", + "pqSignature": "0x0a0bf11b4718451eb818dc27b12a4b8a9a319596804fe960c8fca557e15c4e31f5757b1a56dd93a446f11e6be18d38c72192e12e5ffeced90f0e4a7e8deddf983347b4834125696a18bc33d010ce10f54ccec944fa05fbd897f53519356a0387aa390816bd12702a74db62b4254bf229af453aff9036ea51da865092d54f6b4041a2e5d12217e88e79c1b699a0d0159292ffc528f6034a1176edbbae75fb9afb38c10b01ff40ce7d3890e930e534ed616753d44531f079c9a405971bb4a14a101f09893264ae908548ac381055283de0b17a9c0ac2a6d867bbf345df7e701f0afe5c1d028133eb9e9c8aec0ce667a5f4ab25037d5a9ae8ec476a6dda0ebbc5346e86d6e80ca429feef69cd1f52566e162268537a6717faabacfbc5a9a403ef95e8bf7b42e55afd58f7177aebe099a2839493c426dcd05d0961e158166585e2334a186648f80589ccd5b3cc5227ee243d0552823fe8560c6e5d91e26fbba887c1d971e8d3c0d11841b20b625e680b192bb0e145409f4ca92d53acde8e8f50e98be42f13fde1e26dff7a4412b76ed3d30026d04f1430c2e0e90b9bddb85239720ceb281c56613cbd11291be969ce032d91139315cc476596e86de24d8f61df46ca7d5dd202d52b0cc4c7fd0d0578a42e79c8f84f5c51363da2b9552c06cf3c853f6585c5a291550628eb162b18ae8026a946bd75a85bb2a021281546eb4175bd6ff257f2571f1e70d5aac32c1d6074ce743f08c195d626216540386d8cc612177cd9e72a9ba836724e5d64d1d0e87babbbef088011c58081e1f6089cf5bad11a84522c30d382cf93a558be4545bac9bd6b52922e87963ccf86443ef83b8d8382966d9da8f246b563c964d0f6ca36a229cd788f206327edddae0dcc19e577a7b3a10f4f50503cdfa1366e7ab1be279257f310da6bb2c148b420227212400e87e78d8d0e497abbb6c4fa6b1a6143d3e7885f290b40a5b49e1e0c4993d71f33508a5faea029045ca8df0086ec7146e5e0e3cfdea61a2b8f6df93e0ce3c12b2da6e613d5542c9f65098221aca3eb7263138494dea7b18d8a9ccab367314a6d140371a5fbc20e6277c5c39a46617139d19af3a487bb40c53a7f5d7b9451ef5e0ac82125f5c0d4408492480135cae2f18de09a4027ebe13bc0c6cb096dc79b28a6f24122e79452680fddcafe98c1c237d5e32478b39a5a2e3b1fb347d9fc50f5231348492fde8b30171c927d99c0757a83bdd89ca7ca3d7b1643ea1bb2663995017a2022f343e9377ed3699f37e67ec0b0d3bf304aad197536d28cf3902131e1ae02e6905cb8dbd0c6aa682a72f5919cd8d643c6eb7afcb24f9853e37adb57e8832c54e0ec03e487713c4ddab6d4c74e387c78f34760a5bd51967e5a9467eeb15ad1b62af7a0188000fc10227d06e39d570406496549bcbbdeb0c68509a23440ba812f24b65e6ae386c85e5c1ea7de3f9920d49764b0974e98676ccb5746218aa763722ff1c1bb6ef67e699ef7d6b1ef30efee6e319dd61c6c93a9b6b8f08909a75c514b3d4e3224e1b6f6aec86fc9e005efddf856e8aa5aad174afcb33142676f3f27dd268be850b46126082fd7674cd2a04ca4f39f558c5f31361334331b09cf836441c220083d45caae3f3fd329d1f4adac15157af2409f16926d8786f5f8ff768befa21263bdc27c3f29a1f4ca83f15329df798e42ffc494b19faa524d16cc1f620e44168d42f820d22bc98c82e841c606d92cddaaf417bc4e78926d7422e48851633f316c15a0247a3343a7ac089043fd0a86442659078e5d8d0ffb47d9a20faf51d4bbbf58493ac213fc1d0a816808aff161456ddc84c858c8b0b432c09299a69168dbe0f4df87c09bfd5d74d9b2624b105e7fe25780ccb25e78686cfb559a557a20d78d0f953e1f32eb33d2ca57a96a614743ca7c4b0b9a966f7b98a871b78adb50abc1c56e43a1b05a1daab91e7e8f49c37528bd77b961d4b653add1a01216e6a960d8881eefeb4c8cd684d04160f4b9465d1c88314484f47a74356d0f192c7fcc8bd73addcc15f438824adb8ee96d99897fa90b075328d82a6a2fb768cd6077a44396b908abf6428cd7cb122887147127af53c5582edacbdc3e58731436cede58b1721398123fd11d1f7f58d320dbadf763ddec89b202075079bc2745a4206f429c158cef4b985dc7c38b2c7b9a3eed147b273937ae078b4cecde16c77e1ec128f52e7579e6e5f9dc4aa5374f1540c57e5a67e9ed7d517d7756581fe3a80be91f8afc259dd30ab19cad8dad68346cbc17ca7448e3239bd531f60227e55d36f770e44c2b458172ce096d8e2cc29a1237c469c15bfed0bedf597e2766d97c6734ac641e62622cce3a4664d849d5f4f1e8e28d70eac0aabe23342bbad533454ddeb6b71e020837c75309f8237d33193836a10fce34a9f2e4465b028c7719382d1568f4349cdf27826c17fe26b79c76d3a433f75b125018c7315fc3912a11d8ce946963c9ac278e87d83e514d3a333ffabb0e646744aae0658071d703930d3a95f0c540f912dae981fa860d8d9979c7392f7ba94bb7f81cc7bcd7c7bf52317866fd89c5e2d8a181cff1d927874d18f45fb2edf871a0a471232a04e5c4c723a5776290be45c0aa49cf1ca01c7ff438735a2875d10a1a186b47f2adb34d050df91257c403a358b5358a1d56e7aca612f8b970fe477f3a78f9d647a73429070ae7c266c43f8030d0e4957adeb8be26786f015c51829517e2809588c7d199dde34cb7113e504778f94a2e1a91d9e93df052c32319e7c44bccaf697d2aebdb8bf67700ba3d12882d8bd8da4f0fd3a4978eeb3daaa047a538be33c970ac23264367dba05ecf4f55ad56cdcdf45e4e8dbd6c553da29a916995a556cd35438c895d18be1794d69948f51f9eb95e6d6d7f7bbe1c8d9c437e65147246c145067c0a723bebfd0b792609235675e9ae4d4f7282406114521e012d8df43d412ec65b605264be77149b9d835afa62e77ecbc60b80b0ec1b777ea14bbe761484bcf54df563923716350d6c5f215405c293c2470805e7fe917eb0b5bf16b722ee662a4503e531d660b576ec621937f36bafecc4814959546058b4658efc68655a213a1023484b77d662a2902f3d93928bf9f58176ba81ddefa406c55e622f7a1c03a460a023735990629821c92ecd97f9a82538d3f06ea06a2f00c3f71263bace271dff8be395c76ab5c4e5ea375847a837a6d6c4d5aa09e51d4f7917e284ec29a79b18c70438e073627a6a10b89fd0ca1a065b8e03de10ab63e26b7dd208b126728fbda80caf109a4fa3d7ac58b21f991bf28147406e20926c3a64f510360e433413fbaad41044623777ad24574358e77fa131b435aa8c274f4c3368e8b6c50e03f48545c7e9234008db43d646cb800eac876fff414d402c4e70191f7b558711bc273fb2a70c6a29d083c77934980925f0654beb70ca730148591cd5c43784558dd634c3c123c47d87896717e6f63746b9d6d0b84a98455ddba71dda6880fd2e890a71bd4a729c08ec0c784b10e894afb30e8c6e7348e708d79de6a8deea8bbc11714132c81491101c58df001e0e1effb17747bd18a7d2a852b996c578e48aaa72511e0315c54365439944d946831362cc8b89fbde0a005e16178ecc726cfa7385e59cda1420006117795180423f0ecf874bb9b979470509bed70f3bb9b00b3d50a446f76f172672e869c080bfdfe4c2b28ebcaf18bb543e797b46e43f9f6ef7dd54ee937ccbeeaf90717274981ef14d0b63c7d8f70869f998e8b782979422df1c3c8a39c6511232bd6c21e2f14b9098f7d38906c97888495ed07b59ce99c33d98ef285b0d57a30b45792760585b466b7c74ef8627a124d68960c0e06a9886a17f03eeaab36fdf0e02ae05074059ad10806d056b75f7aa77158df0c27f1d04161b3afd29e7974a08ef12bc0bf37330c946db04ce8fe60c62032ae8da98e8325ab57479d5cec0e303ecc3b7368c9963bcc5c3bf1111eb4a6fd5ce2e6bffda6399f0243309be4957df088a92985e4601ef3087fc58df68160de5e20626b237166e45234522c0b5d394d660f7f130aa1c1b371ca2a48f7f20d90d4ef02a766b49aea5c147aff6b0adf776f198152001b360c07ddbf8b1c0b182919795da8f8e155c00d94df2a8a06803bc059dacd9e2c2926a3636162f73f3ce25571d65791999d9e524ea0c188aa0425b7e0959e9311cf4332a9593cb368226058a66773209a49e7728b7ee4b31132e8bae024907278acbc08680cbd682aac3ae5b94aaeb6d62ac62ef7289a639d3166189803d5d881ed53a176b5728c6287c1ba01fa9231eb0b4e6d3b1d0a3db95a1e0903fc4bf1d4882ec8f746a798ecd93a882493d6d34d1ea0dbc3001e879c32e1c52c902a36caaffae636de4a7bfb689b0324864769810eb65d7ae2817606c74e4e3695b9dd31527c2899f459a6d1f59b92144aea03ca9a44562ab1665262bbf215bd51e11dda153797bfdd87230de9d98c41dc107bd59e24ba67db2bb0bc73acc39a3c0450911de8d1106b51ad3419c10176af810404f19257a064a825f9fc7a4fedff0a50c282fb4df1cf363354c62632d30376a9edae4eef51b2f4c96d2052e6ab3225a717f929ab7ff0a7c93a3a40952616f82dd000000000000000000000000000000000000090e121a1f25" + }, + "notarization": null +} \ No newline at end of file diff --git a/ai-trust/fixturi/model-weights.bin b/ai-trust/fixturi/model-weights.bin new file mode 100644 index 0000000000000000000000000000000000000000..91e047ac630e151bb9bec211f1629e074ce57628 GIT binary patch literal 4096 zcmV+b5dZIj2JEYa$Wu|nO*{$dQVDp>!!MqtgxsvND&XQOLA_+>8z~ti%4a)6pGiXP zW`4)ntqr0HaLQBlfFzshQQZ>jh$|4h0QympnKHS5vGWiW-BTvQaPZ&Xb?Ho4q##b_ zLBE4s2;THT)zk%A0$hGaKLmz=iO*$8j?hQxidTnwh^C$)w)Id{-okwcC1H4dx7Jbyc{-shx}lO+5r zq_jf8#reuXs4oeDpP1kRTz*}yRk7;O_loZNL!u`aHAfXRe<9G)?L&nGA(q+g`|Ry>7-E_nb4ZoR z#EvGog=%exY&ddL%uW1}a=v%PH|X|cW8Ylnr}{EQ9S?erhb@Es23Se)7qB^PqL`_3 zf^z%@cS5VVoAJeT%*q!twaYB*;pNG`-h}u_jpq9$!=A#vs3Ieho;N&@nhQie=WW@1 z?@DfhFK85C36R7N0F#F^(B$X@9vR*{XJEUA`Q9QHteS1=l7U;~ho!#Y@Y|ILrCKl~ z8(9ELQ`?;1(QL;u9(P83FRp9;#0?-uWA=D9%Hro_YucRA7FGoR?fL}C9vRWG{~T#V4_ zy^=!wNCFYe-NWK1oD!l#Cl8zu&d(o{_ayU#sBuCs(VQ&7J2@9{5}ca|SdFm_Q=Cqg zh_4fyAT&m~G||fpwq`AdF=_HqYU#23iEF2=)h|@k&pAt+lf#ed{S5@lBO;`j&-b;v zm|9A zNxGs31IGnazu~z}@8*>)F6G`qL44d0w+gTZpttrE3kUY2B{)vP5D;(;0o4xhMXNl%w>AiqF z`I}OGA*7e`rli1n5?OHd6eX7l?RNc*LVd3}de;DpIn+He!_O^Y_Tl17r+)^??gq#z z@c#h0@mO31vDPwKwQZByTRbMmKnR;{?`f!44bvjuq^cWEzB3k&WpKLxk0z+9X)EVY zGG;AGP*&7jdqp!m6B|v~)sAyLPg(Akoe9JS(X|t1XAUKM;*~3uQY`s>JD8j8g_Ue> z6cgch9B)?CUQWK+Z1ySza&H2#S z{!s89pG95K(Kq(5fDsB#pxLdg&#nA3=XfIE#9mj6{gzUlAS`+i&w>lvm8FXcioRrvY z4U~}p3?~zH@}C=p117;jj!N>L0S8(&Z!(DUdPZAx=(R69ziK=`$AWc&^n|LrOf=O9 zGAZ<|EE+t^XKtueSV`USxfr-MAbfw#sh5A$*~9DfdMOuG6|BLcp9n?*5fx`-K<|bI z_sibMogP<^<4z+2vZ}Rj&iEJfNUqzdL@xs$OU3F1d5MUGHF2>oR8}j{W8d*q*zWDApw|L1WP$_fz70X8Ne+*>_@^IL6asYp?g^ZbUl{p(z@HQKcQB|uex zk*<$DU0y^T2;;|3((-c3>7M>Jc-&uHj&8)jetta^ph+Ypvm&PpWMkAr_jLh71Bp{H zyZYnKh#m#Hu>jkO$xT0aaS82aFMpK?Ry{3Ir@brFgp71K9f7)@W~V8T!G6vVM5Ki! zVOAF}vi_Rk%PmbA4T0m)t|&pK+wZ^AFS=J?gm$XeD3)_N1=d#|98|CFq?6sQQ*diJ zXmToj9yJ#EB9K3q)|KyMMJ4?JV5b<1Vpm1EvbGQ$oj$i(9pJiw^qzIgs78OVMWd}* zs{*#^68T#}g~`2Aa`q!z(Y2z_ck#B9{{-sb{0o8$+=9l}NZbus0t zJM^x#YAzQ9U4BdPgxFXeXNDI|UnqoKXd;u=@#mDKsXy=N02)Kgb|~r8e~49^-}ujw zbpZOvhLc@o83ESJhS%{#E#1zz#Xe)kTK(3kKW7-a3?eB2N~b)|7Ee%(s)slX>yY&h z`>!cNKh1?fP~kM9=hQ$O<3~wt?*mL28jf(RtQjT5NUsO9UaLh;+URzCGEje<+@t}`N9K7QE$>sF8OJqs+4SPHU6!hCd^fRNqJ_m4*ma~gXAJP8(U-jD zx~3Ii)XsW3o7~OH45aJH4FoZ$cSn2}IbomX|4?~3bK}TJQ_^A!VD_2*^2)a8lux!N2V+z=M3wPI@q2=f zYdjW@>`l4W4Ff!V$cnZi_t_Ibk3K)OHm*Riy-^lMA!GY^Zv;1=*JeuLj6Ha(@p&PxArvt`ra*Q*gn^olTxA)d0npAQXq) zmmDZk9fv8b*5XtT>6+Vged>$GCXW>{6#wT|aNtz{#DE!$%>-fe>ap^A`N}0qwhw9s zaM_!rXl1$8uLAr;{ArZyg|jCp{nEZ;Wi*kaj!S+uWc`9)n>6FX@lOYUlLy&f8GwnQ zCaV;V4Gs(yrbrOuDs^rL%EDub`6KyD6j?>5`GkhiK|i$X91Ut6M59fnV+YxL(h6B) z5k*a~I0aN(iLx&`nk=>`;PYs3P8!2@o-$G-STxX_NZsXl&*kLRZ4=u?2;vWy>bC#Rq;Gd zJs+_`{7$2q9?8n&8CN4^HXMZ`expm`T{Eq?BHDwPZ2RRB)zug3NMt>pK1D z&N;PAgJm^+QYcOJ_PfyzwH~sNc$cv#E z$gH*oJOX_kRKf0Q8`!`TH@WvI=*-B;dWnkufz;WkV3*GW-nPB#R3O@wFZs}-Pvj1P zj;P;H(&H_xHAe&4(WSbD@YXxMXqtK~rpe;adX)`zSHopv8MQFIsl$d4cU?khSJz7-3rY%(#eyqN{qA(Zji980>)$o&r{iyoZC!W@9+?w@cXR@I>CKeYPDhg&RUf%)A zWci(-z5yAtQ9yl+GW2ZNMu#8&F)wjuA`I5Z^R!q&{_I-rUWO7= '0x' + crypto.createHash('sha256').update(b).digest('hex'); +const lower = (x) => (typeof x === 'string' ? x.toLowerCase() : x); +const lvl = (state, extra = {}) => ({ state, ...extra }); + +/** the hash a ledger entry stores for its provenance: the ledger's sha over the canonical JSON (with or without 0x, both forms matched) */ +export const provenanceHash = (envelope) => sha(Buffer.from(canonical(envelope), 'utf8')); +const sameHash = (a, b) => String(a || '').replace(/^0x/, '').toLowerCase() === String(b || '').replace(/^0x/, '').toLowerCase() && String(a || '').length > 0; + +/** build the bundle from the pieces (files already parsed); the links are not computed here, the verifier recomputes them */ +export function construieste({ ledger, policy, proofOfAi, software = null, runtime = null, witness = null, createdAt = new Date().toISOString() }) { + if (!ledger || !Array.isArray(ledger.entries) || !ledger.publicKeyPem || !ledger.agentId) throw new Error('a ledger (agent-ledger export) is required'); + if (!policy) throw new Error('the policy is required'); + if (!proofOfAi || !proofOfAi.statement || !H32.test(String(proofOfAi.statementHash || ''))) throw new Error('a Proof of AI envelope is required'); + const b = { + format: FORMAT, createdAt, + identity: { agentId: ledger.agentId, publicKeyPem: ledger.publicKeyPem }, + permissions: { policy: policy.policy || policy, policyHash: policy.policyHash || null, ledger, revocations: [] }, + provenance: { proofOfAi }, + version: software ? { attestation: software.attestation || software, artifactName: software.artifactName || null } : null, + runtime: runtime ? { record: runtime.record, auditLog: runtime.auditLog || null, head: runtime.head || null } : null, + witness: witness ? { heads: witness.heads || [], chainId: witness.chainId || null } : null, + }; + if (policy.revocations) b.permissions.revocations = policy.revocations; + return b; +} + +/** run the AIP-23 reference verifier on an envelope; returns its JSON (levels as a map) or null */ +export function judecatorAip23(verifyProof, envelope, { rpc = null, chain = null } = {}) { + return new Promise((ok) => { + const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-ai-trust-')); const f = path.join(d, 'plic.json'); fs.writeFileSync(f, JSON.stringify(envelope)); + const args = [verifyProof, f, '--json', ...(rpc ? ['--rpc', rpc] : []), ...(chain ? ['--chain', String(chain)] : [])]; + const c = spawn(process.execPath, args); let out = ''; + c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', () => {}); + const t = setTimeout(() => c.kill(), 300000); + const gata = (j) => { clearTimeout(t); fs.rmSync(d, { recursive: true, force: true }); ok(j); }; + c.on('error', () => gata(null)); + c.on('close', () => { try { const j = JSON.parse(out); ok(gata({ verdict: j.verdict, levels: Object.fromEntries((j.levels || []).map((l) => [l.level, l])) })); } catch { gata(null); } }); + }); +} +const parsed = (j, name) => (j && j.levels && j.levels[name]) ? j.levels[name] : { state: 'UNMEASURED', detail: 'the reference verifier gave no ' + name + ' level' }; +const envelopeOk = (j) => j && ['form', 'integrity'].every((n) => parsed(j, n).state === 'PASSED') && (parsed(j, 'signature').state !== 'FAILED'); + +export async function verifica(bundle, { verifyProof = VERIFY_PROOF_DEFAULT, rpc = null, chain = null, now = Math.floor(Date.now() / 1000) } = {}) { + const levels = Object.fromEntries(LEVELS.map((l) => [l, lvl('ABSENT')])); + const assumptions = {}; + const fail = (l, reason, detail) => { levels[l] = lvl('FAILED', { reason, detail }); }; + const unm = (l, reason, detail) => { levels[l] = lvl('UNMEASURED', { reason, detail }); }; + // 1. form + const isObj = (x) => x !== null && typeof x === 'object' && !Array.isArray(x); + if (!isObj(bundle) || bundle.format !== FORMAT || !isObj(bundle.identity) || !isObj(bundle.permissions) || !isObj(bundle.provenance) || !isObj(bundle.permissions.ledger) || !isObj(bundle.provenance.proofOfAi)) { + fail('form', 'form', 'not an ' + FORMAT + ' bundle with identity, permissions.ledger and provenance.proofOfAi'); + return verdict(levels, assumptions); + } + levels.form = lvl('PASSED'); + const poa = bundle.provenance.proofOfAi, st = poa.statement || {}, ledger = bundle.permissions.ledger; + const outputAt = Math.floor(Date.parse(st.createdAt || '') / 1000); + // 2. identity: the id is derived from the key, and the ledger, the bundle and the output name the same agent + try { + const derived = agentIdFromKey(crypto.createPublicKey(bundle.identity.publicKeyPem)); + const same = derived === bundle.identity.agentId && derived === ledger.agentId && derived === st.agentId /*PAZA:identity*/; + if (!same) fail('identity', 'identity-mismatch', `key derives ${derived}; bundle says ${bundle.identity.agentId}, ledger ${ledger.agentId}, output ${st.agentId}`); + else levels.identity = lvl('PASSED', { detail: `${derived} (ML-DSA-65 key; the id is derived from the key, nobody can claim it without the key)` }); + } catch (e) { fail('identity', 'identity-key', 'the public key cannot be read: ' + String(e.message).slice(0, 80)); } + assumptions.identity = ['ML-DSA-65 unbroken; the agent key holder is the agent']; + // 3. version: the model artifact attested (Proof of Software), and the output names that very artifact + let attestedSha = null; + if (bundle.version && bundle.version.attestation) { + const att = bundle.version.attestation; + const j = await judecatorAip23(verifyProof, att, { rpc: att.notarization ? rpc : null, chain }); + if (!j) unm('version', 'version-verifier', 'the AIP-23 reference verifier did not answer for the attestation'); + else if (!envelopeOk(j)) fail('version', 'version-attestation', 'the Proof of Software attestation does not verify: ' + ['form', 'integrity', 'signature'].map((n) => n + ' ' + parsed(j, n).state).join(', ')); + else { + const arts = (att.statement && att.statement.artifacts) || []; + const w = lower(st.model && st.model.weightsSha256); + const hit = arts.find((a) => lower(a.sha256) === w); + const ok = !!(w && hit) /*PAZA:version*/; + if (!w) unm('version', 'model-weights-not-named', 'the Proof of AI names no model.weightsSha256, so no artifact can be matched'); + else if (!ok) fail('version', 'version-artifact-mismatch', `the output names model weights ${w.slice(0, 14)}.., the attestation covers ${arts.map((a) => a.name + ' ' + String(a.sha256).slice(0, 14) + '..').join(', ') || 'no artifact'}`); + else { + const v = att.statement.subject && att.statement.subject.version; const mv = st.model && st.model.version; + if (v && mv && String(v) !== String(mv)) fail('version', 'version-label-mismatch', `the output says model version ${mv}, the attestation ${v}`); + else { const h = hit || arts[0] || { name: '?', sha256: w }; attestedSha = lower(h.sha256); levels.version = lvl('PASSED', { detail: `${h.name} ${String(h.sha256).slice(0, 18)}.. attested as ${att.statement.subject ? att.statement.subject.name + '@' + att.statement.subject.version : '?'}; signature ${parsed(j, 'signature').state}${att.notarization ? ', finality ' + parsed(j, 'finality').state : ''}` }); } + } + } + assumptions.version = ['the attestation proves what its signing keys attested (who holds them is a separate judgement: pos.mjs verify --signer); the artifact digest is what links it to the output']; + } + // 4. runtime: the host declared that THIS artifact ran, in its audit log, before the output + if (bundle.runtime && bundle.runtime.record) { + const rec = bundle.runtime.record, rs = rec.statement || {}; + const j = await judecatorAip23(verifyProof, rec, { rpc: rec.notarization ? rpc : null, chain }); + if (!j) unm('runtime', 'runtime-verifier', 'the AIP-23 reference verifier did not answer for the runtime record'); + else if (!envelopeOk(j) || rec.kind !== 'aere-proof-of-runtime-attestation') fail('runtime', 'runtime-record', 'the runtime record is not a verified aere-proof-of-runtime-attestation'); + else { + const target = attestedSha || lower(st.model && st.model.weightsSha256); + const ok = !!(target && lower(rs.artifactSha256) === target && rs.matches === true) /*PAZA:runtime*/; + if (!target) unm('runtime', 'runtime-no-artifact', 'no attested artifact to match the runtime record against'); + else if (!ok) fail('runtime', 'runtime-artifact-mismatch', `the host ran ${String(rs.artifactSha256).slice(0, 14)}.. (matches ${rs.matches}), the output names ${target.slice(0, 14)}..`); + else { + const runAt = Math.floor(Date.parse(rs.createdAt || '') / 1000); + const ordered = Number.isFinite(runAt) && Number.isFinite(outputAt) && runAt <= outputAt + TOLERANCE_S /*PAZA:order*/; + if (!ordered) fail('runtime', 'runtime-after-output', `the runtime record is dated ${rs.createdAt}, the output ${st.createdAt}`); + else { + let note = `host ${rs.host} declared the artifact running at ${rs.createdAt}`; + const log = bundle.runtime.auditLog; + if (Array.isArray(log)) { + const v = verificaJurnal(log); + if (!v.ok) { fail('runtime', 'audit-log-broken', `the audit log chain breaks at seq ${v.rupt}: ${v.motiv}`); } + else if (!log.some((e) => e.proof && lower(e.proof.statementHash) === lower(rec.statementHash)) /*PAZA:audit*/) fail('runtime', 'runtime-not-in-audit-log', 'the runtime record is not an entry of the audit log given'); + else { + note += `; in the host's audit log (${log.length} entries, chain intact)`; + if (bundle.runtime.head) { + const c = verificaFataDeCap(log, bundle.runtime.head); + if (!c.ok) fail('runtime', 'audit-head', 'the log does not continue the attested head: ' + (c.motiv || '')); + else { note += `; continues the attested head (count ${bundle.runtime.head.statement && bundle.runtime.head.statement.count})`; if (bundle.runtime.head.notarization && rpc) { const hj = await judecatorAip23(verifyProof, bundle.runtime.head, { rpc, chain }); note += `; head finality ${hj ? parsed(hj, 'finality').state : 'UNMEASURED'}`; } } + } + } + } else note += '; no audit log given (the record stands alone)'; + if (levels.runtime.state !== 'FAILED') levels.runtime = lvl('PASSED', { detail: note }); + } + } + } + assumptions.runtime = ['the runtime record is the HOST\'s declaration (the sidecar reads the export the operator makes); it proves what the host said ran, not that it did; the audit log has no key and is judged against a head published outside the host']; + } + // 5. permissions: the ledger verifies under its policy, the output is an entry of it, allowed, at a matching time + let anchors = [], witnessNote = null; + if (bundle.witness && Array.isArray(bundle.witness.heads) && bundle.witness.heads.length) { + if (!rpc) { unm('witness', 'witness-needs-rpc', 'the notarized heads can be read only through an endpoint of the chain (--rpc)'); } + else { + const a = await anchorsFromNotarizedHeads(ledger, bundle.witness.heads, { verifyProof, rpc, chain: chain || bundle.witness.chainId || null }); + if (a.stare === 'OK' && a.anchors.length) { anchors = a.anchors; witnessNote = a.randuri.map((r) => r.detail).join(' | '); } + else if (a.stare === 'FAILED') fail('witness', 'witness-head', a.randuri.filter((r) => !r.pass).map((r) => r.detail).join(' | ')); + else unm('witness', 'witness-unmeasured', a.randuri.map((r) => r.detail).join(' | ') || 'no anchor could be read'); + } + } + try { + const { policy, policyHash } = hashPolicy(bundle.permissions.policy); + if (bundle.permissions.policyHash && lower(bundle.permissions.policyHash) !== policyHash) fail('permissions', 'policy-hash', 'the bundle pins another policy hash than the policy given'); + else { + const v = verifyLedger(ledger, { policy, policyHash, revocations: bundle.permissions.revocations || [], anchors, maxTime: now + TOLERANCE_S }); + if (!v.ok) fail('permissions', 'ledger-invalid', String(v.error || '').slice(0, 200)); + else { + const ph = provenanceHash(poa); + const entry = ledger.entries.find((e) => e.body && sameHash(e.body.provenance, ph)) /*PAZA:output-in-ledger*/; + if (!entry) fail('permissions', 'output-not-in-ledger', 'no ledger entry links this Proof of AI envelope (provenance hash ' + ph.slice(0, 14) + '..)'); + else if (!(entry.body.decision && entry.body.decision.allowed === true) /*PAZA:allowed*/) fail('permissions', 'action-refused', `the ledger entry seq ${entry.seq} was refused: ${entry.body.decision && entry.body.decision.reason}`); + else if (!(Number.isFinite(outputAt) && Math.abs(Number(entry.body.at) - outputAt) <= TOLERANCE_S)) fail('permissions', 'ledger-time-mismatch', `the ledger entry is at ${entry.body.at}, the output at ${outputAt} (${st.createdAt})`); + else levels.permissions = lvl('PASSED', { detail: `entry seq ${entry.seq} (${entry.body.action && entry.body.action.kind}${entry.body.action && entry.body.action.tool ? ' ' + entry.body.action.tool : ''}) allowed under policy ${policyHash.slice(0, 14)}..; ${ledger.entries.length} entries verified, revocations ${v.revocations ? v.revocations.given : 0}, time ${v.time ? v.time.lowerBound : '?'}` }); + if (anchors.length && levels.witness.state === 'ABSENT') levels.witness = lvl('PASSED', { detail: `${anchors.length} anchor(s) from notarized heads under a post-quantum certified state: ${witnessNote}` }); + } + } + } catch (e) { fail('permissions', 'policy', String(e.message).slice(0, 160)); } + assumptions.permissions = ['the policy given is the one pinned (its hash is recomputed); revocations come from the owner, outside the ledger, and only those given are judged']; + // 6. provenance: the Proof of AI envelope itself + { + const j = await judecatorAip23(verifyProof, poa, { rpc: poa.notarization ? rpc : null, chain }); + if (!j) unm('provenance', 'provenance-verifier', 'the AIP-23 reference verifier did not answer'); + else if (poa.kind !== 'aere-proof-of-ai-attestation' || !envelopeOk(j)) fail('provenance', 'provenance-envelope', 'the Proof of AI envelope does not verify: ' + ['form', 'integrity', 'signature'].map((n) => n + ' ' + parsed(j, n).state).join(', ')); + else if (poa.notarization && !rpc) unm('provenance', 'provenance-finality-needs-rpc', 'the envelope claims a notarization that only an endpoint can confirm'); + else if (poa.notarization && parsed(j, 'finality').state !== 'PASSED') { const f = parsed(j, 'finality'); (f.state === 'FAILED' ? fail : unm)('provenance', 'provenance-finality', String(f.detail).slice(0, 200)); } + else levels.provenance = lvl('PASSED', { detail: `model ${st.model && st.model.name}@${st.model && st.model.version}, prompt ${String(st.promptHash).slice(0, 12)}.., output ${String(st.outputHash).slice(0, 12)}.., tools [${(st.tools || []).join(', ')}], requester ${String(st.requesterHash).slice(0, 12)}..; signature ${parsed(j, 'signature').state}${poa.notarization ? ', finality ' + parsed(j, 'finality').state : ''}` }); + assumptions.provenance = ['digests only: the prompt, input, output and requester are proven to exist as the bytes whose hashes these are, not shown; the envelope\'s time is its producer\'s statement unless notarized']; + } + if (levels.witness.state !== 'ABSENT') assumptions.witness = ['the time is the chain\'s (the notary records the block time; on 28001 and 2800 every validator is run by the Foundation): whoever notarizes can only notarize late']; + else assumptions.witness = ['no witness: every time in this bundle is the statement of whoever holds the keys']; + return verdict(levels, assumptions); +} +function verdict(levels, assumptions) { + const failed = LEVELS.find((l) => levels[l].state === 'FAILED'); + let v, reason = null; + if (failed) { v = 'INVALID'; reason = levels[failed].reason; } + else { + const unmL = LEVELS.find((l) => levels[l].state === 'UNMEASURED'); + if (unmL) { v = 'PARTIAL'; reason = levels[unmL].reason; } + else if (levels.witness.state !== 'PASSED') { v = 'PARTIAL'; reason = 'unwitnessed'; } + else v = 'VALID'; + } + return { verdict: v, reason, levels: Object.fromEntries(LEVELS.map((l) => [l, levels[l].state])), details: levels, assumptions, + notice: 'A verdict says that the bytes link and who vouches for each link. It does not say the model is good, the host is honest, or the output is true.' }; +} diff --git a/ai-trust/proba-lant-de-incredere.mjs b/ai-trust/proba-lant-de-incredere.mjs new file mode 100644 index 0000000..a32cb83 --- /dev/null +++ b/ai-trust/proba-lant-de-incredere.mjs @@ -0,0 +1,162 @@ +#!/usr/bin/env node +// proba-lant-de-incredere.mjs: the AI trust chain end to end in the LABORATORY, with the real children (Proof of Software attest, +// the verify-layer sidecar, the agent ledger, Proof of AI, the AIP-23 reference verifier), then the fixtures (each link broken on +// purpose, refused for its named reason), then the plantings (--control-negativ: a copy of the verifier with one check removed must +// turn exactly its fixture ROSU). --viu adds the witness: the ledger head notarized on testnet 28001 with the L1 test key, read back +// under a post-quantum certified anchor, and the bundle judged VALID with --rpc. +// node proba-lant-de-incredere.mjs [--viu] [--control-negativ] +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { newAgentIdentity, openLedger } from '../agents/agent-ledger.mjs'; +import { definePolicy } from '../agents/agent-policy.mjs'; +import { buildProofOfAi } from '../proof-of-ai/proof-of-ai.mjs'; +import { ledgerHeadEnvelope, notarizeHead, cheiaDinFisier } from '../agents/agent-ancora.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const RAD = path.resolve(AICI, '..', '..'); +// the children: the sidecar lives beside this component (public copy: ../verify-layer/, rewritten at staging); the AIP-23 reference +// verifier comes from AERE_VERIFY_PROOF (public copy, as for agents/) or from the development repository; without it: NEMASURAT (2) +const SIDECAR = fileURLToPath(new URL('../verify-layer/sidecar.mjs', import.meta.url)); +const VERIFY_PROOF = process.env.AERE_VERIFY_PROOF || path.join(RAD, 'verificator-falcon', 'verify-proof.mjs'); +const RPC_28001 = process.env.AERE_TESTNET_RPC || 'https://testnet-rpc.aere.network'; +// --viu: the notarization key (gas only, a test key) and the folder whose node_modules hold ethers +const CHEIE = process.env.AERE_NOTARY_KEY_FILE || path.join(RAD, 'tools', 'aere-quantum-shield', 'dovezi-l1', '.chei', 'cheie-de-proba-28001.key'); +const ETHERS_DIR = process.env.AERE_ETHERS_DIR || path.join(RAD, 'contracts'); +// the model artifact and its Proof of Software attestation are a COMMITTED fixture (fixturi/: 4 KB of random bytes attested with +// laboratory keys by tools/proof-of-software/pos.mjs, which is not part of this component); regenerate with +// node ../proof-of-software/pos.mjs keygen --out k.json && node ../proof-of-software/pos.mjs attest --out fixturi/atestare-model.json --name aere-model-proba --version 1.0.0 --keys k.json fixturi/model-weights.bin +const FIXTURI = path.join(AICI, 'fixturi'); +if (!fs.existsSync(VERIFY_PROOF)) { console.log(`NEMASURAT: verificatorul AIP-23 nu e la ${VERIFY_PROOF}; AERE_VERIFY_PROOF=`); process.exit(2); } +const sha = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); +const run = (args, cwd) => { const r = spawnSync(process.execPath, args, { cwd, encoding: 'utf8', timeout: 180000 }); if (r.status !== 0) throw new Error(`${path.basename(args[0])} ${args[1]} exited ${r.status}: ${(r.stdout + r.stderr).slice(-300)}`); return r.stdout; }; +const rehash = (env) => ({ ...env, statementHash: sha(Buffer.from(JSON.stringify(env.statement), 'utf8')) }); +const clone = (x) => JSON.parse(JSON.stringify(x)); + +/** the laboratory pieces: a model artifact attested, a host that ran it, an agent allowed to use it, an output */ +export function laborator({ toolAllowed = true } = {}) { + const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-ai-trust-lab-')); + // version: the committed fixture (the artifact and its hybrid-signed Proof of Software attestation); the digest is recomputed here, + // never read from the attestation, so a fixture whose artifact and attestation drifted apart is caught at P1 + const art = path.join(T, 'model-weights.bin'); fs.copyFileSync(path.join(FIXTURI, 'model-weights.bin'), art); const artSha = sha(fs.readFileSync(art)); + const attestation = JSON.parse(fs.readFileSync(path.join(FIXTURI, 'atestare-model.json'), 'utf8')); + // runtime: the host declares the artifact running, in its audit log, 60 s before the output + const t0 = Math.floor(Date.now() / 1000) - 120, t1 = t0 + 60; + const iso = (t) => new Date(t * 1000).toISOString(); + run([SIDECAR, 'record', '--kind', 'runtime', '--artifact', art, '--attested', artSha, '--host', 'lab-probe', '--log', path.join(T, 'audit.log'), '--at', iso(t0)], T); + run([SIDECAR, 'attest-head', '--log', path.join(T, 'audit.log'), '--host', 'lab-probe', '--out', path.join(T, 'head.json')], T); + const auditLog = fs.readFileSync(path.join(T, 'audit.log'), 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l)); + const record = auditLog[0].proof; const head = JSON.parse(fs.readFileSync(path.join(T, 'head.json'), 'utf8')); + // permissions + identity: an agent allowed (or not) to call the model tool + const identity = newAgentIdentity(); + const { policy, policyHash } = definePolicy({ agentId: identity.agentId, tools: toolAllowed ? ['model:aere-model-proba'] : ['model:other'] }); + const ledger = openLedger({ identity, policy, policyHash, now: () => t1 }); + // provenance: the output, naming the attested artifact and the agent + const proofOfAi = buildProofOfAi({ model: { name: 'aere-model-proba', version: '1.0.0', weightsSha256: artSha }, prompt: 'summarise document X', input: 'document X, 12 kB', output: 'a summary of document X', tools: ['model:aere-model-proba'], agentId: identity.agentId, requester: 'tester', createdAt: iso(t1) }); + const r = ledger.record({ kind: 'tool', tool: 'model:aere-model-proba', args: { promptHash: proofOfAi.statement.promptHash } }, { provenance: proofOfAi, at: t1 }); + return { T, art, artSha, attestation, record, auditLog, head, identity, policy, policyHash, ledger, proofOfAi, allowed: r.allowed, t0, t1 }; +} + +export async function proba(mod, cer, { viu = false } = {}) { + const L = laborator(); + const bundle = mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi, software: { attestation: L.attestation }, runtime: { record: L.record, auditLog: L.auditLog, head: L.head } }); + const v0 = await mod.verifica(bundle, { verifyProof: VERIFY_PROOF }); + const toate = ['identity', 'version', 'runtime', 'permissions', 'provenance']; + cer('P1', 'laborator: toate cele cinci fatete PASSED, martorul ABSENT, verdict PARTIAL (unwitnessed)', toate.every((l) => v0.levels[l] === 'PASSED') && v0.levels.witness === 'ABSENT' && v0.verdict === 'PARTIAL' && v0.reason === 'unwitnessed' ? 'VERDE' : 'ROSU', `${v0.verdict} (${v0.reason}) ${JSON.stringify(v0.levels)}`); + cer('P2', 'intrarea registrului a fost PERMISA de politica (unealta model:aere-model-proba in lista)', L.allowed ? 'VERDE' : 'ROSU'); + const v1 = await mod.verifica(mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi }), { verifyProof: VERIFY_PROOF }); + cer('P3', 'fara versiune si fara runtime: nivelurile ABSENT, restul PASSED, tot PARTIAL unwitnessed (ce lipseste e spus, nu inventat)', v1.levels.version === 'ABSENT' && v1.levels.runtime === 'ABSENT' && v1.levels.permissions === 'PASSED' && v1.verdict === 'PARTIAL' ? 'VERDE' : 'ROSU', JSON.stringify(v1.levels)); + // fixtures: each link broken on purpose + const fix = [ + ['F1', 'iesirea schimbata in Proof of AI (hash refacut): nu mai e intrarea din registru -> permissions FAILED output-not-in-ledger', (b) => { b.provenance.proofOfAi.statement.outputHash = sha(Buffer.from('another output')); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'permissions', 'output-not-in-ledger'], + ['F2', 'digestul ponderilor schimbat in Proof of AI: nu e artefactul atestat -> version FAILED version-artifact-mismatch', (b) => { b.provenance.proofOfAi.statement.model.weightsSha256 = '0x' + 'ab'.repeat(32); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'version', 'version-artifact-mismatch'], + ['F3', 'alt agentId in Proof of AI: identity FAILED identity-mismatch', (b) => { b.provenance.proofOfAi.statement.agentId = 'aere-agent:' + '11'.repeat(20); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'identity', 'identity-mismatch'], + ['F4', 'inregistrarea de runtime numeste alt artefact: runtime FAILED runtime-artifact-mismatch', (b) => { b.runtime.record.statement.artifactSha256 = '0x' + 'cd'.repeat(32); b.runtime.record = rehash(b.runtime.record); }, 'runtime', 'runtime-artifact-mismatch'], + ['F6', 'inregistrarea de runtime datata DUPA iesire: runtime FAILED runtime-after-output', (b) => { b.runtime.record.statement.createdAt = new Date((L.t1 + 3600) * 1000).toISOString(); b.runtime.record = rehash(b.runtime.record); }, 'runtime', 'runtime-after-output'], + ['F7', 'o intrare a registrului modificata (argumentele): permissions FAILED ledger-invalid', (b) => { b.permissions.ledger.entries[0].body.action.args = { promptHash: '0x' + '00'.repeat(32) }; }, 'permissions', 'ledger-invalid'], + ['F8', 'jurnalul de audit fara inregistrarea de runtime: runtime FAILED runtime-not-in-audit-log', (b) => { b.runtime.auditLog = []; b.runtime.head = null; }, 'runtime', 'runtime-not-in-audit-log'], + ['F9', 'atestarea Proof of Software modificata dupa hash: version FAILED version-attestation', (b) => { b.version.attestation.statement.artifacts[0].sha256 = '0x' + 'ef'.repeat(32); }, 'version', 'version-attestation'], + ['F10', 'jurnalul de audit cu lantul rupt (hash-ul intrarii schimbat): runtime FAILED audit-log-broken', (b) => { b.runtime.auditLog[0].hash = '0x' + '77'.repeat(32); }, 'runtime', 'audit-log-broken'], + ]; + for (const [id, eticheta, muta, nivel, motiv] of fix) { + const b = clone(bundle); muta(b); + const v = await mod.verifica(b, { verifyProof: VERIFY_PROOF }); + const ok = v.verdict === 'INVALID' && v.levels[nivel] === 'FAILED' && v.details[nivel].reason === motiv; + cer(id, eticheta, ok ? 'VERDE' : 'ROSU', `${v.verdict} (${v.reason}); ${nivel} ${v.levels[nivel]} ${v.details[nivel].reason || ''}`); + } + { + const L2 = laborator({ toolAllowed: false }); + const b = mod.construieste({ ledger: L2.ledger.export(), policy: { policy: L2.policy, policyHash: L2.policyHash }, proofOfAi: L2.proofOfAi, software: { attestation: L2.attestation }, runtime: { record: L2.record, auditLog: L2.auditLog } }); + const v = await mod.verifica(b, { verifyProof: VERIFY_PROOF }); + cer('F5', 'unealta nepermisa de politica: intrarea REFUZATA exista, iesirea nu poate fi "permisa" -> permissions FAILED action-refused', !L2.allowed && v.levels.permissions === 'FAILED' && v.details.permissions.reason === 'action-refused' ? 'VERDE' : 'ROSU', `${v.verdict} ${v.details.permissions.reason || ''}`); + } + if (viu) { + // the witness: the ledger head notarized on 28001 with the laboratory test key (gas only), read back under a certified anchor + const require = createRequire(path.join(ETHERS_DIR, 'package.json')); + const { ethers } = require('ethers'); + const headEnv = ledgerHeadEnvelope(L.ledger.export()); + let notarized; + // notarizeHead returns { ok, envelope }: the ENVELOPE (with its notarization) is the head the witness level reads + try { const r = await notarizeHead({ envelope: headEnv, rpc: RPC_28001, keyFile: CHEIE, ethers }); if (!r || !r.ok) throw new Error('notarizeHead: ' + JSON.stringify(r).slice(0, 160)); notarized = r.envelope; } catch (e) { cer('V1', 'capul registrului notarizat pe 28001', 'STRICAT', String(e.message).slice(0, 160)); return; } + cer('V1', 'capul registrului notarizat pe 28001 (cheia de proba L1, numai gaz)', notarized && notarized.notarization ? 'VERDE' : 'ROSU', notarized && notarized.notarization ? `tx ${String(notarized.notarization.txHash).slice(0, 14)}.. block ${notarized.notarization.block}` : JSON.stringify(notarized).slice(0, 120)); + const bw = mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi, software: { attestation: L.attestation }, runtime: { record: L.record, auditLog: L.auditLog, head: L.head }, witness: { heads: [notarized], chainId: 28001 } }); + let vw = null; + for (let i = 0; i < 20; i++) { vw = await mod.verifica(bw, { verifyProof: VERIFY_PROOF, rpc: RPC_28001, chain: 28001 }); if (vw.levels.witness !== 'UNMEASURED') break; await new Promise((ok) => setTimeout(ok, 8000)); } + cer('V2', 'cu martorul citit sub o ancora certificata (--rpc 28001): witness PASSED, verdict VALID', vw && vw.verdict === 'VALID' && vw.levels.witness === 'PASSED' ? 'VERDE' : (vw && vw.levels.witness === 'UNMEASURED' ? 'STRICAT' : 'ROSU'), vw ? `${vw.verdict} (${vw.reason}); witness ${vw.levels.witness}: ${String(vw.details.witness.detail || vw.details.witness.reason || '').slice(0, 160)}` : 'fara verdict'); + const vb = await mod.verifica(bw, { verifyProof: VERIFY_PROOF }); + cer('V3', 'acelasi pachet FARA --rpc: martorul UNMEASURED (witness-needs-rpc), nu PASSED', vb.levels.witness === 'UNMEASURED' && vb.verdict === 'PARTIAL' ? 'VERDE' : 'ROSU', `${vb.verdict} (${vb.reason})`); + const d = path.join(RAD, 'tools', 'aere-ai-trust', 'dovezi'); fs.mkdirSync(d, { recursive: true }); + const f = path.join(d, `lant-28001-${new Date().toISOString().replace(/[:.]/g, '').slice(0, 15)}Z.json`); + fs.writeFileSync(f, JSON.stringify({ bundle: bw, verdict: vw }, null, 1)); console.log(' dovada scrisa: ' + f); + } +} + +if (process.argv.includes('--control-negativ')) { + const SURSA = fs.readFileSync(path.join(AICI, 'lant-de-incredere.mjs'), 'utf8'); + const PLANTARI = [ + { nume: '(a) identitatea necomparata', de: '/*PAZA:identity*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F3'] }, + { nume: '(b) artefactul atestat necomparat cu ponderile', de: '/*PAZA:version*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F2'] }, + { nume: '(c) artefactul rulat necomparat', de: '/*PAZA:runtime*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F4'] }, + { nume: '(d) ordinea timpilor nejudecata', de: '/*PAZA:order*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F6'] }, + { nume: '(e) iesirea necautata in registru (prima intrare luata oricum)', de: '/*PAZA:output-in-ledger*/', cu: '|| ledger.entries[0] /*PLANTAT*/', rosii: ['F1'] }, + // (f) and (g) sit inside a NEGATED condition (`if (!(x) /*PAZA*/)`): the planting that silences the check is `&& false`, not `|| true` + // (measured 2026-10-02: `|| true` made the honest rows fail instead of letting the fixture pass) + { nume: '(f) decizia refuzata luata drept permisa', de: '/*PAZA:allowed*/', cu: "&& String(1) === '2' /*PLANTAT*/", rosii: ['F5'] }, + { nume: '(g) inregistrarea necautata in jurnalul de audit', de: '/*PAZA:audit*/', cu: "&& String(1) === '2' /*PLANTAT*/", rosii: ['F8'] }, + { nume: '(h) controlul controlului: o ancora care nu exista', de: '/*PAZA:nu-exista*/', cu: 'x', rosii: [], asteapta: 'STRICAT' }, + ]; + let bune = 0; + for (const [k, p] of PLANTARI.entries()) { + const n = SURSA.split(p.de).length - 1; + const copie = path.join(AICI, `_plantat-lant-${process.pid}-${k}.mjs`); + let stare, detaliu = ''; + try { + if (n !== 1) { stare = 'STRICAT'; detaliu = `ancora apare de ${n} ori (cerut 1)`; } + else { + fs.writeFileSync(copie, SURSA.replace(p.de, p.cu)); + const mod = await import(pathToFileURL(copie).href); + const randuri = []; + // a probe that THROWS on the planted copy is STRICAT for that planting, not the end of the control + try { await proba(mod, (id, eticheta, st, det) => randuri.push({ id, stare: st, det })); } catch (e) { randuri.push({ id: 'proba', stare: 'STRICAT', det: String(e.message).slice(0, 160) }); } + const stricate = randuri.filter((x) => x.stare === 'STRICAT'); const rosii = randuri.filter((x) => x.stare === 'ROSU').map((x) => x.id); + if (stricate.length) { stare = 'STRICAT'; detaliu = 'proba a cazut: ' + stricate.map((x) => x.id + ' ' + x.det).join(' | ').slice(0, 200); } + else { const prezise = p.rosii.every((id) => rosii.includes(id)); const numai = rosii.every((id) => p.rosii.includes(id)); stare = prezise && numai ? 'VERDE' : 'ROSU'; detaliu = `rosii: ${rosii.join(',') || '-'} (prezis ${p.rosii.join(',') || '-'})`; } + } + } finally { fs.rmSync(copie, { force: true }); } + const ok = stare === (p.asteapta || 'VERDE'); if (ok) bune++; + console.log(` ${ok ? 'BINE' : 'ESEC'} ${p.nume}: ${stare}${p.asteapta ? ' (prezis ' + p.asteapta + ')' : ''} ${detaliu}`); + } + const ramase = fs.readdirSync(AICI).filter((f) => f.startsWith('_plantat-lant-')); + console.log(`CONTROL: ${bune} din ${PLANTARI.length} cum s-a prezis${ramase.length ? '; copii ramase: ' + ramase.join(',') : ''}`); + process.exitCode = bune === PLANTARI.length && !ramase.length ? 0 : 1; +} else { + const mod = await import('./lant-de-incredere.mjs'); + let verzi = 0, rosii = 0, stricate = 0; + await proba(mod, (id, eticheta, stare, det) => { if (stare === 'VERDE') verzi++; else if (stare === 'ROSU') rosii++; else stricate++; console.log(` [${stare.padEnd(7)}] ${id.padEnd(4)} ${eticheta}${det ? ' ' + det : ''}`); }, { viu: process.argv.includes('--viu') }); + console.log(`\nlantul de incredere pentru AI: ${verzi} VERDE, ${rosii} ROSU, ${stricate} STRICAT`); + process.exitCode = rosii || stricate ? 1 : 0; +} diff --git a/proof-of-ai/README.md b/proof-of-ai/README.md new file mode 100644 index 0000000..72af9d5 --- /dev/null +++ b/proof-of-ai/README.md @@ -0,0 +1,18 @@ +# AERE Proof of AI (roadmap B2 / #38) + +Un plic AERE Proof Protocol (AIP-23) care dovedeste provenienta unei actiuni de agent AI - ce model, ce versiune, ce prompt, ce +unelte, ce iesire, ce agent, cine a cerut - verificat de ACELASI verificator ca orice alt proof AERE, fara niciun cod nou de +verificare. Notarizarea si finalitatea post-cuantica vin din AIP-23 (statementHash notarizat pe AereNotary, acoperit de o ancora PQ). + +``` +import { buildProofOfAi } from './proof-of-ai.mjs'; +const plic = buildProofOfAi({ model:{name,version,weightsSha256}, prompt, input, output, tools:[...], agentId, requester, createdAt }); +// -> plic AIP-23; se verifica cu tools/aere-proof-protocol/verify.mjs +``` + +Plicul tine numai DIGESTURI (promptul, intrarea, iesirea, cine a cerut se stocheaza ca SHA-256, nu brut). Se aliniaza cu contractul +de pe lant `contracts/contracts/compliance/AereAIProof.sol` (modelId, inputHash, outputHash, requesterHash), dar e PORTABIL: exista +fara lant, il cere doar pentru finalitate. + +Proba: `node proba-proof-of-ai.mjs` (6/6, cu control negativ: iesire schimbata dupa hash -> INVALID). Teza demonstrata: un standard, +multe feluri de dovada - acelasi verificator, un fel de proof nou, zero cod nou. diff --git a/proof-of-ai/proba-proof-of-ai.mjs b/proof-of-ai/proba-proof-of-ai.mjs new file mode 100644 index 0000000..6b24a78 --- /dev/null +++ b/proof-of-ai/proba-proof-of-ai.mjs @@ -0,0 +1,61 @@ +'use strict'; +// Proba Proof-of-AI (B2): construieste un plic, il verifica cu ACELASI verificator AIP-23 (fara cod nou de verificare), si un +// CONTROL NEGATIV: un plic caruia i se atinge statement-ul dupa hash pica la integritate. Dovedeste teza "un standard, multe feluri". +// node proba-proof-of-ai.mjs -> 0 toate cum trebuia, 1 altfel + +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { buildProofOfAi, KIND } from './proof-of-ai.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +// Verificatorul AIP-23: AERE_VERIFY_PROOF= (copia publica, ca la agents/) sau, in depozitul de dezvoltare, +// ../aere-proof-protocol/verify.mjs; fara el proba nu poate masura nimic si iese 2 (NEMASURAT), nu verde +const VERIFY = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs'); +if (!fs.existsSync(VERIFY)) { console.log(`NEMASURAT: verificatorul AIP-23 nu e la ${VERIFY}; AERE_VERIFY_PROOF=`); process.exit(2); } +const T = fs.mkdtempSync(path.join(os.tmpdir(), 'poai-')); +let rele = 0; +const cer = (n, c) => { console.log(` [${c ? 'OK ' : 'RAU '}] ${n}`); if (!c) rele++; }; +function verif(plicPath) { + try { return { cod: 0, out: execFileSync(process.execPath, [VERIFY, plicPath, '--json'], { encoding: 'utf8' }) }; } + catch (e) { return { cod: e.status ?? 2, out: (e.stdout || '') + (e.stderr || '') }; } +} +const verdict = (o) => { try { return JSON.parse(o).verdict; } catch { return '?'; } }; +// verificatorul AIP-23 e in ENGLEZA (public-ready, D-402/B4): niveluri->levels, nivel->level, stare->state, TRECUT->PASSED, PICAT->FAILED +const nivel = (o, n) => { try { return JSON.parse(o).levels.find((x) => x.level === n).state; } catch { return '?'; } }; + +try { + const plic = buildProofOfAi({ + model: { name: 'aere-fable', version: '5.1', weightsSha256: '0x' + '22'.repeat(32) }, + prompt: 'rezuma documentul X', + input: 'documentul X, 12kb', + output: 'rezumatul: ...', + tools: ['retrieval', 'calculator'], + agentId: '0xa11ce00000000000000000000000000000000001', + requester: 'client-42', + createdAt: '2026-09-26T09:00:00.000Z', + }); + cer('plicul are kind aere-proof-of-ai-attestation', plic.kind === KIND + '-attestation'); + cer('statement-ul are model, promptHash, inputHash, outputHash, tools', !!(plic.statement.model && plic.statement.promptHash && plic.statement.inputHash && plic.statement.outputHash && Array.isArray(plic.statement.tools))); + cer('nu contine continut brut (doar digesturi)', !JSON.stringify(plic).includes('rezuma documentul X') && !JSON.stringify(plic).includes('rezumatul: ...')); + + const f = path.join(T, 'ai.json'); fs.writeFileSync(f, JSON.stringify(plic, null, 1)); + const r = verif(f); + cer('verificatorul AIP-23 il valideaza (fara cod nou)', verdict(r.out) === 'VALID' && nivel(r.out, 'integrity') === 'PASSED' && r.cod === 0); + + // CONTROL NEGATIV: schimb outputHash dupa ce statementHash e fixat -> integritate PICAT + const rau = JSON.parse(JSON.stringify(plic)); rau.statement.outputHash = '0x' + '33'.repeat(32); + const fr = path.join(T, 'ai-rau.json'); fs.writeFileSync(fr, JSON.stringify(rau, null, 1)); + const rr = verif(fr); + cer('CONTROL: iesire schimbata dupa hash -> INVALID', verdict(rr.out) === 'INVALID' && nivel(rr.out, 'integrity') === 'FAILED' && rr.cod === 1); + + // acelasi statement -> acelasi hash (determinist) + const plic2 = buildProofOfAi({ model: { name: 'aere-fable', version: '5.1', weightsSha256: '0x' + '22'.repeat(32) }, prompt: 'rezuma documentul X', input: 'documentul X, 12kb', output: 'rezumatul: ...', tools: ['retrieval', 'calculator'], agentId: '0xa11ce00000000000000000000000000000000001', requester: 'client-42', createdAt: '2026-09-26T09:00:00.000Z' }); + cer('determinist: acelasi continut -> acelasi statementHash', plic2.statementHash === plic.statementHash); +} finally { + fs.rmSync(T, { recursive: true, force: true }); +} +console.log(`\nB2 proof-of-ai: ${6 - rele}/6 cum trebuia`); +process.exit(rele ? 1 : 0); diff --git a/proof-of-ai/proof-of-ai.mjs b/proof-of-ai/proof-of-ai.mjs new file mode 100644 index 0000000..65dd94e --- /dev/null +++ b/proof-of-ai/proof-of-ai.mjs @@ -0,0 +1,52 @@ +'use strict'; +// AERE Proof of AI (roadmap B2 / #38): un plic AERE Proof Protocol (AIP-23) care dovedeste provenienta unei actiuni de agent AI - +// ce model, ce versiune, ce prompt, ce unelte, ce iesire, ce agent, cine a cerut - astfel incat un tert sa verifice fara sa aiba +// incredere in producator, cu ACELASI verificator ca orice alt proof AERE (tools/aere-proof-protocol/verify.mjs). Nu adauga niciun +// cod nou de verificare: asta e chiar teza standardului ("un plic, multe feluri de dovada"). Notarizarea + finalitatea post-cuantica +// vin gratis din AIP-23 (statementHash notarizat pe AereNotary, acoperit de o ancora PQ). Numai Node 24, node:crypto. +// +// Se aliniaza cu contractul de pe lant AereAIProof.sol (modelId, inputHash, outputHash, requesterHash): campurile de mai jos se pot +// mapa la o atestare on-chain, dar plicul e PORTABIL (nu cere lantul ca sa existe; il cere doar ca sa capete finalitate). + +import crypto from 'node:crypto'; + +export const KIND = 'aere-proof-of-ai'; +const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); +const hexOf = (v) => { + if (v == null) return null; + if (typeof v === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v)) return v.toLowerCase(); // deja un digest + return sha256(Buffer.from(typeof v === 'string' ? v : JSON.stringify(v), 'utf8')); +}; + +/** + * Construieste un plic Proof-of-AI (AIP-23). Nu tine niciun continut sensibil: doar digesturi. + * @param {object} p + * @param {{name:string, version?:string, weightsSha256?:string}} p.model + * @param {string} [p.prompt] promptul brut (se stocheaza doar digestul) SAU + * @param {string} [p.promptHash] digestul promptului (0x+64) daca nu vrei sa-l dai brut + * @param {string} [p.input] intrarea (digest stocat) SAU p.inputHash + * @param {string} [p.output] iesirea (digest stocat) SAU p.outputHash + * @param {string[]} [p.tools] uneltele pe care agentul avea voie sa le foloseasca / le-a folosit + * @param {string} [p.agentId] identitatea agentului (adresa / id) + * @param {string} [p.requester] cine a cerut (digest stocat) SAU p.requesterHash + * @param {string} p.createdAt RFC3339 + * @returns {object} plicul AIP-23 { v, kind, statement, statementHash } + */ +export function buildProofOfAi(p) { + if (!p || !p.model || !p.model.name) throw new Error('proof-of-ai: model.name e obligatoriu'); + if (!p.createdAt) throw new Error('proof-of-ai: createdAt e obligatoriu (RFC3339)'); + const statement = { + v: 1, + kind: KIND, + model: { name: p.model.name, version: p.model.version || null, weightsSha256: p.model.weightsSha256 || null }, + promptHash: p.promptHash ? p.promptHash.toLowerCase() : hexOf(p.prompt), + inputHash: p.inputHash ? p.inputHash.toLowerCase() : hexOf(p.input), + outputHash: p.outputHash ? p.outputHash.toLowerCase() : hexOf(p.output), + tools: Array.isArray(p.tools) ? [...p.tools] : [], + agentId: p.agentId || null, + requesterHash: p.requesterHash ? p.requesterHash.toLowerCase() : hexOf(p.requester), + createdAt: p.createdAt, + }; + const text = JSON.stringify(statement); // canonic AIP-23: ordinea de inserare, SHA-256 + return { v: 1, kind: KIND + '-attestation', statement, statementHash: sha256(Buffer.from(text, 'utf8')) }; +} diff --git a/verifiable-data/README.md b/verifiable-data/README.md new file mode 100644 index 0000000..5a7cff0 --- /dev/null +++ b/verifiable-data/README.md @@ -0,0 +1,63 @@ +# AERE Verifiable Data + +A dataset whose every answer can be checked against a short commitment, by anyone, without the data and without trusting the server. +Built from pieces that already exist beside it: the RFC 9162 Merkle tree of the verification layer (`verify-layer/arbore-merkle.mjs`), +the AIP-23 `data` envelope (`proof-kinds/`), the notary call of the agents (`agents/agent-ancora.mjs`) and the AIP-23 reference +verifier for the time of a version under a post-quantum certified anchor. + +| what | how it is proven | with | +|---|---|---| +| a record is in the dataset at a version | inclusion proof in the version's log (RFC 9162, log2(n) hashes) | `verifica.mjs inregistrare` | +| a newer version only appended | consistency proof between the two log roots; the newer commitment names the older as `previous` | `verifica.mjs consistenta` | +| a query answer is correct AND complete | every row proves itself in the log and at its position in the SORTED index of the queried field; the rows are one contiguous run; the neighbour before the run is below the range and the one after is above it, each with its proof | `verifica.mjs raspuns` | +| a row reveals only what was asked | every field is committed as SHA-256(salt \| field \| value) in the record leaf; undisclosed fields stay hidden, disclosed ones cannot change | the same | +| the publisher serves what it committed to | availability sampling: random positions fetched and verified against the commitment; a withheld record fails the sample that lands on it | `verifica.mjs disponibilitate` | +| when a version existed | the commitment's AIP-23 envelope notarized on AereNotary and read back under a post-quantum certified anchor (`finality` of the reference verifier) | `verifica.mjs angajament --rpc` | + +``` +node vd.mjs creeaza ./set --nume users --index country,age +node vd.mjs adauga ./set records.ndjson # {"id":"u1","country":"RO","age":31,"email":"..."} per line +node vd.mjs angajeaza ./set --out c1.json # version 1: commitment + plicuri/1.json (its AIP-23 envelope) +node vd.mjs interogheaza ./set 1 --camp country --eq '"RO"' --campuri age --out a.json +node verifica.mjs raspuns a.json --angajament c1.json # VALID (n rows) | INVALID + reasons +node vd.mjs serveste ./set --port 8797 # GET /v1/vd/users/{commitments|record/1/7|query/1?field=age&from=30&to=40|consistency/1/2} +node verifica.mjs disponibilitate --url http://127.0.0.1:8797/v1/vd/users --angajament c1.json --esantioane 32 +``` + +A commitment is `{ name, version, size, logRoot, indexes: { field: { root, size } }, createdAt, previous }`; its hash is the SHA-256 of its +canonical JSON, and that hash is the `sha256` of the version's `aere-proof-of-data` envelope. Records are flat JSON objects with a +string `id`; values are strings, finite numbers, booleans or null (an index needs a total order: by type, then numerically or by code +unit, in one function both sides use). A dataset appends; it never rewrites. + +## What it is not, and what is not measured + +- **Not zero knowledge.** Selective disclosure here is hash-based: the reader learns the disclosed values and the commitments of the + rest. A proof ABOUT rows without revealing them (a count, a sum, membership in a set, "some row satisfies P") needs a proving system; + the one this network verifies on chain (SP1, Groth16 over BN254) needs about 24 GiB to prove, measured 2026-09-26, and no such + circuit is built. Until it is, a "ZK query" is not offered, and this README says so instead of calling disclosure zero knowledge. +- **Completeness rests on the index being sorted.** A single answer proves its rows and its two neighbours against the committed index + root; that no matching row was omitted follows only if the index is in order, which the reader checks locally on the rows returned + (an unsorted pair is refused) and which anyone holding the data checks in full with `vd.mjs auditeaza`. A publisher that commits an + unsorted index is caught by the first answer that shows two rows out of order, or by an audit, not by a one-row answer. +- **Availability is retrievability under sampling, not a data-availability scheme.** k verified samples make a publisher that hides a + share s of its records survive with probability (1 - s)^k; there is no erasure coding and no network of samplers. +- **The time of a version is the chain's only when notarized**; an unnotarized commitment carries the publisher's `createdAt`. +- **Not storage.** The records live where the publisher keeps them (a folder here); this component commits, proves and serves, it does + not replicate, encrypt or back up. +- Index trees are rebuilt per version (sorted); consistency proofs cover the LOG only. Queries on fields that are not indexed are not + offered. Field values above 4096 characters, arrays and nested objects are refused. + +## Probes + +``` +AERE_VERIFY_PROOF= node proba-verifiable-data.mjs # 22 rows: honest answers VALID, nine forged ones INVALID + # for their named reason, sampling on a local server +AERE_VERIFY_PROOF=... node proba-verifiable-data.mjs --control-negativ # 5 plantings on a copy of baza.mjs; each must make its + # forged fixture pass, which proves the check is load-bearing +AERE_VERIFY_PROOF=... AERE_NOTARY_KEY_FILE= AERE_ETHERS_DIR= node proba-verifiable-data.mjs --viu + # plus a notarization on testnet 28001 (a test key) read back + # under a post-quantum certified anchor; evidence in dovezi/ +``` + +Without the AIP-23 verifier the envelope row is reported NEMASURAT and the exit code is 2. The probe never touches chain 2800; the notary +call refuses 2800 unless `AERE_CONFIRM_MAINNET=yes`. diff --git a/verifiable-data/baza.mjs b/verifiable-data/baza.mjs new file mode 100644 index 0000000..2acf0fd --- /dev/null +++ b/verifiable-data/baza.mjs @@ -0,0 +1,272 @@ +// baza.mjs: the verifiable dataset (roadmap master point 20, pista B, 2026-10-02). A dataset is an append-only list of records; a +// VERSION is a commitment to it: the RFC 9162 Merkle root of the record leaves (the log, `../verify-layer/arbore-merkle.mjs`), its size, +// and for each indexed field the root of a SORTED index tree over (value, record). From a commitment anyone can verify, without the data: +// - a record is in the dataset at a version (inclusion proof in the log, log2(n) hashes); +// - a newer version only APPENDED (consistency proof between the two log roots): a version that is not a prefix of the next is a +// rewritten history; +// - a query answer (equality or range on an indexed field) is COMPLETE and CORRECT: every returned row proves itself in the log and in +// the index, the rows are the contiguous run of the sorted index, and the two neighbours outside the range are shown with their +// proofs, so no matching row could have been left out; the completeness rests on the index being sorted, which anyone holding the +// data audits in full (`auditeaza`) and no single query can; +// - a row reveals only the fields the query asked for (selective disclosure): every field is committed as +// SHA-256(salt | field | value) in the record leaf, so an undisclosed field stays hidden and a disclosed one cannot be changed. This +// is hash-based disclosure, NOT zero knowledge: the verifier learns the disclosed values; a proof about rows without revealing them +// (counts, sums, membership in a set) needs a proving system and is not built here (see README). +// The commitment itself is an AIP-23 `data` envelope (proof-kinds), notarizable on AereNotary and read back under a post-quantum +// certified anchor by the reference verifier: the TIME of a version is then the chain's, not the publisher's. +// Only node:crypto and the Merkle tree of the verification layer. Records: JSON objects with a string `id` (unique) and flat fields +// whose values are strings, finite numbers, booleans or null; arrays and nested objects are refused (an index needs a total order). +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { leafHash, merkleTree, verifyInclusion, verifyConsistency } from '../verify-layer/arbore-merkle.mjs'; +import { buildProof } from '../proof-kinds/proof-kinds.mjs'; + +export const FORMAT = 'aere-verifiable-dataset-1'; +export const KIND_RASPUNS = 'aere-verifiable-query-answer'; +export const KIND_INREGISTRARE = 'aere-verifiable-record'; +export const KIND_CONSISTENTA = 'aere-verifiable-consistency'; +const H = /^0x[0-9a-f]{64}$/; +const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); +const utf8 = (s) => Buffer.from(s, 'utf8'); + +/** Canonical JSON: keys sorted at every depth, no whitespace; refuses what JSON cannot carry faithfully. */ +export function canonical(x) { + if (x === null) return 'null'; + if (typeof x === 'string') return JSON.stringify(x); + if (typeof x === 'boolean') return x ? 'true' : 'false'; + if (typeof x === 'number') { if (!Number.isFinite(x)) throw new Error('canonical: a number must be finite'); return JSON.stringify(x); } + if (Array.isArray(x)) return '[' + x.map(canonical).join(',') + ']'; + if (typeof x === 'object') return '{' + Object.keys(x).sort().map((k) => { if (x[k] === undefined) throw new Error('canonical: undefined under ' + k); return JSON.stringify(k) + ':' + canonical(x[k]); }).join(',') + '}'; + throw new Error('canonical: unsupported value of type ' + typeof x); +} +const tip = (v) => v === null ? 'null' : typeof v; +const TIPURI = ['boolean', 'null', 'number', 'string']; +/** Total order on field values: by type name, then by value (numbers numerically, strings by code unit). One place, used by both sides. */ +export function comparaValori(a, b) { + const ta = tip(a), tb = tip(b); + if (!TIPURI.includes(ta) || !TIPURI.includes(tb)) throw new Error('compare: a field value must be a string, a finite number, a boolean or null'); + if (ta !== tb) return ta < tb ? -1 : 1; + if (ta === 'number') return a < b ? -1 : a > b ? 1 : 0; + if (ta === 'boolean') return a === b ? 0 : a ? 1 : -1; + const sa = String(a), sb = String(b); return sa < sb ? -1 : sa > sb ? 1 : 0; +} +export const angajamentCamp = (field, value, salt) => sha256(utf8(String(salt) + '|' + field + '|' + canonical(value))); +export const frunzaInregistrare = (id, c) => leafHash(utf8(canonical({ id, c }))); +export const frunzaIndex = (field, value, i, recordLeaf) => leafHash(utf8(canonical({ field, value, i, recordLeaf }))); +export const hashAngajament = (commitment) => sha256(utf8(canonical(commitment))); + +function valideazaInregistrare(r) { + if (!r || typeof r !== 'object' || Array.isArray(r)) throw new Error('a record is a JSON object'); + if (typeof r.id !== 'string' || !r.id.length || r.id.length > 256) throw new Error('a record needs a string id (1..256 chars)'); + const fields = r.fields && typeof r.fields === 'object' ? r.fields : Object.fromEntries(Object.entries(r).filter(([k]) => k !== 'id')); + for (const [k, v] of Object.entries(fields)) { + if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(k)) throw new Error('field name "' + k + '" is not [A-Za-z_][A-Za-z0-9_]{0,63}'); + if (!TIPURI.includes(tip(v)) || (typeof v === 'number' && !Number.isFinite(v))) throw new Error('field "' + k + '" must be a string, a finite number, a boolean or null'); + if (typeof v === 'string' && v.length > 4096) throw new Error('field "' + k + '" is longer than 4096 chars'); + } + return { id: r.id, fields }; +} + +/** Creates a dataset folder: set.json (name, indexed fields), an empty record file, no versions. */ +export function creeaza(dir, { name, indexes = [] }) { + if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(String(name))) throw new Error('dataset name: [a-z0-9][a-z0-9-]{0,63}'); + if (!Array.isArray(indexes) || indexes.some((f) => !/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(f))) throw new Error('indexes: a list of field names'); + fs.mkdirSync(dir, { recursive: true }); + if (fs.existsSync(path.join(dir, 'set.json'))) throw new Error('the dataset already exists at ' + dir); + fs.writeFileSync(path.join(dir, 'set.json'), JSON.stringify({ format: FORMAT, name, indexes: [...new Set(indexes)].sort() }, null, 1)); + fs.writeFileSync(path.join(dir, 'inregistrari.ndjson'), ''); + fs.writeFileSync(path.join(dir, 'versiuni.json'), '[]'); + return deschide(dir); +} +export function deschide(dir) { + const set = JSON.parse(fs.readFileSync(path.join(dir, 'set.json'), 'utf8')); + if (set.format !== FORMAT) throw new Error('not a ' + FORMAT + ' dataset: ' + dir); + const text = fs.readFileSync(path.join(dir, 'inregistrari.ndjson'), 'utf8'); + const inregistrari = text.split('\n').filter(Boolean).map((l) => JSON.parse(l)); + const versiuni = JSON.parse(fs.readFileSync(path.join(dir, 'versiuni.json'), 'utf8')); + return { dir, set, inregistrari, versiuni, _cache: new Map() }; +} +/** Appends records (never rewrites): each gets a fresh 16-byte salt per field; ids must be new. */ +export function adauga(db, records) { + const ids = new Set(db.inregistrari.map((r) => r.id)); const noi = []; + for (const r of records) { + const { id, fields } = valideazaInregistrare(r); + if (ids.has(id)) throw new Error('duplicate id "' + id + '" (a dataset appends; it never rewrites a record)'); + ids.add(id); + const salts = Object.fromEntries(Object.keys(fields).map((k) => [k, crypto.randomBytes(16).toString('hex')])); + noi.push({ id, fields, salts }); + } + fs.appendFileSync(path.join(db.dir, 'inregistrari.ndjson'), noi.map((r) => JSON.stringify(r) + '\n').join('')); + db.inregistrari.push(...noi); db._cache.clear(); + return noi.length; +} +const angajamente = (r) => Object.fromEntries(Object.keys(r.fields).sort().map((k) => [k, angajamentCamp(k, r.fields[k], r.salts[k])])); +/** The trees of a version of `size` records: the log and one sorted index per indexed field (memoized per size). */ +function arbori(db, size) { + if (db._cache.has(size)) return db._cache.get(size); + if (!Number.isInteger(size) || size < 1 || size > db.inregistrari.length) throw new Error('version size ' + size + ' outside 1..' + db.inregistrari.length); + const recs = db.inregistrari.slice(0, size).map((r, i) => { const c = angajamente(r); return { i, id: r.id, c, leaf: frunzaInregistrare(r.id, c) }; }); + const log = merkleTree(recs.map((r) => r.leaf)); + const index = {}; + for (const f of db.set.indexes) { + const intrari = recs.filter((r) => f in db.inregistrari[r.i].fields).map((r) => ({ value: db.inregistrari[r.i].fields[f], i: r.i, recordLeaf: r.leaf })); + intrari.sort((a, b) => comparaValori(a.value, b.value) || a.i - b.i); + index[f] = { intrari, tree: merkleTree(intrari.map((e) => frunzaIndex(f, e.value, e.i, e.recordLeaf))) }; + } + const t = { recs, log, index }; db._cache.set(size, t); return t; +} +/** Commits the current records as a new version; returns the commitment (also appended to versiuni.json). */ +export function angajeaza(db, { createdAt = new Date().toISOString() } = {}) { + const size = db.inregistrari.length; if (!size) throw new Error('nothing to commit: the dataset is empty'); + const prev = db.versiuni[db.versiuni.length - 1] || null; + if (prev && prev.size === size) throw new Error('nothing new since version ' + prev.version + ' (' + size + ' records)'); + const t = arbori(db, size); + const commitment = { v: 1, format: FORMAT, name: db.set.name, version: db.versiuni.length + 1, size, logRoot: t.log.root(), createdAt, + indexes: Object.fromEntries(db.set.indexes.map((f) => [f, { root: t.index[f].tree.root(), size: t.index[f].intrari.length }])), + previous: prev ? hashAngajament(prev) : null }; + db.versiuni.push(commitment); + fs.writeFileSync(path.join(db.dir, 'versiuni.json'), JSON.stringify(db.versiuni, null, 1)); + return commitment; +} +export function versiune(db, n) { const c = db.versiuni.find((x) => x.version === Number(n)); if (!c) throw new Error('no version ' + n); return c; } +const fara = (fields, care) => care === '*' ? { ...fields } : Object.fromEntries(Object.entries(fields).filter(([k]) => care.includes(k))); +const dezvaluie = (r, care) => Object.fromEntries(Object.keys(fara(r.fields, care)).map((k) => [k, { value: r.fields[k], salt: r.salts[k] }])); +const dovada = (p) => ({ index: p.index, treeSize: p.treeSize, path: p.path }); +/** One record at a version, with its log inclusion proof; `fields` = '*' (all) or a list of field names to disclose. */ +export function dovadaInregistrare(db, n, i, { fields = '*' } = {}) { + const c = versiune(db, n); const t = arbori(db, c.size); const r = t.recs[i]; if (!r) throw new Error('record ' + i + ' outside version ' + n + ' (' + c.size + ' records)'); + return { v: 1, kind: KIND_INREGISTRARE, name: c.name, version: c.version, commitmentHash: hashAngajament(c), i, id: r.id, c: r.c, disclosed: dezvaluie(db.inregistrari[i], fields), logProof: dovada(t.log.inclusionProof(i, c.size)) }; +} +/** Equality (eq) or range (from..to, inclusive, either side open) on an indexed field; `fields` = the fields to disclose besides it. */ +export function interogheaza(db, n, { field, eq, from, to, fields = [], limit = 1000 } = {}) { + const c = versiune(db, n); if (!c.indexes[field]) throw new Error('field "' + field + '" is not indexed in ' + c.name); + if (eq !== undefined) { from = eq; to = eq; } + if (from !== undefined && to !== undefined && comparaValori(from, to) > 0) throw new Error('from is above to'); + const t = arbori(db, c.size); const ix = t.index[field]; const e = ix.intrari; + let start = 0; while (start < e.length && from !== undefined && comparaValori(e[start].value, from) < 0) start++; + let end = start; while (end < e.length && (to === undefined || comparaValori(e[end].value, to) <= 0)) end++; + if (end - start > limit) throw new Error('the answer would have ' + (end - start) + ' rows, above the limit ' + limit); + const care = fields === '*' ? '*' : [...new Set([field, ...fields])]; + const rows = e.slice(start, end).map((x, k) => ({ pos: start + k, i: x.i, id: t.recs[x.i].id, c: t.recs[x.i].c, disclosed: dezvaluie(db.inregistrari[x.i], care), + logProof: dovada(t.log.inclusionProof(x.i, c.size)), indexProof: dovada(ix.tree.inclusionProof(start + k, e.length)) })); + const vecin = (pos) => pos < 0 || pos >= e.length ? null : ({ pos, value: e[pos].value, i: e[pos].i, recordLeaf: e[pos].recordLeaf, indexProof: dovada(ix.tree.inclusionProof(pos, e.length)) }); + return { v: 1, kind: KIND_RASPUNS, name: c.name, version: c.version, commitmentHash: hashAngajament(c), field, from: from === undefined ? null : from, to: to === undefined ? null : to, + indexSize: e.length, rows, before: vecin(start - 1), after: vecin(end) }; +} +/** The log of version a is a prefix of the log of version b (RFC 9162 consistency). */ +export function dovadaConsistenta(db, a, b) { + const ca = versiune(db, a), cb = versiune(db, b); if (ca.size > cb.size) throw new Error('version ' + a + ' is larger than version ' + b); + const t = arbori(db, cb.size); + return { v: 1, kind: KIND_CONSISTENTA, name: cb.name, from: { version: ca.version, commitmentHash: hashAngajament(ca) }, to: { version: cb.version, commitmentHash: hashAngajament(cb) }, proof: t.log.consistencyProof(ca.size, cb.size) }; +} +/** Rebuilds every version from the records and compares the roots: the full audit anyone holding the data runs. */ +export function auditeaza(db) { + const probleme = []; + for (const c of db.versiuni) { + try { const t = arbori(db, c.size); if (t.log.root() !== c.logRoot) probleme.push('version ' + c.version + ': log root differs'); + for (const f of Object.keys(c.indexes)) { if (!t.index[f] || t.index[f].tree.root() !== c.indexes[f].root || t.index[f].intrari.length !== c.indexes[f].size) probleme.push('version ' + c.version + ': index ' + f + ' differs'); } + } catch (e) { probleme.push('version ' + c.version + ': ' + e.message); } + } + for (let k = 1; k < db.versiuni.length; k++) if (db.versiuni[k].previous !== hashAngajament(db.versiuni[k - 1])) probleme.push('version ' + db.versiuni[k].version + ' does not name version ' + db.versiuni[k - 1].version + ' as previous'); + return { ok: probleme.length === 0, probleme }; +} + +// ----- verification, without the data (what a reader runs on a commitment and an answer) ----- +const ok = (motive, cond, m) => { if (!cond) motive.push(m); return cond; }; +function verificaRand(r, commitment, field, motive, eticheta) { + if (!r || typeof r !== 'object' || !r.c || typeof r.c !== 'object' || !r.disclosed) return ok(motive, false, eticheta + ': malformed row'); + for (const [k, d] of Object.entries(r.disclosed)) { + if (!d || typeof d.salt !== 'string') return ok(motive, false, eticheta + ': disclosed field ' + k + ' without a salt'); + const okCamp = r.c[k] === angajamentCamp(k, d.value, d.salt) /*PAZA:camp*/; + if (!ok(motive, okCamp, eticheta + ': the disclosed value of "' + k + '" does not match its commitment')) return false; + } + const leaf = frunzaInregistrare(r.id, r.c); + if (!ok(motive, r.logProof && verifyInclusion({ ...r.logProof, leafHash: leaf, treeSize: commitment.size, rootHash: commitment.logRoot }) && r.logProof.index === r.i, eticheta + ': the record is not in the log of version ' + commitment.version)) return false; + if (field !== null) { + if (!ok(motive, r.disclosed[field] !== undefined, eticheta + ': the queried field "' + field + '" is not disclosed')) return false; + const ix = commitment.indexes[field]; + const okIndex = r.indexProof && verifyInclusion({ ...r.indexProof, leafHash: frunzaIndex(field, r.disclosed[field].value, r.i, leaf), treeSize: ix.size, rootHash: ix.root }) && r.indexProof.index === r.pos; + if (!ok(motive, okIndex, eticheta + ': the row is not at position ' + r.pos + ' of the index "' + field + '"')) return false; + } + return true; +} +/** A query answer against a commitment: every row proves itself, the rows are the whole contiguous run, the neighbours are outside. */ +export function verificaRaspuns(answer, commitment) { + const motive = []; + if (!answer || answer.kind !== KIND_RASPUNS || !commitment) return { verdict: 'INVALID', motive: ['not a ' + KIND_RASPUNS], rows: 0 }; + ok(motive, answer.commitmentHash === hashAngajament(commitment) && answer.version === commitment.version && answer.name === commitment.name, 'the answer names another commitment'); + const f = answer.field; const ix = commitment.indexes && commitment.indexes[f]; + if (!ok(motive, !!ix, 'the field "' + f + '" is not indexed in the commitment')) return { verdict: 'INVALID', motive, rows: 0 }; + ok(motive, answer.indexSize === ix.size, 'the answer\'s index size differs from the commitment\'s'); + const from = answer.from, to = answer.to; const rows = Array.isArray(answer.rows) ? answer.rows : []; + if (from !== null && to !== null) ok(motive, comparaValori(from, to) <= 0, 'from is above to'); + const inRange = (v) => (from === null || comparaValori(v, from) >= 0) && (to === null || comparaValori(v, to) <= 0); + let start = rows.length ? rows[0].pos : (answer.before ? answer.before.pos + 1 : 0); + for (const [k, r] of rows.entries()) { + if (!verificaRand(r, commitment, f, motive, 'row ' + k)) continue; + ok(motive, r.pos === start + k, 'row ' + k + ' breaks the contiguous run of the index'); + ok(motive, inRange(r.disclosed[f].value), 'row ' + k + ' is outside the queried range'); + if (k) { const okOrdine = comparaValori(rows[k - 1].disclosed[f].value, r.disclosed[f].value) < 0 || (comparaValori(rows[k - 1].disclosed[f].value, r.disclosed[f].value) === 0 && rows[k - 1].i < r.i) /*PAZA:ordine*/; ok(motive, okOrdine, 'rows ' + (k - 1) + ' and ' + k + ' are not in index order'); } + } + const end = start + rows.length; + const vecin = (n, pos, cond, m) => { + if (!n) return ok(motive, cond, m); + const okVecin = n.pos === pos && n.indexProof && verifyInclusion({ ...n.indexProof, leafHash: frunzaIndex(f, n.value, n.i, n.recordLeaf), treeSize: ix.size, rootHash: ix.root }) && n.indexProof.index === pos; + return ok(motive, okVecin, 'the neighbour at position ' + pos + ' does not prove itself in the index'); + }; + // completeness: the row before the run is below the range (or the run starts at 0), the row after is above it (or the run ends the index) + const okInainte = vecin(answer.before, start - 1, start === 0, 'the run does not start at 0 and no neighbour before it is shown') && (!answer.before || (from !== null && comparaValori(answer.before.value, from) < 0)) /*PAZA:completitudine*/; + ok(motive, okInainte, 'a row before the run could still be in the range (the answer may be incomplete)'); + const okDupa = vecin(answer.after, end, end === ix.size, 'the run does not end the index and no neighbour after it is shown') && (!answer.after || (to !== null && comparaValori(answer.after.value, to) > 0)) /*PAZA:completitudine*/; + ok(motive, okDupa, 'a row after the run could still be in the range (the answer may be incomplete)'); + return { verdict: motive.length ? 'INVALID' : 'VALID', motive, rows: rows.length }; +} +/** One record (from `dovadaInregistrare` or the serving endpoint) against a commitment. */ +export function verificaInregistrare(rec, commitment) { + const motive = []; + if (!rec || rec.kind !== KIND_INREGISTRARE) return { ok: false, motive: ['not a ' + KIND_INREGISTRARE] }; + ok(motive, rec.commitmentHash === hashAngajament(commitment) && rec.version === commitment.version, 'the record names another commitment'); + verificaRand(rec, commitment, null, motive, 'record ' + rec.i); + return { ok: motive.length === 0, motive }; +} +/** A consistency proof between two commitments: the older log is a prefix of the newer (nothing rewritten, nothing removed). */ +export function verificaConsistentaVersiuni(d, older, newer) { + const motive = []; + if (!d || d.kind !== KIND_CONSISTENTA || !d.proof) return { ok: false, motive: ['not a ' + KIND_CONSISTENTA] }; + ok(motive, d.from.commitmentHash === hashAngajament(older) && d.to.commitmentHash === hashAngajament(newer), 'the proof names other commitments'); + const okRadacini = d.proof.firstRoot === older.logRoot && d.proof.secondRoot === newer.logRoot && d.proof.firstSize === older.size && d.proof.secondSize === newer.size /*PAZA:consistenta*/; + ok(motive, okRadacini, 'the proof\'s roots or sizes are not the commitments\''); + ok(motive, verifyConsistency(d.proof), 'the older log is not a prefix of the newer (rewritten history)'); + ok(motive, newer.previous === hashAngajament(older) || newer.version !== older.version + 1, 'the newer version does not name the older as previous'); + return { ok: motive.length === 0, motive }; +} +/** + * Availability sampling: k record positions drawn from a seed (random when none; the seed is printed so the draw can be repeated), each + * fetched from `url` + /record// through `ia(url) -> JSON | null` and verified against the commitment. A record that is not + * served, or does not verify, fails the sample. What this measures: the publisher serves what it committed to, NOW, at the sampled + * positions; k samples bound the share of unserved records a publisher can hide only probabilistically ((1 - share)^k survive). + */ +export async function esantioneaza({ url, commitment, k = 16, seed = null, ia }) { + const n = commitment.size; const cerute = Math.min(Math.max(Number(k) || 1, 1), n); + const s = seed || crypto.randomBytes(16).toString('hex'); const pozitii = new Set(); let ctr = 0; + while (pozitii.size < cerute) { const h = crypto.createHash('sha256').update(s + ':' + ctr++).digest(); pozitii.add(Number(h.readBigUInt64BE(0) % BigInt(n))); } + const lipsa = [], motive = []; let verificate = 0; + for (const i of [...pozitii].sort((x, y) => x - y)) { + let rec = null; try { rec = await ia(url.replace(/\/$/, '') + '/record/' + commitment.version + '/' + i); } catch (e) { motive.push('record ' + i + ': ' + String(e.message).slice(0, 80)); } + if (!rec) { lipsa.push(i); continue; } + const r = verificaInregistrare(rec, commitment); + if (r.ok && rec.i === i) verificate++; else motive.push('record ' + i + ': ' + (rec.i !== i ? 'another record was served' : r.motive.join('; '))); + } + const okEsantion = lipsa.length === 0 && verificate === cerute /*PAZA:esantion*/; + if (lipsa.length) motive.push('not served: ' + lipsa.join(', ')); + return { ok: okEsantion, seed: s, cerute, verificate, lipsa, motive }; +} +/** The AIP-23 `data` envelope of a commitment: sha256 = SHA-256 of the canonical commitment, name = dataset@version. */ +export function plicAngajament(commitment, { createdAt = commitment.createdAt } = {}) { + return buildProof('data', { content: utf8(canonical(commitment)), name: commitment.name + '@' + commitment.version, createdAt }); +} +export function verificaPlicAngajament(env, commitment) { + const st = env && env.statement; + return !!st && env.kind === 'aere-proof-of-data-attestation' && st.sha256 === hashAngajament(commitment) && st.name === commitment.name + '@' + commitment.version && H.test(String(env.statementHash)); +} diff --git a/verifiable-data/dovezi/notarizare-28001-2026-10-02T0820Z.json b/verifiable-data/dovezi/notarizare-28001-2026-10-02T0820Z.json new file mode 100644 index 0000000..cf04f66 --- /dev/null +++ b/verifiable-data/dovezi/notarizare-28001-2026-10-02T0820Z.json @@ -0,0 +1,67 @@ +{ + "commitment": { + "v": 1, + "format": "aere-verifiable-dataset-1", + "name": "proba", + "version": 2, + "size": 400, + "logRoot": "0x373a9e95032d46ea613cfedb887c7e79b11b2e9c211dc8ce4f8051c7d6534bb1", + "createdAt": "2026-10-02T08:01:00.000Z", + "indexes": { + "tara": { + "root": "0x9df01d08ddc09b3bf6a966f2446f0345148031100b5ca7d2d1cc27a6e4753c15", + "size": 400 + }, + "varsta": { + "root": "0xe5ca27f5a9e5308f719c74fc2b42c80b554e4321d512e785ce951da03f09d4d1", + "size": 400 + } + }, + "previous": "0x60000ce734e7479355b910c41cf1be305cd0242d16fd1d4a65ebcf43fcbc68f3" + }, + "envelope": { + "v": 1, + "kind": "aere-proof-of-data-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-data", + "sha256": "0xea7441856ff78c64bc549b763816a76fbe40b049c024843427d42761981741cb", + "name": "proba@2", + "createdAt": "2026-10-02T08:01:00.000Z" + }, + "statementHash": "0xeb177d94c7bfa1de42c35a7096b454bda04c9456dad0ca6e5922f78b92192260", + "notarization": { + "chainId": 28001, + "notary": "0x70099E62735500AA2F85B60C518551a57B202d54", + "firstSeen": 1790929167, + "txHash": "0x39938e8ef28f62f60ad3d5f6a205d248e499fe1eda0e1e1851b1c43cc8f08c62", + "block": 4400052 + } + }, + "verdict": { + "verdict": "VALID", + "statementHash": "0xeb177d94c7bfa1de42c35a7096b454bda04c9456dad0ca6e5922f78b92192260", + "levels": [ + { + "level": "form", + "state": "PASSED", + "detail": "kind=aere-proof-of-data-attestation" + }, + { + "level": "integrity", + "state": "PASSED", + "detail": "sha256(statement) == statementHash (0xeb177d94c7bfa1de..)" + }, + { + "level": "signature", + "state": "ABSENT", + "detail": "the envelope carries no signature" + }, + { + "level": "finality", + "state": "PASSED", + "detail": "post-quantum: the state of block 4400063, the parent of anchor 4400064 whose post-quantum certificate verified here records this statementHash in AereNotary 0x70099e62735500aa2f85b60c518551a57b202d54 as first seen at 2026-10-02T08:19:27.000Z (1790929167); first seen in block 4400052, transaction 0x39938e8ef28f62f6.. (located through the endpoint's logs, not part of what is certified)" + } + ] + } +} \ No newline at end of file diff --git a/verifiable-data/proba-verifiable-data.mjs b/verifiable-data/proba-verifiable-data.mjs new file mode 100644 index 0000000..6fbd58f --- /dev/null +++ b/verifiable-data/proba-verifiable-data.mjs @@ -0,0 +1,171 @@ +#!/usr/bin/env node +// proba-verifiable-data.mjs: the laboratory of the verifiable dataset (roadmap point 20), three states per row: VERDE (the named +// verdict), ROSU (another verdict), STRICAT (the probe itself broke). No network, except --viu. +// node proba-verifiable-data.mjs the laboratory: 24 rows (honest answers VALID; nine forged ones INVALID, each +// refused for its named reason; availability sampling on a local server) +// node proba-verifiable-data.mjs --control-negativ plantings on a COPY of baza.mjs (PAZA markers): each must turn its forged +// fixture green-by-mistake, which proves the check is load-bearing +// node proba-verifiable-data.mjs --viu plus: the newest commitment notarized on testnet 28001 with a test key and read +// back under a post-quantum certified anchor (AERE_NOTARY_KEY_FILE, AERE_ETHERS_DIR) +// Env: AERE_VERIFY_PROOF (the AIP-23 verifier; without it the envelope rows are NEMASURAT and the exit code is 2), AERE_VD_MODUL (the +// module under test; the control sets it to its planted copy). +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { createRequire } from 'node:module'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { leafHash, merkleTree } from '../verify-layer/arbore-merkle.mjs'; +import { porneste } from './serveste.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const RAD = path.resolve(AICI, '..', '..'); +const MODUL = process.env.AERE_VD_MODUL || path.join(AICI, 'baza.mjs'); +const VERIFY_PROOF = process.env.AERE_VERIFY_PROOF || path.join(RAD, 'verificator-falcon', 'verify-proof.mjs'); +const RPC_28001 = process.env.AERE_TESTNET_RPC || 'https://testnet-rpc.aere.network'; +const CHEIE = process.env.AERE_NOTARY_KEY_FILE || path.join(RAD, 'tools', 'aere-quantum-shield', 'dovezi-l1', '.chei', 'cheie-de-proba-28001.key'); +const ETHERS_DIR = process.env.AERE_ETHERS_DIR || path.join(RAD, 'contracts'); +const a = process.argv.slice(2); const viu = a.includes('--viu'); const control = a.includes('--control-negativ'); +const B = await import(pathToFileURL(MODUL).href + '?t=' + Date.now()); + +const TARI = ['RO', 'DE', 'FR', 'ES', 'IT']; +function inregistrari(n, de = 0) { const r = []; for (let k = de; k < de + n; k++) r.push({ id: 'r' + k, tara: TARI[k % 5], varsta: 18 + ((k * 37) % 63), email: 'u' + k + '@example.test', privat: 'nu-se-dezvaluie-' + k }); return r; } +const clona = (x) => JSON.parse(JSON.stringify(x)); + +/** The fixtures: each returns { verdict, detail }; `asteptat` is the verdict the honest module must give. */ +export async function fixturi(B, { dir, server }) { + const F = []; + const db = B.deschide(dir); const c1 = B.versiune(db, 1), c2 = B.versiune(db, 2); + const brut = (f) => db.inregistrari.slice(0, c2.size).filter((r) => f(r.fields)).length; // the count straight from the records, to compare with the answer + F.push({ nume: 'L1 a record of version 1 proves itself (log inclusion, field commitments)', asteptat: 'VALID', r: () => { const x = B.verificaInregistrare(B.dovadaInregistrare(db, 1, 7), c1); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } }); + F.push({ nume: 'L2 version 1 is a prefix of version 2 (consistency)', asteptat: 'VALID', r: () => { const x = B.verificaConsistentaVersiuni(B.dovadaConsistenta(db, 1, 2), c1, c2); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } }); + F.push({ nume: 'L3 equality query tara=RO at version 2: VALID, as many rows as the data has, only tara disclosed', asteptat: 'VALID', r: () => { + const q = B.interogheaza(db, 2, { field: 'tara', eq: 'RO' }); const x = B.verificaRaspuns(q, c2); + const n = brut((r) => r.tara === 'RO'); const doarTara = q.rows.every((r) => Object.keys(r.disclosed).join() === 'tara' && !JSON.stringify(r).includes('nu-se-dezvaluie')); + return { verdict: x.verdict === 'VALID' && x.rows === n && doarTara ? 'VALID' : 'INVALID', detail: `${x.rows} rows (data: ${n}); only tara: ${doarTara}; ${x.motive.join('; ')}` }; } }); + F.push({ nume: 'L4 range query varsta 30..40 with email disclosed too: VALID, count as the data', asteptat: 'VALID', r: () => { + const q = B.interogheaza(db, 2, { field: 'varsta', from: 30, to: 40, fields: ['email'] }); const x = B.verificaRaspuns(q, c2); + const n = brut((r) => r.varsta >= 30 && r.varsta <= 40); const cuEmail = q.rows.every((r) => r.disclosed.email && r.disclosed.varsta && !r.disclosed.privat); + return { verdict: x.verdict === 'VALID' && x.rows === n && cuEmail ? 'VALID' : 'INVALID', detail: `${x.rows} rows (data: ${n}); varsta+email only: ${cuEmail}; ${x.motive.join('; ')}` }; } }); + F.push({ nume: 'L5 open range varsta <= 20 (run starts at index 0, no neighbour before) and varsta >= 79 (ends the index)', asteptat: 'VALID', r: () => { + const q1 = B.interogheaza(db, 2, { field: 'varsta', to: 20 }); const q2 = B.interogheaza(db, 2, { field: 'varsta', from: 79 }); + const x1 = B.verificaRaspuns(q1, c2), x2 = B.verificaRaspuns(q2, c2); + return { verdict: x1.verdict === 'VALID' && x2.verdict === 'VALID' && q1.before === null && q2.after === null && x1.rows === brut((r) => r.varsta <= 20) && x2.rows === brut((r) => r.varsta >= 79) ? 'VALID' : 'INVALID', detail: `${x1.rows} + ${x2.rows} rows; ${x1.motive.concat(x2.motive).join('; ')}` }; } }); + F.push({ nume: 'L6 a query with no match: zero rows, the two neighbours prove the gap', asteptat: 'VALID', r: () => { const q = B.interogheaza(db, 2, { field: 'tara', eq: 'GB' }); const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: `${x.rows} rows, before=${q.before && q.before.value}, after=${q.after && q.after.value}; ${x.motive.join('; ')}` }; } }); + F.push({ nume: 'L7 the audit rebuilds both versions from the records: every root matches', asteptat: 'VALID', r: () => { const x = B.auditeaza(db); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.probleme.join('; ') }; } }); + // forged answers: each must be INVALID for its named reason + F.push({ nume: 'N1 a disclosed value changed after commitment (varsta 31 -> 32)', asteptat: 'INVALID', paza: 'camp', r: () => { const q = B.interogheaza(db, 2, { field: 'tara', eq: 'DE', fields: ['varsta'] }); q.rows[0].disclosed.varsta.value += 1; const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } }); + F.push({ nume: 'N2 a row dropped from the middle of the run', asteptat: 'INVALID', r: () => { const q = B.interogheaza(db, 2, { field: 'tara', eq: 'FR' }); q.rows.splice(3, 1); const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } }); + F.push({ nume: 'N3 the last matching row dropped and shown as the neighbour after (its value is still in range)', asteptat: 'INVALID', paza: 'completitudine', r: () => { + const q = B.interogheaza(db, 2, { field: 'tara', eq: 'IT' }); const ultima = q.rows.pop(); + q.after = { pos: ultima.pos, value: ultima.disclosed.tara.value, i: ultima.i, recordLeaf: B.frunzaInregistrare(ultima.id, ultima.c), indexProof: ultima.indexProof }; + const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } }); + F.push({ nume: 'N4 the first matching row dropped and shown as the neighbour before', asteptat: 'INVALID', paza: 'completitudine', r: () => { + const q = B.interogheaza(db, 2, { field: 'varsta', from: 50, to: 60 }); const prima = q.rows.shift(); + q.before = { pos: prima.pos, value: prima.disclosed.varsta.value, i: prima.i, recordLeaf: B.frunzaInregistrare(prima.id, prima.c), indexProof: prima.indexProof }; + const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } }); + F.push({ nume: 'N5 an answer of version 1 presented against the commitment of version 2', asteptat: 'INVALID', r: () => { const q = B.interogheaza(db, 1, { field: 'tara', eq: 'RO' }); const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: x.motive.join('; ') }; } }); + F.push({ nume: 'N6 a rewritten history: a "version 2" built from the records with one changed, with the honest consistency proof', asteptat: 'INVALID', paza: 'consistenta', r: () => { + const fals = clona(c2); fals.logRoot = '0x' + crypto.createHash('sha256').update('alt arbore').digest('hex'); + const d = B.dovadaConsistenta(db, 1, 2); d.to.commitmentHash = B.hashAngajament(fals); + const x = B.verificaConsistentaVersiuni(d, c1, fals); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } }); + F.push({ nume: 'N7 a consistency proof with one hash of the path changed', asteptat: 'INVALID', r: () => { const d = B.dovadaConsistenta(db, 1, 2); d.proof.path[0] = '0x' + 'ab'.repeat(32); const x = B.verificaConsistentaVersiuni(d, c1, c2); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } }); + F.push({ nume: 'N8 a publisher whose index is NOT sorted (two rows with valid proofs, out of order)', asteptat: 'INVALID', paza: 'ordine', r: () => { + // two records, the index built in the wrong order (b before a), everything else honest: the reader sees it on the first answer with two rows + const recs = [{ id: 'x', fields: { k: 'b' }, salts: { k: '00'.repeat(16) } }, { id: 'y', fields: { k: 'a' }, salts: { k: '11'.repeat(16) } }]; + const rc = recs.map((r) => { const c = { k: B.angajamentCamp('k', r.fields.k, r.salts.k) }; return { ...r, c, leaf: B.frunzaInregistrare(r.id, c) }; }); + const log = merkleTree(rc.map((r) => r.leaf)); const idx = merkleTree(rc.map((r, i) => B.frunzaIndex('k', r.fields.k, i, r.leaf))); // unsorted: 'b' at 0, 'a' at 1 + const c = { v: 1, format: B.FORMAT, name: 'nesortat', version: 1, size: 2, logRoot: log.root(), createdAt: '2026-10-02T00:00:00.000Z', indexes: { k: { root: idx.root(), size: 2 } }, previous: null }; + const rand = (i) => ({ pos: i, i, id: rc[i].id, c: rc[i].c, disclosed: { k: { value: rc[i].fields.k, salt: rc[i].salts.k } }, logProof: log.inclusionProof(i, 2), indexProof: idx.inclusionProof(i, 2) }); + const q = { v: 1, kind: B.KIND_RASPUNS, name: 'nesortat', version: 1, commitmentHash: B.hashAngajament(c), field: 'k', from: 'a', to: 'b', indexSize: 2, rows: [rand(0), rand(1)], before: null, after: null }; + const x = B.verificaRaspuns(q, c); return { verdict: x.verdict, detail: x.motive.join('; ') }; } }); + F.push({ nume: 'N9 a record whose log proof points at another position', asteptat: 'INVALID', r: () => { const d = B.dovadaInregistrare(db, 2, 5); d.i = 6; d.logProof.index = 6; const x = B.verificaInregistrare(d, c2); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } }); + // availability sampling against the local server + const ia = async (u) => { const r = await fetch(u, { signal: AbortSignal.timeout(10000) }); if (!r.ok) return null; return r.json(); }; + F.push({ nume: 'A1 availability sampling, 32 records drawn from a seed, all served and verified', asteptat: 'VALID', r: async () => { const x = await B.esantioneaza({ url: server.url, commitment: c2, k: 32, seed: 'proba-a1', ia }); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: `${x.verificate}/${x.cerute}; ${x.motive.join('; ')}` }; } }); + F.push({ nume: 'A2 a publisher that withholds record 123: the full sample finds it', asteptat: 'INVALID', paza: 'esantion', r: async () => { const x = await B.esantioneaza({ url: server.ascuns, commitment: c2, k: c2.size, seed: 'proba-a2', ia }); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: `${x.verificate}/${x.cerute}; not served: ${x.lipsa.join(',')}` }; } }); + F.push({ nume: 'A3 the served query answer (HTTP) verifies like the local one', asteptat: 'VALID', r: async () => { const q = await ia(server.url + '/query/2?field=tara&eq=%22ES%22&fields=varsta'); const x = B.verificaRaspuns(q, c2); return { verdict: x.verdict, detail: `${x.rows} rows; ${x.motive.join('; ')}` }; } }); + F.push({ nume: 'A4 the served consistency proof 1 -> 2 verifies', asteptat: 'VALID', r: async () => { const d = await ia(server.url + '/consistency/1/2'); const x = B.verificaConsistentaVersiuni(d, c1, c2); return { verdict: x.ok ? 'VALID' : 'INVALID', detail: x.motive.join('; ') }; } }); + // the commitment as an AIP-23 envelope + F.push({ nume: 'E1 the envelope of version 2 names this commitment (sha256 of its canonical bytes); a changed commitment does not', asteptat: 'VALID', r: () => { const env = B.plicAngajament(c2); const alt = clona(c2); alt.size += 1; return { verdict: B.verificaPlicAngajament(env, c2) && !B.verificaPlicAngajament(env, alt) ? 'VALID' : 'INVALID', detail: env.statementHash.slice(0, 18) + '..' }; } }); + F.push({ nume: 'E2 the AIP-23 reference verifier accepts the envelope (no new verification code)', asteptat: 'VALID', nemasurat: !fs.existsSync(VERIFY_PROOF), r: () => { + const f = path.join(os.tmpdir(), 'vd-plic-' + process.pid + '.json'); fs.writeFileSync(f, JSON.stringify(B.plicAngajament(c2))); + try { const r = spawnSync(process.execPath, [VERIFY_PROOF, f, '--json'], { encoding: 'utf8', timeout: 120000 }); let v; try { v = JSON.parse(r.stdout); } catch { return { verdict: 'STRICAT', detail: 'verifier gave no JSON' }; } return { verdict: v.verdict, detail: (v.levels || []).map((l) => l.level + ':' + l.state).join(' ') }; } finally { fs.rmSync(f, { force: true }); } } }); + return F; +} + +/** Builds the dataset of the laboratory: 300 records committed as version 1, 100 more as version 2. */ +export function laborator(dir) { + const db = B.creeaza(path.join(dir, 'set'), { name: 'proba', indexes: ['tara', 'varsta'] }); + B.adauga(db, inregistrari(300)); B.angajeaza(db, { createdAt: '2026-10-02T08:00:00.000Z' }); + B.adauga(db, inregistrari(100, 300)); B.angajeaza(db, { createdAt: '2026-10-02T08:01:00.000Z' }); + return path.join(dir, 'set'); +} + +const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-vd-')); +let codIesire = 0; +try { + const dir = laborator(T); + const s1 = await porneste({ dir }); const s2 = await porneste({ dir, ascunde: [123] }); + const server = { url: s1.url, ascuns: s2.url }; + try { + if (!control) { + let verzi = 0, rosii = 0, stricate = 0, nem = 0; + for (const f of await fixturi(B, { dir, server })) { + if (f.nemasurat) { console.log(` [NEMASURAT] ${f.nume}: the AIP-23 verifier is not at ${VERIFY_PROOF} (AERE_VERIFY_PROOF)`); nem++; continue; } + let r; try { r = await f.r(); } catch (e) { r = { verdict: 'STRICAT', detail: String(e.stack || e.message).split('\n').slice(0, 2).join(' ').slice(0, 200) }; } + const stare = r.verdict === 'STRICAT' ? 'STRICAT' : r.verdict === f.asteptat ? 'VERDE' : 'ROSU'; + if (stare === 'VERDE') verzi++; else if (stare === 'ROSU') rosii++; else stricate++; + console.log(` [${stare.padEnd(7)}] ${f.nume}: ${r.verdict}${r.detail ? ' (' + r.detail.slice(0, 160) + ')' : ''}`); + } + if (viu) { + const db = B.deschide(dir); const c2 = B.versiune(db, 2); const env = B.plicAngajament(c2); + const { notarizeEnvelope } = await import('../agents/agent-ancora.mjs'); + const { ethers } = createRequire(path.join(ETHERS_DIR, 'package.json'))('ethers'); + const n = await notarizeEnvelope({ envelope: env, kind: 'aere-proof-of-data', rpc: RPC_28001, keyFile: CHEIE, ethers }); + if (!n.ok) { console.log(` [STRICAT] V1 notarization on 28001: ${n.motiv}`); stricate++; } + else { + const f = path.join(T, 'plic-notarizat.json'); fs.writeFileSync(f, JSON.stringify(n.envelope, null, 1)); + console.log(` [VERDE ] V1 version 2 notarized on 28001: firstSeen ${n.envelope.notarization.firstSeen}${n.envelope.notarization.txHash ? ', tx ' + n.envelope.notarization.txHash + ' block ' + n.envelope.notarization.block : ' (already)'}`); verzi++; + let v = null; + for (let k = 0; k < 24; k++) { const r = spawnSync(process.execPath, [VERIFY_PROOF, f, '--json', '--rpc', RPC_28001, '--chain', '28001'], { encoding: 'utf8', timeout: 300000 }); try { v = JSON.parse(r.stdout); } catch { v = null; } const fin = v && (v.levels || []).find((l) => l.level === 'finality'); if (fin && fin.state !== 'PENDING') break; await new Promise((ok) => setTimeout(ok, 5000)); } + const fin = v && (v.levels || []).find((l) => l.level === 'finality'); + const okFin = v && v.verdict === 'VALID' && fin && fin.state === 'PASSED'; + console.log(` [${okFin ? 'VERDE ' : 'ROSU '}] V2 read back under a post-quantum certified anchor: ${v ? v.verdict : '?'}; finality ${fin ? fin.state + ': ' + String(fin.detail).slice(0, 140) : '?'}`); okFin ? verzi++ : rosii++; + const dov = path.join(AICI, 'dovezi'); fs.mkdirSync(dov, { recursive: true }); + const out = path.join(dov, 'notarizare-28001-' + new Date().toISOString().replace(/[:.]/g, '').slice(0, 15) + 'Z.json'); + fs.writeFileSync(out, JSON.stringify({ commitment: c2, envelope: n.envelope, verdict: v }, null, 1)); console.log(' evidence: ' + path.relative(RAD, out)); + } + } + console.log(`\nverifiable data: ${verzi} VERDE, ${rosii} ROSU, ${stricate} STRICAT${nem ? ', ' + nem + ' NEMASURAT' : ''}`); + codIesire = stricate || rosii ? 1 : nem ? 2 : 0; + } else { + // the control: a planting per PAZA marker on a copy of baza.mjs; the fixture that the marker guards must turn green-by-mistake + const SURSA = fs.readFileSync(MODUL, 'utf8'); let bune = 0; const marcaje = ['camp', 'ordine', 'completitudine', 'consistenta', 'esantion']; + const toate = await fixturi(B, { dir, server }); + for (const m of marcaje) { + const copie = path.join(AICI, `_plantat-baza-${process.pid}-${m}.mjs`); const n = SURSA.split(`/*PAZA:${m}*/`).length - 1; + const tinte = toate.filter((f) => f.paza === m); + let stare, detaliu; + try { + if (n < 1 || !tinte.length) { stare = 'STRICAT'; detaliu = `marker ${n} times, ${tinte.length} fixtures`; } + else { + fs.writeFileSync(copie, SURSA.split(`/*PAZA:${m}*/`).join("|| String(1) === '1' /*PLANTAT*/")); + const P = await import(pathToFileURL(copie).href + '?t=' + Date.now()); + const fx = await fixturi(P, { dir, server }); const rez = []; + for (const t of tinte) { const f = fx.find((x) => x.nume === t.nume); const r = await f.r(); rez.push(`${t.nume.split(' ')[0]}:${r.verdict}`); } + const verzi = rez.every((x) => x.endsWith(':VALID')); + stare = verzi ? 'VERDE' : 'ROSU'; detaliu = (verzi ? 'the forged fixture is accepted with the check planted out: ' : 'the planting did not change the verdict: ') + rez.join(' '); + } + } catch (e) { stare = 'STRICAT'; detaliu = String(e.message).slice(0, 160); } finally { fs.rmSync(copie, { force: true }); } + if (stare === 'VERDE') bune++; + console.log(` [${stare.padEnd(7)}] PAZA:${m} (${n} marker${n === 1 ? '' : 's'}) ${detaliu}`); + } + const ramase = fs.readdirSync(AICI).filter((f) => f.startsWith('_plantat-baza-')); + console.log(`CONTROL: ${bune} of ${marcaje.length} checks proven load-bearing${ramase.length ? '; copies left: ' + ramase.join(',') : ''}`); + codIesire = bune === marcaje.length && !ramase.length ? 0 : 1; + } + } finally { await s1.inchide(); await s2.inchide(); } +} finally { fs.rmSync(T, { recursive: true, force: true }); } +process.exitCode = codIesire; diff --git a/verifiable-data/serveste.mjs b/verifiable-data/serveste.mjs new file mode 100644 index 0000000..2b6d55c --- /dev/null +++ b/verifiable-data/serveste.mjs @@ -0,0 +1,52 @@ +// serveste.mjs: the read-only HTTP face of a verifiable dataset (roadmap point 20, "availability"): commitments, records with their +// inclusion proofs, query answers with their completeness proofs, consistency proofs between versions. Every answer is checkable +// against a commitment with verifica.mjs, without trusting this server. What this server proves is only that the bytes were served +// when asked: availability sampling (verifica.mjs disponibilitate) draws random record positions and verifies each against the +// commitment; a publisher that withholds a record fails the sample that lands on it. This is retrievability under sampling, not an +// erasure-coded data-availability scheme. +// GET /v1/vd//commitments every version's commitment (+ its AIP-23 envelope, notarized or not) +// GET /v1/vd//commitment/ +// GET /v1/vd//record// one record at version n with its log proof (all fields, or none with --privat) +// GET /v1/vd//query/?field=f&eq=J | &from=J&to=J [&fields=a,b|*] [&limit=N] J = a JSON value ("x", 42, true, null) +// GET /v1/vd//consistency// +// Only GET; JSON; the dataset is re-read when versiuni.json changes; bodies are bounded by the query limit (default 1000 rows). +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; +import { deschide, versiune, dovadaInregistrare, interogheaza, dovadaConsistenta } from './baza.mjs'; + +const json = (res, cod, o) => { const b = Buffer.from(JSON.stringify(o)); res.writeHead(cod, { 'content-type': 'application/json', 'content-length': b.length, 'cache-control': 'no-store' }); res.end(b); }; +const dinJson = (s) => { if (s === null || s === undefined) return undefined; try { return JSON.parse(s); } catch { throw new Error('a query value is JSON: "x", 42, true or null (got ' + String(s).slice(0, 40) + ')'); } }; + +/** Starts the server; resolves with { server, url, inchide }. `ascunde`: record positions the server withholds (a test hook, for the + * availability control: a publisher that does not serve a record). */ +export function porneste({ dir, port = 0, host = '127.0.0.1', privat = false, limit = 1000, ascunde = [] } = {}) { + let db = deschide(dir); let mtime = fs.statSync(path.join(dir, 'versiuni.json')).mtimeMs; + const plic = (n) => { const f = path.join(dir, 'plicuri', n + '.json'); return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null; }; + const recarca = () => { const m = fs.statSync(path.join(dir, 'versiuni.json')).mtimeMs; if (m !== mtime) { db = deschide(dir); mtime = m; } }; + const ascunse = new Set(ascunde.map(Number)); + const server = http.createServer((req, res) => { + try { + if (req.method !== 'GET') return json(res, 405, { error: 'method_not_allowed' }); + const url = new URL(req.url, 'http://x'); const p = url.pathname.split('/').filter(Boolean); + if (p[0] !== 'v1' || p[1] !== 'vd' || p.length < 4) return json(res, 404, { error: 'not_found' }); + recarca(); + if (p[2] !== db.set.name) return json(res, 404, { error: 'no_such_dataset' }); + const n = (s) => { const x = Number(s); if (!Number.isInteger(x) || x < 0) throw new Error('not a non-negative integer: ' + s); return x; }; + if (p[3] === 'health') return json(res, 200, { ok: true, name: db.set.name, versions: db.versiuni.length, records: db.inregistrari.length }); + if (p[3] === 'commitments' && p.length === 4) return json(res, 200, { name: db.set.name, commitments: db.versiuni.map((c) => ({ commitment: c, envelope: plic(c.version) })) }); + if (p[3] === 'commitment' && p.length === 5) { const c = versiune(db, n(p[4])); return json(res, 200, { commitment: c, envelope: plic(c.version) }); } + if (p[3] === 'record' && p.length === 6) { const i = n(p[5]); if (ascunse.has(i)) return json(res, 404, { error: 'record_not_served' }); return json(res, 200, dovadaInregistrare(db, n(p[4]), i, { fields: privat ? [] : '*' })); } + if (p[3] === 'query' && p.length === 5) { + const q = url.searchParams; const fields = q.get('fields'); const lim = Math.min(Number(q.get('limit') || limit) || limit, limit); + return json(res, 200, interogheaza(db, n(p[4]), { field: q.get('field'), eq: dinJson(q.get('eq')), from: dinJson(q.get('from')), to: dinJson(q.get('to')), fields: fields === '*' ? '*' : (fields ? fields.split(',').filter(Boolean) : []), limit: lim })); + } + if (p[3] === 'consistency' && p.length === 6) return json(res, 200, dovadaConsistenta(db, n(p[4]), n(p[5]))); + return json(res, 404, { error: 'not_found' }); + } catch (e) { return json(res, 400, { error: 'bad_request', detail: String(e.message).slice(0, 200) }); } + }); + return new Promise((ok, nu) => { + server.on('error', nu); + server.listen(port, host, () => { const a = server.address(); ok({ server, url: `http://${host}:${a.port}/v1/vd/${db.set.name}`, inchide: () => new Promise((r) => server.close(r)) }); }); + }); +} diff --git a/verifiable-data/vd.mjs b/verifiable-data/vd.mjs new file mode 100644 index 0000000..8cf8e33 --- /dev/null +++ b/verifiable-data/vd.mjs @@ -0,0 +1,61 @@ +#!/usr/bin/env node +// vd.mjs: the publisher's command line of a verifiable dataset (roadmap point 20). Values on the command line are JSON ("x", 42). +// node vd.mjs creeaza --nume [--index f1,f2] a new dataset folder +// node vd.mjs adauga append records ({id, ...fields}); never rewrites +// node vd.mjs angajeaza [--out c.json] a new version: commitment + its AIP-23 envelope (plicuri/.json) +// node vd.mjs notarizeaza --rpc URL --key-file f --ethers-dir d [--notary 0x..] +// the envelope's statementHash on AereNotary (refuses 2800 unless AERE_CONFIRM_MAINNET=yes) +// node vd.mjs interogheaza --camp f (--eq J | [--de-la J] [--pana-la J]) [--campuri a,b|*] [--out answer.json] +// node vd.mjs inregistrare [--campuri a,b|*] [--out rec.json] +// node vd.mjs consistenta [--out proof.json] +// node vd.mjs auditeaza rebuild every version from the records and compare the roots +// node vd.mjs serveste [--port 8797] [--privat] the read-only HTTP face (serveste.mjs) +import fs from 'node:fs'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { creeaza, deschide, adauga, angajeaza, versiune, dovadaInregistrare, interogheaza, dovadaConsistenta, auditeaza, plicAngajament, hashAngajament } from './baza.mjs'; +import { notarizeEnvelope } from '../agents/agent-ancora.mjs'; +import { porneste } from './serveste.mjs'; + +const a = process.argv.slice(2); const cmd = a[0]; const dir = a[1]; +const opt = (k, d = null) => { const i = a.indexOf(k); return i >= 0 && i + 1 < a.length ? a[i + 1] : d; }; +const are = (k) => a.includes(k); +const J = (s) => (s === null ? undefined : JSON.parse(s)); +const scrie = (o) => { const f = opt('--out'); const t = JSON.stringify(o, null, 1); if (f) { fs.writeFileSync(f, t); console.log('written ' + f); } else console.log(t); }; +const plicuri = (d) => { const p = path.join(d, 'plicuri'); fs.mkdirSync(p, { recursive: true }); return p; }; + +try { + if (cmd === 'creeaza') { const db = creeaza(dir, { name: opt('--nume'), indexes: (opt('--index', '') || '').split(',').filter(Boolean) }); console.log('created ' + db.set.name + ' at ' + dir + ' (indexes: ' + (db.set.indexes.join(', ') || 'none') + ')'); } + else if (cmd === 'adauga') { + const db = deschide(dir); const t = fs.readFileSync(a[2], 'utf8'); + const recs = a[2].endsWith('.ndjson') ? t.split('\n').filter(Boolean).map((l) => JSON.parse(l)) : JSON.parse(t); + console.log('appended ' + adauga(db, Array.isArray(recs) ? recs : [recs]) + ' records (' + db.inregistrari.length + ' in total)'); + } + else if (cmd === 'angajeaza') { + const db = deschide(dir); const c = angajeaza(db); const env = plicAngajament(c); + fs.writeFileSync(path.join(plicuri(dir), c.version + '.json'), JSON.stringify(env, null, 1)); + console.log(`version ${c.version}: ${c.size} records, log root ${c.logRoot.slice(0, 18)}.., commitment ${hashAngajament(c).slice(0, 18)}.., envelope ${env.statementHash.slice(0, 18)}..`); + scrie(c); + } + else if (cmd === 'notarizeaza') { + const db = deschide(dir); const c = versiune(db, a[2]); const f = path.join(dir, 'plicuri', c.version + '.json'); + if (!fs.existsSync(f)) throw new Error('no envelope for version ' + c.version + ' (run angajeaza first)'); + const env = JSON.parse(fs.readFileSync(f, 'utf8')); if (env.notarization) { console.log('already notarized: firstSeen ' + env.notarization.firstSeen); process.exitCode = 0; } + else { + const ethersDir = opt('--ethers-dir'); if (!ethersDir || !opt('--rpc') || !opt('--key-file')) throw new Error('--rpc, --key-file and --ethers-dir are required'); + const { ethers } = createRequire(path.join(path.resolve(ethersDir), 'package.json'))('ethers'); + const r = await notarizeEnvelope({ envelope: env, kind: 'aere-proof-of-data', rpc: opt('--rpc'), keyFile: opt('--key-file'), ethers, notary: opt('--notary') }); + if (!r.ok) { console.log(r.motiv); process.exitCode = r.cod || 1; } + else { fs.writeFileSync(f, JSON.stringify(r.envelope, null, 1)); const n = r.envelope.notarization; console.log(`notarized on chain ${n.chainId}: firstSeen ${n.firstSeen}${n.txHash ? ', tx ' + n.txHash + ' block ' + n.block : ' (already)'}`); } + } + } + else if (cmd === 'interogheaza') { + const db = deschide(dir); const campuri = opt('--campuri', ''); + scrie(interogheaza(db, a[2], { field: opt('--camp'), eq: J(opt('--eq')), from: J(opt('--de-la')), to: J(opt('--pana-la')), fields: campuri === '*' ? '*' : campuri.split(',').filter(Boolean), limit: Number(opt('--limita', 1000)) })); + } + else if (cmd === 'inregistrare') { const db = deschide(dir); const campuri = opt('--campuri', '*'); scrie(dovadaInregistrare(db, a[2], Number(a[3]), { fields: campuri === '*' ? '*' : campuri.split(',').filter(Boolean) })); } + else if (cmd === 'consistenta') { const db = deschide(dir); scrie(dovadaConsistenta(db, a[2], a[3])); } + else if (cmd === 'auditeaza') { const db = deschide(dir); const r = auditeaza(db); console.log(r.ok ? `AUDIT OK: ${db.versiuni.length} versions rebuilt from ${db.inregistrari.length} records, every root matches` : 'AUDIT FAILED:\n ' + r.probleme.join('\n ')); process.exitCode = r.ok ? 0 : 1; } + else if (cmd === 'serveste') { const s = await porneste({ dir, port: Number(opt('--port', 8797)), privat: are('--privat') }); console.log('serving ' + s.url + (are('--privat') ? ' (records without field values)' : '')); } + else { console.log('usage: vd.mjs creeaza|adauga|angajeaza|notarizeaza|interogheaza|inregistrare|consistenta|auditeaza|serveste ... (see the header)'); process.exitCode = 2; } +} catch (e) { console.log('ERROR: ' + String(e.message).slice(0, 300)); process.exitCode = 1; } diff --git a/verifiable-data/verifica.mjs b/verifiable-data/verifica.mjs new file mode 100644 index 0000000..778f4c3 --- /dev/null +++ b/verifiable-data/verifica.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +// verifica.mjs: the reader's side of a verifiable dataset, without the data (roadmap point 20). Everything is judged against a +// COMMITMENT (a version's JSON) and prints VALID / INVALID with the reasons; exit 0 VALID, 1 INVALID, 2 could not measure. +// node verifica.mjs raspuns --angajament a query answer: correct, complete, in order +// node verifica.mjs inregistrare --angajament one record with its proof +// node verifica.mjs consistenta --vechi --nou the newer version only appended +// node verifica.mjs disponibilitate --url --angajament [--esantioane 16] [--seed ] +// random records fetched and verified (retrievability) +// node verifica.mjs angajament --plic [--verify-proof ] [--rpc URL --chain 28001] +// the AIP-23 envelope is this commitment's; with --rpc, +// its notarization under a post-quantum certified anchor +import fs from 'node:fs'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { verificaRaspuns, verificaInregistrare, verificaConsistentaVersiuni, verificaPlicAngajament, esantioneaza, hashAngajament } from './baza.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const a = process.argv.slice(2); const cmd = a[0]; +const opt = (k, d = null) => { const i = a.indexOf(k); return i >= 0 && i + 1 < a.length ? a[i + 1] : d; }; +const citeste = (f) => JSON.parse(fs.readFileSync(f, 'utf8')); +const iesi = (verdict, motive = [], extra = '') => { console.log(`${verdict}${extra ? ' ' + extra : ''}${motive.length ? '\n ' + motive.join('\n ') : ''}`); process.exitCode = verdict === 'VALID' ? 0 : verdict === 'INVALID' ? 1 : 2; }; +const ruleaza = (verifyProof, envFile, { rpc, chain }) => new Promise((ok) => { + const args = [verifyProof, envFile, '--json', ...(rpc ? ['--rpc', rpc] : []), ...(chain ? ['--chain', String(chain)] : [])]; + const c = spawn(process.execPath, args); let out = ''; c.stdout.on('data', (d) => { out += d; }); c.stderr.on('data', () => {}); + const ceas = setTimeout(() => c.kill(), 300000); c.on('error', () => { clearTimeout(ceas); ok(null); }); c.on('close', () => { clearTimeout(ceas); try { ok(JSON.parse(out)); } catch { ok(null); } }); +}); + +async function main() { + if (cmd === 'raspuns') { const r = verificaRaspuns(citeste(a[1]), citeste(opt('--angajament'))); return iesi(r.verdict, r.motive, `(${r.rows} rows)`); } + if (cmd === 'inregistrare') { const r = verificaInregistrare(citeste(a[1]), citeste(opt('--angajament'))); return iesi(r.ok ? 'VALID' : 'INVALID', r.motive); } + if (cmd === 'consistenta') { const r = verificaConsistentaVersiuni(citeste(a[1]), citeste(opt('--vechi')), citeste(opt('--nou'))); return iesi(r.ok ? 'VALID' : 'INVALID', r.motive); } + if (cmd === 'disponibilitate') { + const url = opt('--url'); const c = citeste(opt('--angajament')); if (!url) throw new Error('--url is required'); + const ia = async (u) => { const r = await fetch(u, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(20000) }); if (!r.ok) return null; return r.json(); }; + const r = await esantioneaza({ url, commitment: c, k: Number(opt('--esantioane', 16)), seed: opt('--seed', null), ia }); + return iesi(r.ok ? 'VALID' : 'INVALID', r.motive, `(${r.verificate} of ${r.cerute} sampled records verified, seed ${r.seed}${r.lipsa.length ? '; not served: ' + r.lipsa.join(',') : ''})`); + } + if (cmd === 'angajament') { + const c = citeste(a[1]); const env = citeste(opt('--plic')); + if (!verificaPlicAngajament(env, c)) return iesi('INVALID', ['the envelope is not this commitment\'s (sha256/name differ, or not an aere-proof-of-data attestation)']); + const vp = opt('--verify-proof', process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs')); + if (!fs.existsSync(vp)) return iesi('UNMEASURED', ['the AIP-23 verifier is not at ' + vp + ' (AERE_VERIFY_PROOF or --verify-proof)'], `commitment ${hashAngajament(c)} is the envelope's`); + const v = await ruleaza(vp, opt('--plic'), { rpc: opt('--rpc'), chain: opt('--chain') }); + if (!v) return iesi('UNMEASURED', ['the verifier gave no JSON']); + const fin = (v.levels || []).find((l) => l.level === 'finality'); + const extra = `envelope ${v.verdict}; finality ${fin ? fin.state + ': ' + String(fin.detail).slice(0, 160) : 'absent'}`; + if (v.verdict !== 'VALID') return iesi('INVALID', [], extra); + if (opt('--rpc') && !(fin && fin.state === 'PASSED')) return iesi(fin && fin.state === 'FAILED' ? 'INVALID' : 'UNMEASURED', [], extra); + return iesi('VALID', [], extra); + } + console.log('usage: verifica.mjs raspuns|inregistrare|consistenta|disponibilitate|angajament ... (see the header)'); process.exitCode = 2; +} +try { await main(); } catch (e) { iesi('UNMEASURED', [String(e.message).slice(0, 200)]); }