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/src/book.js
CHANGED
|
@@ -18,10 +18,12 @@ import { MAX_PASSES, validatePassContent } from './pass.js';
|
|
|
18
18
|
import { CLOCK_ALLOWANCE_MS, decodeStamps, keySetFingerprint, validateSealContent } from './seal.js';
|
|
19
19
|
import { verifySignature } from './signatures.js';
|
|
20
20
|
import { validateAcknowledgementContent, validateCancellationContent, validateVouchingContent, validateWithdrawalContent } from './standing.js';
|
|
21
|
+
import { validateDenialContent } from './denial.js';
|
|
22
|
+
import { MAX_WITNESSES, validateWitnessContent } from './witness.js';
|
|
21
23
|
import { requestOf, validateApprovalContent } from './approval.js';
|
|
22
24
|
import { compareWithSlip, newTally, settlePeriods } from './compare.js';
|
|
23
25
|
import { validateRefusalContent, validateTermsContent } from './service.js';
|
|
24
|
-
import { checkPasskeySignature, checkSlip } from './slip.js';
|
|
26
|
+
import { checkOriginsOption, checkPasskeySignature, checkSlip } from './slip.js';
|
|
25
27
|
import { validateCountersignatureContent, validateStubContent } from './stub.js';
|
|
26
28
|
import { checkStamp } from './timestamp.js';
|
|
27
29
|
import { inclusionPath, treeBuilder, treeRoot, verifyInclusion } from './tree.js';
|
|
@@ -36,6 +38,8 @@ export const MAX_PAGES = 64;
|
|
|
36
38
|
export const MAX_HELD = 16;
|
|
37
39
|
/** The most acknowledgements that may be handed over with one cancellation. */
|
|
38
40
|
export const MAX_ACKNOWLEDGEMENTS = 4;
|
|
41
|
+
/** The most seals held elsewhere that may be handed over beside a book. */
|
|
42
|
+
export const MAX_HELD_SEALS = 64;
|
|
39
43
|
|
|
40
44
|
// What can be said about time, with and without an outside time-stamp.
|
|
41
45
|
const NOT_STAMPED =
|
|
@@ -47,6 +51,10 @@ const stampedWords = (sealed) =>
|
|
|
47
51
|
? blockWords(sealed)
|
|
48
52
|
: `An outside time-stamp covers the first ${sealed.entries} entries: they existed by ${sealed.when}, as the time-stamp service states. What came after is not time-stamped, and a later seal with everything after it could have been removed: compare the top fingerprint and the number of entries with a copy you trust. Time-stamp services sign with methods of today's kind, which are not quantum-safe.`;
|
|
49
53
|
|
|
54
|
+
// What a witness the reader trusts shows, and what it does not (format description, section 29.4).
|
|
55
|
+
const witnessedWords = (w) =>
|
|
56
|
+
`A witness you named as trusted signed for the seal at entry ${w.seal}, which covers the first ${w.entries} entries. This book starts from a permission signed by a passkey you named, every permission before the seal is from that passkey for the same agent, and the book is sealed by a recorder you named. For that recorder, that passkey and the agent the permission names, the witness states that it saw this seal, and that it will sign for no seal that does not extend it, in this book or a later one, whichever permission from that passkey to that agent the book starts from. So a second history of those entries, sealed by the same recorder and starting from a permission that passkey gave that agent, shown to someone else, would need that witness to sign for it too. It does not cover a history sealed with other keys, even keys you also named, nor one that starts from a permission for another agent. It does not show when the seal was made, that nothing was cut off after a later seal, nor anything never shown to the witness: to see whether the end of the book is missing, ask the witness for the latest seal it holds for this recorder, passkey and agent, and hand that seal over beside the book it is of.`;
|
|
57
|
+
|
|
50
58
|
/** What no check of a draft version 0 record can show. Stated with every result. */
|
|
51
59
|
export const LIMITS = [
|
|
52
60
|
'It shows what was recorded, not what was left out. An agent that acts and writes no stub leaves no trace here.',
|
|
@@ -61,8 +69,8 @@ export const LIMITS = [
|
|
|
61
69
|
|
|
62
70
|
/**
|
|
63
71
|
* Write one entry of a book as its line (without the line feed).
|
|
64
|
-
* @param {object} entry one of: {slip}, {stub, countersignature?, approval?}, {refusal}, {terms}, {seal, stamps?},
|
|
65
|
-
* {cancellation, stamps?}, {acknowledgement}, {vouching}, {withdrawal}, {pass}
|
|
72
|
+
* @param {object} entry one of: {slip}, {stub, countersignature?, approval?}, {refusal}, {terms}, {seal, stamps?, witnesses?},
|
|
73
|
+
* {cancellation, stamps?}, {acknowledgement}, {vouching}, {withdrawal}, {pass}, {denial}
|
|
66
74
|
* @returns {string}
|
|
67
75
|
*/
|
|
68
76
|
export function entryLine(entry) {
|
|
@@ -155,7 +163,7 @@ function newReader(options, whole) {
|
|
|
155
163
|
// The seals so far: the last one, how far an outside time-stamp reaches,
|
|
156
164
|
// and the tree as it grows, so that each seal can be compared with the
|
|
157
165
|
// entries before it.
|
|
158
|
-
seals: { count: 0, last: null, lastWhen: -Infinity, lastStampTime: -Infinity, latest: -Infinity, next: 0, sealed: null },
|
|
166
|
+
seals: { count: 0, last: null, lastWhen: -Infinity, lastStampTime: -Infinity, latest: -Infinity, next: 0, sealed: null, book: null, firstPrevious: null },
|
|
159
167
|
tree: whole ? treeBuilder() : null,
|
|
160
168
|
// Problems with what was handed over beside the record (disclosures), which are not faults of an entry.
|
|
161
169
|
handedProblems: [],
|
|
@@ -192,7 +200,8 @@ async function readLine(reader, item) {
|
|
|
192
200
|
const names = value !== null && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value).sort().join(',') : '';
|
|
193
201
|
// A reader split off to try one more stub reads nothing else: a stub
|
|
194
202
|
// is the one kind of entry that changes nothing read before it.
|
|
195
|
-
|
|
203
|
+
// A denial changes nothing read before it either.
|
|
204
|
+
if (reader.forked && !STUB_ENTRIES.includes(names) && names !== 'denial') throw new Error('only a stub or a denial may be tried');
|
|
196
205
|
if (names === 'slip') {
|
|
197
206
|
entry.kind = 'slip';
|
|
198
207
|
const check = await checkSlip(value.slip, options, item.disclosures);
|
|
@@ -260,8 +269,10 @@ async function readLine(reader, item) {
|
|
|
260
269
|
} else if (names === 'terms') {
|
|
261
270
|
entry.kind = 'terms';
|
|
262
271
|
await checkTermsEntry(entry, value, terms, ids, without);
|
|
263
|
-
} else if (names
|
|
272
|
+
} else if (SEAL_ENTRIES.includes(names)) {
|
|
264
273
|
entry.kind = 'seal';
|
|
274
|
+
// The line itself is kept, out of sight, so that a seal held elsewhere can be compared with it (checkHeldSeals).
|
|
275
|
+
sealLines.set(entry, line);
|
|
265
276
|
// The top fingerprint of the entries before it is worked out only once the seal has been read.
|
|
266
277
|
const before = whole ? async () => ({ size: index, root: toBase64url(await tree.root()) }) : null;
|
|
267
278
|
await checkSealEntry(entry, value, seals, ids, options, before, entries, !whole);
|
|
@@ -278,6 +289,9 @@ async function readLine(reader, item) {
|
|
|
278
289
|
} else if (names === 'acknowledgement') {
|
|
279
290
|
entry.kind = 'acknowledgement';
|
|
280
291
|
await checkAcknowledgementEntry(entry, value, slips, passes, ids, entries, without, whole);
|
|
292
|
+
} else if (names === 'denial') {
|
|
293
|
+
entry.kind = 'denial';
|
|
294
|
+
await checkDenialEntry(entry, value, slips, passes, ids, without, whole);
|
|
281
295
|
} else if (names === 'pass') {
|
|
282
296
|
entry.kind = 'pass';
|
|
283
297
|
await checkPassEntry(entry, value, slips, passes, ids, without, whole);
|
|
@@ -330,7 +344,7 @@ function settleLate(reader) {
|
|
|
330
344
|
// An entry as an answer gives it: with the findings that were worked out
|
|
331
345
|
// last, and with no time of its own if it did not pass its check. The
|
|
332
346
|
// entry the reader keeps is left as it is.
|
|
333
|
-
function answered(entry, late, entries) {
|
|
347
|
+
function answered(entry, late, entries, why = () => null) {
|
|
334
348
|
const more = late.get(entry.breaches);
|
|
335
349
|
const copy = more && more.length > 0 ? { ...entry, breaches: [...entry.breaches, ...more] } : { ...entry };
|
|
336
350
|
// A cancellation that passed when its acknowledgement was read may have
|
|
@@ -339,6 +353,8 @@ function answered(entry, late, entries) {
|
|
|
339
353
|
// pass its check.
|
|
340
354
|
if (entry.kind === 'acknowledgement' && entry.cancellation !== undefined && entries[entry.cancellation].problems.length > 0) failAcknowledgement(copy);
|
|
341
355
|
if (copy.problems.length > 0) delete copy.stampedAt;
|
|
356
|
+
// Whether each witness statement counts is settled now that the seal's own check is complete.
|
|
357
|
+
if (copy.kind === 'seal' && Array.isArray(copy.witnesses)) Object.assign(copy, witnessesCounted(copy, why(copy)));
|
|
342
358
|
return copy;
|
|
343
359
|
}
|
|
344
360
|
|
|
@@ -376,6 +392,7 @@ function forkReader(reader, whole = false) {
|
|
|
376
392
|
twinEntry.set(e, copy);
|
|
377
393
|
twinList.set(e.breaches, copy.breaches);
|
|
378
394
|
if (stampedTimes.has(e)) stampedTimes.set(copy, stampedTimes.get(e));
|
|
395
|
+
if (sealLines.has(e)) sealLines.set(copy, sealLines.get(e));
|
|
379
396
|
return copy;
|
|
380
397
|
})
|
|
381
398
|
: reader.entries.slice();
|
|
@@ -439,7 +456,7 @@ function forkReader(reader, whole = false) {
|
|
|
439
456
|
// worked out when it is asked for.
|
|
440
457
|
// The fixed copies made so far. Nothing changes one once it is made.
|
|
441
458
|
const FIXED = new WeakSet();
|
|
442
|
-
const OPTION_NAMES = ['issuerKeys', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'disclosures', 'cancellations', 'expectedRoot', 'expectedSize', 'withoutMethods'];
|
|
459
|
+
const OPTION_NAMES = ['issuerKeys', 'issuerOrigins', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'witnesses', 'disclosures', 'cancellations', 'seals', 'expectedRoot', 'expectedSize', 'withoutMethods'];
|
|
443
460
|
|
|
444
461
|
/**
|
|
445
462
|
* The options of a check, fixed: read once and copied, so that nothing done
|
|
@@ -792,6 +809,36 @@ async function checkAcknowledgementEntry(entry, value, slips, passes, ids, entri
|
|
|
792
809
|
}
|
|
793
810
|
}
|
|
794
811
|
|
|
812
|
+
// A denial: the agent's side states that it did not take an action that was
|
|
813
|
+
// asked for, and why. It is signed as an acknowledgement is, with the keys of
|
|
814
|
+
// the agent the slip names or of the helper agent its pass names. It is in
|
|
815
|
+
// no chain and is compared with nothing: a checker does not judge whether
|
|
816
|
+
// the refusal was right, and never counts it against the agent.
|
|
817
|
+
const DENIED = 'The agent\'s side states that it did not take this action, and why. That is its own word: it is not counted against the agent, and it does not show that the action was not taken in some other way.';
|
|
818
|
+
async function checkDenialEntry(entry, value, slips, passes, ids, without, whole) {
|
|
819
|
+
entry.signatures = [];
|
|
820
|
+
const parsed = await parseRecord(value.denial, 'denial');
|
|
821
|
+
entry.fingerprint = parsed.fingerprint;
|
|
822
|
+
validateDenialContent(parsed.content);
|
|
823
|
+
const c = parsed.content;
|
|
824
|
+
entry.content = c;
|
|
825
|
+
entry.slip = c.slip;
|
|
826
|
+
noteId(entry, ids, c.id);
|
|
827
|
+
const slip = findSlip(slips, c.slip, whole);
|
|
828
|
+
const pass = Object.hasOwn(c, 'pass') ? findPass(passes, c.pass, c.slip, whole) : null;
|
|
829
|
+
if (pass) entry.pass = c.pass;
|
|
830
|
+
entry.signatures = await checkKeySetSignatures(parsed, pass ? pass.content.to.keys : slip.content.agent.keys, without);
|
|
831
|
+
if (overall(entry.signatures) === 'invalid') {
|
|
832
|
+
entry.problems.push({
|
|
833
|
+
code: 'signature-invalid',
|
|
834
|
+
message: pass ? 'A signature on the denial does not fit the keys of the helper agent the pass names.' : 'A signature on the denial does not fit the agent\'s keys.',
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
entry.verified = verifiedBy(entry.signatures);
|
|
838
|
+
underConfirmedSlip(entry, slip);
|
|
839
|
+
if (entry.problems.length === 0 && entry.verified !== 'none') entry.notes.push(DENIED);
|
|
840
|
+
}
|
|
841
|
+
|
|
795
842
|
// The acknowledgements handed over with the person's own copy of a
|
|
796
843
|
// cancellation. Each must be for exactly that cancellation, and signed with
|
|
797
844
|
// the keys of the slip's agent, or of a helper agent under a pass that the
|
|
@@ -943,6 +990,8 @@ function markNotShownEarlier(slip, time, message, late = null, strict = null) {
|
|
|
943
990
|
|
|
944
991
|
// The time of the first counted time-stamp that covers an entry, as a number.
|
|
945
992
|
const stampedTimes = new WeakMap();
|
|
993
|
+
// The line of each seal in the book, exactly as it stands there.
|
|
994
|
+
const sealLines = new WeakMap();
|
|
946
995
|
|
|
947
996
|
// The outside time-stamps beside a record. Each states that the record
|
|
948
997
|
// existed at a time. Returns the earliest time stated by a service the
|
|
@@ -1114,6 +1163,9 @@ function laterThanSeals(seal, pages) {
|
|
|
1114
1163
|
const named = others.find((p) => p.fingerprint === seal.content.previous);
|
|
1115
1164
|
broken = named !== undefined && (!(named.index < seal.index) || earlier.some((p) => p.index > named.index));
|
|
1116
1165
|
}
|
|
1166
|
+
// The seals of one book all give the same book number (section 21.6).
|
|
1167
|
+
const number = (s) => (Object.hasOwn(s.content, 'book') ? s.content.book : 1);
|
|
1168
|
+
if (earlier.some((p) => number(p) !== number(seal))) broken = true;
|
|
1117
1169
|
if (broken) {
|
|
1118
1170
|
seal.problems.push({ code: 'seal-chain-broken', message: 'The seal does not name the seal before it among the pages: a seal is missing, moved or changed.' });
|
|
1119
1171
|
}
|
|
@@ -1194,11 +1246,13 @@ function creditStamp(earlier, stampedAt, time) {
|
|
|
1194
1246
|
* @param {(() => Promise<{size: number, root: string}>)|null} before works out the entries before the seal, where they are at hand
|
|
1195
1247
|
* @param {any[]} entries the entries so far, so that the ones a time-stamp covers can be marked
|
|
1196
1248
|
* @param {boolean} [asPage] whether the seal is a single page of a Show, with nothing to compare it with
|
|
1249
|
+
* @param {boolean} [held] whether the seal is one held elsewhere and handed over beside the book, with no place of its own
|
|
1197
1250
|
*/
|
|
1198
|
-
async function checkSealEntry(entry, value, seals, ids, options, before, entries, asPage = false) {
|
|
1251
|
+
async function checkSealEntry(entry, value, seals, ids, options, before, entries, asPage = false, held = false) {
|
|
1199
1252
|
const without = Array.isArray(options.withoutMethods) ? options.withoutMethods : [];
|
|
1200
1253
|
entry.signatures = [];
|
|
1201
1254
|
entry.stamps = [];
|
|
1255
|
+
entry.witnesses = [];
|
|
1202
1256
|
const parsed = await parseRecord(value.seal, 'seal');
|
|
1203
1257
|
entry.fingerprint = parsed.fingerprint;
|
|
1204
1258
|
validateSealContent(parsed.content);
|
|
@@ -1226,7 +1280,7 @@ async function checkSealEntry(entry, value, seals, ids, options, before, entries
|
|
|
1226
1280
|
const when = parseTime(c.when);
|
|
1227
1281
|
const at = before ? await before() : null;
|
|
1228
1282
|
// A seal sits after exactly as many entries as it covers.
|
|
1229
|
-
if (!at && c.size !== entry.index) {
|
|
1283
|
+
if (!at && !held && c.size !== entry.index) {
|
|
1230
1284
|
entry.problems.push({ code: 'seal-mismatch', message: 'A seal sits after exactly as many entries as it covers, and this one does not.' });
|
|
1231
1285
|
}
|
|
1232
1286
|
if (asPage) {
|
|
@@ -1238,9 +1292,20 @@ async function checkSealEntry(entry, value, seals, ids, options, before, entries
|
|
|
1238
1292
|
if (c.size !== at.size || c.root !== at.root) {
|
|
1239
1293
|
entry.problems.push({ code: 'seal-mismatch', message: 'The seal does not fit the entries before it: an entry was changed, added or removed after the seal was made.' });
|
|
1240
1294
|
}
|
|
1241
|
-
|
|
1295
|
+
// The book's number (section 21.6): absent is the first book. The first seal of a book after the first names,
|
|
1296
|
+
// as the seal before it, the last seal of the book before, which is not in this book: it is not compared here,
|
|
1297
|
+
// and is kept, so that a copy of that seal handed over beside the book can be (checkHeldSeals).
|
|
1298
|
+
const number = Object.hasOwn(c, 'book') ? c.book : 1;
|
|
1299
|
+
if (seals.last === null && number > 1) {
|
|
1300
|
+
seals.firstPrevious = c.previous;
|
|
1301
|
+
entry.notes.push(`This is the first seal of book ${number} in this book. It names, as the seal before it, the last seal of book ${number - 1}, which is not in this book and was not compared. To compare the two, check book ${number - 1} with that seal handed over beside it.`);
|
|
1302
|
+
} else if ((Object.hasOwn(c, 'previous') ? c.previous : null) !== seals.last) {
|
|
1242
1303
|
entry.problems.push({ code: 'seal-chain-broken', message: 'The seal does not name the seal before it: a seal is missing, moved or changed.' });
|
|
1243
1304
|
}
|
|
1305
|
+
if (seals.last !== null && number !== seals.book) {
|
|
1306
|
+
entry.problems.push({ code: 'seal-chain-broken', message: 'The seal gives another book number than the seals before it in this book.' });
|
|
1307
|
+
}
|
|
1308
|
+
if (seals.last === null) seals.book = number;
|
|
1244
1309
|
if (when < seals.lastWhen) {
|
|
1245
1310
|
entry.problems.push({ code: 'time-went-backwards', message: 'This seal is dated before the seal ahead of it.' });
|
|
1246
1311
|
}
|
|
@@ -1268,6 +1333,15 @@ async function checkSealEntry(entry, value, seals, ids, options, before, entries
|
|
|
1268
1333
|
: null;
|
|
1269
1334
|
if (Object.hasOwn(value, 'stamps') && overall(entry.signatures) === 'invalid') decodeStamps(value.stamps);
|
|
1270
1335
|
|
|
1336
|
+
// The witness statements on the seal's own line. (Those handed over beside
|
|
1337
|
+
// a seal held elsewhere are checked apart: see ownChecks.)
|
|
1338
|
+
if (!held && Object.hasOwn(value, 'witnesses')) {
|
|
1339
|
+
const { items, problems } = await checkWitnesses(value.witnesses, parsed, options, without, ids);
|
|
1340
|
+
entry.witnesses = items;
|
|
1341
|
+
entry.problems.push(...problems);
|
|
1342
|
+
noteWitnesses(entry);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1271
1345
|
// A seal counts as signed only if all three of its signatures were
|
|
1272
1346
|
// checked. Only then is a time-stamp on it credited.
|
|
1273
1347
|
const credited = entry.problems.length === 0 && entry.verified === 'all' && stampedTime !== null;
|
|
@@ -1318,9 +1392,178 @@ async function checkSealEntry(entry, value, seals, ids, options, before, entries
|
|
|
1318
1392
|
seals.sealed = { entries: c.size, when: entry.stampedAt, seal: entry.index, by };
|
|
1319
1393
|
}
|
|
1320
1394
|
|
|
1395
|
+
/**
|
|
1396
|
+
* The witness statements beside a seal (format description, section 29.2):
|
|
1397
|
+
* on the seal's line, or handed over with a seal held elsewhere. Each is
|
|
1398
|
+
* checked by itself: its envelope and content, its two signatures against
|
|
1399
|
+
* the keys it gives, and that it speaks of this seal. One that does not
|
|
1400
|
+
* pass is a problem. One from a witness the reader did not name is shown,
|
|
1401
|
+
* and not counted. Whether a statement counts is settled with the answer
|
|
1402
|
+
* (witnessesCounted), once it is known whether its seal passed.
|
|
1403
|
+
*
|
|
1404
|
+
* @param {any} list the statements, as handed over
|
|
1405
|
+
* @param {any} seal the seal they speak of, as parseRecord read it
|
|
1406
|
+
* @param {any} options the checker's options
|
|
1407
|
+
* @param {string[]} without signing methods to treat as not built in
|
|
1408
|
+
* @param {Set<string>|null} ids the unique numbers of the book so far, for statements on a line of the book
|
|
1409
|
+
* @returns {Promise<{items: object[], problems: {code: string, message: string}[]}>}
|
|
1410
|
+
*/
|
|
1411
|
+
async function checkWitnesses(list, seal, options, without, ids) {
|
|
1412
|
+
const items = [];
|
|
1413
|
+
const problems = [];
|
|
1414
|
+
try {
|
|
1415
|
+
f.list(list, 1, MAX_WITNESSES, 'witnesses');
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
return { items, problems: [problemFrom(e)] };
|
|
1418
|
+
}
|
|
1419
|
+
const seen = new Set();
|
|
1420
|
+
// Every place is read by its number, a gap included: a gap is not a statement.
|
|
1421
|
+
for (let i = 0; i < list.length; i++) {
|
|
1422
|
+
const item = { fingerprint: null, witness: null, name: null, when: null, state: 'invalid', trusted: false, counted: false, signatures: [] };
|
|
1423
|
+
items.push(item);
|
|
1424
|
+
try {
|
|
1425
|
+
const parsed = await parseRecord(list[i], 'witness');
|
|
1426
|
+
item.fingerprint = parsed.fingerprint;
|
|
1427
|
+
validateWitnessContent(parsed.content);
|
|
1428
|
+
const c = parsed.content;
|
|
1429
|
+
item.witness = await keySetFingerprint(c.by.keys);
|
|
1430
|
+
item.name = c.by.name;
|
|
1431
|
+
item.when = c.when;
|
|
1432
|
+
if (ids) {
|
|
1433
|
+
if (ids.has(c.id)) throw new Refusal('duplicate-id', 'Another record has the same unique number.');
|
|
1434
|
+
ids.add(c.id);
|
|
1435
|
+
}
|
|
1436
|
+
item.signatures = await checkKeySetSignatures(parsed, c.by.keys, without);
|
|
1437
|
+
if (overall(item.signatures) === 'invalid') throw new Refusal('witness-invalid', 'A signature on the witness statement does not fit the keys it gives.');
|
|
1438
|
+
if (c.seal !== seal.fingerprint || c.size !== seal.content.size || c.root !== seal.content.root) {
|
|
1439
|
+
throw new Refusal('witness-invalid', 'The witness statement speaks of another seal: its seal, its number of entries or its top fingerprint is not this seal\'s.');
|
|
1440
|
+
}
|
|
1441
|
+
if (parseTime(c.when) < parseTime(seal.content.when) - CLOCK_ALLOWANCE_MS) {
|
|
1442
|
+
throw new Refusal('witness-invalid', 'The witness statement is dated more than 300 seconds before the seal it speaks of.');
|
|
1443
|
+
}
|
|
1444
|
+
if (seen.has(item.witness)) throw f.fail('witnesses', 'the same witness signs twice.');
|
|
1445
|
+
seen.add(item.witness);
|
|
1446
|
+
item.state = overall(item.signatures) === 'valid' ? 'valid' : 'unavailable';
|
|
1447
|
+
item.trusted = Array.isArray(options.witnesses) && options.witnesses.includes(item.witness);
|
|
1448
|
+
} catch (e) {
|
|
1449
|
+
item.state = 'invalid';
|
|
1450
|
+
const p = problemFrom(e);
|
|
1451
|
+
problems.push({ code: p.code, message: `Witness statement ${i + 1} beside the seal did not pass its check. ${p.message}` });
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return { items, problems };
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// What a reader is told of the statements beside a seal that passed their check.
|
|
1458
|
+
function noteWitnesses(entry) {
|
|
1459
|
+
entry.witnesses.forEach((w, i) => {
|
|
1460
|
+
if (w.state === 'unavailable') entry.notes.push(`This device could not check witness statement ${i + 1} beside this seal, so it was not counted.`);
|
|
1461
|
+
else if (w.state === 'valid' && !w.trusted) {
|
|
1462
|
+
entry.notes.push(`Witness statement ${i + 1} beside this seal is from a witness you did not name as trusted, so it was not counted. The fingerprint of its key set is ${w.witness}.`);
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// Whether a seal's first two signatures (Ed25519 and ML-DSA-87) were both
|
|
1468
|
+
// checked on this device. A statement counts on those two, as a seal held
|
|
1469
|
+
// elsewhere does: its third (SLH-DSA) may be one this device cannot check.
|
|
1470
|
+
function firstTwoChecked(signatures) {
|
|
1471
|
+
const state = (method) => signatures.find((s) => s.method === method)?.state;
|
|
1472
|
+
return state('Ed25519') === 'valid' && state('ML-DSA-87') === 'valid';
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Why a witness's statement cannot count for this book, or null where it
|
|
1477
|
+
* can (format description, section 29.2). A witness knows a book by the
|
|
1478
|
+
* permission on its first line and the keys of its recorder, and the
|
|
1479
|
+
* recorder chooses both. So a statement counts only where the reader fixed
|
|
1480
|
+
* both: the book's first line is a permission that passed its check, signed
|
|
1481
|
+
* by a passkey the reader named, and the reader named the recorders they
|
|
1482
|
+
* expect (so that a seal by any other recorder is a problem).
|
|
1483
|
+
* @param {any[]} entries the entries of the book, or the pages of a Show
|
|
1484
|
+
* @param {any} options the options the lines were read with
|
|
1485
|
+
* @returns {string|null}
|
|
1486
|
+
*/
|
|
1487
|
+
function unfixedStart(entries, options) {
|
|
1488
|
+
// An option that cannot be read names nothing here; a check that reads it elsewhere reports it.
|
|
1489
|
+
const isList = (name) => {
|
|
1490
|
+
try {
|
|
1491
|
+
return Array.isArray(options[name]);
|
|
1492
|
+
} catch {
|
|
1493
|
+
return false;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
if (!isList('issuerKeys')) return 'you named no passkey you trust, so the permission this book starts from is not fixed';
|
|
1497
|
+
if (!isList('sealKeys')) return 'you named no recorder you expect, so anyone could have sealed it';
|
|
1498
|
+
const first = entries[0];
|
|
1499
|
+
if (first === undefined || first.index !== 0 || first.kind !== 'slip' || first.problems.length > 0 || !first.signature || first.signature.state !== 'valid') {
|
|
1500
|
+
return 'this book does not start with a permission that passed its check here, signed by a passkey you named';
|
|
1501
|
+
}
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* For each seal: why a witness's statement cannot count for it, or null
|
|
1506
|
+
* where it can (format description, section 29.2). Beyond a start the
|
|
1507
|
+
* reader fixed (unfixedStart), every permission among the entries the seal
|
|
1508
|
+
* covers must name the passkey and the agent's key set of the permission
|
|
1509
|
+
* the book starts from. A witness knows a sequence by those two and the
|
|
1510
|
+
* recorder's keys, so a book that starts with one person's permission for
|
|
1511
|
+
* one agent, and holds another's further on, could be the first book of a
|
|
1512
|
+
* sequence the witness never saw, and so a fork of another. Among the pages
|
|
1513
|
+
* of a Show, the pages must hold every entry the seal covers: single pages
|
|
1514
|
+
* cannot show that no other permission stands between them.
|
|
1515
|
+
* @param {any[]} entries the entries of the book, or the pages of a Show
|
|
1516
|
+
* @param {any} options the options the lines were read with
|
|
1517
|
+
* @param {Set<number>|null} [pages] for a Show: the places of its pages
|
|
1518
|
+
* @returns {(seal: any) => string|null}
|
|
1519
|
+
*/
|
|
1520
|
+
function whyFor(entries, options, pages = null) {
|
|
1521
|
+
const start = unfixedStart(entries, options);
|
|
1522
|
+
if (start !== null) return () => start;
|
|
1523
|
+
const first = entries[0];
|
|
1524
|
+
const agent = canonicalJson(first.content.agent.keys);
|
|
1525
|
+
const others = entries.filter((e) => e.kind === 'slip' && (!e.content || e.issuerKey !== first.issuerKey || canonicalJson(e.content.agent.keys) !== agent));
|
|
1526
|
+
return (seal) => {
|
|
1527
|
+
const size = seal.content ? seal.content.size : 0;
|
|
1528
|
+
if (pages !== null) {
|
|
1529
|
+
for (let i = 0; i < size; i++) {
|
|
1530
|
+
if (!pages.has(i)) return 'the pages do not hold every entry this seal covers, and single pages cannot show that every permission among them is from the passkey and for the agent of the permission the book starts from';
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (others.some((e) => e.index < size)) {
|
|
1534
|
+
return 'the entries this seal covers hold a permission from another passkey, or for another agent, than the permission the book starts from, and a witness keeps the books of each person and agent apart';
|
|
1535
|
+
}
|
|
1536
|
+
return null;
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
const unfixedWords = (why) =>
|
|
1540
|
+
`The statements beside this seal from witnesses you trust were not counted: ${why}. A witness knows a book only by the passkey and the agent of the permission it starts from and the keys of its recorder, which the recorder chooses, so a statement counts only for a book that starts with a permission signed by a passkey you named, sealed by a recorder you named.`;
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* The statements beside a seal, each with whether it counts: it passed its
|
|
1544
|
+
* check, its witness is one the reader named, the book's start is one the
|
|
1545
|
+
* reader fixed (unfixedStart), and the seal passed its own check with its
|
|
1546
|
+
* first two signatures checked on this device. A new list: the one the
|
|
1547
|
+
* reader keeps is left as it is. Where a statement from a witness the
|
|
1548
|
+
* reader trusts is not counted because the start is not fixed, the reader
|
|
1549
|
+
* is told why, in "notes", a new list too.
|
|
1550
|
+
* @param {any} seal an entry of kind "seal", or a seal held elsewhere
|
|
1551
|
+
* @param {string|null} why what whyFor gives for the seal
|
|
1552
|
+
* @param {boolean} [sound] whether the seal counts as sound here
|
|
1553
|
+
* @returns {{witnesses: object[], notes: string[]}}
|
|
1554
|
+
*/
|
|
1555
|
+
function witnessesCounted(seal, why, sound = seal.problems.length === 0) {
|
|
1556
|
+
const ok = sound && why === null && firstTwoChecked(seal.signatures ?? []);
|
|
1557
|
+
const witnesses = (seal.witnesses ?? []).map((w) => ({ ...w, counted: ok && w.state === 'valid' && w.trusted }));
|
|
1558
|
+
const notes = why !== null && sound && witnesses.some((w) => w.state === 'valid' && w.trusted) ? [...seal.notes, unfixedWords(why)] : seal.notes;
|
|
1559
|
+
return { witnesses, notes };
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1321
1562
|
// A stub may have with it, on the same line, the service's countersignature
|
|
1322
1563
|
// and the person's approval.
|
|
1323
1564
|
const STUB_ENTRIES = ['stub', 'countersignature,stub', 'approval,stub', 'approval,countersignature,stub'];
|
|
1565
|
+
// A seal may have beside it, on the same line, time-stamps and witness statements.
|
|
1566
|
+
const SEAL_ENTRIES = ['seal', 'seal,stamps', 'seal,witnesses', 'seal,stamps,witnesses'];
|
|
1324
1567
|
|
|
1325
1568
|
function findSlip(slips, fingerprint, whole) {
|
|
1326
1569
|
const slip = slips.get(fingerprint);
|
|
@@ -1587,7 +1830,27 @@ async function checkStubEntry(entry, value, slips, terms, passes, ids, without,
|
|
|
1587
1830
|
entry.breaches.push({ code: 'approval-missing', message: 'The slip asks for the person\'s own approval of this action, and there is none.' });
|
|
1588
1831
|
}
|
|
1589
1832
|
}
|
|
1590
|
-
|
|
1833
|
+
// How far a witness the reader trusts reaches: the seals that passed their
|
|
1834
|
+
// own check, with their first two signatures checked on this device, for
|
|
1835
|
+
// which at least one statement counts (on the seal's line, or handed over
|
|
1836
|
+
// beside the book with a copy of the seal). Null where there is none.
|
|
1837
|
+
function witnessedReach(entries, heldWitnessed) {
|
|
1838
|
+
const seals = [];
|
|
1839
|
+
let last = null;
|
|
1840
|
+
for (const e of entries) {
|
|
1841
|
+
if (e.kind !== 'seal' || !e.content || e.problems.length > 0 || !firstTwoChecked(e.signatures ?? [])) continue;
|
|
1842
|
+
const by = new Set([...(e.witnesses ?? []).filter((w) => w.counted).map((w) => w.witness), ...(heldWitnessed.get(e.index) ?? [])]);
|
|
1843
|
+
if (by.size === 0) continue;
|
|
1844
|
+
seals.push(e.index);
|
|
1845
|
+
last = { seal: e.index, entries: e.content.size, by: [...by].sort() };
|
|
1846
|
+
}
|
|
1847
|
+
return last && { ...last, seals };
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// Beside a Show's witnessed answer: the limit of a Show (format description, section 11).
|
|
1851
|
+
const SHOW_WITNESSED_WORDS = 'These are single pages of a book: they cannot show that a limit over the whole book was kept (section 11), so being witnessed says nothing here about totals.';
|
|
1852
|
+
|
|
1853
|
+
function summarise(entries, bookProblems, sealed = null, held = [], heldWitnessed = new Map(), isShow = false) {
|
|
1591
1854
|
// Methods this device lacked, and whether any signature was never reached
|
|
1592
1855
|
// because its entry, or the whole record, could not be read or used.
|
|
1593
1856
|
const missing = new Set();
|
|
@@ -1604,6 +1867,7 @@ function summarise(entries, bookProblems, sealed = null, held = []) {
|
|
|
1604
1867
|
let vouchingCount = 0;
|
|
1605
1868
|
let passCount = 0;
|
|
1606
1869
|
let acknowledgementCount = 0;
|
|
1870
|
+
let denialCount = 0;
|
|
1607
1871
|
const note = (signatures) => {
|
|
1608
1872
|
if (signatures.length === 0) skipped = true;
|
|
1609
1873
|
for (const s of signatures) if (s.state === 'unavailable') missing.add(s.method);
|
|
@@ -1633,14 +1897,16 @@ function summarise(entries, bookProblems, sealed = null, held = []) {
|
|
|
1633
1897
|
} else if (e.kind === 'seal') {
|
|
1634
1898
|
sealCount++;
|
|
1635
1899
|
note(e.signatures ?? []);
|
|
1900
|
+
for (const w of e.witnesses ?? []) note(w.signatures);
|
|
1636
1901
|
for (const s of e.stamps ?? []) if (s.state === 'unavailable') missing.add('the time-stamp service\'s method');
|
|
1637
1902
|
} else if (e.kind === 'cancellation') {
|
|
1638
1903
|
cancellations++;
|
|
1639
1904
|
if (!e.signature) skipped = true;
|
|
1640
1905
|
else if (e.signature.state === 'unavailable') missing.add(e.signature.alg);
|
|
1641
1906
|
for (const s of e.stamps ?? []) if (s.state === 'unavailable') missing.add('the time-stamp service\'s method');
|
|
1642
|
-
} else if (e.kind === 'vouching' || e.kind === 'withdrawal' || e.kind === 'pass' || e.kind === 'acknowledgement') {
|
|
1907
|
+
} else if (e.kind === 'vouching' || e.kind === 'withdrawal' || e.kind === 'pass' || e.kind === 'acknowledgement' || e.kind === 'denial') {
|
|
1643
1908
|
if (e.kind === 'vouching') vouchingCount++;
|
|
1909
|
+
if (e.kind === 'denial') denialCount++;
|
|
1644
1910
|
if (e.kind === 'pass') passCount++;
|
|
1645
1911
|
if (e.kind === 'acknowledgement') acknowledgementCount++;
|
|
1646
1912
|
note(e.signatures ?? []);
|
|
@@ -1650,10 +1916,14 @@ function summarise(entries, bookProblems, sealed = null, held = []) {
|
|
|
1650
1916
|
}
|
|
1651
1917
|
for (const h of held) {
|
|
1652
1918
|
for (const s of h.stamps) if (s.state === 'unavailable') missing.add('the time-stamp service\'s method');
|
|
1653
|
-
for (const a of h.acknowledgements) note(a.signatures);
|
|
1919
|
+
for (const a of h.acknowledgements ?? []) note(a.signatures);
|
|
1920
|
+
// A seal held elsewhere: each of its signatures that this device could not check, and those of the witness statements handed over with it.
|
|
1921
|
+
for (const s of h.kind === 'seal' ? h.signatures : []) if (s.state === 'unavailable') missing.add(s.method);
|
|
1922
|
+
for (const w of h.kind === 'seal' ? h.witnesses : []) note(w.signatures);
|
|
1654
1923
|
}
|
|
1655
1924
|
const problemFound = bookProblems.length > 0 || entries.some((e) => e.problems.length > 0);
|
|
1656
1925
|
const fullyChecked = !skipped && missing.size === 0;
|
|
1926
|
+
const witnessed = problemFound ? null : witnessedReach(entries, heldWitnessed);
|
|
1657
1927
|
const firstBreach = entries.find((e) => e.breaches.length > 0);
|
|
1658
1928
|
// "Within its slip" is said only if every stub, and every pass, was
|
|
1659
1929
|
// compared with what it rests on, and none was found outside it.
|
|
@@ -1676,12 +1946,15 @@ function summarise(entries, bookProblems, sealed = null, held = []) {
|
|
|
1676
1946
|
seals: sealCount,
|
|
1677
1947
|
cancellations,
|
|
1678
1948
|
acknowledgements: acknowledgementCount,
|
|
1949
|
+
denials: denialCount,
|
|
1679
1950
|
vouchings: vouchingCount,
|
|
1680
1951
|
passes: passCount,
|
|
1681
1952
|
},
|
|
1682
1953
|
// How far an outside time-stamp from a trusted service reaches, if any does.
|
|
1683
1954
|
sealed: problemFound ? null : sealed,
|
|
1684
|
-
|
|
1955
|
+
// How far a witness the reader trusts reaches, if any does.
|
|
1956
|
+
witnessed,
|
|
1957
|
+
limits: [...(sealed && !problemFound ? [...LIMITS.slice(0, -1), stampedWords(sealed)] : LIMITS), ...(witnessed ? [isShow ? `${witnessedWords(witnessed)} ${SHOW_WITNESSED_WORDS}` : witnessedWords(witnessed)] : [])],
|
|
1685
1958
|
};
|
|
1686
1959
|
}
|
|
1687
1960
|
|
|
@@ -1822,6 +2095,237 @@ async function checkHeld(result, reader, options, late, beside) {
|
|
|
1822
2095
|
}
|
|
1823
2096
|
}
|
|
1824
2097
|
|
|
2098
|
+
/**
|
|
2099
|
+
* Seals held elsewhere, handed over beside the book: by the person, an
|
|
2100
|
+
* auditor, or any copy kept out of the recorder's reach.
|
|
2101
|
+
*
|
|
2102
|
+
* Whoever keeps a book can cut off its end, the last seal with it, and what
|
|
2103
|
+
* is left still checks. A seal that someone else holds shows it: a seal at
|
|
2104
|
+
* entry n covers the n entries before it, so the book must hold that very
|
|
2105
|
+
* seal at entry n. A book that ends before it was cut short; a book that
|
|
2106
|
+
* holds another line there was changed, or is not the book the seal is of.
|
|
2107
|
+
*
|
|
2108
|
+
* Each seal handed over is checked as any seal is: its three signatures and
|
|
2109
|
+
* its time-stamps. It counts only where its recorder is one the person
|
|
2110
|
+
* named, and where this device checked its Ed25519 and ML-DSA-87
|
|
2111
|
+
* signatures; its third (SLH-DSA) may be one this device cannot check, as
|
|
2112
|
+
* in the check before acting (guard.js). The book holds it where the seal
|
|
2113
|
+
* at its place has the same content (the same fingerprint); other
|
|
2114
|
+
* signatures over the same content are said, and are not a fault of the
|
|
2115
|
+
* book. A seal handed over says nothing
|
|
2116
|
+
* for the book, only against it, so this widens nothing. One that does not
|
|
2117
|
+
* pass its own check is a problem with the whole check, never set aside:
|
|
2118
|
+
* the person handed it over to have it counted.
|
|
2119
|
+
*
|
|
2120
|
+
* Its time-stamps are checked and shown, but are not used for the entries
|
|
2121
|
+
* of the book. Nothing the reader keeps is changed.
|
|
2122
|
+
*
|
|
2123
|
+
* Witness statements handed over with it (format description, section
|
|
2124
|
+
* 29.2) are checked as statements on a seal's line are. One that does not
|
|
2125
|
+
* pass is a problem with the whole check, and is not used; the seal is used
|
|
2126
|
+
* all the same. A statement that counts, for a seal the book holds at its
|
|
2127
|
+
* place, counts for the book's seal there.
|
|
2128
|
+
*
|
|
2129
|
+
* @returns {Promise<Map<number, string[]>>} for each entry of the book that is a seal, the key set fingerprints
|
|
2130
|
+
* of the witnesses whose statements, handed over beside the book, count for it
|
|
2131
|
+
*/
|
|
2132
|
+
async function checkHeldSeals(result, carried, options) {
|
|
2133
|
+
const witnessed = new Map();
|
|
2134
|
+
const handed = options.seals;
|
|
2135
|
+
if (handed === undefined) return witnessed;
|
|
2136
|
+
// Up to sixty-four, none included: an empty list is none.
|
|
2137
|
+
f.list(handed, 0, MAX_HELD_SEALS, 'seals');
|
|
2138
|
+
const own = await ownChecks(carried, handed, options);
|
|
2139
|
+
const entries = carried.reader.entries;
|
|
2140
|
+
// Whether a statement handed over can count for the book's seals (whyFor).
|
|
2141
|
+
const why = whyFor(entries, carried.options);
|
|
2142
|
+
// This book's number (section 21.6), from its first seal; null where it holds no seal.
|
|
2143
|
+
const bookNumber = carried.reader.seals.book;
|
|
2144
|
+
const firstSeal = entries.find((e) => e.kind === 'seal' && e.content);
|
|
2145
|
+
// The problems, in the order of the seals handed over.
|
|
2146
|
+
const found = [];
|
|
2147
|
+
for (let i = 0; i < own.length; i++) {
|
|
2148
|
+
const n = i + 1;
|
|
2149
|
+
const { sealText, stampsText, witnessProblems } = own[i];
|
|
2150
|
+
const held = structuredClone(own[i].held);
|
|
2151
|
+
result.held.push(held);
|
|
2152
|
+
for (const p of witnessProblems) {
|
|
2153
|
+
found.push({ code: p.code, message: `A witness statement handed over with seal ${n} of those beside the book did not pass its check, so it was not used. The seal itself was not set aside because of it. ${p.message}` });
|
|
2154
|
+
}
|
|
2155
|
+
// Whether each statement handed over with it counts is settled once it is known whether the seal is used.
|
|
2156
|
+
const settle = () => {
|
|
2157
|
+
Object.assign(held, witnessesCounted(held, why(held), held.used));
|
|
2158
|
+
// A statement that counts, for a seal the book holds at its place, counts for the book's seal there.
|
|
2159
|
+
if (held.inBook !== null) {
|
|
2160
|
+
const by = held.witnesses.filter((w) => w.counted).map((w) => w.witness);
|
|
2161
|
+
if (by.length > 0) witnessed.set(held.inBook, [...(witnessed.get(held.inBook) ?? []), ...by]);
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
try {
|
|
2165
|
+
if (held.problems.length > 0) {
|
|
2166
|
+
for (const p of held.problems) found.push({ code: p.code, message: `Seal ${n} of those handed over beside the book did not pass its check, so it was not used. ${p.message}` });
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
// It counts only where the person named its recorder.
|
|
2170
|
+
if (!Array.isArray(options.sealKeys)) {
|
|
2171
|
+
held.notes.push('It was not used: a seal handed over beside the book counts only where you named its recorder as one you expect.');
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
const state = (method) => held.signatures.find((s) => s.method === method)?.state;
|
|
2175
|
+
if (state('Ed25519') !== 'valid' || state('ML-DSA-87') !== 'valid') {
|
|
2176
|
+
held.notes.push('It was not used: a seal handed over beside the book counts only where this device checked its Ed25519 and ML-DSA-87 signatures, and this device could not check both.');
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
if (state(SEAL_THIRD) !== 'valid') {
|
|
2180
|
+
held.notes.push(`This device could not check its third signature (${SEAL_THIRD}). It was used on the strength of its Ed25519 and ML-DSA-87 signatures, which were checked here.`);
|
|
2181
|
+
}
|
|
2182
|
+
held.used = true;
|
|
2183
|
+
// A seal of another book of the same permission is not compared by its place (section 21.6), so it is not
|
|
2184
|
+
// used: it never counts as compared with the book, whatever it shows about the books around this one. The
|
|
2185
|
+
// number is the recorder's own word, and a book cut short could be sealed again under another number.
|
|
2186
|
+
const heldNumber = Object.hasOwn(held.content, 'book') ? held.content.book : 1;
|
|
2187
|
+
if (bookNumber !== null && heldNumber !== bookNumber) {
|
|
2188
|
+
held.used = false;
|
|
2189
|
+
const notCompared = `It was not compared with this book, so it gives no pass: a book is compared only with seals that give its own book number.`;
|
|
2190
|
+
if (heldNumber === bookNumber - 1 && held.fingerprint === carried.reader.seals.firstPrevious) {
|
|
2191
|
+
// The last seal of the book before, which the first seal of this book names.
|
|
2192
|
+
if (held.sealer !== firstSeal.sealer) {
|
|
2193
|
+
found.push({ code: 'seal-chain-broken', message: `Seal ${n} of those handed over beside the book is the seal the first seal of this book names as the last seal of the book before, but it was signed with other keys than this book's first seal.` });
|
|
2194
|
+
} else if (parseTime(held.content.when) > parseTime(firstSeal.content.when)) {
|
|
2195
|
+
found.push({ code: 'time-went-backwards', message: `Seal ${n} of those handed over beside the book is the last seal of the book before, which the first seal of this book names, but it is dated after that first seal.` });
|
|
2196
|
+
} else {
|
|
2197
|
+
held.notes.push(`It is the last seal of book ${heldNumber}, which the first seal of this book (book ${bookNumber}) names as the seal before it: this book follows on from it. It does not show whether book ${heldNumber} was cut short after it: check book ${heldNumber} with it. ${notCompared}`);
|
|
2198
|
+
}
|
|
2199
|
+
} else if (heldNumber === bookNumber - 1) {
|
|
2200
|
+
held.notes.push(`It is a seal of book ${heldNumber}, but the first seal of this book (book ${bookNumber}) names another seal as the last seal of book ${heldNumber}. So this book does not show whether book ${heldNumber} was cut short, nor that this book follows on from that book: check book ${heldNumber} with this seal. ${notCompared}`);
|
|
2201
|
+
} else if (heldNumber === bookNumber + 1 && entries.some((e) => e.kind === 'seal' && e.fingerprint === held.content.previous)) {
|
|
2202
|
+
const named = entries.find((e) => e.kind === 'seal' && e.fingerprint === held.content.previous);
|
|
2203
|
+
held.notes.push(`It is a seal of book ${heldNumber}, the next book, and names the seal at entry ${named.index} of this book as the seal before it: the next book follows on from this one there. It does not show whether this book was cut short. ${notCompared}`);
|
|
2204
|
+
} else if (heldNumber > bookNumber) {
|
|
2205
|
+
held.notes.push(`It is a seal of book ${heldNumber}, a later book than this one (book ${bookNumber}). This book does not show whether the books after it were cut short. Hand over seals of the book being checked. ${notCompared}`);
|
|
2206
|
+
} else {
|
|
2207
|
+
held.notes.push(`It is a seal of book ${heldNumber}, an earlier book than this one (book ${bookNumber}). This book does not show whether that book was cut short. Hand over seals of the book being checked. ${notCompared}`);
|
|
2208
|
+
}
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
const at = held.place;
|
|
2212
|
+
if (at >= entries.length) {
|
|
2213
|
+
found.push({
|
|
2214
|
+
code: 'book-cut-short',
|
|
2215
|
+
message: `Seal ${n} of those handed over beside the book stands at entry ${at}, but the book ends at entry ${entries.length - 1}. The end of the book was cut off after that seal was made.`,
|
|
2216
|
+
});
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
// The book holds this seal at its place where the seal there has the
|
|
2220
|
+
// same content: its fingerprint fixes the number of entries and their
|
|
2221
|
+
// top fingerprint, and so the entries before it.
|
|
2222
|
+
const there = entries[at];
|
|
2223
|
+
if (there.kind !== 'seal' || there.fingerprint !== held.fingerprint) {
|
|
2224
|
+
found.push({
|
|
2225
|
+
code: 'seal-not-in-book',
|
|
2226
|
+
message: `Seal ${n} of those handed over beside the book stands at entry ${at}, but the book holds another line there. The book was changed after that seal was made, or the seal is of another book.`,
|
|
2227
|
+
});
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
held.inBook = at;
|
|
2231
|
+
if (there.problems.length > 0) held.notes.push(`The book holds this seal at entry ${at}, but that entry did not pass its check.`);
|
|
2232
|
+
// The same content with other signatures, or with other time-stamps
|
|
2233
|
+
// beside it, is not a fault of the book: it is said.
|
|
2234
|
+
const line = JSON.parse(sealLines.get(there));
|
|
2235
|
+
if (canonicalJson(line.seal) !== sealText) {
|
|
2236
|
+
held.notes.push(
|
|
2237
|
+
`The book holds this seal at entry ${at} with the same content, but with other signatures than this copy. The same content fixes the same entries, so this is not a fault of the book. Only someone who holds the recorder's keys can sign a seal again.`,
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
if ((Object.hasOwn(line, 'stamps') ? canonicalJson(line.stamps) : null) !== stampsText) {
|
|
2241
|
+
held.notes.push(
|
|
2242
|
+
`The book holds this seal at entry ${at}, with other time-stamps beside it than this copy. Time-stamps stand outside a seal's signatures, so either side may have added one. This copy's own are shown here, and are not used for the entries of the book.`,
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
} finally {
|
|
2246
|
+
settle();
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
result.problems.push(...found);
|
|
2250
|
+
return witnessed;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* The check of each seal handed over that rests on the seal alone: its
|
|
2255
|
+
* signatures, its recorder and its time-stamps. It does not depend on the
|
|
2256
|
+
* book, so a checker that is carried on, and the stub writer, make it once
|
|
2257
|
+
* and keep it for as long as they are given the same seals with the same
|
|
2258
|
+
* options; each answer starts from a copy of it.
|
|
2259
|
+
*/
|
|
2260
|
+
async function ownChecks(carried, handed, options) {
|
|
2261
|
+
const key = [handed, options.sealKeys, options.stampServices, options.blocks, options.witnesses, options.withoutMethods];
|
|
2262
|
+
const kept = carried.heldSeals;
|
|
2263
|
+
if (kept && kept.key.every((value, i) => value === key[i])) return kept.items;
|
|
2264
|
+
const seen = new Set();
|
|
2265
|
+
const items = [];
|
|
2266
|
+
for (let i = 0; i < handed.length; i++) {
|
|
2267
|
+
const value = handed[i];
|
|
2268
|
+
const held = { kind: 'seal', fingerprint: null, sealer: null, place: null, inBook: null, used: false, verified: 'none', problems: [], notes: [], signatures: [], stamps: [], witnesses: [] };
|
|
2269
|
+
let sealText = null;
|
|
2270
|
+
let stampsText = null;
|
|
2271
|
+
// The problems with the witness statements handed over with it: reported, without setting the seal aside.
|
|
2272
|
+
let witnessProblems = [];
|
|
2273
|
+
try {
|
|
2274
|
+
f.members(value, ['seal'], ['stamps', 'witnesses'], `seals[${i}]`);
|
|
2275
|
+
const parsed = await parseRecord(value.seal, 'seal');
|
|
2276
|
+
held.fingerprint = parsed.fingerprint;
|
|
2277
|
+
if (seen.has(parsed.fingerprint)) throw f.fail(`seals[${i}]`, 'the same seal is handed over twice.');
|
|
2278
|
+
seen.add(parsed.fingerprint);
|
|
2279
|
+
// Checked as a seal that stands alone: its own unique number is the
|
|
2280
|
+
// book's seal's own, and its place is the one it states.
|
|
2281
|
+
const fresh = { count: 0, last: null, lastWhen: -Infinity, lastStampTime: -Infinity, latest: -Infinity, next: 0, sealed: null };
|
|
2282
|
+
await checkSealEntry(held, value, fresh, new Set(), options, null, [], false, true);
|
|
2283
|
+
sealText = canonicalJson(value.seal);
|
|
2284
|
+
stampsText = Object.hasOwn(value, 'stamps') ? canonicalJson(value.stamps) : null;
|
|
2285
|
+
// Witness statements that arrived after the seal was written into the book, handed over with it.
|
|
2286
|
+
// They are not entries of the book, so their unique numbers are not held against its own.
|
|
2287
|
+
if (held.problems.length === 0 && Object.hasOwn(value, 'witnesses')) {
|
|
2288
|
+
const without = Array.isArray(options.withoutMethods) ? options.withoutMethods : [];
|
|
2289
|
+
const { items: statements, problems } = await checkWitnesses(value.witnesses, parsed, options, without, null);
|
|
2290
|
+
held.witnesses = statements;
|
|
2291
|
+
witnessProblems = problems;
|
|
2292
|
+
noteWitnesses(held);
|
|
2293
|
+
if (problems.length > 0) held.notes.push('A witness statement handed over with it did not pass its check, so that statement was not used. See the problems at the top.');
|
|
2294
|
+
}
|
|
2295
|
+
} catch (e) {
|
|
2296
|
+
held.problems.push(problemFrom(e));
|
|
2297
|
+
}
|
|
2298
|
+
if (held.content) held.place = held.content.size;
|
|
2299
|
+
items.push({ held, sealText, stampsText, witnessProblems });
|
|
2300
|
+
}
|
|
2301
|
+
carried.heldSeals = { key, items };
|
|
2302
|
+
return items;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
/**
|
|
2306
|
+
* Check a seal record by itself, as a seal held elsewhere is checked: its
|
|
2307
|
+
* envelope, its content and its three signatures against the keys it
|
|
2308
|
+
* gives. Nothing is said about where it stands in a book. For a witness
|
|
2309
|
+
* (witnessing.js). Not part of the public interface.
|
|
2310
|
+
* @param {any} record the seal record
|
|
2311
|
+
* @param {string[]} [withoutMethods] signing methods to treat as not built in
|
|
2312
|
+
* @returns {Promise<{fingerprint: string|null, content?: any, signatures: object[], problems: object[]}>}
|
|
2313
|
+
*/
|
|
2314
|
+
export async function checkSealAlone(record, withoutMethods = []) {
|
|
2315
|
+
const entry = { kind: 'seal', index: null, fingerprint: null, verified: 'none', problems: [], breaches: [], notes: [], signatures: [], stamps: [], witnesses: [] };
|
|
2316
|
+
try {
|
|
2317
|
+
const fresh = { count: 0, last: null, lastWhen: -Infinity, lastStampTime: -Infinity, latest: -Infinity, next: 0, sealed: null };
|
|
2318
|
+
await checkSealEntry(entry, { seal: record }, fresh, new Set(), { withoutMethods }, null, [], false, true);
|
|
2319
|
+
} catch (e) {
|
|
2320
|
+
entry.problems.push(problemFrom(e));
|
|
2321
|
+
}
|
|
2322
|
+
return entry;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// The third signing method of a seal, which some devices (the Python
|
|
2326
|
+
// version, and every browser when tested) do not have.
|
|
2327
|
+
const SEAL_THIRD = 'SLH-DSA-SHA2-256s';
|
|
2328
|
+
|
|
1825
2329
|
// The blocks a person trusts are named by their fingerprints: 64 hex
|
|
1826
2330
|
// characters each. Anything else is refused, never set aside.
|
|
1827
2331
|
function checkBlocksOption(options) {
|
|
@@ -1855,6 +2359,8 @@ function compareTrusted(result, options) {
|
|
|
1855
2359
|
* @param {string} text the book
|
|
1856
2360
|
* @param {object} [options]
|
|
1857
2361
|
* @param {string[]} [options.issuerKeys] thumbprints of the issuer keys the checker expects
|
|
2362
|
+
* @param {string[]} [options.issuerOrigins] the addresses of the pages at which the checker expects every slip to have been signed,
|
|
2363
|
+
* each in the form of section 4.1 (https://, a website name in lower case, a port only if needed). At most 64
|
|
1858
2364
|
* @param {string} [options.expectedRoot] the top fingerprint the checker already trusts
|
|
1859
2365
|
* @param {number} [options.expectedSize] the number of entries the checker already trusts
|
|
1860
2366
|
* @param {string[]} [options.stampServices] the certificate fingerprints of the time-stamp services the checker trusts
|
|
@@ -1864,6 +2370,9 @@ function compareTrusted(result, options) {
|
|
|
1864
2370
|
* @param {Record<string, string[]>} [options.disclosures] for covered fields: the disclosures handed over, by the fingerprint of the slip they belong to
|
|
1865
2371
|
* @param {object[]} [options.cancellations] the person's own copies of cancellations, each as the entry it would be in a book:
|
|
1866
2372
|
* {cancellation, stamps?}. At most 16. They count even where the book leaves them out
|
|
2373
|
+
* @param {object[]} [options.seals] seals held elsewhere (by the person, an auditor, a copy kept apart), each as the
|
|
2374
|
+
* entry it is in a book: {seal, stamps?, witnesses?}. At most 64. The book must hold each at the place it states
|
|
2375
|
+
* @param {string[]} [options.witnesses] the key set fingerprints of the witnesses the checker trusts (section 29)
|
|
1867
2376
|
* @param {string[]} [options.withoutMethods] signing methods to treat as not built into this device
|
|
1868
2377
|
* @returns {Promise<object>} see the README, "What the checker returns".
|
|
1869
2378
|
* The content of an entry that has problems, or whose "verified" is
|
|
@@ -1909,10 +2418,13 @@ export function startCarried(options = {}) {
|
|
|
1909
2418
|
closed: true,
|
|
1910
2419
|
// A problem with the book as a whole, which no later line can mend: an option that cannot be used, or too many entries.
|
|
1911
2420
|
failure: null,
|
|
2421
|
+
// The check of the seals handed over that rests on the seals alone, made once (ownChecks).
|
|
2422
|
+
heldSeals: null,
|
|
1912
2423
|
};
|
|
1913
2424
|
try {
|
|
1914
2425
|
lookIntoOptions(options);
|
|
1915
2426
|
checkBlocksOption(options);
|
|
2427
|
+
checkOriginsOption(options);
|
|
1916
2428
|
// Every line is read with this option, so one that cannot be read is a
|
|
1917
2429
|
// problem with the whole check, found before any line is read.
|
|
1918
2430
|
Array.isArray(options.withoutMethods);
|
|
@@ -1980,6 +2492,8 @@ export async function answerOf(carried, options = carried.options) {
|
|
|
1980
2492
|
let sealed = null;
|
|
1981
2493
|
let late = new Map();
|
|
1982
2494
|
const beside = new Set();
|
|
2495
|
+
// For each seal of the book, the witnesses whose statements, handed over beside it, count for it.
|
|
2496
|
+
let heldWitnessed = new Map();
|
|
1983
2497
|
if (options === null || typeof options !== 'object') options = {};
|
|
1984
2498
|
try {
|
|
1985
2499
|
if (!carried.any) throw new Refusal('not-json', 'There is no record to check.');
|
|
@@ -1995,13 +2509,16 @@ export async function answerOf(carried, options = carried.options) {
|
|
|
1995
2509
|
result.problems.push(...reader.handedProblems);
|
|
1996
2510
|
unusedDisclosures(result, options);
|
|
1997
2511
|
await checkHeld(result, reader, options, late, beside);
|
|
2512
|
+
heldWitnessed = await checkHeldSeals(result, carried, options);
|
|
1998
2513
|
compareTrusted(result, options);
|
|
1999
2514
|
} catch (e) {
|
|
2000
2515
|
result.problems.push(problemFrom(e));
|
|
2001
2516
|
}
|
|
2002
2517
|
const read = result.entries;
|
|
2003
|
-
|
|
2004
|
-
|
|
2518
|
+
// Whether a witness's statement can count for each seal (whyFor).
|
|
2519
|
+
const why = whyFor(read, carried.options);
|
|
2520
|
+
result.entries = read.map((e) => answered(e, late, read, why));
|
|
2521
|
+
result.summary = summarise(result.entries, result.problems, sealed, result.held, heldWitnessed);
|
|
2005
2522
|
return { result, state, beside };
|
|
2006
2523
|
}
|
|
2007
2524
|
|
|
@@ -2111,6 +2628,7 @@ export async function makeShow(text, indexes, options = {}) {
|
|
|
2111
2628
|
* @param {string[]} [options.issuerKeys]
|
|
2112
2629
|
* @param {string[]} [options.stampServices]
|
|
2113
2630
|
* @param {string[]} [options.sealKeys]
|
|
2631
|
+
* @param {string[]} [options.witnesses]
|
|
2114
2632
|
* @param {string[]} [options.withoutMethods]
|
|
2115
2633
|
* @returns {Promise<object>} the same shape as checkBook returns. A seal that comes with the Show is the last entry.
|
|
2116
2634
|
*/
|
|
@@ -2129,6 +2647,7 @@ export async function checkShow(show, options = {}) {
|
|
|
2129
2647
|
}
|
|
2130
2648
|
lookIntoOptions(options);
|
|
2131
2649
|
checkBlocksOption(options);
|
|
2650
|
+
checkOriginsOption(options);
|
|
2132
2651
|
f.members(show, ['pages', 'root', 'size', 'type'], ['seal'], 'show');
|
|
2133
2652
|
if (show.type !== 'provared.show.v0') throw f.fail('show.type', 'must be provared.show.v0.');
|
|
2134
2653
|
f.wholeNumber(show.size, 'show.size');
|
|
@@ -2179,7 +2698,7 @@ export async function checkShow(show, options = {}) {
|
|
|
2179
2698
|
const entry = { index: show.size, kind: 'seal', fingerprint: null, verified: 'none', problems: [], breaches: [], notes: [] };
|
|
2180
2699
|
result.entries.push(entry);
|
|
2181
2700
|
try {
|
|
2182
|
-
f.members(show.seal, ['seal'], ['stamps'], 'show.seal');
|
|
2701
|
+
f.members(show.seal, ['seal'], ['stamps', 'witnesses'], 'show.seal');
|
|
2183
2702
|
// It is held to the pages as a seal in a whole book is held to the
|
|
2184
2703
|
// entries before it: no unique number twice, and no block
|
|
2185
2704
|
// time-stamp counted that states a time before a service's
|
|
@@ -2216,6 +2735,9 @@ export async function checkShow(show, options = {}) {
|
|
|
2216
2735
|
if (options.cancellations !== undefined && !(Array.isArray(options.cancellations) && options.cancellations.length === 0)) {
|
|
2217
2736
|
result.notes.push('Cancellations were handed over beside this Show. They were not used: single pages cannot show which stubs existed before a cancellation. Check the whole book with them.');
|
|
2218
2737
|
}
|
|
2738
|
+
if (options.seals !== undefined && !(Array.isArray(options.seals) && options.seals.length === 0)) {
|
|
2739
|
+
result.notes.push('Seals held elsewhere were handed over beside this Show. They were not used: single pages cannot show what the book holds at the place each seal states, nor that the book goes on to it. Check the whole book with them.');
|
|
2740
|
+
}
|
|
2219
2741
|
} catch (e) {
|
|
2220
2742
|
result.problems.push(problemFrom(e));
|
|
2221
2743
|
}
|
|
@@ -2227,6 +2749,9 @@ export async function checkShow(show, options = {}) {
|
|
|
2227
2749
|
}
|
|
2228
2750
|
// A seal or a cancellation that did not pass its check shows no time of its own.
|
|
2229
2751
|
for (const e of result.entries) if (e.problems.length > 0) delete e.stampedAt;
|
|
2230
|
-
|
|
2752
|
+
// Whether each witness statement counts, now that each seal's own check is complete.
|
|
2753
|
+
const why = whyFor(result.entries, options, new Set(result.entries.map((e) => e.index)));
|
|
2754
|
+
for (const e of result.entries) if (e.kind === 'seal' && Array.isArray(e.witnesses)) Object.assign(e, witnessesCounted(e, why(e)));
|
|
2755
|
+
result.summary = summarise(result.entries, result.problems, sealed, [], new Map(), true);
|
|
2231
2756
|
return result;
|
|
2232
2757
|
}
|