Every artifact's SHA-256 and size, the SBOM's digest, the git commit and tree of the source, a hybrid signature (classical + ML-DSA, both required) and, when notarized, a first-seen time on Aere Network covered by the validators' post-quantum certificate. verify recomputes everything from the files; --rebuild-from repeats an npm pack build from a clone the verifier chose; the SBOM is re-derived from the committed package-lock.json or, new in 1.4.0, from the committed go.mod and go.sum; --signer requires the signing keys you expect (1.4.0); a developer credential binds the keys to a person, judged against a trust root you choose. The builder's declared date can only accuse, never acquit (1.4.0). Includes the GitHub Action and the hybrid signature library it uses. Laid out as in the development repository (tools/proof-of-software/, sdk-pq-sign/, sdk/) so that nothing is rewritten for publication. Tests and negative controls measured on 2026-09-29 are listed in README.md.
95 lines
6.9 KiB
JavaScript
95 lines
6.9 KiB
JavaScript
// Dovada ca CONTROALELE NEGATIVE ale actiunii pot iesi rosii din motivul lor: pe o COPIE a actiunii (setul minim de fisiere +
|
|
// dependintele fixate, intr-un dosar temporar; depozitul nu se atinge) se scoate cate un paznic cu o conditie falsa la RULARE
|
|
// (compileaza, deci un rosu nu poate veni de la sintaxa), se ruleaza test/action.test.mjs pe copie si se cere ca EXACT probele
|
|
// paznicului scos sa iasa ESEC, iar toate celelalte OK.
|
|
// Trei grupe, fiindca paznicii interactioneaza: fara judecarea notarizarii (grupa B), ecoul cheii din raspunsul 403 nu mai
|
|
// ajunge sa fie tiparit, si proba (d) ar ramane verde din alt motiv decat cel masurat; de aceea redactarea (grupa C) se scoate
|
|
// singura, cu notarizarea intacta.
|
|
// Verdict cu trei valori: VERDE (fiecare paznic scos si-a inrosit proba, restul verde), ROSU (un paznic scos si proba lui a
|
|
// ramas verde, sau a picat alta), STRICAT (o ancora de plantare nu s-a gasit exact o data, copia nu compileaza, suita nu a
|
|
// rulat pe copie). Un STRICAT e un esec al dovezii, nu o linie informativa.
|
|
// node aerenew/tools/proof-of-software/test/action-dovada.mjs
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const RADACINA = path.resolve(AICI, '..', '..', '..');
|
|
const SUITA = path.join(AICI, 'action.test.mjs');
|
|
const FALS = "process.env.AERE_POS_NICIODATA === '1'";
|
|
|
|
const GRUPE = {
|
|
A: {
|
|
plantari: [
|
|
['garda fisierelor necomise', ' if (st.out.trim()) {', ` if (st.out.trim() && ${FALS}) {`],
|
|
['refuzul verificarii', ' if (!r.valid) throw new Refuz(', ` if (!r.valid && ${FALS}) throw new Refuz(`],
|
|
['verificarea atestarii recitite', 'if (!r2.valid || inapoi.statementHash', `if ((!r2.valid && ${FALS}) || inapoi.statementHash`],
|
|
['refuzul reconstructiei', " if (picate.some((c) => /^(rebuild|source)/.test(c.name))) throw", ` if (picate.some((c) => /^(rebuild|source)/.test(c.name)) && ${FALS}) throw`],
|
|
],
|
|
rosii: ['CONTROL NEGATIV (a):', 'CONTROL NEGATIV (a, varianta)', 'CONTROL NEGATIV (c):', 'CONTROL NEGATIV (c, varianta)'],
|
|
},
|
|
// redactarea, singura: (d) o cere, si (b) o cere si ea (mesajul lui 403 trebuie sa arate cheia ecou ca ***)
|
|
C: {
|
|
plantari: [
|
|
['redactarea secretelor', 'curata(text) { let t = String(text); for (const r of this.re)', `curata(text) { let t = String(text); for (const r of (${FALS} ? this.re : []))`],
|
|
],
|
|
rosii: ['CONTROL NEGATIV (b):', 'CONTROL NEGATIV (d):'],
|
|
},
|
|
B: {
|
|
plantari: [
|
|
['clientul care judeca raspunsul (inlocuit cu un fetch brut)', 'try { d = await new AereCloud({ apiKey: apiBrut, baseUrl: apiBase, fetch: f }).notarize(att.statementHash); } catch (e) {',
|
|
"try { d = await (await f(apiBase + '/notarize', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': apiBrut }, body: JSON.stringify({ hash: att.statementHash }) })).json(); } catch (e) {"],
|
|
['judecarea codului HTTP', ' if (cod !== 200) throw new Refuz(', ` if (cod !== 200 && ${FALS}) throw new Refuz(`],
|
|
['judecarea chitantei', "|| d.block <= 0) throw new Refuz('notarization answered 200", `|| d.block <= 0) if (${FALS}) throw new Refuz('notarization answered 200`],
|
|
],
|
|
rosii: ['CONTROL NEGATIV (b):', 'CONTROL NEGATIV (b, varianta): 200 fara txHash'],
|
|
},
|
|
};
|
|
|
|
function copie() {
|
|
const L = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-dovada-'));
|
|
for (const f of ['tools/proof-of-software/pos.mjs', 'tools/proof-of-software/action/index.mjs', 'tools/proof-of-software/action/action.yml', 'sdk/index.mjs', 'sdk/package.json', 'sdk-pq-sign/index.mjs', 'sdk-pq-sign/package.json', 'sdk-pq-sign/package-lock.json']) {
|
|
fs.mkdirSync(path.dirname(path.join(L, f)), { recursive: true }); fs.copyFileSync(path.join(RADACINA, f), path.join(L, f));
|
|
}
|
|
fs.cpSync(path.join(RADACINA, 'sdk-pq-sign', 'node_modules'), path.join(L, 'sdk-pq-sign', 'node_modules'), { recursive: true });
|
|
return L;
|
|
}
|
|
|
|
let verdict = 'VERDE';
|
|
const slab = (v) => { if (v === 'STRICAT' || verdict === 'STRICAT') verdict = 'STRICAT'; else verdict = 'ROSU'; };
|
|
for (const [nume, g] of Object.entries(GRUPE)) {
|
|
console.log(`=== grupa ${nume}: ${g.plantari.map((p) => p[0]).join('; ')}`);
|
|
const L = copie(); const act = path.join(L, 'tools', 'proof-of-software', 'action', 'index.mjs');
|
|
let src = fs.readFileSync(act, 'utf8'); let bun = true;
|
|
for (const [ce, ancora, inlocuire] of g.plantari) {
|
|
const n = src.split(ancora).length - 1;
|
|
if (n !== 1) { console.log(` STRICAT: ancora pentru "${ce}" apare de ${n} ori (se cere exact o data)`); bun = false; continue; }
|
|
src = src.replace(ancora, inlocuire);
|
|
}
|
|
if (!bun) { slab('STRICAT'); continue; }
|
|
fs.writeFileSync(act, src);
|
|
const chk = spawnSync(process.execPath, ['--check', act], { encoding: 'utf8' });
|
|
if (chk.status !== 0) { console.log(' STRICAT: copia cu paznicii scosi nu compileaza: ' + (chk.stderr || '').split('\n').slice(0, 3).join(' | ')); slab('STRICAT'); continue; }
|
|
const env = { ...process.env, AERE_POS_ACTIUNE_PROBA: act }; delete env.AERE_POS_NICIODATA;
|
|
const r = spawnSync(process.execPath, [SUITA], { encoding: 'utf8', env, maxBuffer: 1 << 26, timeout: 900000 });
|
|
const out = r.stdout || '';
|
|
if (!out.includes('actiunea: ' + act)) { console.log(' STRICAT: suita nu a rulat pe copie (nu numeste actiunea copiata)'); slab('STRICAT'); continue; }
|
|
const ok = [...out.matchAll(/^ {2}OK {3}(.+)$/gm)].map((m) => m[1]);
|
|
// randul intreg (numele probelor contin si ele ': ', deci numele nu se poate taia la primul)
|
|
const esec = [...out.matchAll(/^ {2}ESEC (.+)$/gm)].map((m) => m[1]);
|
|
if (!/^\d+ treceri, \d+ esecuri$/m.test(out) || ok.length + esec.length < 20) { console.log(` STRICAT: suita nu a terminat (${ok.length} OK, ${esec.length} ESEC citite)`); slab('STRICAT'); continue; }
|
|
const eRosie = (t) => g.rosii.some((p) => t.startsWith(p));
|
|
const rosiiLipsa = g.rosii.filter((p) => !esec.some((t) => t.startsWith(p)));
|
|
const rosiiStraine = esec.filter((t) => !eRosie(t));
|
|
for (const p of g.rosii) console.log(` ${esec.some((t) => t.startsWith(p)) ? 'ROSIE, cum trebuia' : 'VERDE, deci paznicul scos NU e vazut de proba'}: ${p}`);
|
|
for (const m of [...out.matchAll(/^ {2}ESEC (.+)$/gm)].map((x) => x[1])) if (eRosie(m)) console.log(` ${m.slice(0, 40)} ... ${m.slice(-180)}`);
|
|
if (rosiiStraine.length) console.log(' au picat si probe care nu tin de paznicii scosi: ' + rosiiStraine.map((x) => x.slice(0, 160)).join(' | '));
|
|
if (rosiiLipsa.length || rosiiStraine.length) slab('ROSU');
|
|
console.log(` grupa ${nume}: ${ok.length} OK, ${esec.length} ESEC (asteptate ${g.rosii.length})`);
|
|
try { fs.rmSync(L, { recursive: true, force: true, maxRetries: 3 }); } catch { console.log(' (copia temporara nu s-a putut sterge: ' + L + ')'); }
|
|
}
|
|
console.log(`\nDOVADA CONTROALELOR NEGATIVE: ${verdict}`);
|
|
process.exitCode = verdict === 'VERDE' ? 0 : verdict === 'ROSU' ? 1 : 2;
|