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:
parent
aec0ccbead
commit
0b56d8e5df
@ -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
|
||||||
|
|||||||
@ -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'],
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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));
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user