provared 0.5.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 +982 -0
- package/README.md +6 -3
- package/bin/provared-check.mjs +115 -11
- package/docs/guide.md +290 -17
- package/docs/threat-model.md +242 -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 +686 -22
- 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/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 +6 -5
- package/src/timestamp.js +55 -16
- package/src/tools.js +30 -13
- package/src/tree.js +139 -7
- package/src/types.d.ts +257 -9
- package/src/witness.js +63 -0
- package/src/witnessing.js +362 -0
package/src/signatures.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// not built in, which is never a pass.
|
|
15
15
|
|
|
16
16
|
import { Refusal } from './encoding.js';
|
|
17
|
-
import { checkKey, importPublicKey, thumbprint
|
|
17
|
+
import { badEd25519Signature, checkKey, importPublicKey, thumbprint } from './keys.js';
|
|
18
18
|
|
|
19
19
|
const SLH = 'SLH-DSA-SHA2-256s';
|
|
20
20
|
|
|
@@ -99,10 +99,11 @@ export async function verifySignature(jwk, signature, data, without = []) {
|
|
|
99
99
|
if (!Object.hasOwn(VERIFY_PARAMS, jwk.alg)) return 'invalid';
|
|
100
100
|
const params = VERIFY_PARAMS[jwk.alg];
|
|
101
101
|
if (without.includes(jwk.alg) || !(await methodAvailable(jwk.alg))) return 'unavailable';
|
|
102
|
-
// An Ed25519 signature whose first half is a point of small order
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
102
|
+
// An Ed25519 signature whose first half is a point of small order, or
|
|
103
|
+
// whose second half, the number S, is not below the order L, is refused
|
|
104
|
+
// here, whatever the device's own library would say: libraries differ on
|
|
105
|
+
// both (format description, section 3.8).
|
|
106
|
+
if (jwk.alg === 'Ed25519' && badEd25519Signature(signature)) return 'invalid';
|
|
106
107
|
try {
|
|
107
108
|
const key = await importPublicKey(jwk);
|
|
108
109
|
return (await globalThis.crypto.subtle.verify(params, key, signature, data)) ? 'valid' : 'invalid';
|
package/src/timestamp.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// service's certificate. It follows no chain of certificates and asks no
|
|
10
10
|
// outside party: the person checking decides whom to trust.
|
|
11
11
|
|
|
12
|
-
import { TAG, badStamp, children, contentOf, ecdsaToRaw, expect, hexOf, readElement, timeOf, validateDer, wholeOf } from './der.js';
|
|
12
|
+
import { TAG, badStamp, children, contentOf, ecdsaToRaw, expect, hexOf, readElement, setInOrder, timeOf, validateDer, wholeOf } from './der.js';
|
|
13
13
|
import { Refusal, equalBytes, formatTime, sha256, toBase64url } from './encoding.js';
|
|
14
|
-
import { weakEd25519 } from './keys.js';
|
|
14
|
+
import { badEd25519Signature, weakEd25519 } from './keys.js';
|
|
15
15
|
|
|
16
16
|
/** The largest time-stamp, in bytes. */
|
|
17
17
|
export const MAX_STAMP_BYTES = 12288;
|
|
@@ -162,6 +162,8 @@ function checkKeyEncoding(bytes, certificate, method, size) {
|
|
|
162
162
|
// force, and its public key.
|
|
163
163
|
function readCertificate(bytes, element) {
|
|
164
164
|
const whole = wholeOf(bytes, expect(element, TAG.SEQUENCE, 'a certificate'));
|
|
165
|
+
// A certificate is DER (RFC 5280 section 4.1), so the order of every set inside it is held here.
|
|
166
|
+
validateDer(bytes, element);
|
|
165
167
|
const tbs = expect(children(bytes, element, 3)[0], TAG.SEQUENCE, 'the body of a certificate');
|
|
166
168
|
const parts = children(bytes, tbs, 12);
|
|
167
169
|
// The version, in [0], is absent only in the oldest kind of certificate.
|
|
@@ -204,14 +206,17 @@ function signingCertificateOf(bytes, value, v2) {
|
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
const KIND_NOT_ACCEPTED = 'A time-stamp was signed with a kind of key that is not accepted: RSA, ECDSA on the curves P-256 and P-384, and Ed25519 are.';
|
|
209
|
+
const ED25519_DIGEST = 'A time-stamp signed with Ed25519 names a fingerprint method other than SHA-512, or gives the method parameters, which RFC 8419 does not allow.';
|
|
210
|
+
const ED25519_PARAMETERS = 'A time-stamp signed with Ed25519 gives its signing method parameters, which RFC 8419 does not allow.';
|
|
207
211
|
|
|
208
212
|
// Check the service's signature with the public key of its certificate.
|
|
209
213
|
// Returns "valid", "invalid", or "unavailable" where this device has no
|
|
210
214
|
// built-in support for the method. The fingerprint method is the one the
|
|
211
215
|
// signer names (RFC 5652 section 5.4); a signing method that names another
|
|
212
216
|
// is refused; a key of a kind that is not accepted is refused in words that
|
|
213
|
-
// say so.
|
|
214
|
-
|
|
217
|
+
// say so. "parameters" holds the parameters, if any, of the signing method
|
|
218
|
+
// and of the signer's fingerprint method.
|
|
219
|
+
async function verifyWith(certificate, bytes, signatureOid, digestName, signature, signed, without, parameters) {
|
|
215
220
|
const subtle = globalThis.crypto.subtle;
|
|
216
221
|
let importParams;
|
|
217
222
|
let verifyParams;
|
|
@@ -237,7 +242,11 @@ async function verifyWith(certificate, bytes, signatureOid, digestName, signatur
|
|
|
237
242
|
return 'invalid';
|
|
238
243
|
}
|
|
239
244
|
} else if (certificate.keyOid === OID.ed25519) {
|
|
245
|
+
// With Ed25519 the signer's fingerprint method is SHA-512 with no
|
|
246
|
+
// parameters, and the signing method carries none (RFC 8419, section 3.1).
|
|
240
247
|
if (signatureOid !== OID.ed25519) return 'invalid';
|
|
248
|
+
if (digestName !== 'SHA-512' || parameters.digest !== undefined) throw invalid(ED25519_DIGEST);
|
|
249
|
+
if (parameters.signature !== undefined) throw invalid(ED25519_PARAMETERS);
|
|
241
250
|
checkKeyEncoding(bytes, certificate, 'Ed25519');
|
|
242
251
|
method = 'Ed25519';
|
|
243
252
|
importParams = { name: 'Ed25519' };
|
|
@@ -253,8 +262,8 @@ async function verifyWith(certificate, bytes, signatureOid, digestName, signatur
|
|
|
253
262
|
// A device with no Ed25519 says so when the key is loaded.
|
|
254
263
|
return method === 'Ed25519' && e && e.name === 'NotSupportedError' ? 'unavailable' : 'invalid';
|
|
255
264
|
}
|
|
256
|
-
// An Ed25519 signature whose first half is a point of small order is refused, as everywhere (format description, section 3.8).
|
|
257
|
-
if (method === 'Ed25519' && (raw
|
|
265
|
+
// An Ed25519 signature whose first half is a point of small order, or whose number S is not below L, is refused, as everywhere (format description, section 3.8).
|
|
266
|
+
if (method === 'Ed25519' && badEd25519Signature(raw)) return 'invalid';
|
|
258
267
|
try {
|
|
259
268
|
return (await subtle.verify(verifyParams, key, raw, signed)) ? 'valid' : 'invalid';
|
|
260
269
|
} catch {
|
|
@@ -275,7 +284,8 @@ async function verifyWith(certificate, bytes, signatureOid, digestName, signatur
|
|
|
275
284
|
* soundly, by a service the checker did not name. "unavailable": this
|
|
276
285
|
* device cannot check the service's signing method; no time is given.
|
|
277
286
|
* "authority" is the fingerprint of the certificate that signed. "when"
|
|
278
|
-
* is the stated time, to the second, rounded down
|
|
287
|
+
* is the stated time, to the second, rounded down; "time" is it in
|
|
288
|
+
* milliseconds, a fraction of a second kept to the millisecond.
|
|
279
289
|
* @throws {Refusal} "stamp-bad-data", "stamp-wrong-data" or "stamp-invalid"
|
|
280
290
|
*/
|
|
281
291
|
export async function checkStamp(token, fingerprintBytes, options) {
|
|
@@ -287,10 +297,18 @@ export async function checkStamp(token, fingerprintBytes, options) {
|
|
|
287
297
|
if (!(fingerprintBytes instanceof Uint8Array)) throw badStamp('there is no fingerprint to compare it with.');
|
|
288
298
|
|
|
289
299
|
// The outer wrapper, and the signed data inside it (RFC 5652 sections 3
|
|
290
|
-
// and 5.1). Every part is read, and must be
|
|
300
|
+
// and 5.1). Every part the reader can tell apart is read, and must be
|
|
301
|
+
// strict DER, except that the order of a set is held only where a standard
|
|
302
|
+
// requires DER: the signed attributes (RFC 5652 section 5.3), the statement
|
|
303
|
+
// (RFC 3161 section 2.4.2) and each certificate (RFC 5280 section 4.1),
|
|
304
|
+
// each walked again below with the order held. The signed data's own sets
|
|
305
|
+
// (the fingerprint methods, the certificates, the withdrawn certificates,
|
|
306
|
+
// the signers) and the rest of the signer may be in any order: CMS does not
|
|
307
|
+
// require DER of them, and real services list their certificates in chain
|
|
308
|
+
// order, the signing certificate before its issuer.
|
|
291
309
|
const wrapper = expect(readElement(token, 0), TAG.SEQUENCE, 'the wrapper');
|
|
292
310
|
if (wrapper.end !== token.length) throw badStamp('it has bytes left over.');
|
|
293
|
-
validateDer(token, wrapper);
|
|
311
|
+
validateDer(token, wrapper, false);
|
|
294
312
|
const outer = children(token, wrapper, 2);
|
|
295
313
|
if (outer.length !== 2 || hexOf(token, expect(outer[0], TAG.OID, 'the kind of content')) !== OID.signedData) throw badStamp('it is not signed data.');
|
|
296
314
|
const inner = children(token, expect(outer[1], TAG.CONTEXT_0, 'the signed data'), 1);
|
|
@@ -309,7 +327,11 @@ export async function checkStamp(token, fingerprintBytes, options) {
|
|
|
309
327
|
let next = 3;
|
|
310
328
|
const certificates = [];
|
|
311
329
|
if (parts[next] && parts[next].tag === TAG.CONTEXT_0) {
|
|
312
|
-
|
|
330
|
+
// A set of the signed data's own (RFC 5652 section 10.2.3), which CMS does
|
|
331
|
+
// not require to be in DER order; each certificate inside it is DER (RFC
|
|
332
|
+
// 5280 section 4.1), and is held to it when it is read.
|
|
333
|
+
const items = children(token, parts[next], 8);
|
|
334
|
+
for (const c of items) {
|
|
313
335
|
// Other kinds of certificate may sit here; only the ordinary kind is read.
|
|
314
336
|
if (c.tag === TAG.SEQUENCE) certificates.push(readCertificate(token, c));
|
|
315
337
|
}
|
|
@@ -324,9 +346,16 @@ export async function checkStamp(token, fingerprintBytes, options) {
|
|
|
324
346
|
if (!signer[1] || !((signerVersion === '01' && signer[1].tag === TAG.SEQUENCE) || (signerVersion === '03' && signer[1].tag === 0x80))) {
|
|
325
347
|
throw badStamp('the signer is not named as its standard sets out.');
|
|
326
348
|
}
|
|
327
|
-
const
|
|
349
|
+
const digesting = algorithmOf(token, signer[2], 'the signer\'s fingerprint method');
|
|
350
|
+
const digestName = HASHES[digesting.oid];
|
|
328
351
|
const attributes = expect(signer[3], TAG.CONTEXT_0, 'the signed attributes');
|
|
329
|
-
|
|
352
|
+
// A set under a tag of its own, in DER (RFC 5652 section 5.3), so its order,
|
|
353
|
+
// and the order of every set inside it, is held to DER here.
|
|
354
|
+
const attributeList = children(token, attributes, 16);
|
|
355
|
+
if (!setInOrder(token, attributeList)) throw badStamp('the signed attributes are not in the order DER requires.');
|
|
356
|
+
for (const a of attributeList) validateDer(token, a);
|
|
357
|
+
const signing = algorithmOf(token, signer[4], 'the signing method');
|
|
358
|
+
const signatureOid = signing.oid;
|
|
330
359
|
const signature = contentOf(token, expect(signer[5], TAG.OCTET_STRING, 'the signature'));
|
|
331
360
|
if (signer.length > 7 || (signer.length === 7 && signer[6].tag !== TAG.CONTEXT_1)) throw badStamp('the signer holds parts that are not in the order its standard sets out.');
|
|
332
361
|
|
|
@@ -344,7 +373,9 @@ export async function checkStamp(token, fingerprintBytes, options) {
|
|
|
344
373
|
let kindNamed = false;
|
|
345
374
|
let digestNamed = false;
|
|
346
375
|
let named = null;
|
|
347
|
-
|
|
376
|
+
let newerNamed = false;
|
|
377
|
+
let olderNamed = false;
|
|
378
|
+
for (const attribute of attributeList) {
|
|
348
379
|
const pair = children(token, expect(attribute, TAG.SEQUENCE, 'an attribute'), 2);
|
|
349
380
|
const name = hexOf(token, expect(pair[0], TAG.OID, 'the name of an attribute'));
|
|
350
381
|
const values = children(token, expect(pair[1], TAG.SET, 'the value of an attribute'), 4);
|
|
@@ -360,8 +391,16 @@ export async function checkStamp(token, fingerprintBytes, options) {
|
|
|
360
391
|
digestNamed = true;
|
|
361
392
|
} else if (name === OID.signingCertificateV2 || name === OID.signingCertificate) {
|
|
362
393
|
if (values.length !== 1) throw invalid('A time-stamp does not say, in one way, which certificate signed it.');
|
|
363
|
-
//
|
|
364
|
-
|
|
394
|
+
// Each form at most once: RFC 5035 section 3 for the newer form, and
|
|
395
|
+
// for the older section 5.4.2 of RFC 2634 as RFC 5035 section 5 writes
|
|
396
|
+
// it. Where both forms are present, the newer one is used: this
|
|
397
|
+
// format's choice, where RFC 5035 section 7 lets a reader refuse a
|
|
398
|
+
// time-stamp whose two forms disagree.
|
|
399
|
+
const newer = name === OID.signingCertificateV2;
|
|
400
|
+
if (newer ? newerNamed : olderNamed) throw invalid('A time-stamp names the certificate that signed it more than once in the same form, which its standard does not allow.');
|
|
401
|
+
if (newer) newerNamed = true;
|
|
402
|
+
else olderNamed = true;
|
|
403
|
+
if (named === null || newer) named = signingCertificateOf(token, values[0], newer);
|
|
365
404
|
}
|
|
366
405
|
}
|
|
367
406
|
if (!kindNamed || !digestNamed) throw invalid('A time-stamp\'s signature does not cover its statement.');
|
|
@@ -382,7 +421,7 @@ export async function checkStamp(token, fingerprintBytes, options) {
|
|
|
382
421
|
// 5.4). The bytes are copied, so that the caller's time-stamp is not changed.
|
|
383
422
|
const signed = new Uint8Array(wholeOf(token, attributes));
|
|
384
423
|
signed[0] = TAG.SET;
|
|
385
|
-
const state = await verifyWith(certificate, token, signatureOid, digestName, signature, signed, without);
|
|
424
|
+
const state = await verifyWith(certificate, token, signatureOid, digestName, signature, signed, without, { signature: signing.parameters, digest: digesting.parameters });
|
|
386
425
|
if (state === 'unavailable') return { state: 'unavailable', when: null, time: NaN, authority: null };
|
|
387
426
|
if (state !== 'valid') throw invalid('A time-stamp\'s signature does not fit the certificate it names.');
|
|
388
427
|
if (statement.time < certificate.notBefore || statement.time > certificate.notAfter) {
|
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"
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
}
|