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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// A witness: a party, with a key set of its own, that keeps, for each
|
|
2
|
+
// person, agent and recorder, the latest seal it has signed for, and signs
|
|
3
|
+
// a statement for a new seal only if the new seal extends that one: in the
|
|
4
|
+
// same book, or as the first seal of the next book.
|
|
5
|
+
// Format description, section 29.3.
|
|
6
|
+
//
|
|
7
|
+
// This is the part of a witness that decides and signs. Keeping its state
|
|
8
|
+
// on a disk, and taking seals over a network, are the business of whoever
|
|
9
|
+
// runs it. Its state is handed back with every statement, and MUST be saved
|
|
10
|
+
// before the statement is given out: a witness that loses its state can be
|
|
11
|
+
// made to sign a fork.
|
|
12
|
+
//
|
|
13
|
+
// A witness checks a seal's three signatures, so it needs SLH-DSA. Where
|
|
14
|
+
// the device does not have it built in, it signs nothing.
|
|
15
|
+
|
|
16
|
+
import { checkSealAlone } from './book.js';
|
|
17
|
+
import { Refusal, canonicalJson, fingerprint, fromBase64url, fromUtf8, parseCanonical, parseTime, problemFrom, utf8 } from './encoding.js';
|
|
18
|
+
import * as f from './fields.js';
|
|
19
|
+
import { encodeContent, parseRecord, signingInput } from './jws.js';
|
|
20
|
+
import { PASSKEY_METHODS, checkKey, checkKeySet, checkSealKeySet, thumbprint } from './keys.js';
|
|
21
|
+
import { CLOCK_ALLOWANCE_MS, keySetFingerprint } from './seal.js';
|
|
22
|
+
import { verifySignature } from './signatures.js';
|
|
23
|
+
import { consistencyProof, inclusionPath, verifyConsistency, verifyInclusion } from './tree.js';
|
|
24
|
+
import { MAX_WITNESSES, validateWitnessContent, writeWitness } from './witness.js';
|
|
25
|
+
|
|
26
|
+
/** The "type" of a witness's saved state. */
|
|
27
|
+
export const WITNESS_STATE_TYPE = 'provared.witness-state.v0';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How a witness knows which books are one sequence: by the fingerprint of
|
|
31
|
+
* the recorder's key set, with the passkey and the agent that the
|
|
32
|
+
* permission (the slip) on the book's first line names: the thumbprint of
|
|
33
|
+
* its passkey key (RFC 7638) and the fingerprint of the agent's key set.
|
|
34
|
+
* So every permission one person gives one agent, under one recorder, is
|
|
35
|
+
* one sequence, renewals included, and a book that starts with a later
|
|
36
|
+
* permission must follow on from the seal the witness holds, as any new
|
|
37
|
+
* book (section 21.6). This is not written into any record.
|
|
38
|
+
* @param {object[]} keys the recorder's key set, as a seal gives it
|
|
39
|
+
* @param {Uint8Array} firstLine the bytes of the book's first line, without its line feed: a permission
|
|
40
|
+
* @returns {Promise<string>} a fingerprint, in base64url
|
|
41
|
+
*/
|
|
42
|
+
export async function bookIdentity(keys, firstLine) {
|
|
43
|
+
checkSealKeySet(keys, 'keys');
|
|
44
|
+
return identityOf(await keySetFingerprint(keys), await sequenceOf(firstLine));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The fingerprint of {"agent": the agent's key set fingerprint, "passkey": the passkey's thumbprint, "recorder": the
|
|
48
|
+
// recorder's key set fingerprint}, written in the canonical form.
|
|
49
|
+
const identityOf = async (recorder, { agent, passkey }) => fingerprint(encodeContent({ agent, passkey, recorder }).contentBytes);
|
|
50
|
+
|
|
51
|
+
// The passkey and the agent that the permission on a book's first line names. The passkey's signature is not
|
|
52
|
+
// checked here: a witness knows a sequence by them, and a reader counts a statement only where the permission
|
|
53
|
+
// passed and its passkey is one they named. A central sealer reads a book's first line the same way (sealer.js).
|
|
54
|
+
export async function sequenceOf(firstLine) {
|
|
55
|
+
if (!(firstLine instanceof Uint8Array) || firstLine.length === 0) throw f.fail('firstLine', 'must be the bytes of the book\'s first line.');
|
|
56
|
+
if (firstLine[firstLine.length - 1] === 0x0a || firstLine[firstLine.length - 1] === 0x0d) {
|
|
57
|
+
throw f.fail('firstLine', 'must be the book\'s first line without its line feed.');
|
|
58
|
+
}
|
|
59
|
+
let value;
|
|
60
|
+
try {
|
|
61
|
+
value = parseCanonical(fromUtf8(firstLine, 'line-not-canonical'), 'line-not-canonical');
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Refusal('slip-missing', 'The book\'s first line is not a line of a book in its one written form, so it is not a permission. A book starts with its permission.');
|
|
64
|
+
}
|
|
65
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== 1 || !Object.hasOwn(value, 'slip')) {
|
|
66
|
+
throw new Refusal('slip-missing', 'The book\'s first line is not a permission, an entry that holds only a slip. A book starts with its permission.');
|
|
67
|
+
}
|
|
68
|
+
const c = (await parseRecord(value.slip, 'slip')).content;
|
|
69
|
+
try {
|
|
70
|
+
checkKey(c.issuer.key, PASSKEY_METHODS, 'issuer.key');
|
|
71
|
+
checkKeySet(c.agent.keys, 'agent.keys');
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Refusal('slip-unusable', 'The permission on the book\'s first line does not name a passkey key and an agent\'s key set in the forms a slip gives them.');
|
|
74
|
+
}
|
|
75
|
+
return { passkey: await thumbprint(c.issuer.key), agent: await keySetFingerprint(c.agent.keys) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A seal's book number (section 21.6): absent is the first book.
|
|
79
|
+
const numberOf = (content) => (Object.hasOwn(content, 'book') ? content.book : 1);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What a witness needs to be sent with a seal: where it holds an earlier
|
|
83
|
+
* seal of the same book, the consistency proof from that seal's number of
|
|
84
|
+
* entries to the new seal's (RFC 9162, section 2.1.4.1); where it holds
|
|
85
|
+
* none, or the new seal is the first of the next book, the audit path of
|
|
86
|
+
* the book's first line in the new seal's tree (section 2.1.3), which ties
|
|
87
|
+
* the first line to the seal.
|
|
88
|
+
* @param {string} book the book, as text
|
|
89
|
+
* @param {number} size the new seal's number of entries
|
|
90
|
+
* @param {number|null} [from] the number of entries of the seal the witness holds of this book, or null for none
|
|
91
|
+
* (and for the first seal of the next book)
|
|
92
|
+
* @returns {Promise<Uint8Array[]>}
|
|
93
|
+
*/
|
|
94
|
+
export async function witnessProof(book, size, from = null) {
|
|
95
|
+
const lines = book.split('\n');
|
|
96
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
97
|
+
if (!Number.isSafeInteger(size) || size < 1 || size > lines.length) throw new RangeError('the book has no such number of entries');
|
|
98
|
+
const leaves = lines.slice(0, size).map(utf8);
|
|
99
|
+
if (from === null || from === undefined) return inclusionPath(leaves, 0);
|
|
100
|
+
return consistencyProof(leaves, from);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const refused = (code, message) => ({ refused: { code, message } });
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Open a witness.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} o
|
|
109
|
+
* @param {{keys: object[], name: string}} o.by the witness's public key set (Ed25519, then ML-DSA-87) and its name (a label)
|
|
110
|
+
* @param {any[]} o.signers the witness's private keys, or signers (signatures.js), in the order of the key set
|
|
111
|
+
* @param {object} [o.state] the state a witness handed back before, as saved. A witness opened with a state signs
|
|
112
|
+
* nothing until it has been brought up to date (update) and told so (ready)
|
|
113
|
+
* @param {() => number} [o.clock] the time now, in milliseconds since 1970; Date.now by default
|
|
114
|
+
* @param {string[]} [o.withoutMethods] signing methods to treat as not built into this device
|
|
115
|
+
* @returns {Promise<object>} the witness: witness, update, ready, latest, state
|
|
116
|
+
*/
|
|
117
|
+
export async function openWitness({ by, signers, state, clock = Date.now, withoutMethods = [] } = {}) {
|
|
118
|
+
f.members(by, ['keys', 'name'], [], 'by');
|
|
119
|
+
f.label(by.name, 'by.name');
|
|
120
|
+
checkKeySet(by.keys, 'by.keys');
|
|
121
|
+
if (!Array.isArray(signers) || signers.length !== 2) throw f.fail('signers', 'must be the witness\'s two private keys, or signers: Ed25519, then ML-DSA-87.');
|
|
122
|
+
if (typeof clock !== 'function') throw f.fail('clock', 'must be a function that gives the time now.');
|
|
123
|
+
if (!Array.isArray(withoutMethods)) throw f.fail('withoutMethods', 'must be a list of signing methods.');
|
|
124
|
+
// Copied once, so that nothing done to them afterwards changes the witness.
|
|
125
|
+
const me = JSON.parse(canonicalJson({ keys: by.keys, name: by.name }));
|
|
126
|
+
const mine = canonicalJson(me.keys);
|
|
127
|
+
const meFingerprint = await keySetFingerprint(me.keys);
|
|
128
|
+
const keys = signers.slice();
|
|
129
|
+
const without = withoutMethods.slice();
|
|
130
|
+
|
|
131
|
+
/** @type {Map<string, {sequence: {passkey: string, agent: string}, seal: object, statement: object, fingerprint: string, content: any}>} */
|
|
132
|
+
const books = new Map();
|
|
133
|
+
let ready = true;
|
|
134
|
+
if (state !== undefined) {
|
|
135
|
+
await restore(state);
|
|
136
|
+
ready = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The calls wait for one another, so that two seals of one book are never weighed at the same time.
|
|
140
|
+
let turn = Promise.resolve();
|
|
141
|
+
const inTurn = (work) => {
|
|
142
|
+
const run = turn.then(work);
|
|
143
|
+
turn = run.catch(() => {});
|
|
144
|
+
return run;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function snapshot() {
|
|
148
|
+
return {
|
|
149
|
+
type: WITNESS_STATE_TYPE,
|
|
150
|
+
by: meFingerprint,
|
|
151
|
+
books: [...books.values()].map((b) => ({ passkey: b.sequence.passkey, agent: b.sequence.agent, seal: structuredClone(b.seal), statement: structuredClone(b.statement) })),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A statement by this witness, for this seal, that checks: or why not.
|
|
156
|
+
async function ownStatement(record, seal) {
|
|
157
|
+
const parsed = await parseRecord(record, 'witness');
|
|
158
|
+
validateWitnessContent(parsed.content);
|
|
159
|
+
const c = parsed.content;
|
|
160
|
+
if (canonicalJson(c.by.keys) !== mine) throw new Refusal('witness-invalid', 'The statement is not signed with this witness\'s keys.');
|
|
161
|
+
if (c.seal !== seal.fingerprint || c.size !== seal.content.size || c.root !== seal.content.root) {
|
|
162
|
+
throw new Refusal('witness-invalid', 'The statement speaks of another seal.');
|
|
163
|
+
}
|
|
164
|
+
for (let i = 0; i < parsed.signatures.length; i++) {
|
|
165
|
+
const s = parsed.signatures[i];
|
|
166
|
+
const checked = await verifySignature(c.by.keys[i], s.signature, signingInput(s.protectedB64, parsed.payloadB64), without);
|
|
167
|
+
if (checked !== 'valid') throw new Refusal('witness-invalid', 'A signature on the statement does not check on this device.');
|
|
168
|
+
}
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function restore(saved) {
|
|
173
|
+
if (saved === null || typeof saved !== 'object' || Array.isArray(saved)) throw f.fail('state', 'must be a state a witness handed back.');
|
|
174
|
+
f.members(saved, ['books', 'by', 'type'], [], 'state');
|
|
175
|
+
if (saved.type !== WITNESS_STATE_TYPE) throw f.fail('state.type', `must be ${WITNESS_STATE_TYPE}.`);
|
|
176
|
+
if (saved.by !== meFingerprint) throw f.fail('state.by', 'is the state of another witness: its key set is not this witness\'s.');
|
|
177
|
+
if (!Array.isArray(saved.books)) throw f.fail('state.books', 'must be a list.');
|
|
178
|
+
for (let i = 0; i < saved.books.length; i++) {
|
|
179
|
+
const b = saved.books[i];
|
|
180
|
+
f.members(b, ['agent', 'passkey', 'seal', 'statement'], [], `state.books[${i}]`);
|
|
181
|
+
f.fingerprintText(b.passkey, `state.books[${i}].passkey`);
|
|
182
|
+
f.fingerprintText(b.agent, `state.books[${i}].agent`);
|
|
183
|
+
const sequence = { passkey: b.passkey, agent: b.agent };
|
|
184
|
+
const seal = await checkSealAlone(b.seal, without);
|
|
185
|
+
if (seal.problems.length > 0) throw new Refusal(seal.problems[0].code, `The seal of book ${i + 1} in the state does not pass its check. ${seal.problems[0].message}`);
|
|
186
|
+
await ownStatement(b.statement, seal);
|
|
187
|
+
const identity = await identityOf(await keySetFingerprint(seal.content.by.keys), sequence);
|
|
188
|
+
if (books.has(identity)) throw f.fail(`state.books[${i}]`, 'the same person, agent and recorder are given twice.');
|
|
189
|
+
books.set(identity, { sequence, seal: structuredClone(b.seal), statement: structuredClone(b.statement), fingerprint: seal.fingerprint, content: seal.content });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Everything that must hold before this witness takes a seal of a book,
|
|
194
|
+
// whether it is to sign for it (witness) or has signed for it before
|
|
195
|
+
// (update). Returns what it found, or a refusal.
|
|
196
|
+
async function weigh(entry, proof, firstLine, signing) {
|
|
197
|
+
try {
|
|
198
|
+
f.members(entry, ['seal'], ['stamps', 'witnesses'], 'entry');
|
|
199
|
+
} catch (e) {
|
|
200
|
+
return { refused: problemFrom(e) };
|
|
201
|
+
}
|
|
202
|
+
if (!(firstLine instanceof Uint8Array) || firstLine.length === 0) return refused('bad-field', 'The book\'s first line must be given as its bytes, without its line feed.');
|
|
203
|
+
if (!Array.isArray(proof)) return refused('proof-invalid', 'The proof must be a list of fingerprints, each 32 bytes.');
|
|
204
|
+
const path = [];
|
|
205
|
+
for (let i = 0; i < proof.length; i++) {
|
|
206
|
+
if (!(proof[i] instanceof Uint8Array) || proof[i].length !== 32) return refused('proof-invalid', 'The proof must be a list of fingerprints, each 32 bytes.');
|
|
207
|
+
path.push(proof[i]);
|
|
208
|
+
}
|
|
209
|
+
const seal = await checkSealAlone(entry.seal, without);
|
|
210
|
+
if (seal.problems.length > 0) return refused(seal.problems[0].code, `The seal did not pass its check. ${seal.problems[0].message}`);
|
|
211
|
+
if (seal.signatures.some((s) => s.state !== 'valid')) {
|
|
212
|
+
return refused('check-failed', 'A witness signs only for a seal whose three signatures it checked, and this device could not check them all.');
|
|
213
|
+
}
|
|
214
|
+
const c = seal.content;
|
|
215
|
+
let sequence;
|
|
216
|
+
try {
|
|
217
|
+
sequence = await sequenceOf(firstLine);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return { refused: problemFrom(e) };
|
|
220
|
+
}
|
|
221
|
+
const identity = await identityOf(await keySetFingerprint(c.by.keys), sequence);
|
|
222
|
+
const held = books.get(identity) ?? null;
|
|
223
|
+
if (held && held.fingerprint === seal.fingerprint) return { seal, identity, sequence, held, same: true };
|
|
224
|
+
const root = fromBase64url(c.root);
|
|
225
|
+
const firstLineTied = () => verifyInclusion(0, c.size, firstLine, path, root);
|
|
226
|
+
const number = numberOf(c);
|
|
227
|
+
if (held) {
|
|
228
|
+
const heldNumber = numberOf(held.content);
|
|
229
|
+
if (number < heldNumber) {
|
|
230
|
+
return refused('seal-mismatch', `The seal is of book ${number}, and this witness holds a seal of book ${heldNumber} of this person, agent and recorder. It signs for no seal of an earlier book again.`);
|
|
231
|
+
}
|
|
232
|
+
if (number > heldNumber + 1) {
|
|
233
|
+
return refused('seal-chain-broken', `The seal is of book ${number}, and the latest seal this witness holds of this person, agent and recorder is of book ${heldNumber}: the books between were not shown to it. Hand it their seals first, in their order.`);
|
|
234
|
+
}
|
|
235
|
+
if (number === heldNumber) {
|
|
236
|
+
if (c.size <= held.content.size) {
|
|
237
|
+
return refused('seal-mismatch', `This witness holds a seal of this book with ${held.content.size} entries. It signs for no other seal of this book that does not extend that one.`);
|
|
238
|
+
}
|
|
239
|
+
if (signing && (Object.hasOwn(c, 'previous') ? c.previous : null) !== held.fingerprint) {
|
|
240
|
+
return refused('seal-chain-broken', 'The seal does not name, as the seal before it, the latest seal of this book this witness signed for. Hand it the seals in between first, in their order.');
|
|
241
|
+
}
|
|
242
|
+
if (!(await verifyConsistency(held.content.size, c.size, fromBase64url(held.content.root), root, path))) {
|
|
243
|
+
return refused('proof-invalid', 'The consistency proof does not show that the seal extends the latest seal of this book this witness signed for.');
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
// The first seal of the next book: it starts from the latest seal the witness holds of the book before.
|
|
247
|
+
if (c.previous !== held.fingerprint) {
|
|
248
|
+
return refused('seal-chain-broken', `The first seal of book ${number} must name, as the seal before it, the latest seal this witness holds of book ${heldNumber}. Hand it the seals in between first, in their order.`);
|
|
249
|
+
}
|
|
250
|
+
if (!(await firstLineTied())) {
|
|
251
|
+
return refused('proof-invalid', 'The proof does not show that the line given as the book\'s first is the first entry of the book the seal covers.');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (signing && parseTime(c.when) < parseTime(held.content.when)) return refused('time-went-backwards', 'The seal is dated before the latest seal of this person, agent and recorder this witness signed for.');
|
|
255
|
+
} else if (!(await firstLineTied())) {
|
|
256
|
+
return refused('proof-invalid', 'The proof does not show that the line given as the book\'s first is the first entry of the book the seal covers.');
|
|
257
|
+
}
|
|
258
|
+
return { seal, identity, sequence, held, same: false };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
/**
|
|
263
|
+
* Sign a statement for a seal, if it extends the latest seal of its book
|
|
264
|
+
* that this witness holds; or say why not. The state handed back already
|
|
265
|
+
* holds the new seal: save it before the statement is given out.
|
|
266
|
+
* @param {{seal: object, stamps?: any[], witnesses?: any[]}} entry the seal's entry; only "seal" is read
|
|
267
|
+
* @param {Uint8Array[]} proof see witnessProof
|
|
268
|
+
* @param {Uint8Array} firstLine the bytes of the book's first line
|
|
269
|
+
* @returns {Promise<{statement: object, state: object} | {refused: {code: string, message: string}}>}
|
|
270
|
+
*/
|
|
271
|
+
witness(entry, proof, firstLine) {
|
|
272
|
+
return inTurn(async () => {
|
|
273
|
+
if (!ready) {
|
|
274
|
+
return refused('witness-not-ready', 'This witness was opened from a saved state, and has not been brought up to date and told so. It signs nothing until then.');
|
|
275
|
+
}
|
|
276
|
+
const found = await weigh(entry, proof, firstLine, true);
|
|
277
|
+
if (found.refused) return found;
|
|
278
|
+
const { seal, identity, sequence, same } = found;
|
|
279
|
+
// The same seal again: the same statement, with no second signature.
|
|
280
|
+
if (same) return { statement: structuredClone(found.held.statement), state: snapshot() };
|
|
281
|
+
let now;
|
|
282
|
+
try {
|
|
283
|
+
now = clock();
|
|
284
|
+
} catch {
|
|
285
|
+
now = NaN;
|
|
286
|
+
}
|
|
287
|
+
if (typeof now !== 'number' || !Number.isFinite(now)) return refused('check-failed', 'The witness\'s clock did not give a time.');
|
|
288
|
+
if (parseTime(seal.content.when) > now + CLOCK_ALLOWANCE_MS) return refused('time-went-backwards', 'The seal is dated more than 300 seconds after this witness\'s clock.');
|
|
289
|
+
let statement;
|
|
290
|
+
try {
|
|
291
|
+
({ record: statement } = await writeWitness({ seal: seal.fingerprint, size: seal.content.size, root: seal.content.root, by: me, when: now }, keys));
|
|
292
|
+
// Every signature is checked before the statement is given out or kept.
|
|
293
|
+
await ownStatement(statement, seal);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
const p = problemFrom(e);
|
|
296
|
+
return refused('check-failed', `The witness could not sign a statement that checks, so it signed none. ${p.message}`);
|
|
297
|
+
}
|
|
298
|
+
books.set(identity, { sequence, seal: structuredClone(entry.seal), statement, fingerprint: seal.fingerprint, content: seal.content });
|
|
299
|
+
return { statement: structuredClone(statement), state: snapshot() };
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Bring the witness up to date: hand it a seal's entry that holds a
|
|
305
|
+
* statement this witness signed for that seal, from wherever what it
|
|
306
|
+
* gave out was kept (the book, or a copy kept apart from its state). It
|
|
307
|
+
* takes that seal as the latest of its book, where the seal extends the
|
|
308
|
+
* one it holds. It signs nothing.
|
|
309
|
+
* @param {{seal: object, witnesses: any[], stamps?: any[]}} entry
|
|
310
|
+
* @param {Uint8Array[]} proof see witnessProof
|
|
311
|
+
* @param {Uint8Array} firstLine
|
|
312
|
+
* @returns {Promise<{state: object} | {refused: {code: string, message: string}}>}
|
|
313
|
+
*/
|
|
314
|
+
update(entry, proof, firstLine) {
|
|
315
|
+
return inTurn(async () => {
|
|
316
|
+
const found = await weigh(entry, proof, firstLine, false);
|
|
317
|
+
if (found.refused) return found;
|
|
318
|
+
const { seal, identity, sequence, same } = found;
|
|
319
|
+
const list = entry.witnesses;
|
|
320
|
+
if (!Array.isArray(list) || list.length < 1 || list.length > MAX_WITNESSES) return refused('witness-invalid', 'The entry holds no statement by this witness for the seal.');
|
|
321
|
+
let statement = null;
|
|
322
|
+
for (let i = 0; i < list.length && statement === null; i++) {
|
|
323
|
+
try {
|
|
324
|
+
await ownStatement(list[i], seal);
|
|
325
|
+
statement = JSON.parse(canonicalJson(list[i]));
|
|
326
|
+
} catch {
|
|
327
|
+
// Another witness's statement, or one that does not check: not this witness's.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (statement === null) return refused('witness-invalid', 'The entry holds no statement by this witness for the seal.');
|
|
331
|
+
// The seal it holds already: nothing changes.
|
|
332
|
+
if (same) return { state: snapshot() };
|
|
333
|
+
books.set(identity, { sequence, seal: structuredClone(entry.seal), statement, fingerprint: seal.fingerprint, content: seal.content });
|
|
334
|
+
return { state: snapshot() };
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Say that the witness, opened from a saved state, has been brought up
|
|
340
|
+
* to date. The witness cannot tell whether a state is the latest it
|
|
341
|
+
* handed back: this is the word of whoever runs it.
|
|
342
|
+
*/
|
|
343
|
+
ready() {
|
|
344
|
+
ready = true;
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The latest seal of a book that this witness holds, with its statement.
|
|
349
|
+
* @param {string} book the book's fingerprint, as bookIdentity gives it
|
|
350
|
+
* @returns {{seal: object, statement: object} | null}
|
|
351
|
+
*/
|
|
352
|
+
latest(book) {
|
|
353
|
+
const held = typeof book === 'string' ? books.get(book) : undefined;
|
|
354
|
+
return held ? { seal: structuredClone(held.seal), statement: structuredClone(held.statement) } : null;
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
/** The witness's state as it stands, to be saved. */
|
|
358
|
+
state() {
|
|
359
|
+
return snapshot();
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|