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/src/tools.js CHANGED
@@ -5,13 +5,16 @@
5
5
  // every call asks first, is run only if the slip allows it, and leaves a
6
6
  // stub. The agent's software then calls the tools as before.
7
7
  //
8
- // By default the stub of a call names one document, "arguments": the
9
- // fingerprint of the call's arguments. So the record fixes exactly what was
10
- // asked for, without holding it: whoever kept the arguments can show them
11
- // later, and anyone can work out the fingerprint again.
8
+ // By default the stub of a call names one document, "provared.arguments"
9
+ // (a reserved name, format description section 27): the fingerprint of the
10
+ // call's arguments. So the record fixes exactly what was asked for, without
11
+ // holding it: whoever kept the arguments can show them later, and anyone
12
+ // can work out the fingerprint again. Before version 0.6.0 the document was
13
+ // named "arguments"; a checker reads both.
12
14
  //
13
15
  // It names no agent software and depends on none: a tool is any function.
14
16
 
17
+ import { denialCodes } from './denial.js';
15
18
  import { Refusal, fingerprint, utf8 } from './encoding.js';
16
19
  import * as f from './fields.js';
17
20
 
@@ -27,18 +30,22 @@ const NOT_A_GENERATOR = 'must not be a generator function: calling one does not
27
30
  * A call through a recorded tool that was not run: the check before acting
28
31
  * did not allow it, or the record so far has a problem. "answer" is what
29
32
  * the check before acting said (allowed, problems, breaches, needs).
33
+ * "denial", where the stub writer was opened with "denials: true", is what
34
+ * it said of the denial it wrote, or did not write, for the call.
30
35
  */
