provared 0.5.0 → 0.7.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 +1496 -0
- package/README.md +18 -3
- package/bin/provared-check.mjs +128 -13
- package/docs/guide.md +346 -24
- package/docs/threat-model.md +299 -21
- package/net/headers.js +1 -0
- package/package.json +2 -2
- package/page/check-page.js +18 -32
- package/page/index.html +6 -0
- package/page/render.js +183 -10
- package/spec/provared-format.md +771 -37
- package/src/book.js +544 -19
- package/src/check.d.ts +4 -0
- package/src/check.js +3 -1
- package/src/denial.js +123 -0
- package/src/der.js +56 -6
- package/src/dev.d.ts +1 -1
- package/src/dev.js +1 -1
- package/src/documents.js +8 -3
- package/src/guard.js +12 -1
- package/src/index.js +10 -0
- package/src/jws.js +8 -0
- package/src/keys.js +48 -1
- package/src/recorder.js +131 -5
- package/src/seal.js +32 -4
- package/src/sealer.js +812 -0
- package/src/signatures.js +12 -6
- package/src/slip.js +72 -5
- package/src/timestamp.js +55 -16
- package/src/tools.js +30 -13
- package/src/tree.js +139 -7
- package/src/types.d.ts +272 -11
- 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, isPageAddress, 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
|
|
@@ -54,6 +54,20 @@ export function readBlocks(text) {
|
|
|
54
54
|
return { values: lines.filter((line) => /^[0-9a-fA-F]{64}$/.test(line)), bad: lines.filter((line) => !/^[0-9a-fA-F]{64}$/.test(line)) };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Read the box of page addresses, one to a line: the addresses at which the
|
|
59
|
+
* person expects the permissions to have been signed, each in the one form
|
|
60
|
+
* the format allows (https://, a website name in lower case, a port only if
|
|
61
|
+
* it is needed, and nothing more). As with the other boxes, a line that is
|
|
62
|
+
* not one is never dropped silently.
|
|
63
|
+
* @param {string} text
|
|
64
|
+
* @returns {{values: string[], bad: string[]}}
|
|
65
|
+
*/
|
|
66
|
+
export function readOrigins(text) {
|
|
67
|
+
const lines = String(text).split(/\s+/).filter((line) => line !== '');
|
|
68
|
+
return { values: lines.filter((line) => isPageAddress(line)), bad: lines.filter((line) => !isPageAddress(line)) };
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
/**
|
|
58
72
|
* @param {unknown} text
|
|
59
73
|
* @returns {string} the text, safe to place among other words
|
|
@@ -82,6 +96,17 @@ export function el(tag, className, text) {
|
|
|
82
96
|
|
|
83
97
|
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
84
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The heading of the card of the top fingerprint. It names the Seal only
|
|
101
|
+
* where the record holds one: a book, or the pages of a Show, with no seal
|
|
102
|
+
* has a top fingerprint and no seal.
|
|
103
|
+
* @param {{summary?: {counts?: {seals?: number}}}} result the checker's answer
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function topFingerprintHeading(result) {
|
|
107
|
+
return (result?.summary?.counts?.seals ?? 0) > 0 ? 'The top fingerprint (the Seal)' : 'The top fingerprint';
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
/**
|
|
86
111
|
* A time from a record, in words. The checker has already confirmed its
|
|
87
112
|
* form; anything else is shown as it is.
|
|
@@ -149,6 +174,17 @@ function stampRows(list, stamps) {
|
|
|
149
174
|
}
|
|
150
175
|
}
|
|
151
176
|
|
|
177
|
+
// The witness statements beside a seal. What a statement says is shown only where it passed its check.
|
|
178
|
+
function witnessRows(list, witnesses) {
|
|
179
|
+
for (const w of witnesses ?? []) {
|
|
180
|
+
if (w.state === 'invalid') row(list, 'Witness statement, not used', 'it did not pass its check (see the problems)');
|
|
181
|
+
else if (w.state === 'unavailable') row(list, 'Witness statement, not checked', 'this browser cannot check both of its signatures');
|
|
182
|
+
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)`);
|
|
183
|
+
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)`);
|
|
184
|
+
else row(list, 'Witness statement, not counted', `from a witness you did not name as trusted (key set ${w.witness})`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
152
188
|
function cancellationCard(e) {
|
|
153
189
|
const card = el('div', `card slip${e.problems.length ? ' broken' : ''}`);
|
|
154
190
|
card.append(el('h3', 'kind', `Entry ${e.index}: Cancellation (the person ends a slip)`));
|
|
@@ -188,8 +224,39 @@ function acknowledgementCard(e) {
|
|
|
188
224
|
return card;
|
|
189
225
|
}
|
|
190
226
|
|
|
227
|
+
// A seal held elsewhere, handed over beside the book.
|
|
228
|
+
function heldSealCard(h, n) {
|
|
229
|
+
const card = el('div', `card seal${h.problems.length ? ' broken' : ''}`);
|
|
230
|
+
card.append(el('h3', 'kind', `Handed over beside the book, ${n}: a seal of the book held elsewhere`));
|
|
231
|
+
if (h.problems.length) {
|
|
232
|
+
card.append(el('p', 'quiet', 'It did not pass its check, so it was not used. See the problems above.'));
|
|
233
|
+
return card;
|
|
234
|
+
}
|
|
235
|
+
if (h.verified === 'none') {
|
|
236
|
+
card.append(el('p', 'quiet', 'What this seal says is not shown, because this device could not check its signatures.'));
|
|
237
|
+
} else {
|
|
238
|
+
const c = h.content;
|
|
239
|
+
const list = el('dl');
|
|
240
|
+
row(list, 'Covers', `the first ${c.size} entries`);
|
|
241
|
+
row(list, 'Stands at', `entry ${h.place}`);
|
|
242
|
+
row(list, 'Top fingerprint', c.root, true);
|
|
243
|
+
row(list, 'Sealed at', `${showTime(c.when)} (the recorder's own word)`);
|
|
244
|
+
row(list, 'Recorder', `${label(c.by.name)} (a label; it is not checked)`);
|
|
245
|
+
row(list, 'Recorder\'s key set', h.sealer, true);
|
|
246
|
+
row(list, 'Recorder\'s signatures', signatureWords(h.signatures));
|
|
247
|
+
stampRows(list, h.stamps);
|
|
248
|
+
witnessRows(list, h.witnesses);
|
|
249
|
+
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');
|
|
250
|
+
row(list, 'Fingerprint', h.fingerprint, true);
|
|
251
|
+
card.append(list);
|
|
252
|
+
}
|
|
253
|
+
for (const note of h.notes) flag(card, 'note', note);
|
|
254
|
+
return card;
|
|
255
|
+
}
|
|
256
|
+
|
|
191
257
|
// A cancellation the person kept, handed over beside the book.
|
|
192
258
|
function heldCard(h, n) {
|
|
259
|
+
if (h.kind === 'seal') return heldSealCard(h, n);
|
|
193
260
|
const card = el('div', `card slip${h.problems.length ? ' broken' : ''}`);
|
|
194
261
|
card.append(el('h3', 'kind', `Handed over beside the book, ${n}: the person's own copy of a cancellation`));
|
|
195
262
|
if (h.problems.length) {
|
|
@@ -365,6 +432,29 @@ function refusalCard(e) {
|
|
|
365
432
|
return card;
|
|
366
433
|
}
|
|
367
434
|
|
|
435
|
+
// A denial: the agent's own side says it did not take an action, and why.
|
|
436
|
+
function denialCard(e) {
|
|
437
|
+
const card = el('div', `card refusal${e.problems.length ? ' broken' : ''}`);
|
|
438
|
+
card.append(el('h3', 'kind', `Entry ${e.index}: Denial (the agent's side says it did not take an action)`));
|
|
439
|
+
if (hidden(e)) {
|
|
440
|
+
card.append(el('p', 'quiet', hidden(e)));
|
|
441
|
+
return card;
|
|
442
|
+
}
|
|
443
|
+
const c = e.content;
|
|
444
|
+
const list = el('dl');
|
|
445
|
+
row(list, 'From', c.pass ? `the helper agent of pass ${c.pass}` : 'the slip\'s own agent');
|
|
446
|
+
row(list, 'When', `${showTime(c.when)} (the agent's side's own word)`);
|
|
447
|
+
row(list, 'Not taken', c.action + (c.amount ? `, ${c.amount.value} ${c.amount.unit}` : '') + (c.with ? `, with [${c.with}]` : ''));
|
|
448
|
+
if (c.details) row(list, 'Documents named', `${c.details.length}, by their fingerprints only`);
|
|
449
|
+
row(list, 'Who said no', DENIAL_SOURCES[c.source]);
|
|
450
|
+
if (c.codes) row(list, 'Reasons given', c.codes.join(', '));
|
|
451
|
+
row(list, 'Agent\'s signatures', signatureWords(e.signatures));
|
|
452
|
+
row(list, 'Fingerprint', e.fingerprint, true);
|
|
453
|
+
card.append(list);
|
|
454
|
+
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.');
|
|
455
|
+
return card;
|
|
456
|
+
}
|
|
457
|
+
|
|
368
458
|
function sealCard(e) {
|
|
369
459
|
const card = el('div', `card seal${e.problems.length ? ' broken' : ''}`);
|
|
370
460
|
card.append(el('h3', 'kind', `Entry ${e.index}: Seal (the book, signed by whoever keeps it)`));
|
|
@@ -381,6 +471,7 @@ function sealCard(e) {
|
|
|
381
471
|
row(list, 'Recorder\'s key set', `${e.sealer}. Compare this fingerprint with the one the recorder gave you.`, true);
|
|
382
472
|
row(list, 'Recorder\'s signatures', signatureWords(e.signatures));
|
|
383
473
|
stampRows(list, e.stamps);
|
|
474
|
+
witnessRows(list, e.witnesses);
|
|
384
475
|
row(list, 'Fingerprint', e.fingerprint, true);
|
|
385
476
|
card.append(list);
|
|
386
477
|
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 +508,79 @@ function termsCard(e) {
|
|
|
417
508
|
* @param {object} [o.options] what the check was told to trust: the green answer needs the passkey the
|
|
418
509
|
* person trusts and, where the record has a seal, the recorder they expect
|
|
419
510
|
*/
|
|
511
|
+
// A copy of a cancellation, a seal, or a file of disclosures, is small.
|
|
512
|
+
const MAX_HANDED_BYTES = 1000000;
|
|
513
|
+
const MAX_COPIES = 16;
|
|
514
|
+
const MAX_SEALS = 64;
|
|
515
|
+
|
|
516
|
+
async function jsonFrom(file, what) {
|
|
517
|
+
if (file.size > MAX_HANDED_BYTES) throw new Error(`The file of ${what}, ${shown(file.name)}, is too large. Nothing was checked.`);
|
|
518
|
+
try {
|
|
519
|
+
return JSON.parse(await file.text());
|
|
520
|
+
} catch {
|
|
521
|
+
throw new Error(`The file of ${what}, ${shown(file.name)}, is not JSON. Nothing was checked. Choose another file, or clear these files.`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* What the person was handed with the record: their own copies of
|
|
527
|
+
* cancellations (up to 16), seals held elsewhere (up to 64), and one file of
|
|
528
|
+
* disclosures for covered fields. A file that cannot be read stops the
|
|
529
|
+
* check: it is never set aside silently.
|
|
530
|
+
* @param {{cancellations?: File[], seals?: File[], disclosures?: File|null}} files the files chosen, as the
|
|
531
|
+
* browser gives them (anything with "name", "size" and "text()")
|
|
532
|
+
* @returns {Promise<{options: object, said: string[]}>} the options to check with, and what to say was handed over
|
|
533
|
+
*/
|
|
534
|
+
export async function readHanded({ cancellations = [], seals = [], disclosures = null } = {}) {
|
|
535
|
+
const options = {};
|
|
536
|
+
const said = [];
|
|
537
|
+
if (cancellations.length > MAX_COPIES) throw new Error('At most 16 copies of cancellations can be handed over at once. Nothing was checked.');
|
|
538
|
+
if (cancellations.length) {
|
|
539
|
+
options.cancellations = [];
|
|
540
|
+
for (const file of cancellations) options.cancellations.push(await jsonFrom(file, 'a cancellation'));
|
|
541
|
+
said.push(cancellations.length === 1 ? 'one copy of a cancellation' : `${cancellations.length} copies of cancellations`);
|
|
542
|
+
}
|
|
543
|
+
if (seals.length > MAX_SEALS) throw new Error('At most 64 seals held elsewhere can be handed over at once. Nothing was checked.');
|
|
544
|
+
if (seals.length) {
|
|
545
|
+
options.seals = [];
|
|
546
|
+
for (const file of seals) options.seals.push(await jsonFrom(file, 'a seal'));
|
|
547
|
+
said.push(seals.length === 1 ? 'one seal held elsewhere' : `${seals.length} seals held elsewhere`);
|
|
548
|
+
}
|
|
549
|
+
if (disclosures) {
|
|
550
|
+
options.disclosures = await jsonFrom(disclosures, 'disclosures');
|
|
551
|
+
said.push('disclosures for covered fields');
|
|
552
|
+
}
|
|
553
|
+
return { options, said };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* What the check rests on: whether the person named the passkey they trust
|
|
558
|
+
* and, where the record has a seal or seals held elsewhere were handed
|
|
559
|
+
* over, the recorder they expect; and whether those seals went unused
|
|
560
|
+
* because no recorder was named. Without the keys the person named, a
|
|
561
|
+
* record agrees only with the keys inside it, and anyone could have made
|
|
562
|
+
* those.
|
|
563
|
+
* @param {object} result what checkBook or checkShow returned
|
|
564
|
+
* @param {object} [options] what the check was told
|
|
565
|
+
*/
|
|
566
|
+
export function trustOf(result, options = {}) {
|
|
567
|
+
const s = result.summary;
|
|
568
|
+
const issuerNamed = Array.isArray(options.issuerKeys) && options.issuerKeys.length > 0;
|
|
569
|
+
const sealerNamed = Array.isArray(options.sealKeys) && options.sealKeys.length > 0;
|
|
570
|
+
const sealsHanded = Array.isArray(options.seals) && options.seals.length > 0;
|
|
571
|
+
const named = issuerNamed && ((s.counts.seals === 0 && !sealsHanded) || sealerNamed);
|
|
572
|
+
// A seal handed over that was not compared with the book (it gives another book number, or could not be used) gives no pass.
|
|
573
|
+
// (Where no recorder was named, the seals handed over were not compared at all, and heldUnused says so.)
|
|
574
|
+
const heldCompared = !sealsHanded || !sealerNamed || (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
|
|
575
|
+
return { issuerNamed, named, sure: s.intact && named && heldCompared, heldUnused: sealsHanded && !sealerNamed };
|
|
576
|
+
}
|
|
577
|
+
|
|
420
578
|
export function renderResult(target, result, { isShow = false, checkedBy = 'Checked in this browser.', options = {} } = {}) {
|
|
421
579
|
target.replaceChildren();
|
|
422
580
|
const s = result.summary;
|
|
423
581
|
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;
|
|
582
|
+
const { issuerNamed, named, sure, heldUnused } = trustOf(result, options);
|
|
583
|
+
const heldCompared = heldUnused || (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
|
|
429
584
|
|
|
430
585
|
target.append(el('h2', '', 'The answer'));
|
|
431
586
|
const answers = el('ul', 'answers');
|
|
@@ -433,14 +588,23 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
433
588
|
answer(answers, 'no', 'The record is NOT intact.', 'See the problems marked below. A record that is not intact is not evidence.');
|
|
434
589
|
} else if (sure) {
|
|
435
590
|
answer(answers, 'yes', 'The record is intact.', `Every signature fits its key, every label is right and nothing has been changed. ${checkedBy}`);
|
|
591
|
+
} else if (s.intact && named && !heldCompared) {
|
|
592
|
+
answer(
|
|
593
|
+
answers,
|
|
594
|
+
'partly',
|
|
595
|
+
'Not every seal you handed over was compared with this book.',
|
|
596
|
+
'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.',
|
|
597
|
+
);
|
|
436
598
|
} else if (s.intact) {
|
|
437
599
|
answer(
|
|
438
600
|
answers,
|
|
439
601
|
'partly',
|
|
440
602
|
'Intact only as far as its own keys show.',
|
|
441
|
-
issuerNamed
|
|
442
|
-
? '
|
|
443
|
-
:
|
|
603
|
+
!issuerNamed
|
|
604
|
+
? '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.'
|
|
605
|
+
: s.counts.seals === 0
|
|
606
|
+
? '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.'
|
|
607
|
+
: '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
608
|
);
|
|
445
609
|
} else {
|
|
446
610
|
answer(
|
|
@@ -477,11 +641,16 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
477
641
|
|
|
478
642
|
const held = result.held ?? [];
|
|
479
643
|
if (held.length) target.append(el('h2', '', 'Handed over beside the book'));
|
|
644
|
+
if (heldUnused) {
|
|
645
|
+
const card = el('div', 'card show');
|
|
646
|
+
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.');
|
|
647
|
+
target.append(card);
|
|
648
|
+
}
|
|
480
649
|
held.forEach((h, i) => target.append(heldCard(h, i + 1)));
|
|
481
650
|
|
|
482
651
|
if (result.root) {
|
|
483
652
|
const seal = el('div', 'card seal');
|
|
484
|
-
seal.append(el('h3', 'kind',
|
|
653
|
+
seal.append(el('h3', 'kind', topFingerprintHeading(result)));
|
|
485
654
|
const list = el('dl');
|
|
486
655
|
row(list, 'Top fingerprint', result.root, true);
|
|
487
656
|
row(list, 'Entries in the book', String(result.size));
|
|
@@ -497,6 +666,9 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
497
666
|
: '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
667
|
),
|
|
499
668
|
);
|
|
669
|
+
if (s.witnessed) {
|
|
670
|
+
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.' : ''}`));
|
|
671
|
+
}
|
|
500
672
|
target.append(seal);
|
|
501
673
|
}
|
|
502
674
|
|
|
@@ -510,6 +682,7 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
510
682
|
else if (e.kind === 'seal') card = sealCard(e);
|
|
511
683
|
else if (e.kind === 'cancellation') card = cancellationCard(e);
|
|
512
684
|
else if (e.kind === 'acknowledgement') card = acknowledgementCard(e);
|
|
685
|
+
else if (e.kind === 'denial') card = denialCard(e);
|
|
513
686
|
else if (e.kind === 'pass') card = passCard(e);
|
|
514
687
|
else if (e.kind === 'vouching') card = vouchingCard(e);
|
|
515
688
|
else if (e.kind === 'withdrawal') card = withdrawalCard(e);
|