pqc-migration-toolkit/notarizare/emite-certificat.mjs
Aere Network 04f735eda6 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.
2026-08-17 11:24:34 +03:00

182 lines
10 KiB
JavaScript

#!/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 <fisier> [--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 <cerere> --tx 0x<hash> [--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 <fisier> [--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 <this file> --tx 0x<hash>',
],
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 <cerere-ancorare.json> --tx 0x<hash> [--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 <fisier> | finalizeaza <cerere> --tx 0x<hash>'); process.exitCode = 2; }