provared 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1083 -0
- package/README.md +6 -3
- package/bin/provared-check.mjs +115 -11
- package/docs/guide.md +464 -15
- package/docs/threat-model.md +315 -9
- package/package.json +2 -2
- package/page/check-page.js +11 -32
- package/page/index.html +4 -0
- package/page/render.js +157 -9
- package/spec/provared-format.md +809 -23
- package/src/book.js +539 -18
- package/src/check.d.ts +3 -0
- package/src/check.js +2 -0
- package/src/denial.js +123 -0
- package/src/der.js +55 -6
- package/src/dev.d.ts +1 -1
- package/src/dev.js +1 -1
- package/src/documents.js +149 -0
- package/src/guard.js +13 -2
- package/src/index.js +18 -3
- package/src/jws.js +8 -0
- package/src/keys.js +48 -1
- package/src/pass.js +1 -1
- package/src/recorder.js +267 -15
- package/src/seal.js +36 -7
- package/src/sealer.js +812 -0
- package/src/service.js +4 -4
- package/src/signatures.js +281 -5
- package/src/standing.js +4 -4
- package/src/stub.js +18 -5
- package/src/timestamp.js +55 -16
- package/src/tools.js +30 -13
- package/src/tree.js +139 -7
- package/src/types.d.ts +314 -20
- package/src/witness.js +63 -0
- package/src/witnessing.js +362 -0
package/page/render.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// is kept inside its own box, so that a record cannot hide, reorder or
|
|
8
8
|
// draw over the words around it.
|
|
9
9
|
|
|
10
|
-
import { REFUSAL_REASONS, conditionWords, limitWords, neverWords } from '../src/check.js';
|
|
10
|
+
import { DENIAL_SOURCES, REFUSAL_REASONS, conditionWords, limitWords, neverWords } from '../src/check.js';
|
|
11
11
|
|
|
12
12
|
// The same holds for characters that cannot be seen (zero-width characters,
|
|
13
13
|
// soft hyphens, line separators, fillers), with which two different labels
|
|
@@ -149,6 +149,17 @@ function stampRows(list, stamps) {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// The witness statements beside a seal. What a statement says is shown only where it passed its check.
|
|
153
|
+
function witnessRows(list, witnesses) {
|
|
154
|
+
for (const w of witnesses ?? []) {
|
|
155
|
+
if (w.state === 'invalid') row(list, 'Witness statement, not used', 'it did not pass its check (see the problems)');
|
|
156
|
+
else if (w.state === 'unavailable') row(list, 'Witness statement, not checked', 'this browser cannot check both of its signatures');
|
|
157
|
+
else if (w.counted) row(list, 'Witnessed', `by ${label(w.name)} (a label), a witness you named as trusted (key set ${w.witness}), at ${showTime(w.when)} (the witness's own word)`);
|
|
158
|
+
else if (w.trusted) row(list, 'Witness statement, not counted', `from a witness you named as trusted (key set ${w.witness}), but it was not counted (see the notes on this card)`);
|
|
159
|
+
else row(list, 'Witness statement, not counted', `from a witness you did not name as trusted (key set ${w.witness})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
152
163
|
function cancellationCard(e) {
|
|
153
164
|
const card = el('div', `card slip${e.problems.length ? ' broken' : ''}`);
|
|
154
165
|
card.append(el('h3', 'kind', `Entry ${e.index}: Cancellation (the person ends a slip)`));
|
|
@@ -188,8 +199,39 @@ function acknowledgementCard(e) {
|
|
|
188
199
|
return card;
|
|
189
200
|
}
|
|
190
201
|
|
|
202
|
+
// A seal held elsewhere, handed over beside the book.
|
|
203
|
+
function heldSealCard(h, n) {
|
|
204
|
+
const card = el('div', `card seal${h.problems.length ? ' broken' : ''}`);
|
|
205
|
+
card.append(el('h3', 'kind', `Handed over beside the book, ${n}: a seal of the book held elsewhere`));
|
|
206
|
+
if (h.problems.length) {
|
|
207
|
+
card.append(el('p', 'quiet', 'It did not pass its check, so it was not used. See the problems above.'));
|
|
208
|
+
return card;
|
|
209
|
+
}
|
|
210
|
+
if (h.verified === 'none') {
|
|
211
|
+
card.append(el('p', 'quiet', 'What this seal says is not shown, because this device could not check its signatures.'));
|
|
212
|
+
} else {
|
|
213
|
+
const c = h.content;
|
|
214
|
+
const list = el('dl');
|
|
215
|
+
row(list, 'Covers', `the first ${c.size} entries`);
|
|
216
|
+
row(list, 'Stands at', `entry ${h.place}`);
|
|
217
|
+
row(list, 'Top fingerprint', c.root, true);
|
|
218
|
+
row(list, 'Sealed at', `${showTime(c.when)} (the recorder's own word)`);
|
|
219
|
+
row(list, 'Recorder', `${label(c.by.name)} (a label; it is not checked)`);
|
|
220
|
+
row(list, 'Recorder\'s key set', h.sealer, true);
|
|
221
|
+
row(list, 'Recorder\'s signatures', signatureWords(h.signatures));
|
|
222
|
+
stampRows(list, h.stamps);
|
|
223
|
+
witnessRows(list, h.witnesses);
|
|
224
|
+
row(list, 'In the book at that place', h.used ? (h.inBook === null ? 'no' : `yes, at entry ${h.inBook}`) : 'not compared, because it was not used');
|
|
225
|
+
row(list, 'Fingerprint', h.fingerprint, true);
|
|
226
|
+
card.append(list);
|
|
227
|
+
}
|
|
228
|
+
for (const note of h.notes) flag(card, 'note', note);
|
|
229
|
+
return card;
|
|
230
|
+
}
|
|
231
|
+
|
|
191
232
|
// A cancellation the person kept, handed over beside the book.
|
|
192
233
|
function heldCard(h, n) {
|
|
234
|
+
if (h.kind === 'seal') return heldSealCard(h, n);
|
|
193
235
|
const card = el('div', `card slip${h.problems.length ? ' broken' : ''}`);
|
|
194
236
|
card.append(el('h3', 'kind', `Handed over beside the book, ${n}: the person's own copy of a cancellation`));
|
|
195
237
|
if (h.problems.length) {
|
|
@@ -365,6 +407,29 @@ function refusalCard(e) {
|
|
|
365
407
|
return card;
|
|
366
408
|
}
|
|
367
409
|
|
|
410
|
+
// A denial: the agent's own side says it did not take an action, and why.
|
|
411
|
+
function denialCard(e) {
|
|
412
|
+
const card = el('div', `card refusal${e.problems.length ? ' broken' : ''}`);
|
|
413
|
+
card.append(el('h3', 'kind', `Entry ${e.index}: Denial (the agent's side says it did not take an action)`));
|
|
414
|
+
if (hidden(e)) {
|
|
415
|
+
card.append(el('p', 'quiet', hidden(e)));
|
|
416
|
+
return card;
|
|
417
|
+
}
|
|
418
|
+
const c = e.content;
|
|
419
|
+
const list = el('dl');
|
|
420
|
+
row(list, 'From', c.pass ? `the helper agent of pass ${c.pass}` : 'the slip\'s own agent');
|
|
421
|
+
row(list, 'When', `${showTime(c.when)} (the agent's side's own word)`);
|
|
422
|
+
row(list, 'Not taken', c.action + (c.amount ? `, ${c.amount.value} ${c.amount.unit}` : '') + (c.with ? `, with [${c.with}]` : ''));
|
|
423
|
+
if (c.details) row(list, 'Documents named', `${c.details.length}, by their fingerprints only`);
|
|
424
|
+
row(list, 'Who said no', DENIAL_SOURCES[c.source]);
|
|
425
|
+
if (c.codes) row(list, 'Reasons given', c.codes.join(', '));
|
|
426
|
+
row(list, 'Agent\'s signatures', signatureWords(e.signatures));
|
|
427
|
+
row(list, 'Fingerprint', e.fingerprint, true);
|
|
428
|
+
card.append(list);
|
|
429
|
+
flag(card, 'onesided', 'This shows what the agent\'s side said. It is not counted against the agent, and it does not show that the action was not taken in some other way.');
|
|
430
|
+
return card;
|
|
431
|
+
}
|
|
432
|
+
|
|
368
433
|
function sealCard(e) {
|
|
369
434
|
const card = el('div', `card seal${e.problems.length ? ' broken' : ''}`);
|
|
370
435
|
card.append(el('h3', 'kind', `Entry ${e.index}: Seal (the book, signed by whoever keeps it)`));
|
|
@@ -381,6 +446,7 @@ function sealCard(e) {
|
|
|
381
446
|
row(list, 'Recorder\'s key set', `${e.sealer}. Compare this fingerprint with the one the recorder gave you.`, true);
|
|
382
447
|
row(list, 'Recorder\'s signatures', signatureWords(e.signatures));
|
|
383
448
|
stampRows(list, e.stamps);
|
|
449
|
+
witnessRows(list, e.witnesses);
|
|
384
450
|
row(list, 'Fingerprint', e.fingerprint, true);
|
|
385
451
|
card.append(list);
|
|
386
452
|
if (e.stamps.length === 0) flag(card, 'onesided', 'There is no outside time-stamp: the time is only the recorder\'s own word.');
|
|
@@ -417,15 +483,79 @@ function termsCard(e) {
|
|
|
417
483
|
* @param {object} [o.options] what the check was told to trust: the green answer needs the passkey the
|
|
418
484
|
* person trusts and, where the record has a seal, the recorder they expect
|
|
419
485
|
*/
|
|
486
|
+
// A copy of a cancellation, a seal, or a file of disclosures, is small.
|
|
487
|
+
const MAX_HANDED_BYTES = 1000000;
|
|
488
|
+
const MAX_COPIES = 16;
|
|
489
|
+
const MAX_SEALS = 64;
|
|
490
|
+
|
|
491
|
+
async function jsonFrom(file, what) {
|
|
492
|
+
if (file.size > MAX_HANDED_BYTES) throw new Error(`The file of ${what}, ${shown(file.name)}, is too large. Nothing was checked.`);
|
|
493
|
+
try {
|
|
494
|
+
return JSON.parse(await file.text());
|
|
495
|
+
} catch {
|
|
496
|
+
throw new Error(`The file of ${what}, ${shown(file.name)}, is not JSON. Nothing was checked. Choose another file, or clear these files.`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* What the person was handed with the record: their own copies of
|
|
502
|
+
* cancellations (up to 16), seals held elsewhere (up to 64), and one file of
|
|
503
|
+
* disclosures for covered fields. A file that cannot be read stops the
|
|
504
|
+
* check: it is never set aside silently.
|
|
505
|
+
* @param {{cancellations?: File[], seals?: File[], disclosures?: File|null}} files the files chosen, as the
|
|
506
|
+
* browser gives them (anything with "name", "size" and "text()")
|
|
507
|
+
* @returns {Promise<{options: object, said: string[]}>} the options to check with, and what to say was handed over
|
|
508
|
+
*/
|
|
509
|
+
export async function readHanded({ cancellations = [], seals = [], disclosures = null } = {}) {
|
|
510
|
+
const options = {};
|
|
511
|
+
const said = [];
|
|
512
|
+
if (cancellations.length > MAX_COPIES) throw new Error('At most 16 copies of cancellations can be handed over at once. Nothing was checked.');
|
|
513
|
+
if (cancellations.length) {
|
|
514
|
+
options.cancellations = [];
|
|
515
|
+
for (const file of cancellations) options.cancellations.push(await jsonFrom(file, 'a cancellation'));
|
|
516
|
+
said.push(cancellations.length === 1 ? 'one copy of a cancellation' : `${cancellations.length} copies of cancellations`);
|
|
517
|
+
}
|
|
518
|
+
if (seals.length > MAX_SEALS) throw new Error('At most 64 seals held elsewhere can be handed over at once. Nothing was checked.');
|
|
519
|
+
if (seals.length) {
|
|
520
|
+
options.seals = [];
|
|
521
|
+
for (const file of seals) options.seals.push(await jsonFrom(file, 'a seal'));
|
|
522
|
+
said.push(seals.length === 1 ? 'one seal held elsewhere' : `${seals.length} seals held elsewhere`);
|
|
523
|
+
}
|
|
524
|
+
if (disclosures) {
|
|
525
|
+
options.disclosures = await jsonFrom(disclosures, 'disclosures');
|
|
526
|
+
said.push('disclosures for covered fields');
|
|
527
|
+
}
|
|
528
|
+
return { options, said };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* What the check rests on: whether the person named the passkey they trust
|
|
533
|
+
* and, where the record has a seal or seals held elsewhere were handed
|
|
534
|
+
* over, the recorder they expect; and whether those seals went unused
|
|
535
|
+
* because no recorder was named. Without the keys the person named, a
|
|
536
|
+
* record agrees only with the keys inside it, and anyone could have made
|
|
537
|
+
* those.
|
|
538
|
+
* @param {object} result what checkBook or checkShow returned
|
|
539
|
+
* @param {object} [options] what the check was told
|
|
540
|
+
*/
|
|
541
|
+
export function trustOf(result, options = {}) {
|
|
542
|
+
const s = result.summary;
|
|
543
|
+
const issuerNamed = Array.isArray(options.issuerKeys) && options.issuerKeys.length > 0;
|
|
544
|
+
const sealerNamed = Array.isArray(options.sealKeys) && options.sealKeys.length > 0;
|
|
545
|
+
const sealsHanded = Array.isArray(options.seals) && options.seals.length > 0;
|
|
546
|
+
const named = issuerNamed && ((s.counts.seals === 0 && !sealsHanded) || sealerNamed);
|
|
547
|
+
// A seal handed over that was not compared with the book (it gives another book number, or could not be used) gives no pass.
|
|
548
|
+
// (Where no recorder was named, the seals handed over were not compared at all, and heldUnused says so.)
|
|
549
|
+
const heldCompared = !sealsHanded || !sealerNamed || (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
|
|
550
|
+
return { issuerNamed, named, sure: s.intact && named && heldCompared, heldUnused: sealsHanded && !sealerNamed };
|
|
551
|
+
}
|
|
552
|
+
|
|
420
553
|
export function renderResult(target, result, { isShow = false, checkedBy = 'Checked in this browser.', options = {} } = {}) {
|
|
421
554
|
target.replaceChildren();
|
|
422
555
|
const s = result.summary;
|
|
423
556
|
const missing = s.methodsMissing.join(' or ');
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const issuerNamed = Array.isArray(options.issuerKeys) && options.issuerKeys.length > 0;
|
|
427
|
-
const named = issuerNamed && (s.counts.seals === 0 || (Array.isArray(options.sealKeys) && options.sealKeys.length > 0));
|
|
428
|
-
const sure = s.intact && named;
|
|
557
|
+
const { issuerNamed, named, sure, heldUnused } = trustOf(result, options);
|
|
558
|
+
const heldCompared = heldUnused || (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
|
|
429
559
|
|
|
430
560
|
target.append(el('h2', '', 'The answer'));
|
|
431
561
|
const answers = el('ul', 'answers');
|
|
@@ -433,14 +563,23 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
433
563
|
answer(answers, 'no', 'The record is NOT intact.', 'See the problems marked below. A record that is not intact is not evidence.');
|
|
434
564
|
} else if (sure) {
|
|
435
565
|
answer(answers, 'yes', 'The record is intact.', `Every signature fits its key, every label is right and nothing has been changed. ${checkedBy}`);
|
|
566
|
+
} else if (s.intact && named && !heldCompared) {
|
|
567
|
+
answer(
|
|
568
|
+
answers,
|
|
569
|
+
'partly',
|
|
570
|
+
'Not every seal you handed over was compared with this book.',
|
|
571
|
+
'A seal held elsewhere that you handed over gives another book number than this book, or could not be used, so it was not compared with the book. See the note beside it. This is not a pass.',
|
|
572
|
+
);
|
|
436
573
|
} else if (s.intact) {
|
|
437
574
|
answer(
|
|
438
575
|
answers,
|
|
439
576
|
'partly',
|
|
440
577
|
'Intact only as far as its own keys show.',
|
|
441
|
-
issuerNamed
|
|
442
|
-
? '
|
|
443
|
-
:
|
|
578
|
+
!issuerNamed
|
|
579
|
+
? 'You named no passkey you trust, so anyone could have made a record like this. Put the thumbprint of the passkey you trust in its box. This is not a pass.'
|
|
580
|
+
: s.counts.seals === 0
|
|
581
|
+
? 'You were handed seals held elsewhere, and you named no recorder you expect, so those seals were not compared with the book. Put the fingerprint of the recorder you expect in its box. This is not a pass.'
|
|
582
|
+
: 'The record has a seal, and you named no recorder you expect, so anyone could have sealed it. Put the fingerprint of the recorder you expect in its box. This is not a pass.',
|
|
444
583
|
);
|
|
445
584
|
} else {
|
|
446
585
|
answer(
|
|
@@ -477,6 +616,11 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
477
616
|
|
|
478
617
|
const held = result.held ?? [];
|
|
479
618
|
if (held.length) target.append(el('h2', '', 'Handed over beside the book'));
|
|
619
|
+
if (heldUnused) {
|
|
620
|
+
const card = el('div', 'card show');
|
|
621
|
+
flag(card, 'note', 'The seals held elsewhere that you handed over were not compared with the book, because you named no recorder you expect. Put the fingerprint of the recorder you expect in its box.');
|
|
622
|
+
target.append(card);
|
|
623
|
+
}
|
|
480
624
|
held.forEach((h, i) => target.append(heldCard(h, i + 1)));
|
|
481
625
|
|
|
482
626
|
if (result.root) {
|
|
@@ -497,6 +641,9 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
497
641
|
: 'It stands for the whole book. No outside time-stamp from a service you trust covers it: compare it, and the number of entries, with a copy you already trust.',
|
|
498
642
|
),
|
|
499
643
|
);
|
|
644
|
+
if (s.witnessed) {
|
|
645
|
+
seal.append(el('p', 'quiet', `A witness you named as trusted signed for the seal at entry ${s.witnessed.seal}, which covers the first ${s.witnessed.entries} entries (${s.witnessed.seals.length} of ${s.counts.seals} seals witnessed). Being witnessed excuses nothing.${isShow ? ' These are single pages: they cannot show that a limit over the whole book was kept, so being witnessed says nothing here about totals.' : ''}`));
|
|
646
|
+
}
|
|
500
647
|
target.append(seal);
|
|
501
648
|
}
|
|
502
649
|
|
|
@@ -510,6 +657,7 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
510
657
|
else if (e.kind === 'seal') card = sealCard(e);
|
|
511
658
|
else if (e.kind === 'cancellation') card = cancellationCard(e);
|
|
512
659
|
else if (e.kind === 'acknowledgement') card = acknowledgementCard(e);
|
|
660
|
+
else if (e.kind === 'denial') card = denialCard(e);
|
|
513
661
|
else if (e.kind === 'pass') card = passCard(e);
|
|
514
662
|
else if (e.kind === 'vouching') card = vouchingCard(e);
|
|
515
663
|
else if (e.kind === 'withdrawal') card = withdrawalCard(e);
|