aere-proof-of-software/tools/proof-of-software/test/sbom.test.mjs
Aere Network 065f84e34a Aere Proof of Software 1.4.0: an attestation of what was built, from what, by whom and when, verifiable without trusting Aere Network
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.
2026-09-29 22:46:57 +03:00

84 lines
7.1 KiB
JavaScript

// Proof of Software 1.3.0: SBOM-ul atestat e judecat fata de lockfile-ul COMIS (verify --rebuild-from). Fiecare afirmatie cu perechea
// ei negativa. Offline: un depozit git temporar cu un pachet si un package-lock.json scris aici; `npm sbom --package-lock-only` nu
// cere retea.
// node test/sbom.test.mjs iesire 0 = toate cum trebuia
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { attest, verify, packNpmFromTree, sbomSemantica, sbomNpmFromTree } from '../pos.mjs';
let treceri = 0; const esecuri = [];
async function test(nume, fn) { try { await fn(); treceri++; console.log(' OK ' + nume); } catch (e) { esecuri.push(nume); console.log(' ESEC ' + nume + ' — ' + (e.message || e)); } }
const cere = (c, m) => { if (!c) throw new Error(m); };
const g = (args, cwd) => execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
const check = (r, re) => r.checks.find((c) => re.test(c.name));
const G = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-g-'));
g(['init', '-q'], G); g(['config', 'user.email', 'proba@aere.invalid'], G); g(['config', 'user.name', 'proba'], G); g(['config', 'core.autocrlf', 'false'], G);
const P = path.join(G, 'pkg'); fs.mkdirSync(path.join(P, 'lib'), { recursive: true });
fs.writeFileSync(path.join(P, 'package.json'), JSON.stringify({ name: 'proba-sbom', version: '0.1.0', license: 'MIT', main: 'lib/index.js', dependencies: { 'dep-a': '1.0.0', 'dep-b': '2.0.0' } }, null, 2) + '\n');
fs.writeFileSync(path.join(P, 'lib', 'index.js'), 'module.exports = 1;\n');
const integ = (s) => 'sha512-' + Buffer.from(s.padEnd(64, 'x')).toString('base64');
const lock = {
name: 'proba-sbom', version: '0.1.0', lockfileVersion: 3, requires: true,
packages: {
'': { name: 'proba-sbom', version: '0.1.0', license: 'MIT', dependencies: { 'dep-a': '1.0.0', 'dep-b': '2.0.0' } },
'node_modules/dep-a': { version: '1.0.0', resolved: 'https://registry.npmjs.org/dep-a/-/dep-a-1.0.0.tgz', integrity: integ('dep-a-1.0.0'), license: 'MIT', dependencies: { 'dep-c': '3.0.0' } },
'node_modules/dep-b': { version: '2.0.0', resolved: 'https://registry.npmjs.org/dep-b/-/dep-b-2.0.0.tgz', integrity: integ('dep-b-2.0.0'), license: 'MIT' },
'node_modules/dep-c': { version: '3.0.0', resolved: 'https://registry.npmjs.org/dep-c/-/dep-c-3.0.0.tgz', integrity: integ('dep-c-3.0.0'), license: 'ISC' },
},
};
fs.writeFileSync(path.join(P, 'package-lock.json'), JSON.stringify(lock, null, 2) + '\n');
// un al doilea pachet, FARA lockfile comis
fs.mkdirSync(path.join(G, 'fara'), { recursive: true });
fs.writeFileSync(path.join(G, 'fara', 'package.json'), JSON.stringify({ name: 'fara-lock', version: '0.0.1', license: 'MIT' }) + '\n');
g(['add', '-A'], G); g(['commit', '-q', '-m', 'pachet de proba'], G);
const C = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-c-')); g(['clone', '-q', G, C], os.tmpdir());
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-o-'));
const TGZ = packNpmFromTree(G, 'HEAD:pkg', OUT);
const scrieSbom = (nume, bom) => { const f = path.join(OUT, nume); fs.writeFileSync(f, JSON.stringify(bom, null, 2)); return f; };
const r1 = sbomNpmFromTree(G, 'HEAD:pkg'); const r2 = sbomNpmFromTree(G, 'HEAD:pkg');
const S = scrieSbom('proba-sbom.sbom.cdx.json', r1.bom);
const att = await attest({ artifacts: [TGZ], sbom: S, name: 'proba-sbom', version: '0.1.0', sourcePath: 'pkg', build: 'npm-pack', cwd: G });
await test('npm sbom din lockfile-ul comis: trei componente, si doua rulari difera in octeti (numar de serie, timp) dar nu in continut', () => {
cere(r1.bom && r1.bom.components.length === 3, 'componente: ' + (r1.bom && r1.bom.components.length) + (r1.lipsa ? ' ' + r1.lipsa : ''));
cere(JSON.stringify(r1.bom) !== JSON.stringify(r2.bom), 'doua rulari ar trebui sa difere in octeti (altfel testul de mai jos nu masoara nimic)');
cere(JSON.stringify(sbomSemantica(r1.bom)) === JSON.stringify(sbomSemantica(r2.bom)), 'forma semantica trebuie sa fie aceeasi');
});
await test('verify --rebuild-from cu SBOM-ul dat: SBOM-ul spune ce spune lockfile-ul comis (VALID)', async () => {
const r = await verify(att, [TGZ, S], { rebuildFrom: C });
cere(r.valid && check(r, /attested SBOM says what the committed lockfile says/) && check(r, /attested SBOM says/).pass === true, JSON.stringify(r.checks.filter((c) => c.pass !== true)));
});
await test('un SBOM regenerat (alt numar de serie, alt timp) si re-atestat trece: judecata e pe continut, nu pe octeti', async () => {
const S2 = scrieSbom('proba-sbom.sbom.cdx.json', sbomNpmFromTree(G, 'HEAD:pkg').bom);
const a2 = await attest({ artifacts: [TGZ], sbom: S2, name: 'proba-sbom', version: '0.1.0', sourcePath: 'pkg', build: 'npm-pack', cwd: G });
const r = await verify(a2, [TGZ, S2], { rebuildFrom: C }); cere(r.valid, 'trebuia VALID');
fs.writeFileSync(S, JSON.stringify(r1.bom, null, 2));
});
async function editat(nume, schimba) {
const bom = JSON.parse(JSON.stringify(r1.bom)); schimba(bom);
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-sbom-e-')); const f = path.join(d, 'proba-sbom.sbom.cdx.json'); fs.writeFileSync(f, JSON.stringify(bom, null, 2));
const a = await attest({ artifacts: [TGZ], sbom: f, name: 'proba-sbom', version: '0.1.0', sourcePath: 'pkg', build: 'npm-pack', cwd: G });
const simplu = await verify(a, [TGZ, f]);
const r = await verify(a, [TGZ, f], { rebuildFrom: C });
cere(simplu.valid, `${nume}: fara reconstructie, digestul singur trece (de asta conteaza comparatia)`);
cere(!r.valid && check(r, /attested SBOM says/).pass === false, `${nume}: trebuia prins; ${JSON.stringify(check(r, /attested SBOM says/))}`);
}
await test('CONTROL: o componenta scoasa din SBOM de mana, re-atestat -> prins la reconstructie', () => editat('scoasa', (b) => { b.components = b.components.filter((c) => !/dep-c/.test(c.name)); b.dependencies = (b.dependencies || []).map((d) => ({ ...d, dependsOn: (d.dependsOn || []).filter((x) => !/dep-c/.test(x)) })).filter((d) => !/dep-c/.test(d.ref)); }));
await test('CONTROL: o versiune schimbata in SBOM (si purl-ul ei) -> prins', () => editat('versiune', (b) => { const c = b.components.find((x) => x.name === 'dep-b'); c.version = '2.0.1'; c.purl = c.purl.replace('2.0.0', '2.0.1'); }));
await test('CONTROL: un hash de integritate schimbat in SBOM -> prins', () => editat('hash', (b) => { const c = b.components.find((x) => (x.hashes || []).length); c.hashes[0].content = 'ab'.repeat(64); }));
await test('SBOM-ul atestat nedat lui verify: comparatia nu se face, si se spune', async () => {
const r = await verify(att, [TGZ], { rebuildFrom: C });
cere(check(r, /^rebuild: SBOM$/) && check(r, /^rebuild: SBOM$/).pass === null, 'trebuia raportat nefacut');
});
await test('un arbore fara package-lock.json comis: SBOM-ul nu se poate re-deriva, si se spune (nu e trecut drept VALID)', () => {
const r = sbomNpmFromTree(G, 'HEAD:fara');
cere(!r.bom && /no package-lock/.test(r.lipsa || ''), JSON.stringify(r).slice(0, 120));
});
for (const d of [G, C, OUT]) fs.rmSync(d, { recursive: true, force: true });
console.log(`\n${treceri} treceri, ${esecuri.length} esecuri`);
process.exitCode = esecuri.length ? 1 : 0;