verify-layer: verify-consistency names the signer of each head (or unsigned); identity: Travel Rule requires a replay store
- verify-consistency without --signer now says whether each head is signed and by which key, and that no expected signer was checked (verify-inclusion already did; the README said the output does) - openMessage opens nothing without a replay store (seen: an object with has/add that keeps the ids for at least maxAgeS); before, the same signed and sealed message could be opened any number of times when no store was given Tests: verify-layer tree 20/20, negative control 14/14; sidecar 44/44, 9/9; travel rule 19/19, negative control 19/19.
This commit is contained in:
parent
082b862d78
commit
8f5f0bd00d
@ -120,7 +120,9 @@ import { generateKemKeys, bindKemKeys, acceptBeneficiary, sealMessage, openMessa
|
|||||||
```
|
```
|
||||||
|
|
||||||
`openMessage` refuses a message not signed by the originator it names, sealed for another VASP or other keys, older or newer than 300
|
`openMessage` refuses a message not signed by the originator it names, sealed for another VASP or other keys, older or newer than 300
|
||||||
seconds on its clock, already received (`seen`), or whose originator is not proven a VASP. `travelRuleRecord` writes an AIP-23
|
seconds on its clock, already received, or whose originator is not proven a VASP. The replay store `seen` is required (a `Set`, or
|
||||||
|
any object with `has` and `add`, that keeps the ids for at least the 300 seconds): without one, `openMessage` opens nothing, since
|
||||||
|
the same signed and sealed message could otherwise be opened any number of times. `travelRuleRecord` writes an AIP-23
|
||||||
`compliance` envelope with no personal data and no amount: a pseudonymous transfer reference, the policy name, the result and a digest
|
`compliance` envelope with no personal data and no amount: a pseudonymous transfer reference, the policy name, the result and a digest
|
||||||
of the exchange. What it does not do: validate the IVMS101 schema (it carries the object as given and requires `originator` and
|
of the exchange. What it does not do: validate the IVMS101 schema (it carries the object as given and requires `originator` and
|
||||||
`beneficiary`), find the beneficiary's VASP from an address (that is a discovery protocol's job), or say that a transfer is lawful.
|
`beneficiary`), find the beneficiary's VASP from an address (that is a discovery protocol's job), or say that a transfer is lawful.
|
||||||
@ -139,8 +141,8 @@ node proba-identity.mjs # 43: the paths above, and each attack of t
|
|||||||
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 46 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 # 18: 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
|
||||||
node control-negativ-travel-rule.mjs # on a copy, each of 18 guards removed -> its own named test turns red
|
node control-negativ-travel-rule.mjs # on a copy, each of 19 guards removed -> its own named test turns red
|
||||||
```
|
```
|
||||||
|
|
||||||
The envelope test needs the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs>`); without it that test is reported as
|
The envelope test needs the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs>`); without it that test is reported as
|
||||||
|
|||||||
@ -22,7 +22,9 @@ const PLANTARI = [
|
|||||||
['destinatarul nu se mai compara', [['if (m.to.id !== me) motive.push(', 'if (false) motive.push(']], 'ATAC: alt VASP (C)'],
|
['destinatarul nu se mai compara', [['if (m.to.id !== me) motive.push(', 'if (false) motive.push(']], 'ATAC: alt VASP (C)'],
|
||||||
['cheile KEM ale destinatarului nu se mai compara', [['if (canonical(m.to.kem) !== canonical(kem.public)) motive.push(', 'if (false) motive.push(']], 'ATAC: alt VASP (C)'],
|
['cheile KEM ale destinatarului nu se mai compara', [['if (canonical(m.to.kem) !== canonical(kem.public)) motive.push(', 'if (false) motive.push(']], 'ATAC: alt VASP (C)'],
|
||||||
['prospetimea nu se mai cere', [["if (Math.abs(acum - timp(m.createdAt, 'createdAt')) > maxAgeS * 1000) motive.push(", 'if (false) motive.push(']], 'ATAC: acelasi mesaj primit a doua oara'],
|
['prospetimea nu se mai cere', [["if (Math.abs(acum - timp(m.createdAt, 'createdAt')) > maxAgeS * 1000) motive.push(", 'if (false) motive.push(']], 'ATAC: acelasi mesaj primit a doua oara'],
|
||||||
['reluarea nu se mai opreste', [['if (seen && seen.has(m.id)) motive.push(', 'if (false) motive.push(']], 'ATAC: acelasi mesaj primit a doua oara'],
|
['reluarea nu se mai opreste', [[' else if (seen.has(m.id)) motive.push(', ' else if (false) motive.push(']], 'ATAC: acelasi mesaj primit a doua oara'],
|
||||||
|
// B-26: forma publicata in f75c334 (seen optional: fara el, orice reluare trecea)
|
||||||
|
['magazia de reluare iar optionala (B-26)', [[" if (!seen || typeof seen.has !== 'function' || typeof seen.add !== 'function') motive.push(", " if (false) motive.push("], [" else if (seen.has(m.id)) motive.push(", " else if (seen && seen.has && seen.has(m.id)) motive.push("], [' seen.add(m.id);', ' if (seen && seen.add) seen.add(m.id);']], 'B-26'],
|
||||||
['initiatorul nu mai trebuie dovedit VASP', [["motive.push(...v.motive.map((x) => 'originator VASP: ' + x));", '']], 'ATAC: initiatorul nedovedit ca VASP'],
|
['initiatorul nu mai trebuie dovedit VASP', [["motive.push(...v.motive.map((x) => 'originator VASP: ' + x));", '']], 'ATAC: initiatorul nedovedit ca VASP'],
|
||||||
['antetul nu mai e legat de cifru (nici AAD, nici derivarea)', [["const c = crypto.createCipheriv('aes-256-gcm', d.key, d.iv); c.setAAD(aad);", "const c = crypto.createCipheriv('aes-256-gcm', d.key, d.iv);"], ['dc.setAAD(aad); dc.setAuthTag', 'dc.setAuthTag'], ["crypto.createHash('sha256').update(transcript).digest()", 'Buffer.alloc(32)']], 'ATAC: suma din antet schimbata'],
|
['antetul nu mai e legat de cifru (nici AAD, nici derivarea)', [["const c = crypto.createCipheriv('aes-256-gcm', d.key, d.iv); c.setAAD(aad);", "const c = crypto.createCipheriv('aes-256-gcm', d.key, d.iv);"], ['dc.setAAD(aad); dc.setAuthTag', 'dc.setAuthTag'], ["crypto.createHash('sha256').update(transcript).digest()", 'Buffer.alloc(32)']], 'ATAC: suma din antet schimbata'],
|
||||||
['numai secretul X25519 intra in cheie', [['const ikm = Buffer.concat([ssK, ssX]);', 'const ikm = Buffer.concat([ssX]);']], 'KEM hibrid'],
|
['numai secretul X25519 intra in cheie', [['const ikm = Buffer.concat([ssK, ssX]);', 'const ikm = Buffer.concat([ssX]);']], 'KEM hibrid'],
|
||||||
|
|||||||
@ -35,7 +35,7 @@ const bindB = TR.bindKemKeys({ vasp: B, kem: kemB, validUntil: '2026-12-31T00:00
|
|||||||
const accB = TR.acceptBeneficiary({ binding: bindB, presentation: pres(credB, B, A.id, 'n-acc'), registries: [reg.id], originatorId: A.id, nonce: 'n-acc', now: NOW, statusLists: [LISTA] });
|
const accB = TR.acceptBeneficiary({ binding: bindB, presentation: pres(credB, B, A.id, 'n-acc'), registries: [reg.id], originatorId: A.id, nonce: 'n-acc', now: NOW, statusLists: [LISTA] });
|
||||||
const MID = 'urn:uuid:11111111-2222-4333-8444-555555555555';
|
const MID = 'urn:uuid:11111111-2222-4333-8444-555555555555';
|
||||||
const sigileaza = (o = {}) => TR.sealMessage({ from: A, beneficiary: accB.beneficiary, ivms101: IVMS, transfer: TRANSFER, presentation: pres(credA, A, B.id, MID), messageId: MID, now: NOW, ...o });
|
const sigileaza = (o = {}) => TR.sealMessage({ from: A, beneficiary: accB.beneficiary, ivms101: IVMS, transfer: TRANSFER, presentation: pres(credA, A, B.id, MID), messageId: MID, now: NOW, ...o });
|
||||||
const deschide = (m, o = {}) => TR.openMessage(m, { me: B.id, kem: kemB, registries: [reg.id], now: NOW, statusLists: [LISTA], ...o });
|
const deschide = (m, o = {}) => TR.openMessage(m, { me: B.id, kem: kemB, registries: [reg.id], now: NOW, statusLists: [LISTA], seen: new Set(), ...o });
|
||||||
const are = (r, re) => !r.ok && r.motive.some((x) => re.test(x));
|
const are = (r, re) => !r.ok && r.motive.some((x) => re.test(x));
|
||||||
|
|
||||||
// ---------------------------------------------------------------- beneficiarul acceptat
|
// ---------------------------------------------------------------- beneficiarul acceptat
|
||||||
@ -99,8 +99,8 @@ test('ATAC: mesaj semnat de un strain in numele lui A -> refuzat; semnatura lui
|
|||||||
const m2 = clon(M); m2.signature = I.signText('kem-binding', text, A); cere(are(deschide(m2), /not signed by the originator/), 'alt scop a trecut');
|
const m2 = clon(M); m2.signature = I.signText('kem-binding', text, A); cere(are(deschide(m2), /not signed by the originator/), 'alt scop a trecut');
|
||||||
});
|
});
|
||||||
test('ATAC: alt VASP (C) incearca sa deschida mesajul lui B -> refuzat; cu cheile lui KEM in numele lui B -> refuzat', () => {
|
test('ATAC: alt VASP (C) incearca sa deschida mesajul lui B -> refuzat; cu cheile lui KEM in numele lui B -> refuzat', () => {
|
||||||
cere(are(TR.openMessage(M, { me: C.id, kem: kemC, registries: [reg.id], now: NOW, statusLists: [LISTA] }), /is for/), 'C l-a deschis');
|
cere(are(TR.openMessage(M, { me: C.id, kem: kemC, registries: [reg.id], now: NOW, statusLists: [LISTA], seen: new Set() }), /is for/), 'C l-a deschis');
|
||||||
cere(are(TR.openMessage(M, { me: B.id, kem: kemC, registries: [reg.id], now: NOW, statusLists: [LISTA] }), /other KEM keys/), 'cheile lui C au trecut');
|
cere(are(TR.openMessage(M, { me: B.id, kem: kemC, registries: [reg.id], now: NOW, statusLists: [LISTA], seen: new Set() }), /other KEM keys/), 'cheile lui C au trecut');
|
||||||
});
|
});
|
||||||
test('KEM hibrid: fara secretul ML-KEM corect (cheia X25519 buna) sau fara cel X25519 (ML-KEM bun), mesajul nu se descifreaza', () => {
|
test('KEM hibrid: fara secretul ML-KEM corect (cheia X25519 buna) sau fara cel X25519 (ML-KEM bun), mesajul nu se descifreaza', () => {
|
||||||
const k1 = { public: kemB.public }; Object.defineProperty(k1, 'privat', { value: { x25519: kemB.privat.x25519, mlkem768: kemC.privat.mlkem768 } });
|
const k1 = { public: kemB.public }; Object.defineProperty(k1, 'privat', { value: { x25519: kemB.privat.x25519, mlkem768: kemC.privat.mlkem768 } });
|
||||||
@ -108,6 +108,11 @@ test('KEM hibrid: fara secretul ML-KEM corect (cheia X25519 buna) sau fara cel X
|
|||||||
const r1 = deschide(M, { kem: k1 }), r2 = deschide(M, { kem: k2 });
|
const r1 = deschide(M, { kem: k1 }), r2 = deschide(M, { kem: k2 });
|
||||||
cere(!r1.ok && !r2.ok && r1.motive.concat(r2.motive).every((x) => /modified|does not open/.test(x)), r1.motive.concat(r2.motive).join(' | '));
|
cere(!r1.ok && !r2.ok && r1.motive.concat(r2.motive).every((x) => /modified|does not open/.test(x)), r1.motive.concat(r2.motive).join(' | '));
|
||||||
});
|
});
|
||||||
|
// B-26 (2026-09-30, revizuirea adversariala): fara magazia de reluare, acelasi mesaj se deschidea de oricate ori
|
||||||
|
test('B-26: fara magazia de reluare (seen lipsa sau fara has/add) nu se deschide nimic', () => {
|
||||||
|
cere(are(deschide(M, { seen: undefined }), /no replay store/), 'fara seen s-a deschis');
|
||||||
|
cere(are(deschide(M, { seen: {} }), /no replay store/), 'un obiect fara has/add a trecut drept magazie');
|
||||||
|
});
|
||||||
test('ATAC: acelasi mesaj primit a doua oara (reluare) -> refuzat; un mesaj vechi (301 s) -> refuzat', () => {
|
test('ATAC: acelasi mesaj primit a doua oara (reluare) -> refuzat; un mesaj vechi (301 s) -> refuzat', () => {
|
||||||
const seen = new Set(); cere(deschide(M, { seen }).ok, 'prima deschidere');
|
const seen = new Set(); cere(deschide(M, { seen }).ok, 'prima deschidere');
|
||||||
cere(are(deschide(M, { seen }), /replay/), 'reluarea a trecut');
|
cere(are(deschide(M, { seen }), /replay/), 'reluarea a trecut');
|
||||||
|
|||||||
@ -137,6 +137,8 @@ export function sealMessage({ from, beneficiary, ivms101, transfer, presentation
|
|||||||
/**
|
/**
|
||||||
* Beneficiarul deschide un mesaj: semnatura initiatorului, VASP-ul lui (prezentare pentru beneficiar, nonce = id-ul mesajului),
|
* Beneficiarul deschide un mesaj: semnatura initiatorului, VASP-ul lui (prezentare pentru beneficiar, nonce = id-ul mesajului),
|
||||||
* destinatarul (cheile KEM ale beneficiarului), prospetimea, reluarea (`seen`: id-urile deja primite), apoi descifrarea.
|
* destinatarul (cheile KEM ale beneficiarului), prospetimea, reluarea (`seen`: id-urile deja primite), apoi descifrarea.
|
||||||
|
* B-26 (2026-09-30, revizuirea adversariala): `seen` e OBLIGATORIU (un Set sau orice obiect cu has/add care tine id-urile cel putin
|
||||||
|
* maxAgeS); fara el, acelasi mesaj semnat si sigilat se deschidea de oricate ori, deci reluarea era prinsa numai de cine stia sa ceara.
|
||||||
* Intoarce { ok, motive, ivms101, transfer, from, messageHash }.
|
* Intoarce { ok, motive, ivms101, transfer, from, messageHash }.
|
||||||
*/
|
*/
|
||||||
export function openMessage(m, { me, kem, registries, now = new Date(), statusLists = [], seen = null, maxAgeS = 300 }) {
|
export function openMessage(m, { me, kem, registries, now = new Date(), statusLists = [], seen = null, maxAgeS = 300 }) {
|
||||||
@ -149,7 +151,8 @@ export function openMessage(m, { me, kem, registries, now = new Date(), statusLi
|
|||||||
if (m.to.id !== me) motive.push(`the message is for ${m.to.id}, not for ${me}`);
|
if (m.to.id !== me) motive.push(`the message is for ${m.to.id}, not for ${me}`);
|
||||||
if (canonical(m.to.kem) !== canonical(kem.public)) motive.push('the message is sealed to other KEM keys');
|
if (canonical(m.to.kem) !== canonical(kem.public)) motive.push('the message is sealed to other KEM keys');
|
||||||
if (Math.abs(acum - timp(m.createdAt, 'createdAt')) > maxAgeS * 1000) motive.push(`the message was made at ${m.createdAt}, outside ${maxAgeS} s of this clock`);
|
if (Math.abs(acum - timp(m.createdAt, 'createdAt')) > maxAgeS * 1000) motive.push(`the message was made at ${m.createdAt}, outside ${maxAgeS} s of this clock`);
|
||||||
if (seen && seen.has(m.id)) motive.push('replay: this message id was received before');
|
if (!seen || typeof seen.has !== 'function' || typeof seen.add !== 'function') motive.push('no replay store was given (seen: a set of the message ids already received, kept at least maxAgeS)');
|
||||||
|
else if (seen.has(m.id)) motive.push('replay: this message id was received before');
|
||||||
const v = eVasp(m.fromPresentation, { registries, audience: me, nonce: m.id, id: m.from.id, now, statusLists, maxAgeS });
|
const v = eVasp(m.fromPresentation, { registries, audience: me, nonce: m.id, id: m.from.id, now, statusLists, maxAgeS });
|
||||||
motive.push(...v.motive.map((x) => 'originator VASP: ' + x));
|
motive.push(...v.motive.map((x) => 'originator VASP: ' + x));
|
||||||
if (motive.length) return { ok: false, motive };
|
if (motive.length) return { ok: false, motive };
|
||||||
@ -168,7 +171,7 @@ export function openMessage(m, { me, kem, registries, now = new Date(), statusLi
|
|||||||
try { const dc = crypto.createDecipheriv('aes-256-gcm', d.key, d.iv); dc.setAAD(aad); dc.setAuthTag(ct.subarray(ct.length - 16)); pt = Buffer.concat([dc.update(ct.subarray(0, ct.length - 16)), dc.final()]); }
|
try { const dc = crypto.createDecipheriv('aes-256-gcm', d.key, d.iv); dc.setAAD(aad); dc.setAuthTag(ct.subarray(ct.length - 16)); pt = Buffer.concat([dc.update(ct.subarray(0, ct.length - 16)), dc.final()]); }
|
||||||
catch { return { ok: false, motive: ['the ciphertext or its header was modified, or it is not for these keys'] }; }
|
catch { return { ok: false, motive: ['the ciphertext or its header was modified, or it is not for these keys'] }; }
|
||||||
finally { d.key.fill(0); }
|
finally { d.key.fill(0); }
|
||||||
if (seen) seen.add(m.id);
|
seen.add(m.id);
|
||||||
return { ok: true, motive: [], ivms101: JSON.parse(pt.toString('utf8')), transfer: m.transfer, from: { id: m.from.id, claims: v.claims }, messageHash: sha(Buffer.from(canonical(semnat), 'utf8')), payloadHash: sha(pt) };
|
return { ok: true, motive: [], ivms101: JSON.parse(pt.toString('utf8')), transfer: m.transfer, from: { id: m.from.id, claims: v.claims }, messageHash: sha(Buffer.from(canonical(semnat), 'utf8')), payloadHash: sha(pt) };
|
||||||
} catch (e) { return { ok: false, motive: [...motive, 'the message cannot be read: ' + e.message] }; }
|
} catch (e) { return { ok: false, motive: [...motive, 'the message cannot be read: ' + e.message] }; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,14 +103,16 @@ and can be notarized. An inclusion proof carries the entry itself: the verifier
|
|||||||
envelope, so an internal node cannot be passed off as an entry. A proof names a tree by its size and root; bind it to a head you
|
envelope, so an internal node cannot be passed off as an entry. A proof names a tree by its size and root; bind it to a head you
|
||||||
received (`--head`), not to the numbers in the proof. Two heads of the same log with no consistency proof between them mean the
|
received (`--head`), not to the numbers in the proof. Two heads of the same log with no consistency proof between them mean the
|
||||||
history was rewritten. What it does not prove, as for the chain: that the entries are true (the host writes them), or that no
|
history was rewritten. What it does not prove, as for the chain: that the entries are true (the host writes them), or that no
|
||||||
other log with other heads was shown to someone else; whoever receives heads from both, or notarized heads, sees it.
|
other log with other heads was shown to someone else; whoever receives heads from both, or notarized heads, sees it. A head is
|
||||||
|
only as good as where you got it: without `--signer`, anyone can make two unsigned heads of an invented log that extend each
|
||||||
|
other, so both verify commands then say whether each head is signed and by which key, and that no expected signer was checked.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
node proba-sidecar.mjs # 44 checks
|
node proba-sidecar.mjs # 44 checks
|
||||||
node control-negativ-sidecar.mjs # puts each guard back to its absent form and requires the named check to fail
|
node control-negativ-sidecar.mjs # puts each guard back to its absent form and requires the named check to fail
|
||||||
node proba-arbore.mjs # 19: the tree (the reference roots of Certificate Transparency for 1..8 leaves, every proof up to 64 leaves, each attack)
|
node proba-arbore.mjs # 20: the tree (the reference roots of Certificate Transparency for 1..8 leaves, every proof up to 64 leaves, each attack)
|
||||||
node control-negativ-arbore.mjs # 13 guards removed in a copy, each turns its named test red
|
node control-negativ-arbore.mjs # 14 guards removed in a copy, each turns its named test red
|
||||||
|
|
||||||
`proba-sidecar.mjs` records runtime, deployment and envelope entries; checks that a modified entry breaks the chain at its
|
`proba-sidecar.mjs` records runtime, deployment and envelope entries; checks that a modified entry breaks the chain at its
|
||||||
seq, a changed hash is caught, and nothing is appended to a broken chain; that a chain **rebuilt from genesis** passes
|
seq, a changed hash is caught, and nothing is appended to a broken chain; that a chain **rebuilt from genesis** passes
|
||||||
|
|||||||
@ -22,6 +22,8 @@ const PLANTARI = [
|
|||||||
['semnatarul cerut nu mai trece in verificare', J, [['const s = verificaSemnaturaCap(cap, semnatar);', 'const s = verificaSemnaturaCap(cap, null);']], 'includere: intrarea 7'],
|
['semnatarul cerut nu mai trece in verificare', J, [['const s = verificaSemnaturaCap(cap, semnatar);', 'const s = verificaSemnaturaCap(cap, null);']], 'includere: intrarea 7'],
|
||||||
['dovada de consistenta nu mai trebuie sa porneasca de la capul vechi', J, [['if (vechi.statement.treeSize !== d.firstSize || vechi.statement.rootHash !== d.firstRoot) return', 'if (false) return']], 'ATAC: capetele inversate'],
|
['dovada de consistenta nu mai trebuie sa porneasca de la capul vechi', J, [['if (vechi.statement.treeSize !== d.firstSize || vechi.statement.rootHash !== d.firstRoot) return', 'if (false) return']], 'ATAC: capetele inversate'],
|
||||||
['un jurnal rupt primeste cap', J, [['if (!v.ok) throw new Error(`tree: the log is broken', 'if (false) throw new Error(`tree: the log is broken']], 'un jurnal rupt'],
|
['un jurnal rupt primeste cap', J, [['if (!v.ok) throw new Error(`tree: the log is broken', 'if (false) throw new Error(`tree: the log is broken']], 'un jurnal rupt'],
|
||||||
|
// B-25: forma publicata in 082b862 (iesirea tace despre semnatura capetelor)
|
||||||
|
['verify-consistency tace despre semnatura capetelor', 'sidecar.mjs', [['the history up to the old head was not rewritten)${semn}', 'the history up to the old head was not rewritten)']], 'B-25'],
|
||||||
];
|
];
|
||||||
const FISIERE = [M, J, 'sidecar.mjs', 'proba-arbore.mjs'];
|
const FISIERE = [M, J, 'sidecar.mjs', 'proba-arbore.mjs'];
|
||||||
function copie() {
|
function copie() {
|
||||||
|
|||||||
@ -84,6 +84,8 @@ export function verificaConsistenta(d, { vechi, nou, semnatar = null }) {
|
|||||||
if (vechi.statement.treeSize !== d.firstSize || vechi.statement.rootHash !== d.firstRoot) return { ok: false, motiv: 'the proof does not start at the old head' };
|
if (vechi.statement.treeSize !== d.firstSize || vechi.statement.rootHash !== d.firstRoot) return { ok: false, motiv: 'the proof does not start at the old head' };
|
||||||
if (nou.statement.treeSize !== d.secondSize || nou.statement.rootHash !== d.secondRoot) return { ok: false, motiv: 'the proof does not end at the new head' };
|
if (nou.statement.treeSize !== d.secondSize || nou.statement.rootHash !== d.secondRoot) return { ok: false, motiv: 'the proof does not end at the new head' };
|
||||||
if (!verifyConsistency(d)) return { ok: false, motiv: `the new head (${d.secondSize}) does not extend the old one (${d.firstSize}): the history was rewritten` };
|
if (!verifyConsistency(d)) return { ok: false, motiv: `the new head (${d.secondSize}) does not extend the old one (${d.firstSize}): the history was rewritten` };
|
||||||
return { ok: true };
|
// B-25 (2026-09-30): cine semneaza fiecare cap se intoarce si se spune; fara `semnatar`, doua capete nesemnate (sau semnate de
|
||||||
|
// oricine) care se continua trec, si cititorul trebuie sa vada asta, nu doar "EXTENDS"
|
||||||
|
return { ok: true, semnatVechi: a.semnatDe, semnatNou: b.semnatDe };
|
||||||
} catch (x) { return { ok: false, motiv: 'the proof cannot be read: ' + x.message }; }
|
} catch (x) { return { ok: false, motiv: 'the proof cannot be read: ' + x.message }; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,6 +165,27 @@ test('linia de comanda: tree-head, prove-inclusion, verify-inclusion (0; alt cap
|
|||||||
const w2 = run('verify-consistency', '--proof', 'c.json', '--old', 'h20.json', '--new', 'h12.json'); cere(w2.cod === 1 && /NOT consistent/.test(w2.out), w2.out);
|
const w2 = run('verify-consistency', '--proof', 'c.json', '--old', 'h20.json', '--new', 'h12.json'); cere(w2.cod === 1 && /NOT consistent/.test(w2.out), w2.out);
|
||||||
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
||||||
});
|
});
|
||||||
|
// B-25 (2026-09-30, revizuirea adversariala): fara --signer, doua capete NESEMNATE (pe care le poate fabrica oricine) care se continua
|
||||||
|
// ieseau "EXTENDS" fara niciun cuvant despre semnatura, desi README spunea ca iesirea o spune; verify-inclusion o spunea deja
|
||||||
|
test('B-25: verify-consistency fara --signer spune ca ambele capete sunt nesemnate si ca nu s-a cerut semnatarul; cu --signer, ca e cel asteptat', () => {
|
||||||
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-arbore-b25-')); const S = path.join(AICI, 'sidecar.mjs');
|
||||||
|
const run = (...a) => { const r = spawnSync(process.execPath, [S, ...a], { cwd: T, encoding: 'utf8' }); return { cod: r.status, out: (r.stdout || '') + (r.stderr || '') }; };
|
||||||
|
try {
|
||||||
|
const lg = path.join(T, 'audit.log');
|
||||||
|
fs.writeFileSync(lg, L.slice(0, 12).map((e) => JSON.stringify(e)).join('\n') + '\n');
|
||||||
|
fs.writeFileSync(path.join(T, 'k.pem'), pem(k1)); fs.writeFileSync(path.join(T, 'p.pem'), pub(k1));
|
||||||
|
cere(run('tree-head', '--log', lg, '--out', 'u12.json').cod === 0 && run('tree-head', '--log', lg, '--sign-key', 'k.pem', '--out', 's12.json').cod === 0, 'capete 12');
|
||||||
|
fs.appendFileSync(lg, L.slice(12).map((e) => JSON.stringify(e)).join('\n') + '\n');
|
||||||
|
cere(run('tree-head', '--log', lg, '--out', 'u20.json').cod === 0 && run('tree-head', '--log', lg, '--sign-key', 'k.pem', '--out', 's20.json').cod === 0, 'capete 20');
|
||||||
|
cere(run('prove-consistency', '--log', lg, '--from', '12', '--out', 'c.json').cod === 0, 'prove-consistency');
|
||||||
|
const u = run('verify-consistency', '--proof', 'c.json', '--old', 'u12.json', '--new', 'u20.json');
|
||||||
|
cere(u.cod === 0 && /old head unsigned, new head unsigned/.test(u.out) && /no expected signer was checked/.test(u.out), 'nesemnate: ' + u.out);
|
||||||
|
const s = run('verify-consistency', '--proof', 'c.json', '--old', 's12.json', '--new', 's20.json', '--signer', 'p.pem');
|
||||||
|
cere(s.cod === 0 && /both heads signed by the expected signer/.test(s.out), 'cu --signer: ' + s.out);
|
||||||
|
const r = J.verificaConsistenta(J.dovadaConsistenta(L, 12), { vechi: JSON.parse(fs.readFileSync(path.join(T, 's12.json'))), nou: JSON.parse(fs.readFileSync(path.join(T, 'u20.json'))) });
|
||||||
|
cere(r.ok && r.semnatVechi && r.semnatNou === null, 'modulul intoarce cine a semnat fiecare cap: ' + JSON.stringify(r));
|
||||||
|
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`\naere-arbore: ${treceri}/${treceri + esecuri.length} cum trebuia`);
|
console.log(`\naere-arbore: ${treceri}/${treceri + esecuri.length} cum trebuia`);
|
||||||
process.exitCode = esecuri.length ? 1 : 0;
|
process.exitCode = esecuri.length ? 1 : 0;
|
||||||
|
|||||||
@ -402,7 +402,11 @@ async function main() {
|
|||||||
return r.ok ? 0 : 1;
|
return r.ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
const r = A.verificaConsistenta(citeste(get('--proof'), '--proof'), { vechi: citeste(get('--old'), '--old'), nou: citeste(get('--new'), '--new'), semnatar });
|
const r = A.verificaConsistenta(citeste(get('--proof'), '--proof'), { vechi: citeste(get('--old'), '--old'), nou: citeste(get('--new'), '--new'), semnatar });
|
||||||
console.log(r.ok ? 'the new head EXTENDS the old one (the history up to the old head was not rewritten)' : `NOT consistent: ${r.motiv}`);
|
// B-25 (2026-09-30): ca la verify-inclusion, iesirea spune cine a semnat capetele si daca s-a cerut semnatarul
|
||||||
|
const cine = (k) => (k ? `signed by key ${k.slice(0, 18)}` : 'unsigned');
|
||||||
|
const semn = !r.ok ? '' : semnatar ? `; both heads signed by the expected signer (${r.semnatNou.slice(0, 18)})`
|
||||||
|
: `; old head ${cine(r.semnatVechi)}, new head ${cine(r.semnatNou)}${r.semnatVechi && r.semnatNou && r.semnatVechi !== r.semnatNou ? ' (DIFFERENT keys)' : ''}; no expected signer was checked (--signer): this proves only that the second head you hold extends the first one you hold`;
|
||||||
|
console.log(r.ok ? `the new head EXTENDS the old one (the history up to the old head was not rewritten)${semn}` : `NOT consistent: ${r.motiv}`);
|
||||||
return r.ok ? 0 : 1;
|
return r.ok ? 0 : 1;
|
||||||
} catch (e) { console.log(`${cmd}: ${String(e.message).replace(/[0-9a-fA-F]{32,}/g, '<hex>').slice(0, 200)}`); return 2; }
|
} catch (e) { console.log(`${cmd}: ${String(e.message).replace(/[0-9a-fA-F]{32,}/g, '<hex>').slice(0, 200)}`); return 2; }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user