The issuer key (a public JWK, or the public keys of an AERE identity) is checked first: one that cannot be read, is private, or is not a key for the algorithm asked exits 2 with the reason, instead of reporting the token INVALID. --json prints compact JSON (indented output grew with the square of the claims' nesting depth). --at judges at a given time, as for verify. Test on the RFC 9901 example presentation: valid at its own time, invalid now, a private key refused. Tests: SD-JWT 23/23, negative control 28/28; identity 44/44, negative control 49/49.
78 lines
7.4 KiB
JavaScript
78 lines
7.4 KiB
JavaScript
// Controlul negativ al probei SD-JWT (sdjwt.mjs, proba-sdjwt.mjs): fiecare paznic se strica intr-o COPIE, proba ruleaza pe copie si proba
|
|
// NUMITA trebuie sa iasa rosie, cu proba chiar rulata; pe copia neatinsa, verde. Trei stari: un tipar care nu apare exact o data sau o
|
|
// proba care nu ajunge la rezumat e STRICAT si se numara esec.
|
|
// node control-negativ-sdjwt.mjs iesire 0 = martorul verde si toate plantarile rosii pe proba lor
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const S = 'sdjwt.mjs';
|
|
const PLANTARI = [
|
|
// [nume, tipar, inlocuire, proba (inceputul numelui ei)]
|
|
['alg none primit', 'if (!ALGS.includes(antet.alg)) throw', "if (antet.alg === 'nimic') throw", 'ATAC: alg none'],
|
|
['algoritmul nu mai trebuie sa fie al cheii (confuzia)', 'if (!potriveste(antet.alg, key)) throw', 'if (false) throw', 'ATAC: alg none'],
|
|
['o dezvaluire nelegata de niciun digest primita', 'if (folosite.size !== dupaDigest.size) return', 'if (false) return', 'ATAC: o dezvaluire cu valoarea schimbata'],
|
|
['aceeasi dezvaluire de doua ori primita', "if (dupaDigest.has(dg)) return refuz('the same disclosure is given twice');", '', 'ATAC: o dezvaluire cu valoarea schimbata'],
|
|
['numele _sd, ... sau __proto__ primite', "if (c[1] === '_sd' || c[1] === '...' || c[1] === '__proto__') throw", 'if (false) throw', 'ATAC: un emitent care pune in _sd'],
|
|
['o afirmatie dezvaluita peste una in clar primita', 'if (Object.hasOwn(out, c[1])) throw', 'if (false) throw', 'ATAC: un emitent care pune in _sd'],
|
|
['acelasi digest de doua ori primit', 'const vezi = (dg) => { if (vazute.has(dg)) throw', 'const vezi = (dg) => { if (false) throw', 'ATAC: un emitent care pune in _sd'],
|
|
['sd_hash nu se mai compara', 'if (sha256b64u(baza) !== K.sd_hash) return', 'if (false) return', 'RFC 9901: o dezvaluire scoasa'],
|
|
['tipul KB (kb+jwt) nu se mai cere', "k = verificaJws(kb, payload.cnf.jwk, { typ: 'kb+jwt' });", 'k = verificaJws(kb, payload.cnf.jwk, { typ: null });', 'ATAC: KB semnat de alta cheie'],
|
|
['publicul KB nu se mai compara', 'if (K.aud !== audience) return', 'if (false) return', 'RFC 9901: aceeasi prezentare pentru ALT verificator'],
|
|
['nonce-ul KB nu se mai compara', 'if (K.nonce !== nonce) return', 'if (false) return', 'RFC 9901: aceeasi prezentare pentru ALT verificator'],
|
|
['KB vechi primit', 'if (Math.abs(acum - K.iat) > maxAgeS) return', 'if (false) return', 'RFC 9901: aceeasi prezentare pentru ALT verificator'],
|
|
['exp nu se mai cere', 'if (payload.exp !== undefined && acum >= payload.exp) return', 'if (false) return', 'timpul'],
|
|
['fara toleranta de ceas pe nbf', 'if (payload.nbf !== undefined && payload.nbf - clockSkewS > acum) return', 'if (payload.nbf !== undefined && payload.nbf > acum) return', 'timpul'],
|
|
['Key Binding cerut, dar lipsa primita', "if (requireKeyBinding) return refuz('key binding is required", "if (false) return refuz('key binding is required", 'RFC 9901: SD-JWT-ul standardului cu ALTA cheie'],
|
|
// revizuirea adversariala din 30 sept: forma de dinainte a fiecarei reparatii
|
|
['emitentul semneaza structura SD-JWT din valori', 'if (areStructuraSd(claims)) throw', 'if (false) throw', 'REVIZUIRE: emitentul refuza structura'],
|
|
['prezentarea arata elementul din orice lista', 'obiect(r) && r.in === l.lista && c.length === 2', 'obiect(r) && c.length === 2', 'REVIZUIRE: prezentarea arata elementul'],
|
|
['publicul si nonce-ul nu se mai cer', "if ((requireKeyBinding || kb !== '') && (typeof audience !== 'string'", "if (false && (typeof audience !== 'string'", 'REVIZUIRE: fara publicul si nonce-ul'],
|
|
['exp nu se mai cere implicit', 'if (requireExp && payload.exp === undefined) return', 'if (false) return', 'REVIZUIRE: exp cerut implicit'],
|
|
['tipul jetonului nu se mai verifica implicit', "if (cerut === undefined) return typeof typ === 'string' && /(^|\\+)sd-jwt$/.test(typ);", 'if (cerut === undefined) return true;', 'REVIZUIRE: tipul jetonului'],
|
|
['expectedIssuer nu se mai compara', 'if (expectedIssuer != null && corp.iss !== expectedIssuer) return', 'if (false) return', 'REVIZUIRE: tipul jetonului'],
|
|
['crit primit', "if (Object.hasOwn(antet, 'crit')) throw", 'if (false) throw', 'REVIZUIRE: crit'],
|
|
['base64url necanonic primit', "if (b.toString('base64url') !== s) throw", 'if (false) throw', 'REVIZUIRE: crit'],
|
|
['exp-ul KB nu se mai cere', 'if (K.exp !== undefined && (!Number.isFinite(K.exp) || acum >= K.exp)) return', 'if (false) return', 'REVIZUIRE: crit'],
|
|
['_sd_alg scos de la orice nivel', "!(sus && k === '_sd_alg')", "k !== '_sd_alg'", 'REVIZUIRE: crit'],
|
|
['cheia publica Node nu mai e primita', "(k.type === 'public' ? k : crypto.createPublicKey(k))", 'crypto.createPublicKey(k)', 'REVIZUIRE: cheia emitentului'],
|
|
['o afirmatie __proto__ primita (prototipul schimbat)', "if (Object.hasOwn(x, '__proto__')) throw", 'if (false) throw', 'ATAC: o afirmatie numita __proto__'],
|
|
['elementele de lista nedezvaluite pastrate', 'if (!c) continue; // nedezvaluit: elementul se scoate', "if (!c) { out.push(e); continue; }", 'AERE -> SD-JWT (EdDSA'],
|
|
];
|
|
function copie() {
|
|
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-sdjwt-ctl-'));
|
|
for (const f of [S, 'identity.mjs', 'identity-cli.mjs', 'proba-sdjwt.mjs', 'fixturi-rfc9901.json']) fs.copyFileSync(path.join(AICI, f), path.join(t, f));
|
|
return t;
|
|
}
|
|
function ruleaza(t) {
|
|
return new Promise((resolve) => {
|
|
const c = spawn(process.execPath, [path.join(t, 'proba-sdjwt.mjs')]); let out = '';
|
|
const ceas = setTimeout(() => c.kill(), 180000);
|
|
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; });
|
|
c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /aere-sd-jwt: \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => l.startsWith(' RAU ')) }); });
|
|
});
|
|
}
|
|
async function planteaza([nume, din, inl, tinta]) {
|
|
const t = copie();
|
|
try {
|
|
const f = path.join(t, S); const src = fs.readFileSync(f, 'utf8');
|
|
if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori`];
|
|
fs.writeFileSync(f, src.replace(din, inl));
|
|
const r = await ruleaza(t);
|
|
if (!r.rulat) return [false, ` STRICAT ${nume}: proba nu a ajuns la rezumat (cod ${r.cod})`];
|
|
if (r.rosii.some((l) => l.startsWith(' RAU ' + tinta))) return [true, ` ROSU cum trebuia ${nume} (proba '${tinta}' pica)`];
|
|
return [false, ` CONTROL CAZUT ${nume}: proba '${tinta}' a ramas verde (${r.rosii.length} rosii altundeva)`];
|
|
} finally { fs.rmSync(t, { recursive: true, force: true }); }
|
|
}
|
|
let rele = 0;
|
|
const t0 = copie(); const m = await ruleaza(t0); fs.rmSync(t0, { recursive: true, force: true });
|
|
if (m.rulat && m.cod === 0 && !m.rosii.length) console.log(' OK martorul: copia neatinsa verde'); else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii)`); }
|
|
const rez = new Array(PLANTARI.length); let i = 0;
|
|
await Promise.all(Array.from({ length: 4 }, async () => { while (i < PLANTARI.length) { const k = i++; rez[k] = await planteaza(PLANTARI[k]); } }));
|
|
for (const [bun, linie] of rez) { console.log(linie); if (!bun) rele++; }
|
|
console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `DOVEDIT: martorul verde, ${PLANTARI.length} din ${PLANTARI.length} paznici scosi -> proba lor rosie`);
|
|
process.exitCode = rele ? 1 : 0;
|