aere-quantum/device-identity/proba-device-ancora-testnet.mjs

84 lines
9.9 KiB
JavaScript

#!/usr/bin/env node
// proba-device-ancora-testnet.mjs: the chain as the witness of a device attestation, END TO END on the public testnet 28001.
// The AIP-23 envelope of a real attestation is notarized on AereNotary with a testnet developer key, the published AIP-23 reference
// verifier reads it back under a post-quantum certified anchor, and that reading (made by the verifier, not handed by the device)
// bounds the attestation's time: an attestation dated AFTER its envelope was already on the chain is caught.
// Refuses any chain but 28001 (it reads eth_chainId). The developer key is read from the file named in AERE_TESTNET_KEY_FILE, in
// process, and is never printed. Two transactions are sent, both `notarize(bytes32)`; nothing else.
// Writes the evidence (the bundles, the notarized envelopes, the readings, the verdicts; no private key) in dovezi-ancora-28001/.
// AERE_TESTNET_KEY_FILE=<file> AERE_VERIFY_PROOF=<verify-proof.mjs> node proba-device-ancora-testnet.mjs [--rpc URL]
// Exit 0 = every row as required; 1 = a red row; 2 = NEMASURAT (no key, no verifier, the chain is not 28001, no certified anchor came)
import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { fileURLToPath } from 'node:url';
import * as ID from '../identity/identity.mjs';
import { buildProof } from '../proof-kinds/proof-kinds.mjs';
import { notarizeEnvelope, NOTARI } from '../agents/agent-ancora.mjs';
import { incarcaEthers } from '../agents/x402/wallet.mjs';
import * as D from './device.mjs';
import { citesteAncora } from './device-cli.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const argv = process.argv.slice(2); const RPC = argv.includes('--rpc') ? argv[argv.indexOf('--rpc') + 1] : 'https://testnet-rpc.aere.network';
const kf = process.env.AERE_TESTNET_KEY_FILE;
const DEV_VERIFY = path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs');
const VERIFY = process.env.AERE_VERIFY_PROOF || (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : '');
if (!kf || !fs.existsSync(kf)) { console.log('NEMASURAT: AERE_TESTNET_KEY_FILE does not name a file with a funded testnet key'); process.exit(2); }
if (!VERIFY || !fs.existsSync(VERIFY)) { console.log('NEMASURAT: the AIP-23 reference verifier is missing (AERE_VERIFY_PROOF=<verify-proof.mjs>)'); process.exit(2); }
let ethers; try { ethers = incarcaEthers(); } catch (e) { console.log('NEMASURAT: ' + e.message); process.exit(2); }
const chainId = Number((await new ethers.JsonRpcProvider(RPC).getNetwork()).chainId);
if (chainId !== 28001) { console.log(`NEMASURAT: ${RPC} serves chain ${chainId}, not 28001`); process.exit(2); }
let ok = 0, rau = 0; const cer = (c, ce, d = '') => { console.log(` ${c ? 'OK ' : 'RAU'} ${ce}${c || !d ? '' : ' -- ' + d}`); if (c) ok++; else rau++; };
const ZI = 86400000, acum = Date.now(); const iso = (t) => new Date(t).toISOString();
const [mfr, op] = [ID.generateKeys(), ID.generateKeys()]; const dev = D.generateDeviceKey();
const LID = 'https://manufacturer.example/status/1', AUD = 'https://gateway.example', NONCE = 'n-' + crypto.randomBytes(8).toString('hex');
const cred = D.manufacture({ manufacturer: mfr, deviceKey: dev.publicKey, model: 'AX-200', serial: 'SN-28001', validFrom: iso(acum - 30 * ZI), validUntil: iso(acum + 3650 * ZI), status: { list: LID, index: 7 }, now: new Date(acum - 30 * ZI) });
const fw = D.releaseFirmware({ signer: mfr, model: 'AX-200', version: '2.0.0', sequence: 2, image: fs.readFileSync(path.join(AICI, 'fixturi', 'firmware-2.0.0.bin')), now: new Date(acum - 5 * ZI) });
const t1 = D.transferOwnership({ from: mfr, to: op.public, manufacture: cred, now: new Date(acum - 10 * ZI) }); const cap = D.ownershipHead(cred, [t1]);
const aut = D.authorize({ owner: op, deviceId: dev.deviceId, ownership: cap, actions: ['telemetry.publish'], notBefore: iso(acum - ZI), notAfter: iso(acum + ZI), now: new Date(acum - ZI) });
const adus = { trustedManufacturers: [mfr.id], statusLists: [ID.createStatusList({ issuer: mfr, id: LID, validFrom: iso(acum - 3600000), validUntil: iso(acum + ZI), now: new Date(acum - 3600000) })],
firmwarePolicies: [D.firmwarePolicy({ manufacturer: mfr, model: 'AX-200', minSequence: 2, validFrom: iso(acum - 3600000), validUntil: iso(acum + ZI), now: new Date(acum - 3600000) })],
statusRecords: [D.statusRecord({ by: op, deviceId: dev.deviceId, state: 'active', seq: 1, ownership: cap, validUntil: iso(acum + ZI), now: new Date(acum - 600000) })], nonce: NONCE, audience: AUD };
const atesta = (cand) => D.attest({ device: dev, nonce: NONCE, audience: AUD, firmware: fw, ownership: cap, action: 'telemetry.publish', now: new Date(cand) });
const pachet = (x, anchor) => D.bundle({ manufacture: cred, firmware: fw, transfers: [t1], authorization: aut, attestation: x, anchor });
const noteaza = (x) => notarizeEnvelope({ envelope: D.proofOfDevice(x, buildProof), kind: 'aere-proof-of-device', rpc: RPC, keyFile: kf, ethers });
async function citeste(env) { // the reading needs the next post-quantum certified anchor to cover the notarization: wait up to six minutes
const start = Date.now(); let r = null;
while (Date.now() - start < 360000) { r = await citesteAncora(env, { verifyProof: VERIFY, rpc: RPC, chain: 28001 }); if (r.stare !== 'NEMASURAT') break; await new Promise((g) => setTimeout(g, 20000)); }
return { ...r, secunde: Math.round((Date.now() - start) / 1000) };
}
// the honest attestation, made now; and one the device dated twenty minutes in the future (a wrong or lying clock)
const X1 = atesta(Date.now()), X2 = atesta(Date.now() + 20 * 60000);
const n1 = await noteaza(X1);
cer(n1.ok && n1.envelope.notarization.chainId === 28001 && n1.envelope.notarization.notary === NOTARI[28001], `1. the envelope of an attestation notarized on 28001${n1.ok ? `: firstSeen ${n1.envelope.notarization.firstSeen}, block ${n1.envelope.notarization.block}` : ''}`, n1.motiv);
const n2 = await noteaza(X2);
cer(n2.ok, `1. the envelope of the post-dated attestation notarized on 28001${n2.ok ? `: firstSeen ${n2.envelope.notarization.firstSeen}` : ''}`, n2.motiv);
if (!n1.ok || !n2.ok) { console.log(`\ndevice-ancora-testnet: ${ok}/${ok + rau} as required`); process.exit(1); }
const r1 = await citeste(n1.envelope), r2 = await citeste(n2.envelope);
if (r1.stare === 'NEMASURAT' || r2.stare === 'NEMASURAT') { console.log('NEMASURAT: no post-quantum certified anchor covered the notarization in six minutes: ' + (r1.motiv || r2.motiv)); process.exit(2); }
cer(r1.stare === 'PASSED' && r2.stare === 'PASSED', `2. the published AIP-23 verifier reads both envelopes under a post-quantum certified anchor (after ${r1.secunde} s and ${r2.secunde} s)`, r1.motiv || r2.motiv);
cer(r1.reading && r1.reading.at === n1.envelope.notarization.firstSeen && r1.reading.statementHash === n1.envelope.statementHash, '2. the certified first-seen time is the firstSeen read from the contract, for the hash of this envelope');
const acumV = iso(Date.now());
const v1 = D.verifyDevice(pachet(X1, n1.envelope), { ...adus, now: X1.statement.at, anchors: [r1.reading] });
cer(v1.verdict === 'VALID' && v1.links.witness === 'PASSED', '3. the device bundle with its notarized envelope and the verifier\'s reading: VALID, the witness link PASSED', `${v1.verdict} [${v1.rules}] [${v1.unjudged}]`);
const v1f = D.verifyDevice(pachet(X1, n1.envelope), { ...adus, now: X1.statement.at });
cer(v1f.verdict === 'PARTIAL' && v1f.unjudged.includes('witness-unread'), '3. THE WITNESS OF THE MEASURE: the same bundle without the reading is PARTIAL (a notarization claimed is not a notarization read)', `${v1f.verdict} [${v1f.unjudged}]`);
// the post-dated attestation, judged by a verifier whose clock is at the time the device wrote (an audit made later, or a verifier fooled about the time)
const v2f = D.verifyDevice(pachet(X2, D.proofOfDevice(X2, buildProof)), { ...adus, now: X2.statement.at });
cer(v2f.verdict === 'VALID', '4. THE WITNESS OF THE MEASURE: without the chain, the post-dated attestation passes on a clock set to the time it claims', `${v2f.verdict} [${v2f.rules}]`);
const v2 = D.verifyDevice(pachet(X2, n2.envelope), { ...adus, now: X2.statement.at, anchors: [r2.reading] });
cer(v2.verdict === 'INVALID' && v2.rules.join() === 'witness-time', '4. with the chain\'s reading, the post-dated attestation is caught: its envelope was on the chain before the time it claims', `${v2.verdict} [${v2.rules}]`);
const v3 = D.verifyDevice(pachet(X2, n1.envelope), { ...adus, now: X2.statement.at, anchors: [r1.reading] });
cer(v3.verdict === 'INVALID' && v3.rules.join() === 'witness-envelope', '5. the notarized envelope of ANOTHER attestation does not witness this one', `${v3.verdict} [${v3.rules}]`);
const v4 = D.verifyDevice(pachet(X1, n1.envelope), { ...adus, now: X1.statement.at, anchors: [r2.reading] });
cer(v4.verdict === 'PARTIAL' && v4.unjudged.includes('witness-unread'), '5. a reading of another envelope is not a reading of this one: PARTIAL', `${v4.verdict} [${v4.unjudged}]`);
const DOV = path.join(AICI, 'dovezi-ancora-28001'); fs.mkdirSync(DOV, { recursive: true });
const zi = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
fs.writeFileSync(path.join(DOV, `ancora-dispozitiv-${zi}.json`), JSON.stringify({ v: 1, kind: 'aere-device-anchor-evidence', network: 'eip155:28001', rpc: RPC, measuredAt: acumV,
note: 'testnet 28001: the finality is the testnet\'s. Every key that signed here was made for this run and discarded; the notarizations were paid by a testnet developer key that is not in this file.',
verifierBrought: adus, honest: { bundle: pachet(X1, n1.envelope), reading: r1.reading }, postDated: { bundle: pachet(X2, n2.envelope), reading: r2.reading },
verdicts: { honestWithReading: v1.verdict, honestWithoutReading: v1f.verdict, postDatedWithoutChain: v2f.verdict, postDatedWithReading: [v2.verdict, v2.rules], otherEnvelope: [v3.verdict, v3.rules], otherReading: v4.verdict } }, null, 1) + '\n');
console.log(`\ndevice-ancora-testnet: ${ok}/${ok + rau} as required (evidence in dovezi-ancora-28001/ancora-dispozitiv-${zi}.json)`);
process.exitCode = rau ? 1 : 0;