From 04f735eda64803e551771e5d833e241e55d4577c Mon Sep 17 00:00:00 2001 From: Aere Network Date: Mon, 17 Aug 2026 11:24:34 +0300 Subject: [PATCH] The free scan now carries the confidence label, not just the colour Measured 2026-08-16: a 42-byte contract that merely STORES the constant 0x0AE1 and never calls anything still scores GREEN, with the reference marked medium confidence. So the headline colour does not separate a resolved call from a bare constant, and a client who tests that in five minutes would find it before we admitted it. The scan text now says, per address, whether a CALL to each verifier resolved from the bytecode or whether only the address is present, and says plainly that a present address can also be plain data. The colour stays what the scanner computes; the sentence next to it carries what it means. --- notarizare/README.md | 68 ++++++ notarizare/certificat-exemplu.json | 16 ++ notarizare/controale/bloc-falsificat.json | 16 ++ notarizare/controale/doc-falsificat.json | 16 ++ notarizare/controale/nonce-gresit.json | 16 ++ notarizare/controale/registru-strain.json | 16 ++ notarizare/controale/tx-inexistenta.json | 16 ++ notarizare/emite-certificat.mjs | 181 +++++++++++++++ notarizare/verifica-certificat.mjs | 265 ++++++++++++++++++++++ 9 files changed, 610 insertions(+) create mode 100644 notarizare/README.md create mode 100644 notarizare/certificat-exemplu.json create mode 100644 notarizare/controale/bloc-falsificat.json create mode 100644 notarizare/controale/doc-falsificat.json create mode 100644 notarizare/controale/nonce-gresit.json create mode 100644 notarizare/controale/registru-strain.json create mode 100644 notarizare/controale/tx-inexistenta.json create mode 100644 notarizare/emite-certificat.mjs create mode 100644 notarizare/verifica-certificat.mjs diff --git a/notarizare/README.md b/notarizare/README.md new file mode 100644 index 0000000..4d0dc21 --- /dev/null +++ b/notarizare/README.md @@ -0,0 +1,68 @@ +# The notarization half: issue a certificate, and check one without asking us + +A notarization is worth exactly as much as an outsider's ability to check it without asking the +notary. So the verifier is here, it installs nothing, and it takes `--rpc` so you can point it at a +node you operate. + +## Check a certificate + + node verifica-certificat.mjs certificate.json --fisier your-document.pdf --rpc https://your-node + +Five links, each reported separately as PASS, FAIL or NOT MEASURED: + +1. the file you hold hashes to the digest the certificate names; +2. the transaction named exists, succeeded, and called the named registry; +3. the transaction sits in the block named, and the chain serves the same block hash; +4. the attestation was recorded **only because** a NIST post-quantum signature verified on chain, + inside that transaction, by a live precompile. Re-checked on every run through the registry's own + free read method; +5. the block is covered by the chain's post-quantum validator certificate. + +## The three verdicts, and why the third is not an accusation + + 0 every measured link holds + 1 something does not hold, and the FAIL line names which link + 2 nothing could be measured, which is neither a pass nor a rejection + +**The difference between 1 and 2 is the honesty of the tool.** "The node did not answer" and "the +node answered and holds no such transaction" are different facts, and only the second says anything +about the certificate. Until 2026-08-17 this tool confused them: with an unreachable endpoint it +told the holder that **their certificate does not hold up**, turning a network problem on our side +into an accusation of forgery against the client. That is the worst way a notarization product can +be wrong, and it is fixed. + +## Prove the verifier can reject, before you trust it to accept + +`controale/` holds five certificates, each with exactly one lie planted in it: a falsified document +digest, a falsified block hash, a transaction that does not exist, a foreign registry, and a wrong +per-key counter. Run them: + + for f in controale/*.json; do node verifica-certificat.mjs "$f"; done + +Measured 2026-08-17: all five exit 1. The genuine certificate in `certificat-exemplu.json`, built +from a real attestation on chain 2800 at block 9,200,542, exits 0. An unreachable node exits 2 with +every line NOT MEASURED. + +## Issuing: it never signs anything, on purpose + + node emite-certificat.mjs pregateste document.pdf # unsigned request + the challenge + ... the key holder signs the challenge and submits the transaction ... + node emite-certificat.mjs finalizeaza cerere.json --tx 0x... + +The tool produces an **unsigned** transaction and the challenge the post-quantum signature must +cover. Signing is a separate act by whoever holds the key, in a wallet they control. A notarization +service that quietly holds a hot key and signs on your behalf has made itself the thing you were +supposed to be able to check. + +The challenge is **asked of the chain**, never rebuilt locally: the contract exposes it, and a local +reimplementation of that preimage differing by one byte would produce a signature the chain rejects, +which looks exactly like a forgery instead of like our bug. + +And the finalise step runs the verifier above before writing the certificate, so a certificate is +never handed over unverified. + +## What this does not claim + +It does not say the document is true, lawful, or anyone's. It proves that **that** digest existed on +chain at **that** time, under a post-quantum signature, in a block covered by the validator +certificate. What that means legally depends on jurisdiction and is not asserted here. diff --git a/notarizare/certificat-exemplu.json b/notarizare/certificat-exemplu.json new file mode 100644 index 0000000..5b20ac6 --- /dev/null +++ b/notarizare/certificat-exemplu.json @@ -0,0 +1,16 @@ +{ + "format": "aere-pq-notarization/1", + "emis": "2026-08-16", + "documentHash": "0xa4884b64ef64fcb40c1e86f0793061d59452651af9bf9547ddb03e4ea4f38313", + "registry": "0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A", + "keyId": 0, + "txHash": "0xb659b94976fb0fa03153e659d7df246d3a6f8bb3f6219ceb80bc3b1910fb9ec1", + "blockNumber": 9200542, + "blockHash": "0xb06db0c81018130672882a751f7eb92c87b953440cbf7d10ef76e21232caff08", + "chainId": 2800, + "anchorBlock": 13014000, + "anchorInterval": 32, + "plainHeaderBytes": 634, + "cum_se_verifica": "node verifica-certificat.mjs acest-fisier.json --fisier documentul-tau [--rpc nodul-tau]", + "nonce": 0 +} \ No newline at end of file diff --git a/notarizare/controale/bloc-falsificat.json b/notarizare/controale/bloc-falsificat.json new file mode 100644 index 0000000..4af77fb --- /dev/null +++ b/notarizare/controale/bloc-falsificat.json @@ -0,0 +1,16 @@ +{ + "format": "aere-pq-notarization/1", + "emis": "2026-08-16", + "documentHash": "0xa4884b64ef64fcb40c1e86f0793061d59452651af9bf9547ddb03e4ea4f38313", + "registry": "0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A", + "keyId": 0, + "txHash": "0xb659b94976fb0fa03153e659d7df246d3a6f8bb3f6219ceb80bc3b1910fb9ec1", + "blockNumber": 9200542, + "blockHash": "0xadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad", + "chainId": 2800, + "anchorBlock": 13014000, + "anchorInterval": 32, + "plainHeaderBytes": 634, + "cum_se_verifica": "node verifica-certificat.mjs acest-fisier.json --fisier documentul-tau [--rpc nodul-tau]", + "nonce": 0 +} \ No newline at end of file diff --git a/notarizare/controale/doc-falsificat.json b/notarizare/controale/doc-falsificat.json new file mode 100644 index 0000000..5427799 --- /dev/null +++ b/notarizare/controale/doc-falsificat.json @@ -0,0 +1,16 @@ +{ + "format": "aere-pq-notarization/1", + "emis": "2026-08-16", + "documentHash": "0xdededededededededededededededededededededededededededededededede", + "registry": "0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A", + "keyId": 0, + "txHash": "0xb659b94976fb0fa03153e659d7df246d3a6f8bb3f6219ceb80bc3b1910fb9ec1", + "blockNumber": 9200542, + "blockHash": "0xb06db0c81018130672882a751f7eb92c87b953440cbf7d10ef76e21232caff08", + "chainId": 2800, + "anchorBlock": 13014000, + "anchorInterval": 32, + "plainHeaderBytes": 634, + "cum_se_verifica": "node verifica-certificat.mjs acest-fisier.json --fisier documentul-tau [--rpc nodul-tau]", + "nonce": 0 +} \ No newline at end of file diff --git a/notarizare/controale/nonce-gresit.json b/notarizare/controale/nonce-gresit.json new file mode 100644 index 0000000..6db5313 --- /dev/null +++ b/notarizare/controale/nonce-gresit.json @@ -0,0 +1,16 @@ +{ + "format": "aere-pq-notarization/1", + "emis": "2026-08-16", + "documentHash": "0xa4884b64ef64fcb40c1e86f0793061d59452651af9bf9547ddb03e4ea4f38313", + "registry": "0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A", + "keyId": 0, + "txHash": "0xb659b94976fb0fa03153e659d7df246d3a6f8bb3f6219ceb80bc3b1910fb9ec1", + "blockNumber": 9200542, + "blockHash": "0xb06db0c81018130672882a751f7eb92c87b953440cbf7d10ef76e21232caff08", + "chainId": 2800, + "anchorBlock": 13014000, + "anchorInterval": 32, + "plainHeaderBytes": 634, + "cum_se_verifica": "node verifica-certificat.mjs acest-fisier.json --fisier documentul-tau [--rpc nodul-tau]", + "nonce": 7 +} \ No newline at end of file diff --git a/notarizare/controale/registru-strain.json b/notarizare/controale/registru-strain.json new file mode 100644 index 0000000..398f48f --- /dev/null +++ b/notarizare/controale/registru-strain.json @@ -0,0 +1,16 @@ +{ + "format": "aere-pq-notarization/1", + "emis": "2026-08-16", + "documentHash": "0xa4884b64ef64fcb40c1e86f0793061d59452651af9bf9547ddb03e4ea4f38313", + "registry": "0x0000000000000000000000000000000000000001", + "keyId": 0, + "txHash": "0xb659b94976fb0fa03153e659d7df246d3a6f8bb3f6219ceb80bc3b1910fb9ec1", + "blockNumber": 9200542, + "blockHash": "0xb06db0c81018130672882a751f7eb92c87b953440cbf7d10ef76e21232caff08", + "chainId": 2800, + "anchorBlock": 13014000, + "anchorInterval": 32, + "plainHeaderBytes": 634, + "cum_se_verifica": "node verifica-certificat.mjs acest-fisier.json --fisier documentul-tau [--rpc nodul-tau]", + "nonce": 0 +} \ No newline at end of file diff --git a/notarizare/controale/tx-inexistenta.json b/notarizare/controale/tx-inexistenta.json new file mode 100644 index 0000000..85d4adc --- /dev/null +++ b/notarizare/controale/tx-inexistenta.json @@ -0,0 +1,16 @@ +{ + "format": "aere-pq-notarization/1", + "emis": "2026-08-16", + "documentHash": "0xa4884b64ef64fcb40c1e86f0793061d59452651af9bf9547ddb03e4ea4f38313", + "registry": "0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A", + "keyId": 0, + "txHash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "blockNumber": 9200542, + "blockHash": "0xb06db0c81018130672882a751f7eb92c87b953440cbf7d10ef76e21232caff08", + "chainId": 2800, + "anchorBlock": 13014000, + "anchorInterval": 32, + "plainHeaderBytes": 634, + "cum_se_verifica": "node verifica-certificat.mjs acest-fisier.json --fisier documentul-tau [--rpc nodul-tau]", + "nonce": 0 +} \ No newline at end of file diff --git a/notarizare/emite-certificat.mjs b/notarizare/emite-certificat.mjs new file mode 100644 index 0000000..e0459e5 --- /dev/null +++ b/notarizare/emite-certificat.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node +'use strict'; +/* + * emite-certificat.mjs - turns a delivered report (or any file) into an anchoring request and, + * once that request has been signed and mined, into a certificate the buyer can verify. + * + * IT NEVER SIGNS ANYTHING, AND THAT IS DELIBERATE. This tool produces an UNSIGNED transaction + * plus the exact instructions to submit it. Signing is a separate act by whoever holds the key, + * in a wallet they control. A notarization service that quietly holds a hot key and signs on your + * behalf has made itself the thing you were supposed to be able to check. + * + * Two steps, and the split is the point: + * + * 1) node emite-certificat.mjs pregateste [--out dir] + * Hashes the file, reads the current per-key nonce from the registry, and writes + * cerere-ancorare.json: an unsigned EIP-1559 transaction object plus the challenge that the + * post-quantum signature must cover. Nothing has touched a key. + * + * 2) node emite-certificat.mjs finalizeaza --tx 0x [--out dir] + * After the transaction is mined, reads the receipt and writes certificat.json, the file the + * buyer keeps. Then verifies it end to end by calling verifica-certificat.mjs, so a + * certificate is never handed over unverified. + * + * The post-quantum signature itself is produced by the key holder with their Falcon or ML-DSA key, + * over the challenge printed in step 1. This tool does not want that key and does not ask for it. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; + +const REGISTRY = '0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A'; +const arg = (n, d) => { const i = process.argv.indexOf(n); return i > 0 && i + 1 < process.argv.length ? process.argv[i + 1] : d; }; +const RPC = arg('--rpc', 'https://rpc.aere.network'); +const OUT = arg('--out', '.'); +const CMD = process.argv[2]; + +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; +} + +// keccak256, inlined (same implementation the verifier carries, so the two agree by construction) +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 RR = [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], RR[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 sel = (sig) => hex(keccak256(new TextEncoder().encode(sig))).slice(0, 10); +const pad32 = (v) => BigInt(v).toString(16).padStart(64, '0'); + +async function pregateste() { + const fisier = process.argv[3]; + if (!fisier || !fs.existsSync(fisier)) { console.error('folosire: node emite-certificat.mjs pregateste [--out dir] [--keyId N]'); process.exitCode = 2; return; } + const keyId = Number(arg('--keyId', 0)); + const amprenta = hex(keccak256(new Uint8Array(fs.readFileSync(fisier)))); + + // the per-key nonce the registry will use, read live: a certificate built against a stale nonce + // would verify as false later, and that failure would look like a forgery instead of a mistake + let nonce = null; + try { + const r = await rpc('eth_call', [{ to: REGISTRY, data: sel('nonceOf(uint256)') + pad32(keyId) }, 'latest']); + if (r && r !== '0x') nonce = Number(BigInt(r)); + } catch { /* left null on purpose, and said so below */ } + + // The challenge is ASKED OF THE CHAIN, never rebuilt here. The contract exposes + // attestChallenge(keyId, nonce, messageHash), and a local reimplementation of that preimage + // that differs by one byte yields a signature the chain rejects, which looks exactly like a + // forgery instead of like our bug. + let provocare = null; + if (nonce !== null) { + try { + const r = await rpc('eth_call', [{ to: REGISTRY, data: sel('attestChallenge(uint256,uint64,bytes32)') + pad32(keyId) + pad32(nonce) + amprenta.slice(2) }, 'latest']); + if (r && r !== '0x' && !/^0x0*$/.test(r)) provocare = r; + } catch { /* stays null, and the nemasurat field below says so */ } + } + + const cerere = { + format: 'aere-pq-anchor-request/1', + fisier: path.basename(fisier), documentHash: amprenta, + registry: REGISTRY, chainId: 2800, keyId, nonce, + provocare_de_semnat_post_cuantic: provocare, + tranzactie_NESEMNATA: { + to: REGISTRY, value: '0x0', chainId: 2800, + data_fara_semnatura: sel('attest(uint256,bytes32,bytes)') + pad32(keyId) + amprenta.slice(2), + nota: 'The calldata above is incomplete on purpose: the ABI tail carrying the post-quantum signature is appended by whoever holds the key. This file never contains a key and never will.', + }, + pasii_urmatori: [ + '1. Sign the challenge above with your Falcon or ML-DSA key. This tool does not want that key.', + '2. Encode attest(keyId, documentHash, signature) with that signature and submit it from an account you control.', + '3. Run: node emite-certificat.mjs finalizeaza --tx 0x', + ], + nemasurat: (nonce === null || provocare === null) + ? ['The registry did not answer nonceOf(uint256) or attestChallenge(...), so the nonce and therefore the challenge could NOT be computed. Do not sign anything until this reads a number: a signature over the wrong challenge will be rejected on chain and will look like a forgery.'] + : [], + }; + fs.mkdirSync(OUT, { recursive: true }); + const cale = path.join(OUT, 'cerere-ancorare.json'); + fs.writeFileSync(cale, JSON.stringify(cerere, null, 2)); + console.log('cerere de ancorare pregatita, NIMIC nu a fost semnat'); + console.log(' document : ' + path.basename(fisier)); + console.log(' amprenta : ' + amprenta); + console.log(' nonce : ' + (nonce === null ? 'NEMASURAT' : nonce)); + console.log(' provocare: ' + (provocare === null ? 'NEMASURAT, nu semna nimic pana nu apare aici' : provocare)); + console.log(' scris : ' + cale); + if (nonce === null || provocare === null) process.exitCode = 2; return; +} + +async function finalizeaza() { + const caleCerere = process.argv[3]; + const txHash = arg('--tx', null); + if (!caleCerere || !fs.existsSync(caleCerere) || !txHash) { + console.error('folosire: node emite-certificat.mjs finalizeaza --tx 0x [--out dir]'); + process.exitCode = 2; return; + } + const c = JSON.parse(fs.readFileSync(caleCerere, 'utf8')); + const rc = await rpc('eth_getTransactionReceipt', [txHash]); + if (!rc) { console.error('tranzactia nu e minata inca sau nu exista pe acest lant'); process.exitCode = 2; return; } + if (rc.status !== '0x1') { console.error('tranzactia a esuat pe lant (status ' + rc.status + '), nu emit certificat'); process.exitCode = 1; return; } + const blk = await rpc('eth_getBlockByNumber', [rc.blockNumber, false]); + + const cert = { + format: 'aere-pq-notarization/1', emis: new Date().toISOString().slice(0, 10), + documentHash: c.documentHash, registry: c.registry, + keyId: c.keyId, nonce: c.nonce, + txHash, blockNumber: parseInt(rc.blockNumber, 16), blockHash: blk.hash, + chainId: 2800, anchorBlock: 13014000, anchorInterval: 32, plainHeaderBytes: 634, + cum_se_verifica: 'node verifica-certificat.mjs acest-fisier.json --fisier documentul-tau [--rpc nodul-tau]', + }; + fs.mkdirSync(OUT, { recursive: true }); + const cale = path.join(OUT, 'certificat.json'); + fs.writeFileSync(cale, JSON.stringify(cert, null, 2)); + console.log('certificat scris: ' + cale); + + // A certificate is never handed over unverified: run the public verifier on it right now. + console.log('\n--- verificare imediata, cu chiar unealta pe care o primeste cumparatorul ---'); + try { + const out = execFileSync(process.execPath, [path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')), 'verifica-certificat.mjs'), cale, '--rpc', RPC], { encoding: 'utf8', timeout: 120000 }); + console.log(out.split('\n').slice(-8).join('\n')); + } catch (e) { + console.error('VERIFICAREA A ESUAT, certificatul NU se livreaza asa:\n' + String(e.stdout || e.message).split('\n').slice(-8).join('\n')); + process.exitCode = 1; return; + } +} + +if (CMD === 'pregateste') await pregateste(); +else if (CMD === 'finalizeaza') await finalizeaza(); +else { console.error('folosire: node emite-certificat.mjs pregateste | finalizeaza --tx 0x'); process.exitCode = 2; } diff --git a/notarizare/verifica-certificat.mjs b/notarizare/verifica-certificat.mjs new file mode 100644 index 0000000..961daca --- /dev/null +++ b/notarizare/verifica-certificat.mjs @@ -0,0 +1,265 @@ +#!/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; });