#!/usr/bin/env node 'use strict'; /* * verifica-certificat.mjs - verifies an AERE post-quantum notarization certificate. * * THE POINT OF THIS FILE. A notarization is worth exactly as much as an outsider's ability to * check it without asking the notary. So this verifier: * - installs nothing (no npm, no dependencies; the Keccak it needs is inlined below), * - talks only to a public read endpoint, and accepts a DIFFERENT one via --rpc, so you can * point it at a node you run yourself and never trust ours, * - re-derives every link of the chain from the certificate and the public chain, and prints * each link as PASS, FAIL or NOT MEASURED. NOT MEASURED is never counted as a pass. * * WHAT IT CHECKS, and each is a separate claim: * 1. document the file you hold hashes to the digest the certificate names. * 2. on chain the transaction named by the certificate exists, succeeded, and called the * attestation registry the certificate names. * 3. in the block that transaction sits in the block the certificate names, and the block hash * served by the chain equals the one in the certificate. * 4. post-quantum the attestation was recorded only because a NIST post-quantum signature * verified ON CHAIN, inside the transaction, via a live precompile. Re-checked * here by calling the registry's own free read method. * 5. sealed the block carrying it is covered by the chain's post-quantum validator * certificate, so rewriting that block later needs post-quantum forgeries, not * just a broken elliptic curve. * * Usage: * node verifica-certificat.mjs certificat.json # checks links 2-5 * node verifica-certificat.mjs certificat.json --fisier a.pdf # also checks link 1 * node verifica-certificat.mjs certificat.json --rpc https://your-own-node * * Exit: 0 every measured link passed, 1 something failed, 2 nothing could be measured. */ import fs from 'node:fs'; // ---------- Keccak-256, inlined so this file needs nothing installed ---------- const RC = [1n,0x8082n,0x800000000000808an,0x8000000080008000n,0x808bn,0x80000001n,0x8000000080008081n, 0x8000000000008009n,0x8an,0x88n,0x80008009n,0x8000000an,0x8000808bn,0x800000000000008bn,0x8000000000008089n, 0x8000000000008003n,0x8000000000008002n,0x8000000000000080n,0x800an,0x800000008000000an,0x8000000080008081n, 0x8000000000008080n,0x80000001n,0x8000000080008008n]; const R = [0,1,62,28,27,36,44,6,55,20,3,10,43,25,39,41,45,15,21,8,18,2,61,56,14]; const M = (1n << 64n) - 1n; const rotl = (x, n) => n === 0 ? x : ((x << BigInt(n)) | (x >> BigInt(64 - n))) & M; function keccakF(A) { for (let r = 0; r < 24; r++) { const C = new Array(5); for (let x = 0; x < 5; x++) C[x] = A[x] ^ A[x + 5] ^ A[x + 10] ^ A[x + 15] ^ A[x + 20]; for (let x = 0; x < 5; x++) { const D = C[(x + 4) % 5] ^ rotl(C[(x + 1) % 5], 1); for (let y = 0; y < 25; y += 5) A[x + y] ^= D; } const B = new Array(25); for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl(A[x + 5 * y], R[x + 5 * y]); for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) A[x + 5 * y] = B[x + 5 * y] ^ ((~B[(x + 1) % 5 + 5 * y] & M) & B[(x + 2) % 5 + 5 * y]); A[0] ^= RC[r]; } return A; } function keccak256(bytes) { const rate = 136; const pad = new Uint8Array(rate - (bytes.length % rate)); pad[0] = 0x01; pad[pad.length - 1] |= 0x80; const msg = new Uint8Array(bytes.length + pad.length); msg.set(bytes); msg.set(pad, bytes.length); let A = new Array(25).fill(0n); for (let off = 0; off < msg.length; off += rate) { for (let i = 0; i < rate / 8; i++) { let w = 0n; for (let b = 7; b >= 0; b--) w = (w << 8n) | BigInt(msg[off + i * 8 + b]); A[i] ^= w; } A = keccakF(A); } const out = new Uint8Array(32); for (let i = 0; i < 4; i++) { let w = A[i]; for (let b = 0; b < 8; b++) { out[i * 8 + b] = Number(w & 0xffn); w >>= 8n; } } return out; } const hex = (u8) => '0x' + Array.from(u8).map((b) => b.toString(16).padStart(2, '0')).join(''); const octeti = (h) => { const s = String(h || '').replace(/^0x/, ''); const u = new Uint8Array(s.length / 2); for (let i = 0; i < u.length; i++) u[i] = parseInt(s.substr(i * 2, 2), 16); return u; }; // First item of an RLP list, which for a QBFT extraData is vanityData. This is the ONLY field in // the header that a post-quantum anchor changes and that keccak covers, so it is the field that // tells an anchored header from a merely seal-heavy one. Byte length alone cannot: attaching // seals and anchoring a digest are two different switches, and a header can be large because of // the first while the second never turned on. function primulElementRLP(u8) { if (!u8.length) return null; let i = 0; const b = u8[0]; if (b >= 0xc0 && b <= 0xf7) i = 1; else if (b >= 0xf8) i = 1 + (b - 0xf7); else return null; // not a list if (i >= u8.length) return null; const c = u8[i]; if (c <= 0x7f) return u8.slice(i, i + 1); if (c >= 0x80 && c <= 0xb7) { const n = c - 0x80; return u8.slice(i + 1, i + 1 + n); } if (c >= 0xb8 && c <= 0xbf) { const k = c - 0xb7; let n = 0; for (let j = 0; j < k; j++) n = n * 256 + u8[i + 1 + j]; return u8.slice(i + 1 + k, i + 1 + k + n); } return null; // nested list where a string was expected } const areTextASCII = (u8, s) => { const t = Array.from(u8).map((x) => String.fromCharCode(x)).join(''); return t.includes(s); }; // ---------- report, three verdicts only ---------- const linii = []; const PASS = (id, ce, obs) => linii.push({ id, ce, obs, v: 'PASS' }); const FAIL = (id, ce, obs) => linii.push({ id, ce, obs, v: 'FAIL' }); const NM = (id, ce, de_ce) => linii.push({ id, ce, obs: de_ce, v: 'NOT MEASURED' }); const arg = (n, d) => { const i = process.argv.indexOf(n); return i > 0 && i + 1 < process.argv.length ? process.argv[i + 1] : d; }; const CALE = process.argv[2]; const RPC = arg('--rpc', 'https://rpc.aere.network'); const FISIER = arg('--fisier', null); async function rpc(method, params) { const r = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json', connection: 'close' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(20000) }); if (!r.ok) throw new Error('HTTP ' + r.status); const j = await r.json(); if (j.error) throw new Error(JSON.stringify(j.error)); return j.result; } (async () => { if (!CALE || !fs.existsSync(CALE)) { console.error('folosire: node verifica-certificat.mjs certificat.json [--fisier document] [--rpc URL]'); process.exitCode = 2; return; } const c = JSON.parse(fs.readFileSync(CALE, 'utf8')); console.log('AERE post-quantum notarization, independent verification'); console.log('certificate : ' + CALE); console.log('read node : ' + RPC + (RPC.includes('aere.network') ? ' (point --rpc at your own node; this check does not need ours)' : ' (not ours: good)')); console.log(''); // 1. the document itself if (FISIER) { if (!fs.existsSync(FISIER)) NM('document', 'the file you hold hashes to the digest in the certificate', 'file not found: ' + FISIER); else { const h = hex(keccak256(new Uint8Array(fs.readFileSync(FISIER)))); if (h.toLowerCase() === String(c.documentHash || '').toLowerCase()) PASS('document', 'the file you hold hashes to the digest in the certificate', h); else FAIL('document', 'the file you hold hashes to the digest in the certificate', 'file is ' + h + ', certificate says ' + c.documentHash); } } else NM('document', 'the file you hold hashes to the digest in the certificate', 'no --fisier given, so the document side was not checked'); // 2. the transaction // // The distinction below is the whole honesty of this tool. "The node did not answer" and "the // node answered, and there is no such transaction" are different facts, and only the second one // is about the certificate. Conflating them means a network problem on our side gets reported to // the holder as "your certificate is a forgery", which is both false and the worst possible // failure mode for a notarization product. let rc = null, tx = null, aRaspuns = false; try { [rc, tx] = await Promise.all([rpc('eth_getTransactionReceipt', [c.txHash]), rpc('eth_getTransactionByHash', [c.txHash])]); aRaspuns = true; } catch (e) { NM('on-chain', 'the transaction exists, succeeded, and called the named registry', 'the node did not answer (' + e.message + '), so NOTHING is claimed about this certificate. Try another --rpc, ideally one you operate.'); } if (aRaspuns && rc && tx) { const ok = rc.status === '0x1'; const catre = (tx.to || '').toLowerCase() === String(c.registry || '').toLowerCase(); if (ok && catre) PASS('on-chain', 'the transaction exists, succeeded, and called the named registry', c.txHash); else FAIL('on-chain', 'the transaction exists, succeeded, and called the named registry', (ok ? '' : 'status not success. ') + (catre ? '' : 'called ' + tx.to + ', certificate names ' + c.registry)); } else if (aRaspuns) { // The node answered and said there is no such transaction. That IS a statement about the // certificate, so it is a failure and not an unmeasured line. FAIL('on-chain', 'the transaction exists, succeeded, and called the named registry', 'the node answered and holds no such transaction on this chain'); } // 3. the block if (rc) { try { const blk = await rpc('eth_getBlockByNumber', [rc.blockNumber, false]); const nr = parseInt(rc.blockNumber, 16); const potrivit = blk && blk.hash.toLowerCase() === String(c.blockHash || '').toLowerCase() && nr === Number(c.blockNumber); if (potrivit) PASS('in-block', 'the transaction sits in the block the certificate names, and the chain serves the same block hash', 'block ' + nr.toLocaleString('en-US')); else FAIL('in-block', 'the transaction sits in the block the certificate names', 'chain says block ' + nr + ' hash ' + (blk && blk.hash) + ', certificate says ' + c.blockNumber + ' / ' + c.blockHash); } catch (e) { NM('in-block', 'the block matches the certificate', e.message); } } // 4. the post-quantum signature, re-checked live try { // isValidAttestation(uint256 keyId, uint64 nonce, bytes32 messageHash) -> bool. Signature read // from the contract source, not guessed: the middle argument is uint64, and a wrong ABI here // reverts in a way that looks exactly like an invalid attestation. const sel = hex(keccak256(new TextEncoder().encode('isValidAttestation(uint256,uint64,bytes32)'))).slice(0, 10); const pad = (v) => BigInt(v).toString(16).padStart(64, '0'); const data = sel + pad(c.keyId ?? 0) + pad(c.nonce ?? c.attestationIndex ?? 0) + String(c.documentHash || '').replace(/^0x/, '').padStart(64, '0'); const r = await rpc('eth_call', [{ to: c.registry, data }, 'latest']); const adevarat = r && /1$/.test(r.replace(/0+$/, '') ) === false ? BigInt(r) === 1n : BigInt(r || '0x0') === 1n; if (adevarat) PASS('post-quantum', 'the registry still reports this attestation as valid, and it was recorded only because a NIST post-quantum signature verified ON CHAIN inside the transaction', 'isValidAttestation = true'); else FAIL('post-quantum', 'the registry reports this attestation as valid', 'isValidAttestation returned ' + r); } catch (e) { NM('post-quantum', 'the registry still reports this attestation as valid', 'call failed: ' + e.message); } // 5. the block is under the chain's post-quantum validator certificate // // NOTHING HERE IS A WRITTEN-IN CONSTANT ANY MORE, and that is the point. This check used to // carry `anchorInterval || 32` and `plainHeaderBytes || 634` as fallbacks, plus a fixed // "header must exceed base + 600 bytes" threshold. On 2026-08-09 five gates on this chain went // wrong in one day for exactly that reason: the validator set grew from seven to nine, the // plain header base moved from 525 to 634, and every gate holding the old number reported a // perfect fleet or an empty one. So: the schedule comes from the certificate or the link is // NOT MEASURED, never from yesterday's default; and the plain-header baseline is measured now, // from a neighbouring non-anchor header on the same chain. // // And the discriminator is vanityData, not size. Attaching seals and anchoring a digest are two // separate switches; a size-only test passes on a fleet where anchoring never started. if (rc) { try { const nr = parseInt(rc.blockNumber, 16); const ancora = Number(c.anchorInterval), start = Number(c.anchorBlock); if (!Number.isFinite(ancora) || !Number.isFinite(start) || ancora <= 0) { NM('sealed', 'the block is covered by the chain post-quantum validator certificate', 'this certificate carries no anchorBlock / anchorInterval, and this check will not fall back to a hard-coded schedule: those numbers change when the validator set changes. Re-issue the certificate with them, or verify the anchor by hand.'); } else { const urm = nr <= start ? start : start + Math.ceil((nr - start) / ancora) * ancora; const blk = await rpc('eth_getBlockByNumber', ['0x' + urm.toString(16), false]); if (!blk) NM('sealed', 'the block is covered by the chain post-quantum validator certificate', 'the covering anchor at ' + urm.toLocaleString('en-US') + ' is not on chain yet; re-run later'); else { const vanity = primulElementRLP(octeti(blk.extraData)); const oct = (blk.extraData.length - 2) / 2; // Measured baseline: the header right before the anchor is not an anchor height, so its // size is what a plain header costs on this chain today. let de_baza = null; try { const vecin = await rpc('eth_getBlockByNumber', ['0x' + (urm - 1).toString(16), false]); if (vecin) de_baza = (vecin.extraData.length - 2) / 2; } catch { /* baseline stays unmeasured; the vanityData test below does not need it */ } const marime = de_baza === null ? 'header ' + oct + ' bytes (plain-header baseline NOT MEASURED: the neighbouring header did not load)' : 'header ' + oct + ' bytes against a measured plain header of ' + de_baza + ' bytes at ' + (urm - 1).toLocaleString('en-US') + ', so ' + (oct - de_baza) + ' bytes of validator certificate'; if (!vanity) { NM('sealed', 'the block is covered by the chain post-quantum validator certificate', 'extraData at anchor ' + urm.toLocaleString('en-US') + ' did not decode as an RLP list, so nothing is claimed either way'); } else if (vanity.length === 32 && !areTextASCII(vanity, 'besu')) { PASS('sealed', 'the anchor at or after this block carries a post-quantum validator certificate whose digest is inside the block hash preimage, so rewriting this block later needs post-quantum forgeries', 'anchor ' + urm.toLocaleString('en-US') + ', vanityData is a 32-byte digest ' + hex(vanity) + '; ' + marime); } else { FAIL('sealed', 'the anchor at or after this block carries a post-quantum validator certificate', 'anchor ' + urm.toLocaleString('en-US') + ' carries ordinary vanityData (' + vanity.length + ' bytes' + (areTextASCII(vanity, 'besu') ? ", client string containing 'besu'" : '') + '), not an anchor digest; ' + marime); } } } } catch (e) { NM('sealed', 'the block is covered by the chain post-quantum validator certificate', e.message); } } // ---------- verdict ---------- console.log(''); for (const l of linii) console.log(' [' + l.v.padEnd(12) + '] ' + l.id.padEnd(13) + ' ' + l.ce + '\n' + ' '.repeat(32) + l.obs); const f = linii.filter((l) => l.v === 'FAIL').length, p = linii.filter((l) => l.v === 'PASS').length, n = linii.filter((l) => l.v === 'NOT MEASURED').length; console.log('\n PASS=' + p + ' FAIL=' + f + ' NOT MEASURED=' + n); if (f > 0) { console.log('\nVERDICT: this certificate does NOT hold up. See the FAIL lines above.'); process.exitCode = 1; return; } if (p === 0) { console.log('\nVERDICT: nothing could be measured. That is not a pass.'); process.exitCode = 2; return; } console.log('\nVERDICT: every measured link holds. NOT MEASURED lines are not confirmations, and they say why.'); process.exitCode = 0; return; })().catch((e) => { console.error('ERROR: ' + e.message); process.exitCode = 2; });