aere-proof-of-software/sdk-pq-sign/test/pq-sign.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

109 lines
6.6 KiB
JavaScript

// Probele @aere/pq-sign: fiecare afirmatie cu perechea ei negativa. Offline; cu AERE_CHEIE in mediu ruleaza si
// verificarea INDEPENDENTA pe lant a jumatatii post-cuantice (precompila 2800), in ambele sensuri.
import assert from 'node:assert';
import { generateKeyPair, sign, verify, serialize, parse, envelopeHash, toSign, messageHash, fromHex, toHex, PQ_SCHEMES, CLASSICAL_SCHEMES, verifyOnChain } from '../index.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 flip = (hex, i = -1) => { const b = fromHex(hex); const k = i < 0 ? b.length - 1 : i; b[k] ^= 0x01; return toHex(b); };
const MSG = 'Invoice 2026-0917: 12,400 EUR payable to Aere Foundation';
for (const alg of ['secp256k1+ml-dsa-65', 'ed25519+ml-dsa-65', 'secp256k1+ml-dsa-44', 'ed25519+ml-dsa-87']) {
const keys = generateKeyPair({ alg });
const env = sign(MSG, keys);
await test(`${alg}: sign then verify is valid, both halves`, () => {
const r = verify(MSG, env);
assert.strictEqual(r.valid, true, JSON.stringify(r));
assert.strictEqual(r.classical, true); assert.strictEqual(r.pq, true);
});
await test(`${alg}: sizes are the scheme's (key ${PQ_SCHEMES[alg.split('+')[1]].publicKeyBytes} B, signature ${PQ_SCHEMES[alg.split('+')[1]].signatureBytes} B)`, () => {
const P = PQ_SCHEMES[alg.split('+')[1]], C = CLASSICAL_SCHEMES[alg.split('+')[0]];
assert.strictEqual(fromHex(env.pqPublicKey).length, P.publicKeyBytes);
assert.strictEqual(fromHex(env.pqSignature).length, P.signatureBytes);
assert.strictEqual(fromHex(env.classicalSignature).length, C.signatureBytes);
assert.strictEqual(fromHex(env.classicalPublicKey).length, C.publicKeyBytes);
});
await test(`${alg}: one flipped bit in the post-quantum half invalidates the hybrid (classical alone is not enough)`, () => {
const r = verify(MSG, { ...env, pqSignature: flip(env.pqSignature) });
assert.strictEqual(r.valid, false); assert.strictEqual(r.classical, true); assert.strictEqual(r.pq, false);
assert.match(r.reason, /post-quantum half/);
});
await test(`${alg}: one flipped bit in the classical half invalidates the hybrid (post-quantum alone is not enough)`, () => {
const r = verify(MSG, { ...env, classicalSignature: flip(env.classicalSignature, 10) });
assert.strictEqual(r.valid, false); assert.strictEqual(r.pq, true); assert.strictEqual(r.classical, false);
assert.match(r.reason, /classical half/);
});
await test(`${alg}: another message is refused by both halves`, () => {
const r = verify(MSG + '.', env);
assert.strictEqual(r.valid, false); assert.strictEqual(r.messageHashMatches, false);
assert.strictEqual(r.classical, false); assert.strictEqual(r.pq, false);
});
await test(`${alg}: another post-quantum public key is refused`, () => {
const alta = generateKeyPair({ alg });
const r = verify(MSG, { ...env, pqPublicKey: alta.pq.publicKey });
assert.strictEqual(r.valid, false); assert.strictEqual(r.pq, false);
});
await test(`${alg}: the envelope survives JSON and its digest is stable`, () => {
const back = parse(serialize(env));
assert.deepStrictEqual(back, env);
assert.strictEqual(verify(MSG, back).valid, true);
assert.strictEqual(envelopeHash(back), envelopeHash(env));
assert.match(envelopeHash(env), /^0x[0-9a-f]{64}$/);
});
}
await test('the signed digest binds domain, algorithm and message: same message under another alg has another toSign', () => {
const mh = messageHash(MSG);
assert.notStrictEqual(toHex(toSign('secp256k1+ml-dsa-65', mh)), toHex(toSign('ed25519+ml-dsa-65', mh)));
assert.strictEqual(toHex(toSign('secp256k1+ml-dsa-65', mh)), toHex(toSign('secp256k1+ml-dsa-65', mh)));
});
await test('a deterministic seed gives the same post-quantum key (vectors), classical keys stay fresh', () => {
const seed = '0x' + '11'.repeat(32);
const a = generateKeyPair({ seed }), b = generateKeyPair({ seed });
assert.strictEqual(a.pq.publicKey, b.pq.publicKey);
assert.notStrictEqual(a.classical.publicKey, b.classical.publicKey);
});
await test('a mismatched classical key pair is refused at signing time', () => {
const k = generateKeyPair(); const alt = generateKeyPair();
assert.throws(() => sign(MSG, { ...k, classical: { ...k.classical, publicKey: alt.classical.publicKey } }), /does not match/);
});
await test('an unknown algorithm pair is refused', () => {
assert.throws(() => generateKeyPair({ alg: 'rsa+ml-dsa-65' }), /unknown alg/);
const r = verify(MSG, { v: 1, kind: 'aere-hybrid-signature', hash: 'sha256', alg: 'secp256k1+sphincs' });
assert.strictEqual(r.valid, false); assert.match(r.reason, /malformed|unknown/);
});
await test('a foreign envelope is refused without throwing', () => {
const r = verify(MSG, { v: 2, kind: 'jws' });
assert.strictEqual(r.valid, false); assert.strictEqual(r.reason, 'unknown envelope');
});
await test('binary messages sign and verify like text', () => {
const k = generateKeyPair(); const m = new Uint8Array([0, 255, 1, 2, 3, 254]);
assert.strictEqual(verify(m, sign(m, k)).valid, true);
assert.strictEqual(verify(new Uint8Array([0, 255, 1, 2, 3, 253]), sign(m, k)).valid, false);
});
// optional: the chain precompile judges the ML-DSA half, independently of this code, in both directions.
// The key comes from AERE_CHEIE or is read by this process from the file named in AERE_CHEIE_FISIER (never echoed).
const CHEIE = process.env.AERE_CHEIE || (process.env.AERE_CHEIE_FISIER ? (await import('node:fs')).readFileSync(process.env.AERE_CHEIE_FISIER, 'utf8').trim() : '');
if (CHEIE) {
const { AereCloud } = await import('../../aere-cloud-sdk/index.mjs');
const cloud = new AereCloud({ apiKey: CHEIE });
const keys = generateKeyPair({ alg: 'secp256k1+ml-dsa-44' });
const env = sign(MSG, keys);
await test('on chain (precompile 0x...0ae3): the post-quantum half of a fresh hybrid signature verifies', async () => {
const r = await verifyOnChain(env, cloud);
assert.strictEqual(r.pqValidOnChain, true, JSON.stringify(r)); assert.ok(r.block > 0);
});
await test('on chain: one flipped bit in the post-quantum half is refused by the precompile', async () => {
const r = await verifyOnChain({ ...env, pqSignature: flip(env.pqSignature) }, cloud);
assert.strictEqual(r.pqValidOnChain, false, JSON.stringify(r));
});
} else console.log(' (sarit) verificarea pe lant a jumatatii ML-DSA: pune AERE_CHEIE in mediu');
console.log(`\n${treceri} treceri, ${esecuri.length} esecuri`);
if (esecuri.length) process.exitCode = 1;