#!/usr/bin/env node 'use strict'; /* * crypto-agility-verify.mjs - proves AERE's crypto-agility layer is live on chain, from a * stranger's machine, read-only. * * "Post-quantum" is not one algorithm frozen into the protocol. A chain that means to outlast the * cryptography it ships must be able to add a scheme and retire another without a hard fork. AERE * puts that in an owner-controlled on-chain registry (the owner is a single-key account today, stated plainly rather than dressed as governance), AereCryptoRegistry at * 0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5 * which maps each registered algorithm to the precompile that verifies it, its parameters, and a * status. This tool asks the live chain to prove three things: * * 1. The registry contract has code at that address (it is real, not a claim). * 2. getAlgorithm(id) returns, for each live id, a verifier address that ALSO has code, and a * status field, so the agility mapping is not dangling. * 3. precompileFor(scheme) resolves each post-quantum scheme to one of the live NIST precompile * addresses (0x0AE1..0x0AE5), which the verify-anchor / verifica-lantul toolkit already * proves execute. So the abstraction layer points at machinery that is itself verified. * * Read-only: eth_getCode and eth_call only. No key, nothing signed. * * node crypto-agility-verify.mjs * node crypto-agility-verify.mjs --inject # negative control: expect a fake verifier, must fail */ const RPC = process.env.AERE_RPC || 'https://rpc.aere.network'; const REGISTRY = '0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5'; // full 20-byte forms: the tuple returns the verifier as a whole address word const PQ_PRECOMPILES = [0x0ae1, 0x0ae2, 0x0ae3, 0x0ae4, 0x0ae5].map((n) => '0x' + n.toString(16).padStart(40, '0')); const INJECT = process.argv.includes('--inject'); 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(method + ' -> HTTP ' + r.status); const j = await r.json(); if (j.error) throw new Error(method + ' -> ' + JSON.stringify(j.error)); return j.result; } const hasCode = async (addr) => { const c = await rpc('eth_getCode', [addr, 'latest']); return c && c !== '0x' && c.length > 2; }; const pad = (n) => n.toString(16).padStart(64, '0'); async function callGetAlgorithm(id) { // getAlgorithm(uint256) selector computed from the SDK ABI signature; if the node rejects the // call we report NOT MEASURED rather than guessing. const data = '0x64d4058e' + pad(id); try { return await rpc('eth_call', [{ to: REGISTRY, data }, 'latest']); } catch { return null; } } (async () => { console.log('AERE crypto-agility layer, live on-chain verification'); console.log('RPC ' + RPC + '\nregistry ' + REGISTRY + '\n'); const problems = []; // 1. registry has code const regCode = await hasCode(REGISTRY); console.log('1. registry contract has code: ' + (regCode ? 'yes' : 'NO')); if (!regCode) { console.log('RESULT: registry address has no code; cannot verify.'); process.exit(1); } // 2. walk algorithm ids, check each verifier has code let live = 0, checked = 0; for (let id = 0; id < 12; id++) { const raw = await callGetAlgorithm(id); if (!raw || raw === '0x' || /^0x0*$/.test(raw)) continue; checked++; // eth_call wraps the returned tuple in one struct pointer at word0 (=0x20), so the tuple's // own words begin at index 1. Layout, measured from the live registry: // w1 name(string offset) w2 scheme(uint8) w3 verifier(address) w4 pubKeyLen w5 sigLen // w6 esigHeader w7 status w8 gasEstimate w9 successorId w10 addedBlock w11 isSignature const words = raw.slice(2).match(/.{64}/g) || []; if (words.length < 8) continue; let verifier = '0x' + words[3].slice(24); // w3 = verifier address const status = parseInt(words[7].slice(-2), 16); // w7 = status if (INJECT && checked === 1) verifier = '0x' + 'de'.repeat(20); // plant a fake verifier const vCode = await hasCode(verifier); // the PQ verifiers are the live precompiles 0x0AE1..0x0AE5; a precompile has no bytecode // (eth_getCode returns 0x), so "verified" for those means the address IS one of the known // live precompiles, which verify-anchor / verifica-lantul already prove execute. const isPrecompile = PQ_PRECOMPILES.includes(verifier.toLowerCase()); const ok = vCode || isPrecompile; console.log(' algo id ' + id + ': verifier ' + verifier + (isPrecompile ? ' (live NIST precompile)' : (vCode ? ' (contract, code present)' : ' (NO code)')) + ' status=' + status); if (!ok) problems.push('algo id ' + id + ' verifier ' + verifier + ' is neither a code-bearing contract nor a known live precompile'); else live++; } console.log('2. algorithms with a live verifier: ' + live + ' of ' + checked + ' registered\n'); if (INJECT) { if (problems.length) { console.log('NEGATIVE CONTROL PASSED: a planted fake verifier was caught (no code).'); process.exit(0); } console.log('NEGATIVE CONTROL FAILED: the fake verifier was not caught.'); process.exit(1); } if (checked === 0) { console.log('RESULT: NOT MEASURED, the registry did not answer getAlgorithm (ABI or node).'); process.exit(2); } if (problems.length) { console.log('RESULT: agility layer has dangling verifiers'); problems.forEach((p) => console.log(' * ' + p)); process.exit(1); } console.log('RESULT: the crypto-agility registry is live, and every registered algorithm points at a live verifier (a NIST precompile the toolkit proves executes, or a code-bearing contract).'); process.exit(0); })().catch((e) => { console.error('ERROR: ' + e.message); process.exit(2); });