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.
120 lines
12 KiB
JavaScript
120 lines
12 KiB
JavaScript
// Proof of Software 1.3.0: identitatea constructorului (acreditarea de dezvoltator emisa de o organizatie). Fiecare afirmatie cu
|
|
// perechea ei negativa. Offline: lantul e un obiect `cloud` injectat care raspunde ca Aere Cloud (numai pentru timpul de pe lant).
|
|
// node test/credential.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, issueCredential, revokeCredential, publicKeysOf } from '../pos.mjs';
|
|
import * as pq from '../../../sdk-pq-sign/index.mjs';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
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 check = (r, re) => r.checks.find((c) => re.test(c.name));
|
|
|
|
const D = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pos-cred-'));
|
|
const A = path.join(D, 'app.tgz'); fs.writeFileSync(A, 'artifact bytes');
|
|
const org = pq.generateKeyPair(), alt = pq.generateKeyPair(), dev = pq.generateKeyPair(), strain = pq.generateKeyPair();
|
|
const cred = issueCredential({ issuerKeys: org, issuerName: 'Example Org', subjectKeys: publicKeysOf(dev), subjectName: 'Ana Dev', validFrom: '2026-01-01T00:00:00Z', validUntil: '2027-01-01T00:00:00Z' });
|
|
const acum = new Date('2026-06-01T00:00:00Z');
|
|
const att = await attest({ artifacts: [A], name: 'app', version: '1.0.0', keys: dev, credential: cred, cwd: D, now: acum });
|
|
|
|
await test('1. atestare sub acreditare, verificata cu radacina de incredere a organizatiei: VALIDA, identitatea judecata pe fiecare punct', async () => {
|
|
const r = await verify(att, [A], { trustIssuer: publicKeysOf(org) });
|
|
cere(r.valid, JSON.stringify(r.checks.filter((c) => c.pass === false)));
|
|
for (const re of [/carries the credential/, /signed by the issuer/, /trust root you gave/, /names the keys that signed/]) cere(check(r, re) && check(r, re).pass === true, 'lipseste sau nu trece: ' + re);
|
|
cere(/createdAt, declared by the builder/.test(check(r, /valid at/).name), 'timpul folosit trebuie numit');
|
|
// 2026-09-29 (B-18): pe data declarata, "in valabilitate" nu se poate crede (o alege cine tine cheile): nejudecat, nu trecut
|
|
cere(check(r, /valid at/).pass === null, 'pe data declarata, valabilitatea trebuie raportata nejudecata');
|
|
});
|
|
await test('2. CONTROL: alta radacina de incredere (alta organizatie) -> INVALIDA', async () => {
|
|
const r = await verify(att, [A], { trustIssuer: publicKeysOf(alt) });
|
|
cere(!r.valid && check(r, /trust root/).pass === false, 'trebuia sa pice pe emitent');
|
|
});
|
|
await test('3. fara radacina de incredere: emitentul NU e judecat, si se spune (nu e trecut drept incredere)', async () => {
|
|
const r = await verify(att, [A]);
|
|
cere(check(r, /issuer trust/) && check(r, /issuer trust/).pass === null, 'trebuia raportat nejudecat');
|
|
});
|
|
await test('4. CONTROL: attest cu o acreditare care numeste alte chei decat --keys e refuzat', async () => {
|
|
let aruncat = null; try { await attest({ artifacts: [A], keys: strain, credential: cred, cwd: D }); } catch (e) { aruncat = e.message; }
|
|
cere(/other keys/.test(aruncat || ''), 'trebuia refuzat: ' + aruncat);
|
|
});
|
|
await test('5. CONTROL: acreditarea inlocuita in atestare (declaratia numeste alta) -> INVALIDA', async () => {
|
|
const alta = issueCredential({ issuerKeys: org, issuerName: 'Example Org', subjectKeys: publicKeysOf(dev), subjectName: 'Altcineva', validFrom: '2026-01-01T00:00:00Z', validUntil: '2027-01-01T00:00:00Z' });
|
|
const r = await verify({ ...att, credential: alta }, [A], { trustIssuer: publicKeysOf(org) });
|
|
cere(!r.valid && check(r, /carries the credential/).pass === false, 'trebuia prins');
|
|
});
|
|
await test('6. CONTROL: o acreditare care numeste organizatia dar e semnata de un strain -> INVALIDA (semnatarul nu e emitentul numit)', async () => {
|
|
const fals = issueCredential({ issuerKeys: strain, issuerName: 'Example Org', subjectKeys: publicKeysOf(dev), subjectName: 'Ana Dev', validFrom: '2026-01-01T00:00:00Z', validUntil: '2027-01-01T00:00:00Z' });
|
|
fals.body.issuer.keys = publicKeysOf(org);
|
|
fals.signature = pq.sign(JSON.stringify(fals.body), strain);
|
|
const a2 = await attest({ artifacts: [A], keys: dev, credential: fals, cwd: D, now: acum });
|
|
const r = await verify(a2, [A], { trustIssuer: publicKeysOf(org) });
|
|
cere(!r.valid && check(r, /signed by the issuer/).pass === false, 'trebuia prins');
|
|
});
|
|
await test('7. CONTROL: declaratia semnata de alte chei decat cele din acreditare -> INVALIDA', async () => {
|
|
const a2 = JSON.parse(JSON.stringify(att)); a2.signature = pq.sign(JSON.stringify(a2.statement), strain);
|
|
const r = await verify(a2, [A], { trustIssuer: publicKeysOf(org) });
|
|
cere(!r.valid && check(r, /names the keys that signed/).pass === false, 'trebuia prins');
|
|
});
|
|
await test('8. CONTROL: momentul atestarii in afara valabilitatii -> INVALIDA', async () => {
|
|
const a2 = await attest({ artifacts: [A], keys: dev, credential: cred, cwd: D, now: new Date('2027-03-01T00:00:00Z') });
|
|
const r = await verify(a2, [A], { trustIssuer: publicKeysOf(org) });
|
|
cere(!r.valid && check(r, /valid at/).pass === false, 'trebuia prins');
|
|
});
|
|
await test('9. revocarea: inaintea atestarii -> INVALIDA; dupa ea -> VALIDA; semnata de alt emitent -> ignorata si spusa', async () => {
|
|
const inainte = revokeCredential({ issuerKeys: org, credential: cred, revokedAt: '2026-05-01T00:00:00Z', reason: 'cheie pierduta' });
|
|
const dupa = revokeCredential({ issuerKeys: org, credential: cred, revokedAt: '2026-07-01T00:00:00Z' });
|
|
const straina = revokeCredential({ issuerKeys: alt, credential: cred, revokedAt: '2026-02-01T00:00:00Z' });
|
|
const r1 = await verify(att, [A], { trustIssuer: publicKeysOf(org), revocations: [inainte] });
|
|
cere(!r1.valid && check(r1, /not revoked/).pass === false, 'revocarea de dinainte trebuia sa pice');
|
|
const r2 = await verify(att, [A], { trustIssuer: publicKeysOf(org), revocations: [dupa] });
|
|
cere(r2.valid && check(r2, /not revoked/).pass === null, 'revocarea de dupa nu acuza atestarea, dar pe data declarata nici nu o achita (B-18)');
|
|
const r3 = await verify(att, [A], { trustIssuer: publicKeysOf(org), revocations: [straina] });
|
|
cere(r3.valid && check(r3, /a revocation/).pass === null, 'o revocare straina se ignora si se spune');
|
|
const r4 = await verify(att, [A], { trustIssuer: publicKeysOf(org) });
|
|
// AERE-SINTETIC: nu e un secret; 'credential: revocations' e numele unei verificari
|
|
cere(check(r4, /credential: revocations/).pass === null, 'fara revocari date, se spune ca nu s-a putut vedea');
|
|
});
|
|
await test('10. timpul de pe LANT bate data declarata: un constructor care isi antedateaza atestarea sub o acreditare expirata e prins', async () => {
|
|
// createdAt declarat 2026-06-01 (in valabilitate), dar lantul a vazut-o prima oara pe 2027-02-01 (dupa expirare)
|
|
const a2 = JSON.parse(JSON.stringify(att)); a2.notarization = { block: 123, txHash: '0x' + 'ab'.repeat(32) };
|
|
const cloud = { proof: async () => ({ notarized: true, block: 123, txHash: '0x' + 'ab'.repeat(32), firstSeenAt: Date.parse('2027-02-01T00:00:00Z') / 1000, finality: 'post-quantum', pqAnchor: null }) };
|
|
const r = await verify(a2, [A], { trustIssuer: publicKeysOf(org), cloud });
|
|
cere(!r.valid && check(r, /valid at/).pass === false && /first seen on chain/.test(check(r, /valid at/).name), JSON.stringify(check(r, /valid at/)));
|
|
const cloudBun = { proof: async () => ({ notarized: true, block: 123, txHash: '0x' + 'ab'.repeat(32), firstSeenAt: Date.parse('2026-06-02T00:00:00Z') / 1000, finality: 'post-quantum', pqAnchor: null }) };
|
|
const rb = await verify(a2, [A], { trustIssuer: publicKeysOf(org), cloud: cloudBun });
|
|
cere(rb.valid && /first seen on chain/.test(check(rb, /valid at/).name), 'cu timpul lantului in valabilitate trebuia VALIDA');
|
|
});
|
|
await test('11. CLI cap la cap: pubkey, credential issue, attest --credential, verify --trust-issuer (0), alt emitent (1), revocare (1)', async () => {
|
|
const pos = path.join(AICI, '..', 'pos.mjs'); const f = (n) => path.join(D, n);
|
|
fs.writeFileSync(f('org.json'), JSON.stringify(org)); fs.writeFileSync(f('dev.json'), JSON.stringify(dev)); fs.writeFileSync(f('alt.json'), JSON.stringify(alt));
|
|
const run = (args) => { try { execFileSync(process.execPath, [pos, ...args], { cwd: D, stdio: 'pipe' }); return 0; } catch (e) { return e.status; } };
|
|
cere(run(['pubkey', '--keys', f('org.json'), '--out', f('org.pub.json')]) === 0 && run(['pubkey', '--keys', f('dev.json'), '--out', f('dev.pub.json')]) === 0 && run(['pubkey', '--keys', f('alt.json'), '--out', f('alt.pub.json')]) === 0, 'pubkey');
|
|
cere(!('secretKey' in (JSON.parse(fs.readFileSync(f('org.pub.json'), 'utf8')).classical || {})) && !JSON.stringify(JSON.parse(fs.readFileSync(f('org.pub.json'), 'utf8'))).includes(org.pq.secretKey), 'fisierul public nu are voie sa poarte cheia secreta');
|
|
cere(run(['credential', 'issue', '--issuer-keys', f('org.json'), '--issuer-name', 'Example Org', '--subject-pub', f('dev.pub.json'), '--subject-name', 'Ana Dev', '--valid-days', '30', '--out', f('cred.json')]) === 0, 'credential issue');
|
|
cere(run(['attest', '--out', f('att.json'), '--keys', f('dev.json'), '--credential', f('cred.json'), A]) === 0, 'attest');
|
|
cere(run(['verify', f('att.json'), '--trust-issuer', f('org.pub.json'), A]) === 0, 'verify cu emitentul bun');
|
|
cere(run(['verify', f('att.json'), '--trust-issuer', f('alt.pub.json'), A]) === 1, 'CONTROL: verify cu alt emitent');
|
|
cere(run(['credential', 'revoke', '--issuer-keys', f('org.json'), '--credential', f('cred.json'), '--at', '2020-01-01T00:00:00Z', '--out', f('rev.json')]) === 0, 'credential revoke');
|
|
cere(run(['verify', f('att.json'), '--trust-issuer', f('org.pub.json'), '--revocations', f('rev.json'), A]) === 1, 'CONTROL: verify cu revocarea');
|
|
});
|
|
await test('12. B-18: cu cheile unei acreditari REVOCATE, o atestare antedatata inaintea revocarii NU mai trece drept nerevocata; cu timpul lantului dupa revocare e prinsa, inainte e trecuta', async () => {
|
|
const rev = revokeCredential({ issuerKeys: org, credential: cred, revokedAt: '2026-09-01T00:00:00Z', reason: 'the keys left the company' });
|
|
// hotul semneaza in octombrie, declara iunie
|
|
const r = await verify(att, [A], { trustIssuer: publicKeysOf(org), revocations: [rev] });
|
|
cere(check(r, /not revoked/).pass === null && check(r, /valid at/).pass === null, 'pe data declarata, revocarea si valabilitatea trebuie raportate nejudecate: ' + JSON.stringify(check(r, /not revoked/)));
|
|
const a2 = JSON.parse(JSON.stringify(att)); a2.notarization = { block: 7, txHash: '0x' + 'cd'.repeat(32) };
|
|
const lant = (iso) => ({ proof: async () => ({ notarized: true, block: 7, txHash: '0x' + 'cd'.repeat(32), firstSeenAt: Date.parse(iso) / 1000, finality: 'post-quantum', pqAnchor: null }) });
|
|
const rDupa = await verify(a2, [A], { trustIssuer: publicKeysOf(org), revocations: [rev], cloud: lant('2026-10-02T00:00:00Z') });
|
|
cere(!rDupa.valid && check(rDupa, /not revoked/).pass === false, 'lantul a vazut-o dupa revocare: trebuia prinsa');
|
|
const rInainte = await verify(a2, [A], { trustIssuer: publicKeysOf(org), revocations: [rev], cloud: lant('2026-06-02T00:00:00Z') });
|
|
cere(rInainte.valid && check(rInainte, /not revoked/).pass === true && check(rInainte, /valid at/).pass === true, 'lantul a vazut-o inainte de revocare, in valabilitate: trecuta');
|
|
});
|
|
fs.rmSync(D, { recursive: true, force: true });
|
|
console.log(`\n${treceri} treceri, ${esecuri.length} esecuri`);
|
|
process.exitCode = esecuri.length ? 1 : 0;
|