aere-proof-of-software/tools/proof-of-software/test/sbom-go.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

112 lines
10 KiB
JavaScript

// Proof of Software 1.4.0: SBOM-ul unui modul Go, derivat determinist din go.mod si go.sum COMISE, si judecat la verify --rebuild-from.
// Fixturile sunt fisiere REALE, scrise de unealta Go: go.mod/go.sum din sdk-go (al nostru) si din github.com/golang/mock@v1.6.0 (din
// memoria de module a lui Go, un go.sum cu multe module fixate numai prin go.mod). Fiecare afirmatie cu perechea ei negativa. Offline.
// node test/sbom-go.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 { fileURLToPath } from 'node:url';
import { attest, verify, sbomGo, sbomGoFromTree, sbomSemantica } from '../pos.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const POS = path.resolve(AICI, '..', 'pos.mjs');
const FX = path.join(AICI, 'fixturi-go');
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 citeste = (d) => ({ goMod: fs.readFileSync(path.join(FX, d, 'go.mod'), 'utf8'), goSum: fs.readFileSync(path.join(FX, d, 'go.sum'), 'utf8') });
const SDK = citeste('sdk-go'), MOCK = citeste('gomock');
const bSdk = sbomGo({ ...SDK, version: 'v1.0.0' }), bMock = sbomGo({ ...MOCK, version: 'v1.6.0' });
const prop = (c, n) => ((c.properties || []).find((x) => x.name === n) || {}).value;
await test('sdk-go: trei module cu continut fixat in go.sum, cu purl pkg:golang si hash-ul h1 ca proprietate; radacina din go.mod', () => {
cere(bSdk.components.length === 3, 'componente: ' + bSdk.components.length);
cere(bSdk.components.map((c) => c.purl).join(' ') === 'pkg:golang/github.com/cloudflare/circl@v1.6.4 pkg:golang/golang.org/x/crypto@v0.54.0 pkg:golang/golang.org/x/sys@v0.47.0', bSdk.components.map((c) => c.purl).join(' '));
cere(bSdk.metadata.component.purl === 'pkg:golang/aere.network/pqc@v1.0.0', bSdk.metadata.component.purl);
cere(bSdk.components.every((c) => /^h1:[A-Za-z0-9+/]{43}=$/.test(prop(c, 'aere:go-sum-h1')) && !(c.hashes || []).length), 'h1 ca proprietate, fara hashes SHA-256');
});
await test('gomock: numai modulele cu continut fixat sunt componente; cele fixate numai prin go.mod se numara (numaratoare independenta)', () => {
// numaratoarea independenta, alt cod decat cel masurat: perechi distincte "modul versiune", cu si fara randul de continut
const randuri = MOCK.goSum.split('\n').filter(Boolean).map((l) => l.split(' '));
const cuContinut = new Set(randuri.filter((r) => !r[1].endsWith('/go.mod')).map((r) => r[0] + ' ' + r[1]));
const toate = new Set(randuri.map((r) => r[0] + ' ' + r[1].replace(/\/go\.mod$/, '')));
cere(bMock.components.length === cuContinut.size && cuContinut.size === 5, `componente ${bMock.components.length}, asteptat ${cuContinut.size}`);
cere(Number(prop(bMock.metadata, 'aere:go-sum-go-mod-only')) === toate.size - cuContinut.size && toate.size - cuContinut.size > 10, `numai go.mod: ${prop(bMock.metadata, 'aere:go-sum-go-mod-only')} fata de ${toate.size - cuContinut.size}`);
cere(bMock.components.some((c) => c.purl === 'pkg:golang/golang.org/x/tools@v0.1.1'), 'x/tools v0.1.1 lipseste');
});
await test('determinist: aceleasi fisiere dau aceiasi octeti (fara timp; numarul de serie e UUID derivat din continut)', () => {
const alt = sbomGo({ ...SDK, version: 'v1.0.0' });
cere(JSON.stringify(alt) === JSON.stringify(bSdk), 'doua derivari difera');
cere(/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(bSdk.serialNumber), bSdk.serialNumber);
cere(sbomGo({ ...SDK, version: 'v1.0.1' }).serialNumber !== bSdk.serialNumber, 'CONTROL: alta versiune, alt numar de serie');
});
await test('CONTROL POZITIV al metodei: hash-ul h1 citit din go.sum e chiar cel pe care Go l-a calculat la descarcarea modulului', () => {
const cache = (() => { try { return execFileSync('go', ['env', 'GOMODCACHE'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch { return null; } })();
const perechi = bSdk.components.map((c) => ({ c, f: cache && path.join(cache, 'cache', 'download', ...c.name.split('/'), '@v', c.version + '.ziphash') })).filter((x) => x.f && fs.existsSync(x.f));
if (!perechi.length) { console.log(' SARIT (nicio arhiva a acestor module in memoria de module Go de pe aceasta masina)'); return; }
for (const { c, f } of perechi) cere(fs.readFileSync(f, 'utf8').trim() === prop(c, 'aere:go-sum-h1'), `${c.name}: ${fs.readFileSync(f, 'utf8').trim()} fata de ${prop(c, 'aere:go-sum-h1')}`);
console.log(` (${perechi.length} din ${bSdk.components.length} module comparate cu memoria de module Go)`);
});
await test('CONTROL: un rand de go.sum care nu are forma lui, sau un go.mod fara modul -> refuzat, nu ghicit', () => {
let e1 = null; try { sbomGo({ goMod: SDK.goMod, goSum: SDK.goSum + 'rand stricat\n', version: 'v1' }); } catch (e) { e1 = e.message; }
let e2 = null; try { sbomGo({ goMod: 'go 1.21\n', goSum: SDK.goSum, version: 'v1' }); } catch (e) { e2 = e.message; }
cere(/go.sum line \d+/.test(e1 || '') && /names no module/.test(e2 || ''), `${e1} | ${e2}`);
});
// fluxul intreg, intr-un depozit git temporar: SBOM-ul scris de `sbom-go` din arborele comis, atestat, verificat cu --rebuild-from
const G = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-go-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 M = path.join(G, 'mod'); fs.mkdirSync(M, { recursive: true });
fs.writeFileSync(path.join(M, 'go.mod'), SDK.goMod); fs.writeFileSync(path.join(M, 'go.sum'), SDK.goSum);
fs.writeFileSync(path.join(M, 'main.go'), 'package main\n\nfunc main() {}\n');
fs.mkdirSync(path.join(G, 'fara'), { recursive: true }); fs.writeFileSync(path.join(G, 'fara', 'go.mod'), 'module example.com/fara\n\ngo 1.21\n');
g(['add', '-A'], G); g(['commit', '-q', '-m', 'modul de proba'], G);
const C = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-go-c-')); g(['clone', '-q', G, C], os.tmpdir());
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-go-o-'));
const BIN = path.join(OUT, 'app-linux-amd64'); fs.writeFileSync(BIN, 'binarul construit altfel decat cu npm pack');
const S = path.join(OUT, 'app.sbom.cdx.json');
const cli = execFileSync(process.execPath, [POS, 'sbom-go', '--source-path', 'mod', '--version', 'v1.0.0', '--out', S], { cwd: G, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
const att = await attest({ artifacts: [BIN], sbom: S, name: 'app', version: 'v1.0.0', sourcePath: 'mod', cwd: G });
await test('sbom-go (linia de comanda) scrie din arborele COMIS exact SBOM-ul pe care il re-deriva verificarea', () => {
const r = sbomGoFromTree(G, g(['rev-parse', 'HEAD'], G), 'mod', 'v1.0.0');
cere(/3 module\(s\) pinned/.test(cli), cli);
cere(fs.readFileSync(S, 'utf8') === JSON.stringify(r.bom, null, 1) + '\n', 'fisierul scris difera de derivarea din arbore');
});
await test('verify --rebuild-from: arborele e cel atestat, SBOM-ul spune ce fixeaza go.sum-ul comis, iar artefactele nereconstruite sunt ABSENTE, nu false (VALID)', async () => {
const r = await verify(att, [BIN, S], { rebuildFrom: C });
const sb = check(r, /attested SBOM says what the committed go.sum pins/), art = check(r, /^rebuild: artifacts$/);
cere(r.valid && sb && sb.pass === true && art && art.pass === null, JSON.stringify(r.checks.filter((c) => c.pass !== true)));
});
async function editat(nume, schimba) {
const bom = JSON.parse(fs.readFileSync(S, 'utf8')); schimba(bom);
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-go-e-')); const f = path.join(d, 'app.sbom.cdx.json'); fs.writeFileSync(f, JSON.stringify(bom, null, 1));
const a = await attest({ artifacts: [BIN], sbom: f, name: 'app', version: 'v1.0.0', sourcePath: 'mod', cwd: G });
const simplu = await verify(a, [BIN, f]);
const r = await verify(a, [BIN, f], { rebuildFrom: C });
fs.rmSync(d, { recursive: true, force: true });
cere(simplu.valid, `${nume}: fara reconstructie, digestul singur trece (de asta conteaza comparatia)`);
const sb = check(r, /attested SBOM says/);
cere(!r.valid && sb && sb.pass === false, `${nume}: trebuia prins; ${JSON.stringify(sb)}`);
}
await test('CONTROL: un modul scos din SBOM de mana, re-atestat -> prins la reconstructie', () => editat('scos', (b) => { b.components = b.components.filter((c) => !/circl/.test(c.name)); }));
await test('CONTROL: o versiune schimbata in SBOM (si purl-ul ei) -> prins', () => editat('versiune', (b) => { const c = b.components.find((x) => /x\/sys/.test(x.name)); c.version = 'v0.48.0'; c.purl = c.purl.replace('v0.47.0', 'v0.48.0'); c['bom-ref'] = c.purl; }));
await test('CONTROL: un hash h1 schimbat in SBOM -> prins', () => editat('h1', (b) => { const c = b.components[0]; c.properties = [{ name: 'aere:go-sum-h1', value: 'h1:' + 'A'.repeat(43) + '=' }]; }));
await test('CONTROL: SBOM-ul spune alt modul radacina decat go.mod-ul comis -> prins', () => editat('radacina', (b) => { b.metadata.component.name = 'example.com/altul'; b.metadata.component.purl = 'pkg:golang/example.com/altul@v1.0.0'; }));
await test('un arbore fara go.sum comis: SBOM-ul nu se poate re-deriva, si se spune (nu e trecut drept VALID)', () => {
const r = sbomGoFromTree(G, g(['rev-parse', 'HEAD'], G), 'fara', 'v1');
cere(!r.bom && /no go.sum/.test(r.lipsa || ''), JSON.stringify(r).slice(0, 120));
});
await test('forma semantica: un SBOM npm (fara proprietati aere:) e judecat ca inainte; un SBOM Go poarta h1 in comparatie', () => {
const npm = { metadata: { component: { purl: 'pkg:npm/x@1' } }, components: [{ purl: 'pkg:npm/a@1', hashes: [{ alg: 'SHA-512', content: 'AB' }], properties: [{ name: 'cdx:npm:package:path', value: 'node_modules/a' }] }] };
cere(JSON.stringify(sbomSemantica(npm).componente[0].props) === '[]', 'proprietatile npm nu intra');
cere(sbomSemantica(bSdk).componente.every((c) => c.props.length === 1), 'fiecare componenta Go are h1 in forma semantica');
});
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;