31
36
  export class NotTaken extends Error {
32
37
  /**
33
38
  * @param {string} tool the name of the tool
34
39
  * @param {{allowed: boolean, problems: object[], breaches: object[], needs: string[]}} answer
40
+ * @param {object} [denial]
35
41
  */
36
- constructor(tool, answer) {
42
+ constructor(tool, answer, denial) {
37
43
  const first = [...answer.problems, ...answer.breaches][0];
38
44
  super(`The tool "${tool}" was not run. ${first ? first.message : 'The check before acting did not allow it.'}`);
39
45
  this.name = 'NotTaken';
40
46
  this.tool = tool;
41
47
  this.answer = answer;
48
+ if (denial !== undefined) this.denial = denial;
42
49
  }
43
50
  }
44
51
 
@@ -74,7 +81,7 @@ function oneForm(value, depth = 0) {
74
81
  /**
75
82
  * The fingerprint of the arguments of a call: SHA-256, in base64url, of the
76
83
  * arguments written in the one form of RFC 8785. It is what a recorded
77
- * tool puts into its stub, as the document "arguments".
84
+ * tool puts into its stub, as the document "provared.arguments".
78
85
  * @param {unknown} args JSON: objects, lists, text, numbers, true, false and null
79
86
  * @returns {Promise<string>}
80
87
  * @throws {Refusal} "bad-field" if the arguments are not JSON
@@ -118,8 +125,8 @@ const needsOnlyApproval = (answer) => answer.problems.length === 0 && answer.bre
118
125
  * - "with" (optional): the id of the service the tool deals with, as the slip names it, or a function of the arguments that gives it;
119
126
  * - "amount" (optional): {unit, value}, or a function of the arguments that gives it;
120
127
  * - "details" (optional): a function of the arguments that gives the documents to name in the stub
121
- * ({name, sha256} each), or false for none. By default: one document, "arguments", the fingerprint
122
- * of the arguments (argumentsFingerprint);
128
+ * ({name, sha256} each), or false for none. By default: one document, "provared.arguments", the
129
+ * fingerprint of the arguments (argumentsFingerprint);
123
130
  * - "countersign" (optional): asks the other side to countersign the stub; a function of the stub and the arguments;
124
131
  * - "approve" (optional): asks the person for their approval where the slip asks for one; a function of the
125
132
  * request ({action, amount?, with?, details?}) and the arguments, that gives {record} or nothing.
@@ -139,6 +146,8 @@ export function recordTools(recorder, tools, options = {}) {
139
146
  if (options === null || typeof options !== 'object') options = {};
140
147
  // What the tools are put behind is fixed here too.
141
148
  const act = recorder.act.bind(recorder);
149
+ // Whether the writer writes denials, read once: only then are denials asked for.
150
+ const deny = recorder.denials === true && typeof recorder.deny === 'function' ? recorder.deny.bind(recorder) : null;
142
151
  const onStub = typeof options.onStub === 'function' ? options.onStub : null;
143
152
  const recorded = {};
144
153
  for (const name of Object.keys(tools)) {
@@ -157,12 +166,12 @@ export function recordTools(recorder, tools, options = {}) {
157
166
  for (const member of ['amount', 'with']) if (typeof fixed[member] !== 'function') fixed[member] = copied(fixed[member], `tools.${name}.${member}`);
158
167
  for (const member of ['amount', 'with']) if (fixed[member] !== null && typeof fixed[member] === 'object') Object.freeze(fixed[member]);
159
168
  Object.freeze(fixed);
160
- Object.defineProperty(recorded, name, { enumerable: true, value: (args) => call(act, onStub, name, fixed, args) });
169
+ Object.defineProperty(recorded, name, { enumerable: true, value: (args) => call(act, deny, onStub, name, fixed, args) });
161
170
  }
162
171
  return recorded;
163
172
  }
164
173
 
165
- async function call(act, onStub, name, spec, args) {
174
+ async function call(act, deny, onStub, name, spec, args) {
166
175
  // The arguments are copied when the call is made. The fingerprint is made
167
176
  // from that copy, and the tool is run with a copy of it. Each of the
168
177
  // tool's other functions is handed a copy of its own.
@@ -177,7 +186,7 @@ async function call(act, onStub, name, spec, args) {
177
186
  let details;
178
187
  if (spec.details === false) details = [];
179
188
  else if (typeof spec.details === 'function') details = (await (0, spec.details)(structuredClone(given))) ?? [];
180
- else details = given === undefined ? [] : [{ name: 'arguments', sha256: await argumentsFingerprint(given) }];
189
+ else details = given === undefined ? [] : [{ name: 'provared.arguments', sha256: await argumentsFingerprint(given) }];
181
190
  if (!Array.isArray(details)) throw f.fail(`tools.${name}.details`, 'must give a list of documents.');
182
191
  details = copied(details, `tools.${name}.details`);
183
192
  if (details.length > 0) request.details = details;
@@ -185,14 +194,22 @@ async function call(act, onStub, name, spec, args) {
185
194
  const more = {};
186
195
  if (typeof spec.countersign === 'function') more.countersign = (stub) => (0, spec.countersign)(stub, structuredClone(given));
187
196
  const perform = () => (0, spec.run)(structuredClone(given));
188
- let outcome = await act(request, perform, more);
197
+ // Where the person may be asked for their approval, the first call
198
+ // writes no denial: the action may yet be taken. A denial is written once
199
+ // it is known that it will not be, so that one call never leaves both a
200
+ // stub and a denial.
201
+ const asking = typeof spec.approve === 'function' && deny !== null;
202
+ let outcome = await act(request, perform, asking ? { ...more, denial: false } : more);
189
203
  // Where the slip asks for the person's own approval of this action, and
190
204
  // that is the one thing missing, the person is asked, once.
191
205
  if (!outcome.done && typeof spec.approve === 'function' && needsOnlyApproval(outcome.answer)) {
192
206
  const approval = await (0, spec.approve)(structuredClone(request), structuredClone(given));
193
207
  if (approval) outcome = await act(request, perform, { ...more, approval });
208
+ else if (asking) outcome = { ...outcome, denial: await deny(request, { source: 'person' }) };
209
+ } else if (!outcome.done && asking) {
210
+ outcome = { ...outcome, denial: await deny(request, { source: 'check', codes: denialCodes(outcome.answer) }) };
194
211
  }
195
- if (!outcome.done) throw new NotTaken(name, outcome.answer);
212
+ if (!outcome.done) throw new NotTaken(name, outcome.answer, outcome.denial);
196
213
  if (onStub) await onStub(outcome.stub, name);
197
214
  return outcome.result;
198
215
  }
package/src/tree.js CHANGED
@@ -7,8 +7,13 @@
7
7
  //
8
8
  // The proof that one leaf is in the tree is the audit path of section
9
9
  // 2.1.1. It is checked with the steps of RFC 9162, section 2.1.3.2.
10
+ //
11
+ // The proof that a tree of m leaves is the first part of a tree of n leaves
12
+ // (a consistency proof) is made as RFC 9162, section 2.1.4.1, sets out, and
13
+ // checked with the steps of section 2.1.4.2. RFC 9162 builds the same tree
14
+ // as RFC 6962.
10
15
 
11
- import { concatBytes, equalBytes, sha256 } from './encoding.js';
16
+ import { Refusal, concatBytes, equalBytes, sha256 } from './encoding.js';
12
17
 
13
18
  const LEAF = new Uint8Array([0x00]);
14
19
  const NODE = new Uint8Array([0x01]);
@@ -61,6 +66,15 @@ export async function treeRoot(leaves) {
61
66
  export function treeBuilder(from = []) {
62
67
  /** @type {{hash: Uint8Array, size: number}[]} */
63
68
  const parts = from.slice();
69
+ const addLeafHash = async (hash) => {
70
+ if (!(hash instanceof Uint8Array) || hash.length !== 32) throw new Refusal('bad-field', 'leaf: must be a leaf fingerprint: 32 bytes.');
71
+ let node = { hash: hash.slice(), size: 1 };
72
+ while (parts.length && parts[parts.length - 1].size === node.size) {
73
+ const left = parts.pop();
74
+ node = { hash: await nodeHash(left.hash, node.hash), size: left.size * 2 };
75
+ }
76
+ parts.push(node);
77
+ };
64
78
  return {
65
79
  /** A builder that carries on from here by itself: what is added to one is not added to the other. */
66
80
  fork() {
@@ -68,12 +82,17 @@ export function treeBuilder(from = []) {
68
82
  },
69
83
  /** @param {Uint8Array} leaf the data of the next leaf */
70
84
  async add(leaf) {
71
- let node = { hash: await leafHash(leaf), size: 1 };
72
- while (parts.length && parts[parts.length - 1].size === node.size) {
73
- const left = parts.pop();
74
- node = { hash: await nodeHash(left.hash, node.hash), size: left.size * 2 };
75
- }
76
- parts.push(node);
85
+ await addLeafHash(await leafHash(leaf));
86
+ },
87
+ /**
88
+ * Add the next leaf by its fingerprint alone (leafHash of its data), as a
89
+ * sealer that never sees the lines does.
90
+ * @param {Uint8Array} hash 32 bytes
91
+ */
92
+ addLeafHash,
93
+ /** One fingerprint for each complete part of the tree, the largest first: what a sealer saves. */
94
+ parts() {
95
+ return parts.map((p) => ({ hash: p.hash.slice(), size: p.size }));
77
96
  },
78
97
  /** @returns {Promise<Uint8Array>} the root of the leaves added so far */
79
98
  async root() {
@@ -141,3 +160,116 @@ export async function verifyInclusion(index, size, leaf, path, root) {
141
160
  }
142
161
  return sn === 0 && equalBytes(r, root);
143
162
  }
163
+
164
+ // The largest power of two smaller than n, for n of 2 or more (RFC 9162,
165
+ // section 2.1.1: "k be the largest power of two smaller than n").
166
+ function splitAt(n) {
167
+ let k = 1;
168
+ while (k * 2 < n) k *= 2;
169
+ return k;
170
+ }
171
+
172
+ // The root of the tree over the leaves whose hashes are hashes[lo] to
173
+ // hashes[hi - 1], for hi > lo: MTH of RFC 9162, section 2.1.1.
174
+ async function rangeRoot(hashes, lo, hi) {
175
+ if (hi - lo === 1) return hashes[lo];
176
+ const k = splitAt(hi - lo);
177
+ return nodeHash(await rangeRoot(hashes, lo, lo + k), await rangeRoot(hashes, lo + k, hi));
178
+ }
179
+
180
+ // SUBPROOF(m, D[lo:hi], b) of RFC 9162, section 2.1.4.1, for 0 < m <= hi - lo.
181
+ async function subproof(hashes, m, lo, hi, b) {
182
+ const n = hi - lo;
183
+ if (m === n) return b ? [] : [await rangeRoot(hashes, lo, hi)];
184
+ const k = splitAt(n);
185
+ if (m <= k) return [...(await subproof(hashes, m, lo, lo + k, b)), await rangeRoot(hashes, lo + k, hi)];
186
+ return [...(await subproof(hashes, m - k, lo + k, hi, false)), await rangeRoot(hashes, lo, lo + k)];
187
+ }
188
+
189
+ /**
190
+ * The proof that the first m leaves form a tree that the whole tree extends:
191
+ * the consistency proof of RFC 9162, section 2.1.4.1, from the tree of the
192
+ * first m leaves to the tree of all of them. For m equal to the number of
193
+ * leaves, and for m of 0, the proof is empty (RFC 9162 defines a proof only
194
+ * for 0 < m; every tree extends the empty one).
195
+ * @param {Uint8Array[]} leaves the data of each leaf
196
+ * @param {number} m how many leaves the earlier tree has, from 0 to the number of leaves
197
+ * @returns {Promise<Uint8Array[]>}
198
+ */
199
+ export async function consistencyProof(leaves, m) {
200
+ if (!Array.isArray(leaves)) throw new TypeError('the leaves must be a list');
201
+ if (!Number.isSafeInteger(m) || m < 0 || m > leaves.length) throw new RangeError('no such earlier tree');
202
+ if (m === 0 || m === leaves.length) return [];
203
+ const hashes = [];
204
+ for (const leaf of leaves) hashes.push(await leafHash(leaf));
205
+ return subproof(hashes, m, 0, hashes.length, true);
206
+ }
207
+
208
+ const isHash = (value) => value instanceof Uint8Array && value.length === 32;
209
+
210
+ /**
211
+ * Check a consistency proof (RFC 9162, section 2.1.4.2): that the tree of m
212
+ * leaves whose root is rootM is the first part of the tree of n leaves whose
213
+ * root is rootN. Where m equals n the proof must be empty and the two roots
214
+ * the same; where m is 0 the proof must be empty and rootM the root of the
215
+ * empty tree (the SHA-256 hash of nothing). Anything that is not a proof of
216
+ * these sizes gives false: the function never throws for a bad proof.
217
+ * @param {number} m
218
+ * @param {number} n
219
+ * @param {Uint8Array} rootM 32 bytes
220
+ * @param {Uint8Array} rootN 32 bytes
221
+ * @param {Uint8Array[]} proof each 32 bytes
222
+ * @returns {Promise<boolean>}
223
+ */
224
+ export async function verifyConsistency(m, n, rootM, rootN, proof) {
225
+ if (!Number.isSafeInteger(m) || !Number.isSafeInteger(n) || m < 0 || m > n) return false;
226
+ if (!isHash(rootM) || !isHash(rootN) || !Array.isArray(proof)) return false;
227
+ // Read place by place, a gap included: a gap is not a fingerprint.
228
+ const path = [];
229
+ for (let i = 0; i < proof.length; i++) {
230
+ if (!isHash(proof[i])) return false;
231
+ path.push(proof[i]);
232
+ }
233
+ if (m === n) return path.length === 0 && equalBytes(rootM, rootN);
234
+ if (m === 0) return path.length === 0 && equalBytes(rootM, await sha256(new Uint8Array(0)));
235
+ // The steps of RFC 9162, section 2.1.4.2, in their order.
236
+ // 1. An empty proof fails.
237
+ if (path.length === 0) return false;
238
+ // 2. Where m is an exact power of two, the root of the earlier tree is the first value.
239
+ // (Worked out by doubling, not with the bit operators, which hold only 32 bits.)
240
+ let power = 1;
241
+ while (power < m) power *= 2;
242
+ if (power === m) path.unshift(rootM);
243
+ // 3.
244
+ let fn = m - 1;
245
+ let sn = n - 1;
246
+ // 4. Sizes are below 2 to the power 53, so halving with Math.floor is exact.
247
+ while (fn % 2 === 1) {
248
+ fn = Math.floor(fn / 2);
249
+ sn = Math.floor(sn / 2);
250
+ }
251
+ // 5.
252
+ let fr = path[0];
253
+ let sr = path[0];
254
+ // 6.
255
+ for (let i = 1; i < path.length; i++) {
256
+ const c = path[i];
257
+ if (sn === 0) return false;
258
+ if (fn % 2 === 1 || fn === sn) {
259
+ fr = await nodeHash(c, fr);
260
+ sr = await nodeHash(c, sr);
261
+ if (fn % 2 === 0) {
262
+ while (fn % 2 === 0 && fn !== 0) {
263
+ fn = Math.floor(fn / 2);
264
+ sn = Math.floor(sn / 2);
265
+ }
266
+ }
267
+ } else {
268
+ sr = await nodeHash(sr, c);
269
+ }
270
+ fn = Math.floor(fn / 2);
271
+ sn = Math.floor(sn / 2);
272
+ }
273
+ // 7.
274
+ return equalBytes(fr, rootM) && equalBytes(sr, rootN) && sn === 0;
275
+ }