identity: 60 s of clock allowance on starts (validFrom, notBefore) and, the careful way, on revocations; none on ends

A credential issued on a machine whose clock was one second ahead was "not yet valid" at a verifier synchronized by NTP: measured on
2026-09-30 through the Aere Cloud identity route, the first time a credential was issued on one machine and judged on another.
Starts (a credential's and a status list's validFrom, a delegation's notBefore) are now accepted up to 60 s in the verifier's future
(verifyPresentation clockSkewS, 0..600); a revocation dated up to 60 s ahead already applies; ends (validUntil, notAfter) get no
allowance, since that would extend a validity.

Tests: identity 44/44, negative control 49/49.
This commit is contained in:
Aere Network 2026-09-30 11:36:02 +03:00
parent aec0ccbead
commit 0b56d8e5df
4 changed files with 42 additions and 11 deletions

View File

@ -81,7 +81,10 @@ issuer, a bit set in any of them means revoked. A list is decompressed with a ce
`issuedAt`, `validFrom` and `validUntil` are the issuer's statements; the presentation time is the presenter's, bounded by the `issuedAt`, `validFrom` and `validUntil` are the issuer's statements; the presentation time is the presenter's, bounded by the
verifier's clock (`--max-age`, default 300 s, both ways); a revocation time is the revoker's. Everything is judged on the verifier's verifier's clock (`--max-age`, default 300 s, both ways); a revocation time is the revoker's. Everything is judged on the verifier's
clock. For a time the issuer does not choose, notarize: `proofOfCredential` and `proofOfDelegation` (in `identity.mjs`) build AIP-23 clock, which is never exactly the issuer's: starts (a credential's or a status list's `validFrom`, a delegation's `notBefore`) are
accepted up to 60 s in the verifier's future (`clockSkewS`), and a revocation dated up to 60 s ahead already applies; ends
(`validUntil`, `notAfter`) get no allowance, since that would extend a validity. (Measured 2026-09-30: without it, a credential issued
on a machine whose clock was a second ahead was "not yet valid" at a verifier synchronized by NTP.) For a time the issuer does not choose, notarize: `proofOfCredential` and `proofOfDelegation` (in `identity.mjs`) build AIP-23
envelopes (`identity` and `authorization` kinds of `../proof-kinds`) that the Aere Proof API notarizes and that the AIP-23 reference envelopes (`identity` and `authorization` kinds of `../proof-kinds`) that the Aere Proof API notarizes and that the AIP-23 reference
verifier checks. verifier checks.
@ -147,8 +150,8 @@ line is reported as not judged. The private key files are written with mode 0600
## Tests ## Tests
``` ```
node proba-identity.mjs # 43: the paths above, and each attack of the adversarial review as its own test node proba-identity.mjs # 44: the paths above, and each attack of the adversarial review as its own test
node control-negativ-identity.mjs # on a copy, each of 46 guards removed -> its own named test turns red node control-negativ-identity.mjs # on a copy, each of 49 guards removed -> its own named test turns red
node proba-conformitate.mjs # 14: compliance policies judged on real presentations, the record without personal data, the command line node proba-conformitate.mjs # 14: compliance policies judged on real presentations, the record without personal data, the command line
node control-negativ-conformitate.mjs # on a copy, each of 13 guards removed -> its own named test turns red node control-negativ-conformitate.mjs # on a copy, each of 13 guards removed -> its own named test turns red
node proba-travel-rule.mjs # 19: two VASPs with registry credentials, the whole exchange, and each attack of the review node proba-travel-rule.mjs # 19: two VASPs with registry credentials, the whole exchange, and each attack of the review

View File

