Zero dependencies, no account, no API. Every command takes --rpc, so it runs against a node you operate and never needs ours. That is the point: an exposure measurement you cannot re-run is an opinion with a logo on it. Ships with two self-checks meant to be run by you, not just by us. The second, proba-vocabular.mjs, exists because of a real defect found on 2026-08-16: the scanner emits RED / YELLOW / GREEN, two downstream files each wrote their own copy of that list, and both wrote AMBER, so a hybrid contract, exactly a client who has already started migrating, was reported as unmeasurable. Both sides were self-consistent, so nothing we had could see it. The vocabulary now has one exported source, and the test walks the whole path with a negative control that makes it able to fail. The same scanner returns RED about our own contracts. See VERIFY-US.md.
192 lines
9.7 KiB
JavaScript
192 lines
9.7 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
/*
|
|
* raport.js - produces an Aere Quantum Exposure Report from live on-chain measurement.
|
|
*
|
|
* WHY THIS FILE EXISTS AND WHAT MAKES IT WORTH PAYING FOR. Anyone can write a PDF that says
|
|
* "you are exposed to quantum risk". What a buyer cannot get elsewhere is a report that:
|
|
*
|
|
* - names, for every address, what was MEASURED and what was NOT, and never blurs the two;
|
|
* - carries the exact command that reproduces it, so the buyer can re-run it next quarter,
|
|
* on a node they run themselves, and get the same answer or a documented difference;
|
|
* - carries a manifest of digests so a changed report is detectable;
|
|
* - answers the questions a supervisor actually asks, in the wording of the published
|
|
* criteria, instead of inventing a scoring scheme.
|
|
*
|
|
* A report that cannot be reproduced is an opinion. This one is a measurement with a receipt.
|
|
*
|
|
* node raport.js 0xAddr [0xAddr...] --rpc https://... [--client "Name"] [--out dir]
|
|
* node raport.js --fixture fixtures/x.json --out dir # offline, for demos
|
|
*/
|
|
import { execFileSync } from 'node:child_process';
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { citesteVerdict } from './lib/bytecode.js';
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
const argv = process.argv.slice(2);
|
|
const arg = (n, d) => { const i = argv.indexOf(n); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : d; };
|
|
const has = (n) => argv.includes(n);
|
|
const ADRESE = argv.filter((a) => /^0x[0-9a-fA-F]{40}$/.test(a));
|
|
const RPC = arg('--rpc', null);
|
|
const CLIENT = arg('--client', 'the holder of this report');
|
|
const OUT = arg('--out', 'raport-' + new Date().toISOString().slice(0, 10).replace(/-/g, ''));
|
|
const FIXTURE = arg('--fixture', null);
|
|
|
|
if (!ADRESE.length && !FIXTURE) {
|
|
console.error('folosire: node raport.js 0xAddr [0xAddr...] --rpc URL [--client "Nume"] [--out dir]');
|
|
process.exit(2);
|
|
}
|
|
|
|
const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
|
|
function scaneaza(adr) {
|
|
const a = ['scan.js', adr];
|
|
if (RPC) a.push('--rpc', RPC);
|
|
try {
|
|
const out = execFileSync(process.execPath, a, { cwd: __dirname, encoding: 'utf8', timeout: 120000 });
|
|
return { adr, ok: true, text: out.trim() };
|
|
} catch (e) {
|
|
return { adr, ok: false, text: String((e.stdout || '') + (e.stderr || e.message)).trim() };
|
|
}
|
|
}
|
|
|
|
// Reads the scan text back into the two things a buyer pays for: what was measured, and what the
|
|
// tool itself says it could not settle. The tool marks the latter with [VERIFY] or [MEASURE], and
|
|
// those lines are carried through verbatim rather than summarised away.
|
|
function citeste(text) {
|
|
// Verdictul se citeste prin functia comuna din lib/bytecode.js, NU printr-un tipar scris aici.
|
|
// Aici a fost defectul: acest fisier cauta AMBER, scanerul scrie YELLOW, deci fiecare contract
|
|
// hibrid, adica exact clientul care a inceput deja migrarea, cadea in NOT MEASURED.
|
|
const stare = citesteVerdict(text) || 'NOT MEASURED';
|
|
const precomp = [...text.matchAll(/^\s*-\s*(0x[0-9a-f]+)\s+(.+?)\s*(\[[a-z]+\])?$/gim)].map((m) => m[1] + ' ' + m[2].trim());
|
|
const nemasurat = [...text.matchAll(/^\s*\*\s*(\[(?:VERIFY|MEASURE)\][^\n]*)/gim)].map((m) => m[1].trim());
|
|
const cod = (text.match(/code:\s*(\d+)\s*bytes/) || [])[1] || null;
|
|
return { stare, precomp, nemasurat, cod };
|
|
}
|
|
|
|
const rezultate = ADRESE.map((a) => { const r = scaneaza(a); return { ...r, ...citeste(r.text) }; });
|
|
|
|
const acum = new Date().toISOString().replace('T', ' ').slice(0, 16) + 'Z';
|
|
const comanda = 'node raport.js ' + ADRESE.join(' ') + (RPC ? ' --rpc ' + RPC : '');
|
|
const nrRosu = rezultate.filter((r) => r.stare === 'RED').length;
|
|
const nrGalben = rezultate.filter((r) => r.stare === 'YELLOW').length;
|
|
const nrVerde = rezultate.filter((r) => r.stare === 'GREEN').length;
|
|
const nrNem = rezultate.filter((r) => r.stare === 'NOT MEASURED').length;
|
|
const totalNem = rezultate.reduce((s, r) => s + r.nemasurat.length, 0);
|
|
|
|
let md = `# Aere Quantum Exposure Report
|
|
|
|
**Prepared for:** ${CLIENT}
|
|
**Measured:** ${acum}
|
|
**Scope:** ${ADRESE.length} on-chain address${ADRESE.length === 1 ? '' : 'es'}${RPC ? ', read from ' + RPC : ', offline fixture'}
|
|
|
|
## Read this page first
|
|
|
|
This report states what was **measured** and what was **not**. Those are different, and mixing
|
|
them is how exposure reports become useless. Every line below is one or the other, and the
|
|
not-measured lines say why.
|
|
|
|
**It is reproducible.** Run this and compare:
|
|
|
|
${comanda}
|
|
|
|
Point \`--rpc\` at a node you run yourself. Nothing here depends on trusting the party that wrote
|
|
this report, and that is the point: an exposure report you cannot re-run is an opinion with a
|
|
logo on it.
|
|
|
|
## What was found
|
|
|
|
| address | verdict | code | live PQ verifiers reachable | open questions |
|
|
|---|---|---|---|---|
|
|
${rezultate.map((r) => `| \`${r.adr}\` | **${r.stare}** | ${r.cod ? r.cod + ' B' : 'n/a'} | ${r.precomp.length} | ${r.nemasurat.length} |`).join('\n')}
|
|
|
|
Totals: ${nrRosu} RED, ${nrGalben} YELLOW, ${nrVerde} GREEN, ${nrNem} not measured.
|
|
**${totalNem} question${totalNem === 1 ? '' : 's'} the tool refused to settle on its own** are listed per address below.
|
|
A tool that never says "I could not settle this" is not being careful, it is being quiet.
|
|
|
|
## Per address
|
|
|
|
${rezultate.map((r) => `### \`${r.adr}\` - ${r.stare}
|
|
|
|
${r.precomp.length ? 'Post-quantum verifiers reachable from this bytecode:\n' + r.precomp.map((p) => '- ' + p).join('\n') : 'No live post-quantum verifier was resolved from this bytecode.'}
|
|
|
|
${r.nemasurat.length ? '**Not settled by measurement, carried through verbatim:**\n' + r.nemasurat.map((n) => '- ' + n).join('\n') : '_Nothing was left unsettled for this address._'}
|
|
|
|
<details><summary>raw tool output</summary>
|
|
|
|
\`\`\`
|
|
${r.text}
|
|
\`\`\`
|
|
</details>
|
|
`).join('\n')}
|
|
|
|
## Answers to the questions a supervisor asks
|
|
|
|
These are written against **published criteria**, quoted as criteria and not as a certification.
|
|
This report is not an accredited audit and does not claim to be one.
|
|
|
|
**"Which of your on-chain components already depend on quantum-vulnerable signatures?"**
|
|
Every address in scope authorises through ECDSA secp256k1, because that is what an EVM account
|
|
is. The measured question is a different one: whether a component can *also* verify a
|
|
post-quantum signature, and ${rezultate.filter((r) => r.precomp.length).length} of ${ADRESE.length}
|
|
address${ADRESE.length === 1 ? '' : 'es'} in scope reach a live NIST post-quantum verifier.
|
|
|
|
**"Do you have a migration path that does not require redeploying everything?"**
|
|
Measured per address above. Where a component routes verification through a registry rather than
|
|
a hardcoded address, an algorithm can be added or retired without redeploying the component. That
|
|
property is visible in bytecode and is reported as such, not assumed.
|
|
|
|
**"What is the cost, and who pays it?"**
|
|
The toolkit's \`plan.js\` produces per-account gas figures from measured deployed-account costs.
|
|
Cost is quoted from measurement, never from a vendor estimate.
|
|
|
|
**"How would an auditor check your claim next year, without you?"**
|
|
By running the command at the top of this report against their own node. That is the entire
|
|
answer, and it is the reason this report has a command in it.
|
|
|
|
## What this report does NOT say
|
|
|
|
- It is **not** an accredited audit, and we are **not** an accredited audit firm. It measures the
|
|
on-chain slice: bytecode and what it can reach. Repositories, TLS, dependencies, key custody,
|
|
HSMs and off-chain services are **out of scope** and unmeasured here.
|
|
- It does **not** say "compliant with NIST". It says "measured against the criteria published in
|
|
NIST IR 8547". Compliance is a statement only an accredited body can make.
|
|
- It does **not** claim that a live post-quantum verifier makes an account quantum-safe. A
|
|
post-quantum verifier called from a transaction that is itself authorised with ECDSA gives no
|
|
post-quantum security: the adversary forges the outer transaction. Anyone selling you the
|
|
opposite can be taken apart in five minutes.
|
|
- It does **not** promise legal effect. What a timestamp or an attestation means in court depends
|
|
on jurisdiction and is not asserted here.
|
|
- "harvest now, decrypt later" does **not** apply to signatures: a signature is public and is not
|
|
harvested. The threat that applies to a chain is retroactive rewriting of history with keys
|
|
recovered later, which is a different and more serious problem.
|
|
|
|
## Integrity
|
|
|
|
Digests of every file in this report are in \`MANIFEST.sha256\`. Verify with:
|
|
|
|
sha256sum -c MANIFEST.sha256
|
|
`;
|
|
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
fs.writeFileSync(path.join(OUT, 'raport.md'), md);
|
|
fs.writeFileSync(path.join(OUT, 'raport.json'), JSON.stringify({
|
|
format: 'aere-quantum-exposure/1', client: CLIENT, measuredAt: acum, rpc: RPC || null,
|
|
command: comanda, addresses: rezultate.map((r) => ({ address: r.adr, verdict: r.stare, codeBytes: r.cod ? Number(r.cod) : null, pqVerifiers: r.precomp, notSettled: r.nemasurat })),
|
|
totals: { red: nrRosu, amber: nrGalben, green: nrVerde, notMeasured: nrNem, openQuestions: totalNem },
|
|
}, null, 2));
|
|
|
|
const fisiere = fs.readdirSync(OUT).filter((f) => f !== 'MANIFEST.sha256');
|
|
fs.writeFileSync(path.join(OUT, 'MANIFEST.sha256'),
|
|
fisiere.map((f) => sha(fs.readFileSync(path.join(OUT, f))) + ' ' + f).join('\n') + '\n');
|
|
|
|
console.log('Aere Quantum Exposure Report');
|
|
console.log(' adrese masurate : ' + ADRESE.length + ' RED ' + nrRosu + ', YELLOW ' + nrGalben + ', GREEN ' + nrVerde + ', NEMASURAT ' + nrNem);
|
|
console.log(' intrebari pe care unealta a refuzat sa le inchida singura: ' + totalNem);
|
|
console.log(' scris in : ' + OUT + '/ (raport.md, raport.json, MANIFEST.sha256)');
|
|
console.log(' reproductibil : ' + comanda);
|
|
process.exit(0);
|