#!/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= AERE_VERIFY_PROOF= 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=)'); 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;