@ -13,6 +13,10 @@ const DEV_VERIFY = path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs')
const VERIFY = process.env.AERE_VERIFY_PROOF || (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : ''); const VERIFY = process.env.AERE_VERIFY_PROOF || (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : '');
const L = 'identity.mjs', C = 'identity-cli.mjs'; const L = 'identity.mjs', C = 'identity-cli.mjs';
const PLANTARI = [ const PLANTARI = [
// B-30: forma publicata in aec0ccb (fara toleranta de ceas pe inceputuri, revocarea numai pe ceasul exact)
['fara toleranta pe validFrom (B-30)', L, 'vf - sk <= acum && acum <= vu,', 'vf <= acum && acum <= vu,', 'B-30'],
['toleranta si pe sfarsit (validUntil prelungit)', L, 'vf - sk <= acum && acum <= vu,', 'vf - sk <= acum && acum <= vu + sk,', 'B-30'],
['revocarea fara toleranta prudenta (B-30)', L, 'ok(`${et}: not revoked`, at > acum + sk,', 'ok(`${et}: not revoked`, at > acum,', 'revocarea: detinatorul revoca'],
// [nume, fisier, tipar, inlocuire, proba (inceputul numelui ei)] // [nume, fisier, tipar, inlocuire, proba (inceputul numelui ei)]
['o dezvaluire nesemnata de emitent primita', L, 'if (!sd.has(dg)) {', 'if (false) {', 'ATAC: o dezvaluire fabricata'], ['o dezvaluire nesemnata de emitent primita', L, 'if (!sd.has(dg)) {', 'if (false) {', 'ATAC: o dezvaluire fabricata'],
['aceeasi dezvaluire primita de doua ori', L, 'if (vazuteD.has(dg) || vazuteN.has(d.name)) {', 'if (false) {', 'ATAC: aceeasi dezvaluire de doua ori'], ['aceeasi dezvaluire primita de doua ori', L, 'if (vazuteD.has(dg) || vazuteN.has(d.name)) {', 'if (false) {', 'ATAC: aceeasi dezvaluire de doua ori'],
@ -33,7 +37,7 @@ const PLANTARI = [
['id-ul emitentului nu mai e derivat din chei', L, "ok('credential: the issuer id is the id of its keys', idE && idE === S.issuer.id,", "ok('credential: the issuer id is the id of its keys', true,", 'ATAC: id-ul emitentului schimbat'], ['id-ul emitentului nu mai e derivat din chei', L, "ok('credential: the issuer id is the id of its keys', idE && idE === S.issuer.id,", "ok('credential: the issuer id is the id of its keys', true,", 'ATAC: id-ul emitentului schimbat'],
['tipul cheii publice nu se mai verifica', L, 'if (k.asymmetricKeyType !== tip) throw', 'if (false) throw', 'ATAC: o cheie ML-DSA pusa in campul ed25519'], ['tipul cheii publice nu se mai verifica', L, 'if (k.asymmetricKeyType !== tip) throw', 'if (false) throw', 'ATAC: o cheie ML-DSA pusa in campul ed25519'],
['emitentii de incredere nu se mai cer', L, "ok('credential: issuer trusted', idsIncredere.includes(S.issuer.id),", "ok('credential: issuer trusted', true,", 'CONTROL: emitent in afara celor de incredere'], ['emitentii de incredere nu se mai cer', L, "ok('credential: issuer trusted', idsIncredere.includes(S.issuer.id),", "ok('credential: issuer trusted', true,", 'CONTROL: emitent in afara celor de incredere'],
['fereastra credentialului nu se mai cere', L, 'vf <= acum && acum <= vu,', 'true,', 'CONTROL: emitent in afara celor de incredere'], ['fereastra credentialului nu se mai cere', L, 'vf - sk <= acum && acum <= vu,', 'true,', 'CONTROL: emitent in afara celor de incredere'],
['un fisier de chei cu partea publica a altcuiva primit', L, 'if (canonical(keys.public) !== canonical(j.public) || keys.id !== j.id) throw', 'if (false) throw', 'cheile: exportKeys'], ['un fisier de chei cu partea publica a altcuiva primit', L, 'if (canonical(keys.public) !== canonical(j.public) || keys.id !== j.id) throw', 'if (false) throw', 'cheile: exportKeys'],
['bitul de revocare ignorat', L, '!rev,', 'true,', 'lista de stare: bitul 42'], ['bitul de revocare ignorat', L, '!rev,', 'true,', 'lista de stare: bitul 42'],
['cu doua liste, ultima castiga (revocarea se ridica)', L, 'rev = statusBit(L, S.status.index) || rev;', 'rev = statusBit(L, S.status.index);', 'lista de stare: bitul 42'], ['cu doua liste, ultima castiga (revocarea se ridica)', L, 'rev = statusBit(L, S.status.index) || rev;', 'rev = statusBit(L, S.status.index);', 'lista de stare: bitul 42'],
@ -52,9 +56,9 @@ const PLANTARI = [
['scopul nu mai limiteaza afirmatiile aratate', L, 'ok(`${et}: covers the disclosed claims`, !afara.length,', 'ok(`${et}: covers the disclosed claims`, true,', 'ATAC: delegatul arata o afirmatie din afara scopului'], ['scopul nu mai limiteaza afirmatiile aratate', L, 'ok(`${et}: covers the disclosed claims`, !afara.length,', 'ok(`${et}: covers the disclosed claims`, true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['scopul nu mai limiteaza publicul', L, 'permite(sc.audiences, B.audience),', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'], ['scopul nu mai limiteaza publicul', L, 'permite(sc.audiences, B.audience),', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['scopul nu mai limiteaza credentialul', L, 'permite(sc.credentials, S.id),', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'], ['scopul nu mai limiteaza credentialul', L, 'permite(sc.credentials, S.id),', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['fereastra verigii nu se mai cere', L, 'nb <= acum && acum <= na && nb <= t && t <= na,', 'true,', 'ATAC: veriga expirata'], ['fereastra verigii nu se mai cere', L, 'nb - sk <= acum && acum <= na && nb - sk <= t && t <= na,', 'true,', 'ATAC: veriga expirata'],
['oricine poate revoca o veriga', L, 'const autor = R.by && (R.by.id === D.from.id || R.by.id === S.holder.id);', 'const autor = true;', 'revocarea: detinatorul revoca'], ['oricine poate revoca o veriga', L, 'const autor = R.by && (R.by.id === D.from.id || R.by.id === S.holder.id);', 'const autor = true;', 'revocarea: detinatorul revoca'],
['o revocare se aplica inainte de momentul ei', L, 'ok(`${et}: not revoked`, at > acum,', 'ok(`${et}: not revoked`, false,', 'revocarea: detinatorul revoca'], ['o revocare se aplica inainte de momentul ei', L, 'ok(`${et}: not revoked`, at > acum + sk,', 'ok(`${et}: not revoked`, false,', 'revocarea: detinatorul revoca'],
['revocarea acceptata sub scopul delegarii', L, "verifyText('revocation', canonical(R), r.signature, R.by.keys)", "verifyText('delegation', canonical(R), r.signature, R.by.keys)", 'ATAC: o revocare cu semnatura detinatorului dar alt scop'], ['revocarea acceptata sub scopul delegarii', L, "verifyText('revocation', canonical(R), r.signature, R.by.keys)", "verifyText('delegation', canonical(R), r.signature, R.by.keys)", 'ATAC: o revocare cu semnatura detinatorului dar alt scop'],
['o lista stricata acuza credentialul', L, "verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch { return false; } });", "verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch (e) { throw e; } });", 'intrari stricate date verificatorului'], ['o lista stricata acuza credentialul', L, "verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch { return false; } });", "verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch (e) { throw e; } });", 'intrari stricate date verificatorului'],
['o revocare stricata acuza prezentarea', L, "try { canonical(R); } catch { nejudecat(`${et}: a revocation`, 'ignored: not readable'); continue; }", '', 'intrari stricate date verificatorului'], ['o revocare stricata acuza prezentarea', L, "try { canonical(R); } catch { nejudecat(`${et}: a revocation`, 'ignored: not readable'); continue; }", '', 'intrari stricate date verificatorului'],

View File

@ -280,11 +280,18 @@ export function present({ credential, disclosures = [], reveal = [], presenter,
* Verifica o prezentare. Fiecare verificare e un rand { name, pass, detail }: pass=true tine, false nu tine, null NEJUDECAT (spus de ce). * Verifica o prezentare. Fiecare verificare e un rand { name, pass, detail }: pass=true tine, false nu tine, null NEJUDECAT (spus de ce).
* valid = niciun rand fals. `claims` (afirmatiile in clar si cele dezvaluite) se intorc numai pentru o prezentare valida. * valid = niciun rand fals. `claims` (afirmatiile in clar si cele dezvaluite) se intorc numai pentru o prezentare valida.
*/ */
export function verifyPresentation(p, { audience = null, nonce = null, now = new Date(), trustedIssuers = null, statusLists = [], revocations = [], maxAgeS = 300 } = {}) { // B-30 (2026-09-30, masurat prin API-ul Cloud): ceasul emitentului si al verificatorului difera cu secunde; un credential emis ACUM de
// un emitent cu ceasul inainte cu o secunda era 'not yet valid' la un verificator exact. Toleranta de ceas se da NUMAI pe inceputuri
// (validFrom al credentialului si al listei de stare, notBefore al delegarii) si in sensul prudent pe revocari (o revocare cu momentul
// pana la clockSkewS in viitor se aplica deja); niciodata pe sfarsituri (validUntil, notAfter), unde ar prelungi o valabilitate.
export const CLOCK_SKEW_S = 60;
export function verifyPresentation(p, { audience = null, nonce = null, now = new Date(), trustedIssuers = null, statusLists = [], revocations = [], maxAgeS = 300, clockSkewS = CLOCK_SKEW_S } = {}) {
const rows = []; const rows = [];
const ok = (name, pass, detail = '') => { rows.push({ name, pass: !!pass, detail: pass ? '' : detail }); return !!pass; }; const ok = (name, pass, detail = '') => { rows.push({ name, pass: !!pass, detail: pass ? '' : detail }); return !!pass; };
const nejudecat = (name, detail) => rows.push({ name, pass: null, detail }); const nejudecat = (name, detail) => rows.push({ name, pass: null, detail });
const acum = new Date(now).getTime(); const acum = new Date(now).getTime();
if (!Number.isInteger(clockSkewS) || clockSkewS < 0 || clockSkewS > 600) throw new Error('verifyPresentation: clockSkewS is 0..600 seconds');
const sk = clockSkewS * 1000;
// 2026-09-30 (API-ul Identity din Cloud): o prezentare VALIDA spune si despre cine e (tip, credential, emitent, detinator, cine a // 2026-09-30 (API-ul Identity din Cloud): o prezentare VALIDA spune si despre cine e (tip, credential, emitent, detinator, cine a
// prezentat), ca verificatorul care nu a dat trustedIssuers sa vada pe cine ar trebui sa creada; numai pe drumul care ajunge la capat // prezentat), ca verificatorul care nu a dat trustedIssuers sa vada pe cine ar trebui sa creada; numai pe drumul care ajunge la capat
const gata = (claims = null, subject = null) => { const valid = rows.every((r) => r.pass !== false); return { valid, rows, notJudged: rows.filter((r) => r.pass === null).length, claims: valid ? claims : null, subject: valid ? subject : null }; }; const gata = (claims = null, subject = null) => { const valid = rows.every((r) => r.pass !== false); return { valid, rows, notJudged: rows.filter((r) => r.pass === null).length, claims: valid ? claims : null, subject: valid ? subject : null }; };
@ -309,7 +316,7 @@ export function verifyPresentation(p, { audience = null, nonce = null, now = new
} }
// 2. fereastra, pe ceasul verificatorului // 2. fereastra, pe ceasul verificatorului
const vf = data(S.validFrom, 'validFrom'), vu = data(S.validUntil, 'validUntil'); const vf = data(S.validFrom, 'validFrom'), vu = data(S.validUntil, 'validUntil');
ok(`credential: valid at ${iso(acum)}`, vf <= acum && acum <= vu, `valid from ${S.validFrom} until ${S.validUntil}`); ok(`credential: valid at ${iso(acum)}`, vf - sk <= acum && acum <= vu, `valid from ${S.validFrom} until ${S.validUntil}`);
// 3. starea (revocarea) credentialului // 3. starea (revocarea) credentialului
if (!S.status) nejudecat('credential: status', 'the credential names no status list, so its issuer cannot revoke it'); if (!S.status) nejudecat('credential: status', 'the credential names no status list, so its issuer cannot revoke it');
else { else {
@ -318,7 +325,7 @@ export function verifyPresentation(p, { audience = null, nonce = null, now = new
const aEmitentului = liste.filter((l) => { try { return l.statement.kind === 'aere-status-list' && l.statement.issuer && l.statement.issuer.id === S.issuer.id const aEmitentului = liste.filter((l) => { try { return l.statement.kind === 'aere-status-list' && l.statement.issuer && l.statement.issuer.id === S.issuer.id
&& canonical(l.statement.issuer.keys) === canonical(S.issuer.keys) && verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch { return false; } }); && canonical(l.statement.issuer.keys) === canonical(S.issuer.keys) && verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch { return false; } });
// numai listele emitentului valabile ACUM; cu mai multe, un bit pus in oricare inseamna revocat (revocarea nu se ridica) // numai listele emitentului valabile ACUM; cu mai multe, un bit pus in oricare inseamna revocat (revocarea nu se ridica)
const curente = aEmitentului.filter((l) => { try { return data(l.statement.validFrom, 'status validFrom') <= acum && acum <= data(l.statement.validUntil, 'status validUntil'); } catch { return false; } }); const curente = aEmitentului.filter((l) => { try { return data(l.statement.validFrom, 'status validFrom') - sk <= acum && acum <= data(l.statement.validUntil, 'status validUntil'); } catch { return false; } });
if (!aEmitentului.length) nejudecat(`credential: status in ${S.status.list}`, liste.length ? 'the status list(s) given with this id are not signed by the issuer: ignored' : 'the status list was not handed to the verifier; a revocation it was not handed cannot be seen'); if (!aEmitentului.length) nejudecat(`credential: status in ${S.status.list}`, liste.length ? 'the status list(s) given with this id are not signed by the issuer: ignored' : 'the status list was not handed to the verifier; a revocation it was not handed cannot be seen');
else if (!curente.length) nejudecat(`credential: status in ${S.status.list}`, `the issuer's status list(s) given are not valid at ${iso(acum)}: fetch a current one`); else if (!curente.length) nejudecat(`credential: status in ${S.status.list}`, `the issuer's status list(s) given are not valid at ${iso(acum)}: fetch a current one`);
else { else {
@ -372,7 +379,7 @@ export function verifyPresentation(p, { audience = null, nonce = null, now = new
let sc = null; try { sc = scopNormal(D.scope); } catch (e) { ok(`${et}: scope`, false, e.message); } let sc = null; try { sc = scopNormal(D.scope); } catch (e) { ok(`${et}: scope`, false, e.message); }
if (sc && canonical(sc) !== canonical(D.scope)) ok(`${et}: scope in normal form`, false, 'the scope is not sorted or has duplicates'); if (sc && canonical(sc) !== canonical(D.scope)) ok(`${et}: scope in normal form`, false, 'the scope is not sorted or has duplicates');
const nb = data(D.notBefore, 'notBefore'), na = data(D.notAfter, 'notAfter'); const nb = data(D.notBefore, 'notBefore'), na = data(D.notAfter, 'notAfter');
ok(`${et}: valid at ${iso(acum)} and when the presentation was made`, nb <= acum && acum <= na && nb <= t && t <= na, `valid from ${D.notBefore} until ${D.notAfter}`); ok(`${et}: valid at ${iso(acum)} and when the presentation was made`, nb - sk <= acum && acum <= na && nb - sk <= t && t <= na, `valid from ${D.notBefore} until ${D.notAfter}`);
ok(`${et}: allows the links after it`, Number.isInteger(D.maxDepth) && D.maxDepth >= lant.length - 1 - i, `maxDepth ${D.maxDepth}, ${lant.length - 1 - i} link(s) after it`); ok(`${et}: allows the links after it`, Number.isInteger(D.maxDepth) && D.maxDepth >= lant.length - 1 - i, `maxDepth ${D.maxDepth}, ${lant.length - 1 - i} link(s) after it`);
if (i > 0 && sc) { if (i > 0 && sc) {
const P = lant[i - 1].statement; const P = lant[i - 1].statement;
@ -394,7 +401,7 @@ export function verifyPresentation(p, { audience = null, nonce = null, now = new
let idR = null; try { idR = idOf(R.by.keys); } catch { /* ignorata */ } let idR = null; try { idR = idOf(R.by.keys); } catch { /* ignorata */ }
if (!autor || idR !== R.by.id || !verifyText('revocation', canonical(R), r.signature, R.by.keys)) { nejudecat(`${et}: a revocation`, `ignored: not signed by who gave the link or by the holder (${R.by && R.by.id})`); continue; } if (!autor || idR !== R.by.id || !verifyText('revocation', canonical(R), r.signature, R.by.keys)) { nejudecat(`${et}: a revocation`, `ignored: not signed by who gave the link or by the holder (${R.by && R.by.id})`); continue; }
let at = null; try { at = data(R.at, 'revocation at'); } catch { nejudecat(`${et}: a revocation`, 'ignored: its time is not an RFC 3339 UTC time'); continue; } let at = null; try { at = data(R.at, 'revocation at'); } catch { nejudecat(`${et}: a revocation`, 'ignored: its time is not an RFC 3339 UTC time'); continue; }
ok(`${et}: not revoked`, at > acum, `revoked by ${R.by.id} at ${R.at}`); ok(`${et}: not revoked`, at > acum + sk, `revoked by ${R.by.id} at ${R.at}`);
} }
} }
if (lant.length && !revocations.length) nejudecat('delegation: revocations', 'none given; a revocation the verifier was not handed cannot be seen'); if (lant.length && !revocations.length) nejudecat('delegation: revocations', 'none given; a revocation the verifier was not handed cannot be seen');

View File

@ -242,6 +242,20 @@ test('ATAC: lantul scos sau inversat dupa semnare -> INVALID', () => {
const p2 = clon(p); p2.delegations = [p.delegations[1], p.delegations[0]]; const p2 = clon(p); p2.delegations = [p.delegations[1], p.delegations[0]];
const r2 = I.verifyPresentation(p2, toate); cere(picaPe(r2, /exactly this delegation chain/), fals(r2) || 'inversat trecut'); const r2 = I.verifyPresentation(p2, toate); cere(picaPe(r2, /exactly this delegation chain/), fals(r2) || 'inversat trecut');
}); });
// B-30 (2026-09-30): masurat prin API-ul viu, un credential emis pe un laptop cu ceasul inainte cu ~1 s era 'not yet valid' la rpc2 (NTP exact)
test('B-30: toleranta de ceas pe INCEPUTURI (60 s): validFrom al credentialului si al listei de stare cu 30 s in viitor -> VALID; cu 90 s -> INVALID; sfarsiturile fara toleranta', () => {
const in30 = new Date(NOW.getTime() + 30000), in90 = new Date(NOW.getTime() + 90000);
const c30 = I.issueCredential({ issuer: iss, holder: hol.public, type: 'EmployeeCredential', claims: CLAIMS, disclosable: ['age_over_18'], validUntil: '2027-09-30T00:00:00Z', status: { list: 'urn:example:status:30', index: 1 }, now: in30 });
const L30 = I.createStatusList({ issuer: iss, id: 'urn:example:status:30', revoked: [], validUntil: '2026-10-07T00:00:00Z', now: in30 });
const p30 = I.present({ ...c30, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW });
const r = I.verifyPresentation(p30, { ...toate, statusLists: [L30] }); cere(r.valid && r.notJudged === 0, 'emis cu 30 s in viitor: ' + (fals(r) || 'nejudecate ' + r.notJudged));
const c90 = I.issueCredential({ issuer: iss, holder: hol.public, type: 'EmployeeCredential', claims: CLAIMS, disclosable: ['age_over_18'], validUntil: '2027-09-30T00:00:00Z', now: in90 });
const p90 = I.present({ ...c90, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW });
cere(picaPe(I.verifyPresentation(p90, toate), /valid at/), 'emis cu 90 s in viitor a trecut');
const cExp = I.issueCredential({ issuer: iss, holder: hol.public, type: 'EmployeeCredential', claims: CLAIMS, disclosable: ['age_over_18'], validFrom: '2026-09-01T00:00:00Z', validUntil: new Date(NOW.getTime() - 30000).toISOString(), now: new Date('2026-09-01T00:00:00Z') });
const pExp = I.present({ ...cExp, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW });
cere(picaPe(I.verifyPresentation(pExp, toate), /valid at/), 'expirat de 30 s a trecut: sfarsitul a primit toleranta');
});
test('revocarea: detinatorul revoca veriga telefonului -> INVALID; revocarea unui strain -> ignorata si spusa; una din viitor -> inca nerevocat', () => { test('revocarea: detinatorul revoca veriga telefonului -> INVALID; revocarea unui strain -> ignorata si spusa; una din viitor -> inca nerevocat', () => {
const rv = I.revokeDelegation({ by: hol, delegation: d1, now: NOW }); const rv = I.revokeDelegation({ by: hol, delegation: d1, now: NOW });
const r = I.verifyPresentation(prezD(), { ...toate, revocations: [rv] }); cere(picaPe(r, /not revoked/), fals(r) || 'trecut'); const r = I.verifyPresentation(prezD(), { ...toate, revocations: [rv] }); cere(picaPe(r, /not revoked/), fals(r) || 'trecut');
@ -249,6 +263,9 @@ test('revocarea: detinatorul revoca veriga telefonului -> INVALID; revocarea unu
const r2 = I.verifyPresentation(prezD(), { ...toate, revocations: [rs] }); cere(r2.valid && r2.rows.some((x) => x.pass === null && /ignored/.test(x.detail)), fals(r2) || 'strainul a revocat'); const r2 = I.verifyPresentation(prezD(), { ...toate, revocations: [rs] }); cere(r2.valid && r2.rows.some((x) => x.pass === null && /ignored/.test(x.detail)), fals(r2) || 'strainul a revocat');
const rf = I.revokeDelegation({ by: phone, delegation: d2, at: '2026-09-30T07:00:00Z', now: NOW }); const rf = I.revokeDelegation({ by: phone, delegation: d2, at: '2026-09-30T07:00:00Z', now: NOW });
const r3 = I.verifyPresentation(prezD(), { ...toate, revocations: [rf] }); cere(r3.valid, 'revocarea din viitor s-a aplicat acum: ' + fals(r3)); const r3 = I.verifyPresentation(prezD(), { ...toate, revocations: [rf] }); cere(r3.valid, 'revocarea din viitor s-a aplicat acum: ' + fals(r3));
// B-30: o revocare cu momentul la 30 s in viitor (ceasul celui care revoca inainte) se aplica deja: toleranta merge in sensul prudent
const rp30 = I.revokeDelegation({ by: hol, delegation: d1, at: new Date(NOW.getTime() + 30000).toISOString(), now: NOW });
cere(picaPe(I.verifyPresentation(prezD(), { ...toate, revocations: [rp30] }), /not revoked/), 'B-30: revocarea de peste 30 s nu s-a aplicat');
const r4 = I.verifyPresentation(prezD(), { ...toate, now: new Date('2026-09-30T07:00:01Z'), revocations: [rf] }); const r4 = I.verifyPresentation(prezD(), { ...toate, now: new Date('2026-09-30T07:00:01Z'), revocations: [rf] });
cere(picaPe(r4, /not revoked/), 'dupa momentul ei, revocarea nu s-a aplicat: ' + fals(r4)); cere(picaPe(r4, /not revoked/), 'dupa momentul ei, revocarea nu s-a aplicat: ' + fals(r4));
}); });