@datumesh/verify 0.2.0 → 0.3.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/README.md +3 -3
- package/dist/cjs/ed25519.js +2 -0
- package/dist/cjs/envelope.js +3 -4
- package/dist/cjs/errors.d.ts +6 -3
- package/dist/cjs/errors.js +6 -2
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +4 -3
- package/dist/cjs/json.d.ts +3 -4
- package/dist/cjs/json.js +35 -8
- package/dist/cjs/keys.d.ts +6 -3
- package/dist/cjs/keys.js +64 -5
- package/dist/cjs/uri.js +23 -18
- package/dist/cjs/verify.d.ts +25 -6
- package/dist/cjs/verify.js +27 -15
- package/dist/esm/ed25519.js +2 -0
- package/dist/esm/envelope.js +3 -4
- package/dist/esm/errors.d.ts +6 -3
- package/dist/esm/errors.js +6 -2
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +3 -3
- package/dist/esm/json.d.ts +3 -4
- package/dist/esm/json.js +35 -8
- package/dist/esm/keys.d.ts +6 -3
- package/dist/esm/keys.js +63 -4
- package/dist/esm/uri.js +25 -20
- package/dist/esm/verify.d.ts +25 -6
- package/dist/esm/verify.js +27 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @datumesh/verify
|
|
2
2
|
|
|
3
|
-
Verify [Datumesh](https://datumesh.com) receipts in JavaScript: signed answers for addressable data cells, checked offline against strict RFC 8785 canonical JSON and Ed25519, following the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec) revision `spec-0.
|
|
3
|
+
Verify [Datumesh](https://datumesh.com) receipts in JavaScript: signed answers for addressable data cells, checked offline against strict RFC 8785 canonical JSON and Ed25519, following the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec) revision `spec-0.3.0` and passing all of its test vectors.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install @datumesh/verify
|
|
@@ -31,10 +31,10 @@ console.log(verify(vector.input, keys).code); // OK
|
|
|
31
31
|
|
|
32
32
|
## What it does
|
|
33
33
|
|
|
34
|
-
- `verify(receipt, keys, recompute?, blob?)` runs the verification procedure of SPEC.md 10.1 and returns `{ ok, code, level, hash, key, detail, path, blobChecked }`. It is synchronous, never touches the network and never throws for a bad receipt or bad key material; failures come back as the spec's error codes (`E_SIGNATURE`, `E_KEY_NOT_VALID`, ...). `level` is `"signed"`, or `"recomputed"` when you pass the descriptor, the input receipts and the value you obtained by evaluating the function (the library checks lineage and compares; it does not evaluate functions). For a receipt whose value is a `blob`, pass the content bytes as `blob` to check their length and SHA-256 (`E_BLOB_MISMATCH`; `blobChecked` is then true).
|
|
34
|
+
- `verify(receipt, keys, recompute?, blob?)` runs the verification procedure of SPEC.md 10.1 and returns `{ ok, code, level, hash, key, detail, path, blobChecked }`. It is synchronous, never touches the network and never throws for a bad receipt or bad key material; failures come back as the spec's error codes (`E_SIGNATURE`, `E_KEY_NOT_VALID`, ...). `level` is `"signed"`, or `"recomputed"` when you pass the descriptor, the input receipts and the value you obtained by evaluating the function (the library checks lineage and compares; it does not evaluate functions). If you could not evaluate it, leave out the value: when the descriptor, arguments and inputs check out, the result is `ok` at level `"signed"` with `code` `"E_EVAL_UNAVAILABLE"`, the one code reported with success. For a receipt whose value is a `blob`, pass the content bytes as `blob` to check their length and SHA-256 (`E_BLOB_MISMATCH`; `blobChecked` is then true).
|
|
35
35
|
- `keys` is a key document, the result of `keysFromResponse` / `fetchKeys`, a `{ keyId: publicKeyHex }` map, or an array of these (one per host for lineage).
|
|
36
36
|
- `fetchKeys(host)` is the only function that uses the network: one `GET https://<host>/.well-known/datumesh.json`, no redirects, 10 s timeout, using the platform's `fetch` and TLS verification.
|
|
37
|
-
- `checkEnvelope(status, body, keys, request)` checks a mesh's response envelope (SPEC.md 5.5) against its HTTP status, verifies the embedded receipt through the signature, and checks that it answers your request (`request` is `{ cell_id, asof? }`: the id of the cell you asked for and the `asof` you asked with, if any), returning `{ ok, code, outcome, trust, receiptHash, detail, path }`. The envelope itself is unsigned: `stale`, `superseded_by` and `verified` remain the mesh's claims.
|
|
37
|
+
- `checkEnvelope(status, body, keys, request)` checks a mesh's response envelope (SPEC.md 5.5) against its HTTP status, verifies the embedded receipt through the signature, and checks that it answers your request (`request` is `{ cell_id, asof? }`: the id of the cell you asked for and the `asof` you asked with, if any), returning `{ ok, code, outcome, trust, receiptHash, detail, path }`. The receipt's `asof` must be at or before the requested one, for `ok` as for `stale`. The envelope itself is unsigned: that an `ok` receipt is the latest version, `stale`, `superseded_by` and `verified` remain the mesh's claims.
|
|
38
38
|
- `resolvedUnit(descriptor, cell)` gives the unit of a cell's value from its function's declared units (`"USD/EUR"`, `"degC"`, or `null` for a non-number), using the closed list of `units.md` shipped with the package. The library never converts or checks values against units.
|
|
39
39
|
- `canonicalize(obj, kind)`, `cellHash`, `receiptHash`, `functionHash`, `parseUri(uri, signature)`, `formatUri(ref)` and `keyId` expose canonical forms, hashes and cell URIs. These and `resolvedUnit` throw `DatumeshError` (with `.code` and `.path`).
|
|
40
40
|
|
package/dist/cjs/ed25519.js
CHANGED
|
@@ -59,6 +59,8 @@ function verifyEd25519Strict(sig, msg, pub) {
|
|
|
59
59
|
const h = (0, sha2_1.sha512)(concat(rBytes, pub, msg));
|
|
60
60
|
const k = le(h) % L;
|
|
61
61
|
// R' = [S]B - [k]A (multiplyUnsafe accepts 0; inputs are public, so variable time is fine).
|
|
62
|
+
// subtract() adds the negation of the point [k]A, as SPEC 10.1 step 6 requires; it is not
|
|
63
|
+
// [L - k]A, which differs for a mixed-order A (receipt/ed25519-mixed-order-key).
|
|
62
64
|
const SB = s === 0n ? Point.ZERO : Point.BASE.multiplyUnsafe(s);
|
|
63
65
|
const kA = k === 0n ? Point.ZERO : A.multiplyUnsafe(k);
|
|
64
66
|
const rCheck = SB.subtract(kA).toBytes();
|
package/dist/cjs/envelope.js
CHANGED
|
@@ -223,12 +223,11 @@ function checkEnvelope(status, body, keys, request) {
|
|
|
223
223
|
const cellId = (0, hash_js_1.sha256Hex)((0, jcs_js_1.jcsBytes)(r.receipt.cell));
|
|
224
224
|
if (cellId !== request.cell_id)
|
|
225
225
|
return fail('E_ENVELOPE', `the receipt is for cell ${cellId}, not the requested ${request.cell_id}`, '$.receipt.cell', r.hash);
|
|
226
|
-
// With a requested asof:
|
|
226
|
+
// With a requested asof: at or before it, for ok and stale alike (D31, revised in spec-0.3.0). Instants are fixed
|
|
227
227
|
// width, so string order is time order (SPEC 3.3).
|
|
228
228
|
const asof = r.receipt.asof;
|
|
229
|
-
if (request.asof !== undefined &&
|
|
230
|
-
|
|
231
|
-
return fail('E_ENVELOPE', `the receipt answers asof ${asof}, not ${want}`, '$.receipt.asof', r.hash);
|
|
229
|
+
if (request.asof !== undefined && !(asof <= request.asof)) {
|
|
230
|
+
return fail('E_ENVELOPE', `the receipt answers asof ${asof}, later than the requested ${request.asof}`, '$.receipt.asof', r.hash);
|
|
232
231
|
}
|
|
233
232
|
// Step 6.
|
|
234
233
|
const trust = env.trust;
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** The spec revision this library conforms to. */
|
|
2
|
-
export declare const SPEC_VERSION = "spec-0.
|
|
3
|
-
/**
|
|
4
|
-
|
|
2
|
+
export declare const SPEC_VERSION = "spec-0.3.0";
|
|
3
|
+
/**
|
|
4
|
+
* Every error code defined by SPEC.md 10.2, in the order of that table. `E_EVAL_UNAVAILABLE` is
|
|
5
|
+
* the one code that is not a failure: `verify` reports it with success (10.1 step 7.6).
|
|
6
|
+
*/
|
|
7
|
+
export declare const ERROR_CODES: readonly ["E_PARSE_JSON", "E_PARSE_DUPLICATE_KEY", "E_PARSE_URI", "E_SCHEMA", "E_CANON_NAME", "E_CANON_ARG_NAME", "E_CANON_ARG_DUPLICATE", "E_CANON_ARG_ORDER", "E_CANON_PERCENT", "E_CANON_TYPE", "E_CANON_NUMBER", "E_CANON_UNIT", "E_CANON_DATE", "E_CANON_INSTANT", "E_CANON_STRING", "E_CANON_BLOB", "E_CANON_HEX", "E_CANON_LICENCE", "E_CANON_SET", "E_CANON_LIMIT", "E_SLICE", "E_PUBLISHER_MISMATCH", "E_VALID_MISMATCH", "E_TIME_ORDER", "E_ARG_UNKNOWN", "E_ARG_MISSING", "E_KEY_UNAVAILABLE", "E_KEY_DOC", "E_KEY_UNKNOWN", "E_KEY_NOT_VALID", "E_SIGNATURE", "E_FN_UNAVAILABLE", "E_FN_HASH_MISMATCH", "E_ARG_TYPE", "E_RETURN_TYPE", "E_INPUT_UNAVAILABLE", "E_INPUT_INVALID", "E_INPUT_UNDECLARED", "E_VALUE_MISMATCH", "E_EVAL_UNAVAILABLE", "E_BLOB_MISMATCH", "E_ENVELOPE"];
|
|
5
8
|
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
6
9
|
/** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
|
|
7
10
|
export declare class DatumeshError extends Error {
|
package/dist/cjs/errors.js
CHANGED
|
@@ -3,8 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DatumeshError = exports.ERROR_CODES = exports.SPEC_VERSION = void 0;
|
|
4
4
|
exports.fail = fail;
|
|
5
5
|
/** The spec revision this library conforms to. */
|
|
6
|
-
exports.SPEC_VERSION = 'spec-0.
|
|
7
|
-
/**
|
|
6
|
+
exports.SPEC_VERSION = 'spec-0.3.0';
|
|
7
|
+
/**
|
|
8
|
+
* Every error code defined by SPEC.md 10.2, in the order of that table. `E_EVAL_UNAVAILABLE` is
|
|
9
|
+
* the one code that is not a failure: `verify` reports it with success (10.1 step 7.6).
|
|
10
|
+
*/
|
|
8
11
|
exports.ERROR_CODES = [
|
|
9
12
|
'E_PARSE_JSON',
|
|
10
13
|
'E_PARSE_DUPLICATE_KEY',
|
|
@@ -45,6 +48,7 @@ exports.ERROR_CODES = [
|
|
|
45
48
|
'E_INPUT_INVALID',
|
|
46
49
|
'E_INPUT_UNDECLARED',
|
|
47
50
|
'E_VALUE_MISMATCH',
|
|
51
|
+
'E_EVAL_UNAVAILABLE',
|
|
48
52
|
'E_BLOB_MISMATCH',
|
|
49
53
|
'E_ENVELOPE',
|
|
50
54
|
];
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ export { DatumeshError, ERROR_CODES, SPEC_VERSION, type ErrorCode } from './erro
|
|
|
3
3
|
export { verify, type VerifyResult, type Recompute } from './verify.js';
|
|
4
4
|
export { checkEnvelope, type EnvelopeResult, type EnvelopeRequest, type Outcome, type Trust } from './envelope.js';
|
|
5
5
|
export { parseUri, formatUri, httpsUrl, type CellRef, type TypedScalar, type Signature, type Unit, type UnitRef } from './uri.js';
|
|
6
|
-
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, type KeysResult, type KeyEntry, type KeyMap, type KeySource } from './keys.js';
|
|
6
|
+
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, FETCH_MAX_BYTES, type KeysResult, type KeyEntry, type KeyMap, type KeySource } from './keys.js';
|
|
7
7
|
export type { Kind } from './validate.js';
|
|
8
8
|
export type { JsonValue } from './json.js';
|
|
9
9
|
/** Library version. */
|
|
10
|
-
export declare const VERSION = "0.
|
|
10
|
+
export declare const VERSION = "0.3.0";
|
|
11
11
|
/**
|
|
12
12
|
* Validate `input` as `kind` and return its RFC 8785 canonical bytes. `input` is JSON text,
|
|
13
13
|
* UTF-8 bytes, or an already-parsed value. `kind` is "scalar", "cell", "ref", "function" or
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VERSION = exports.FETCH_TIMEOUT_MS = exports.fetchKeys = exports.keysFromDocument = exports.keysFromResponse = exports.httpsUrl = exports.formatUri = exports.parseUri = exports.checkEnvelope = exports.verify = exports.SPEC_VERSION = exports.ERROR_CODES = exports.DatumeshError = void 0;
|
|
3
|
+
exports.VERSION = exports.FETCH_MAX_BYTES = exports.FETCH_TIMEOUT_MS = exports.fetchKeys = exports.keysFromDocument = exports.keysFromResponse = exports.httpsUrl = exports.formatUri = exports.parseUri = exports.checkEnvelope = exports.verify = exports.SPEC_VERSION = exports.ERROR_CODES = exports.DatumeshError = void 0;
|
|
4
4
|
exports.canonicalize = canonicalize;
|
|
5
5
|
exports.cellHash = cellHash;
|
|
6
6
|
exports.functionHash = functionHash;
|
|
@@ -9,7 +9,7 @@ exports.receiptPayload = receiptPayload;
|
|
|
9
9
|
exports.resolvedUnit = resolvedUnit;
|
|
10
10
|
exports.keyId = keyId;
|
|
11
11
|
/**
|
|
12
|
-
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.
|
|
12
|
+
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.3.0).
|
|
13
13
|
*
|
|
14
14
|
* Everything here is synchronous and offline except fetchKeys, the only function that uses the
|
|
15
15
|
* network.
|
|
@@ -37,8 +37,9 @@ Object.defineProperty(exports, "keysFromResponse", { enumerable: true, get: func
|
|
|
37
37
|
Object.defineProperty(exports, "keysFromDocument", { enumerable: true, get: function () { return keys_js_1.keysFromDocument; } });
|
|
38
38
|
Object.defineProperty(exports, "fetchKeys", { enumerable: true, get: function () { return keys_js_1.fetchKeys; } });
|
|
39
39
|
Object.defineProperty(exports, "FETCH_TIMEOUT_MS", { enumerable: true, get: function () { return keys_js_1.FETCH_TIMEOUT_MS; } });
|
|
40
|
+
Object.defineProperty(exports, "FETCH_MAX_BYTES", { enumerable: true, get: function () { return keys_js_1.FETCH_MAX_BYTES; } });
|
|
40
41
|
/** Library version. */
|
|
41
|
-
exports.VERSION = '0.
|
|
42
|
+
exports.VERSION = '0.3.0';
|
|
42
43
|
function parseAndValidate(input, kind) {
|
|
43
44
|
if (kind !== undefined && kind !== null && !validate_js_1.KINDS.includes(kind))
|
|
44
45
|
throw new TypeError(`unknown kind ${JSON.stringify(kind)}; expected one of ${validate_js_1.KINDS.join(', ')}`);
|
package/dist/cjs/json.d.ts
CHANGED
|
@@ -3,11 +3,10 @@ export interface JsonObject {
|
|
|
3
3
|
[key: string]: JsonValue;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* canonicalisation from exhausting the call stack.
|
|
6
|
+
* SPEC 6.1: at most 64 levels of nesting ([ and { open at once, counted outside strings), checked
|
|
7
|
+
* before anything else in the text. Datumesh objects nest at most five levels.
|
|
9
8
|
*/
|
|
10
|
-
export declare const MAX_DEPTH =
|
|
9
|
+
export declare const MAX_DEPTH = 64;
|
|
11
10
|
export declare function isObject(v: unknown): v is JsonObject;
|
|
12
11
|
export declare function newObject(): JsonObject;
|
|
13
12
|
export declare function has(o: JsonObject, k: string): boolean;
|
package/dist/cjs/json.js
CHANGED
|
@@ -16,11 +16,33 @@ const errors_js_1 = require("./errors.js");
|
|
|
16
16
|
const unicode_js_1 = require("./unicode.js");
|
|
17
17
|
const utf8_js_1 = require("./utf8.js");
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* canonicalisation from exhausting the call stack.
|
|
19
|
+
* SPEC 6.1: at most 64 levels of nesting ([ and { open at once, counted outside strings), checked
|
|
20
|
+
* before anything else in the text. Datumesh objects nest at most five levels.
|
|
22
21
|
*/
|
|
23
|
-
exports.MAX_DEPTH =
|
|
22
|
+
exports.MAX_DEPTH = 64;
|
|
23
|
+
/** SPEC 6.1 order step 3: true when more than MAX_DEPTH brackets are open at once. */
|
|
24
|
+
function tooDeep(s) {
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let inString = false;
|
|
27
|
+
for (let i = 0; i < s.length; i++) {
|
|
28
|
+
const c = s.charCodeAt(i);
|
|
29
|
+
if (inString) {
|
|
30
|
+
if (c === 0x5c)
|
|
31
|
+
i++; // skip the escaped character
|
|
32
|
+
else if (c === 0x22)
|
|
33
|
+
inString = false;
|
|
34
|
+
}
|
|
35
|
+
else if (c === 0x22)
|
|
36
|
+
inString = true;
|
|
37
|
+
else if (c === 0x5b || c === 0x7b) {
|
|
38
|
+
if (++depth > exports.MAX_DEPTH)
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
else if (c === 0x5d || c === 0x7d)
|
|
42
|
+
depth--;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
24
46
|
function isObject(v) {
|
|
25
47
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
26
48
|
}
|
|
@@ -70,7 +92,7 @@ class Parser {
|
|
|
70
92
|
}
|
|
71
93
|
value(path, depth) {
|
|
72
94
|
if (depth > exports.MAX_DEPTH)
|
|
73
|
-
this.err(`nesting deeper than ${exports.MAX_DEPTH} levels
|
|
95
|
+
this.err(`nesting deeper than ${exports.MAX_DEPTH} levels`); // tooDeep found it first
|
|
74
96
|
const s = this.s;
|
|
75
97
|
const c = s[this.i];
|
|
76
98
|
if (c === '{')
|
|
@@ -256,12 +278,16 @@ class Parser {
|
|
|
256
278
|
}
|
|
257
279
|
/** Parse JSON text strictly. */
|
|
258
280
|
function parseJsonText(text, numbers) {
|
|
281
|
+
// SPEC 6.1 order: BOM, (invalid UTF-8: parseJsonBytes), nesting, the first of syntax / NaN /
|
|
282
|
+
// overflow / duplicate in text order, then lone surrogates.
|
|
259
283
|
if (text.charCodeAt(0) === 0xfeff)
|
|
260
284
|
(0, errors_js_1.fail)('E_PARSE_JSON', 'byte order mark', '$');
|
|
261
|
-
if ((
|
|
262
|
-
(0, errors_js_1.fail)('E_PARSE_JSON',
|
|
285
|
+
if (tooDeep(text))
|
|
286
|
+
(0, errors_js_1.fail)('E_PARSE_JSON', `nesting deeper than ${exports.MAX_DEPTH} levels`, '$');
|
|
263
287
|
const p = new Parser(text, numbers);
|
|
264
288
|
const v = p.parseDocument();
|
|
289
|
+
if ((0, unicode_js_1.hasLoneSurrogate)(text))
|
|
290
|
+
(0, errors_js_1.fail)('E_PARSE_JSON', 'lone surrogate in the text', '$');
|
|
265
291
|
if (p.surrogatePath !== null)
|
|
266
292
|
(0, errors_js_1.fail)('E_PARSE_JSON', 'lone surrogate', p.surrogatePath);
|
|
267
293
|
return v;
|
|
@@ -283,7 +309,8 @@ function parseJsonBytes(bytes, numbers) {
|
|
|
283
309
|
* or bytes for that.
|
|
284
310
|
*/
|
|
285
311
|
function normalizeValue(v, path = '$', depth = 0) {
|
|
286
|
-
|
|
312
|
+
const container = typeof v === 'object' && v !== null;
|
|
313
|
+
if (container && depth >= exports.MAX_DEPTH)
|
|
287
314
|
(0, errors_js_1.fail)('E_PARSE_JSON', `nesting deeper than ${exports.MAX_DEPTH} levels (or a cycle)`, path);
|
|
288
315
|
if (v === null || typeof v === 'boolean')
|
|
289
316
|
return v;
|
package/dist/cjs/keys.d.ts
CHANGED
|
@@ -23,10 +23,11 @@ export type KeyMap = Record<string, string>;
|
|
|
23
23
|
export type KeySource = KeysResult | KeyMap | Record<string, unknown> | KeySource[];
|
|
24
24
|
/**
|
|
25
25
|
* Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
|
|
26
|
-
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure.
|
|
27
|
-
*
|
|
26
|
+
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. Only the
|
|
27
|
+
* integer 200 is a retrieved document: any other value (a string "200" included, or undefined)
|
|
28
|
+
* is E_KEY_UNAVAILABLE. `body` is the response body as text or bytes. Never throws.
|
|
28
29
|
*/
|
|
29
|
-
export declare function keysFromResponse(host: string, status:
|
|
30
|
+
export declare function keysFromResponse(host: string, status: unknown, body?: string | Uint8Array | null): KeysResult;
|
|
30
31
|
/**
|
|
31
32
|
* Validate an already-parsed key document (its `host` member names the host it covers).
|
|
32
33
|
* Never throws.
|
|
@@ -34,6 +35,8 @@ export declare function keysFromResponse(host: string, status: number | null, bo
|
|
|
34
35
|
export declare function keysFromDocument(doc: unknown): KeysResult;
|
|
35
36
|
/** Default timeout of fetchKeys, in milliseconds. */
|
|
36
37
|
export declare const FETCH_TIMEOUT_MS = 10000;
|
|
38
|
+
/** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
|
|
39
|
+
export declare const FETCH_MAX_BYTES: number;
|
|
37
40
|
/**
|
|
38
41
|
* Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
|
|
39
42
|
* redirects followed and a timeout, then interpret it with keysFromResponse. The only function
|
package/dist/cjs/keys.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FETCH_TIMEOUT_MS = void 0;
|
|
3
|
+
exports.FETCH_MAX_BYTES = exports.FETCH_TIMEOUT_MS = void 0;
|
|
4
4
|
exports.keysFromResponse = keysFromResponse;
|
|
5
5
|
exports.keysFromDocument = keysFromDocument;
|
|
6
6
|
exports.fetchKeys = fetchKeys;
|
|
@@ -11,6 +11,15 @@ const errors_js_1 = require("./errors.js");
|
|
|
11
11
|
const hash_js_1 = require("./hash.js");
|
|
12
12
|
const json_js_1 = require("./json.js");
|
|
13
13
|
const lexical_js_1 = require("./lexical.js");
|
|
14
|
+
function isDnsName(s) {
|
|
15
|
+
try {
|
|
16
|
+
(0, lexical_js_1.checkDnsName)(s, '$');
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
class KeyDocError extends Error {
|
|
15
24
|
constructor(msg, path) {
|
|
16
25
|
super(msg);
|
|
@@ -32,6 +41,9 @@ function validateKeyDocument(doc, host) {
|
|
|
32
41
|
bad(`missing member ${JSON.stringify(k)}`, '$');
|
|
33
42
|
if (doc.proto !== 'datumesh/1')
|
|
34
43
|
bad("proto must be 'datumesh/1'", '$.proto');
|
|
44
|
+
// 9.2 / 9.3: `host` is a DNS name (2.1); a document for any other host cannot satisfy 9.2.
|
|
45
|
+
if (typeof doc.host !== 'string' || !isDnsName(doc.host))
|
|
46
|
+
bad('host must be a DNS name (SPEC 2.1); keys are resolved only for DNS names', '$.host');
|
|
35
47
|
if (doc.host !== host)
|
|
36
48
|
bad(`host must be ${JSON.stringify(host)}`, '$.host');
|
|
37
49
|
const keys = doc.keys;
|
|
@@ -98,15 +110,21 @@ function docResult(host, parse) {
|
|
|
98
110
|
}
|
|
99
111
|
/**
|
|
100
112
|
* Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
|
|
101
|
-
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure.
|
|
102
|
-
*
|
|
113
|
+
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. Only the
|
|
114
|
+
* integer 200 is a retrieved document: any other value (a string "200" included, or undefined)
|
|
115
|
+
* is E_KEY_UNAVAILABLE. `body` is the response body as text or bytes. Never throws.
|
|
103
116
|
*/
|
|
104
117
|
function keysFromResponse(host, status, body) {
|
|
105
118
|
const h = typeof host === 'string' ? host : String(host);
|
|
106
119
|
try {
|
|
107
|
-
if (status === null
|
|
120
|
+
if (status === null) {
|
|
108
121
|
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'network, DNS, TLS or timeout failure' };
|
|
109
122
|
}
|
|
123
|
+
if (status === undefined)
|
|
124
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'no HTTP status' };
|
|
125
|
+
if (typeof status !== 'number') {
|
|
126
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${JSON.stringify(String(status))} is not an integer (only the integer 200 is a retrieved document)` };
|
|
127
|
+
}
|
|
110
128
|
if (status !== 200) {
|
|
111
129
|
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${String(status)} (only 200 is accepted; redirects are not followed)` };
|
|
112
130
|
}
|
|
@@ -134,6 +152,45 @@ function keysFromDocument(doc) {
|
|
|
134
152
|
}
|
|
135
153
|
/** Default timeout of fetchKeys, in milliseconds. */
|
|
136
154
|
exports.FETCH_TIMEOUT_MS = 10000;
|
|
155
|
+
/** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
|
|
156
|
+
exports.FETCH_MAX_BYTES = 256 * 1024;
|
|
157
|
+
class TooLarge extends Error {
|
|
158
|
+
}
|
|
159
|
+
/** Read a response body, abandoning it once it exceeds FETCH_MAX_BYTES. */
|
|
160
|
+
async function readBounded(res) {
|
|
161
|
+
const reader = res.body && typeof res.body.getReader === 'function' ? res.body.getReader() : null;
|
|
162
|
+
if (!reader) {
|
|
163
|
+
const b = new Uint8Array(await res.arrayBuffer());
|
|
164
|
+
if (b.length > exports.FETCH_MAX_BYTES)
|
|
165
|
+
throw new TooLarge();
|
|
166
|
+
return b;
|
|
167
|
+
}
|
|
168
|
+
const parts = [];
|
|
169
|
+
let n = 0;
|
|
170
|
+
for (;;) {
|
|
171
|
+
const { done, value } = await reader.read();
|
|
172
|
+
if (done)
|
|
173
|
+
break;
|
|
174
|
+
n += value.length;
|
|
175
|
+
if (n > exports.FETCH_MAX_BYTES) {
|
|
176
|
+
try {
|
|
177
|
+
await reader.cancel();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// abandoned anyway
|
|
181
|
+
}
|
|
182
|
+
throw new TooLarge();
|
|
183
|
+
}
|
|
184
|
+
parts.push(value);
|
|
185
|
+
}
|
|
186
|
+
const out = new Uint8Array(n);
|
|
187
|
+
let o = 0;
|
|
188
|
+
for (const p of parts) {
|
|
189
|
+
out.set(p, o);
|
|
190
|
+
o += p.length;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
137
194
|
/**
|
|
138
195
|
* Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
|
|
139
196
|
* redirects followed and a timeout, then interpret it with keysFromResponse. The only function
|
|
@@ -165,9 +222,11 @@ async function fetchKeys(host, timeoutMs = exports.FETCH_TIMEOUT_MS) {
|
|
|
165
222
|
status = 0;
|
|
166
223
|
if (status !== 200)
|
|
167
224
|
return keysFromResponse(host, status);
|
|
168
|
-
body =
|
|
225
|
+
body = await readBounded(res);
|
|
169
226
|
}
|
|
170
227
|
catch (e) {
|
|
228
|
+
if (e instanceof TooLarge)
|
|
229
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `key document larger than 256 KiB; retrieval abandoned` };
|
|
171
230
|
return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `retrieval failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
172
231
|
}
|
|
173
232
|
return keysFromResponse(host, status, body);
|
package/dist/cjs/uri.js
CHANGED
|
@@ -50,20 +50,29 @@ function encodeValue(s) {
|
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* SPEC 5.4: the signature is taken as given (it comes from a descriptor already checked
|
|
54
|
-
* section 7)
|
|
55
|
-
*
|
|
53
|
+
* SPEC 5.4 step 5: the signature is taken as given (it comes from a descriptor already checked
|
|
54
|
+
* under section 7), but it must be readable: an object with an `args` object whose members are
|
|
55
|
+
* objects with a string `type` (E_SCHEMA otherwise, in JCS order of names). Nothing else in it is
|
|
56
|
+
* examined; an unknown type name fails only when a value of that argument is read (step 6).
|
|
56
57
|
*/
|
|
57
58
|
function readSignature(signature) {
|
|
58
59
|
let sig;
|
|
59
60
|
try {
|
|
60
|
-
sig = (0, json_js_1.
|
|
61
|
+
sig = (0, json_js_1.normalizeValue)(signature, 'signature');
|
|
61
62
|
}
|
|
62
63
|
catch {
|
|
63
|
-
|
|
64
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'the signature is not JSON data', 'signature');
|
|
65
|
+
}
|
|
66
|
+
if (!(0, json_js_1.isObject)(sig))
|
|
67
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'the signature is not an object', 'signature');
|
|
68
|
+
const args = sig.args;
|
|
69
|
+
if (!(0, json_js_1.isObject)(args))
|
|
70
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'the signature has no args object', 'signature.args');
|
|
71
|
+
for (const k of (0, json_js_1.sortedKeys)(args)) {
|
|
72
|
+
const d = args[k];
|
|
73
|
+
if (!(0, json_js_1.isObject)(d) || typeof d.type !== 'string')
|
|
74
|
+
(0, errors_js_1.fail)('E_SCHEMA', `the declaration of ${JSON.stringify(k)} is not an object with a string type`, (0, json_js_1.memberPath)('signature.args', k));
|
|
64
75
|
}
|
|
65
|
-
if (!(0, json_js_1.isObject)(sig) || !(0, json_js_1.isObject)(sig.args))
|
|
66
|
-
throw new TypeError('parseUri needs the function signature (descriptor.signature): an object with args');
|
|
67
76
|
return sig;
|
|
68
77
|
}
|
|
69
78
|
/**
|
|
@@ -74,8 +83,6 @@ function readSignature(signature) {
|
|
|
74
83
|
function parseUri(uri, signature) {
|
|
75
84
|
if (typeof uri !== 'string')
|
|
76
85
|
throw new TypeError('uri must be a string');
|
|
77
|
-
const sig = readSignature(signature);
|
|
78
|
-
const declared = sig.args;
|
|
79
86
|
// 1. Outer grammar.
|
|
80
87
|
if (!uri.startsWith(SCHEME) || uri.includes('#'))
|
|
81
88
|
(0, errors_js_1.fail)('E_PARSE_URI', 'not cell://<namespace>/<function>[?<query>] without fragment', '$');
|
|
@@ -85,8 +92,7 @@ function parseUri(uri, signature) {
|
|
|
85
92
|
const query = q < 0 ? null : rest.slice(q + 1);
|
|
86
93
|
if (query === '')
|
|
87
94
|
(0, errors_js_1.fail)('E_PARSE_URI', 'empty query', '$');
|
|
88
|
-
// SPEC 5.1
|
|
89
|
-
// reference tool reports them as E_CANON_NAME; see DEFECTS.md.)
|
|
95
|
+
// SPEC 5.1: an @ or : in the authority is userinfo or a port (E_PARSE_URI).
|
|
90
96
|
const authority = path.split('/')[0];
|
|
91
97
|
if (authority.includes('@'))
|
|
92
98
|
(0, errors_js_1.fail)('E_PARSE_URI', 'a cell URI has no userinfo', '$');
|
|
@@ -135,7 +141,9 @@ function parseUri(uri, signature) {
|
|
|
135
141
|
// 4. Argument count.
|
|
136
142
|
if (argParams.length > lexical_js_1.LIMITS.args)
|
|
137
143
|
(0, errors_js_1.fail)('E_CANON_LIMIT', `more than ${lexical_js_1.LIMITS.args} arguments`, 'uri.query');
|
|
138
|
-
// 5.
|
|
144
|
+
// 5. The signature (readable, E_SCHEMA), then the arguments against it: unknown arguments,
|
|
145
|
+
// then missing ones.
|
|
146
|
+
const declared = readSignature(signature).args;
|
|
139
147
|
for (const p of argParams) {
|
|
140
148
|
if (!Object.prototype.hasOwnProperty.call(declared, p.name))
|
|
141
149
|
(0, errors_js_1.fail)('E_ARG_UNKNOWN', `function declares no argument ${JSON.stringify(p.name)}`, at(p.name));
|
|
@@ -152,13 +160,8 @@ function parseUri(uri, signature) {
|
|
|
152
160
|
args[p.name] = '*';
|
|
153
161
|
continue;
|
|
154
162
|
}
|
|
155
|
-
const
|
|
156
|
-
const type = ((0, json_js_1.isObject)(decl) ? decl.type : undefined);
|
|
163
|
+
const type = declared[p.name].type;
|
|
157
164
|
const s = decodeValue(p.value, at(p.name));
|
|
158
|
-
// The signature is not validated (5.4), but a value is read by its declared type: a
|
|
159
|
-
// declaration without a string type is E_SCHEMA, an unknown type name E_CANON_TYPE.
|
|
160
|
-
if (typeof type !== 'string')
|
|
161
|
-
(0, errors_js_1.fail)('E_SCHEMA', `the signature declares ${p.name} without a string type`, `signature.args.${p.name}`);
|
|
162
165
|
if (!(0, lexical_js_1.isTypeName)(type))
|
|
163
166
|
(0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(type)} in the signature`, `signature.args.${p.name}.type`);
|
|
164
167
|
if (type === 'boolean') {
|
|
@@ -205,5 +208,7 @@ function formatUri(ref) {
|
|
|
205
208
|
function httpsUrl(uri, mesh) {
|
|
206
209
|
if (!uri.startsWith(SCHEME))
|
|
207
210
|
(0, errors_js_1.fail)('E_PARSE_URI', 'not a cell URI', '$');
|
|
211
|
+
// SPEC 5.3: the mesh is a DNS name on the default HTTPS port (no port, IP address or userinfo).
|
|
212
|
+
(0, lexical_js_1.checkDnsName)(mesh, 'mesh');
|
|
208
213
|
return `https://${mesh}/cell/${uri.slice(SCHEME.length)}`;
|
|
209
214
|
}
|
package/dist/cjs/verify.d.ts
CHANGED
|
@@ -2,9 +2,17 @@ import { type ErrorCode } from './errors.js';
|
|
|
2
2
|
import { type JsonObject, type JsonValue } from './json.js';
|
|
3
3
|
import { type Sources } from './keys.js';
|
|
4
4
|
export interface VerifyResult {
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* True when the receipt is signature-valid (and, if recompute was given with a value, fully
|
|
7
|
+
* verified).
|
|
8
|
+
*/
|
|
6
9
|
ok: boolean;
|
|
7
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* "OK" or a SPEC 10.2 error code. With `ok` true it is "OK", or `E_EVAL_UNAVAILABLE` when
|
|
12
|
+
* recompute was given without a value: steps 7.1-7.5 passed but the function was not evaluated
|
|
13
|
+
* (SPEC 10.1 step 7.6), so `level` is "signed" and `path` is "$.value". That is the one code
|
|
14
|
+
* reported with success.
|
|
15
|
+
*/
|
|
8
16
|
code: 'OK' | ErrorCode;
|
|
9
17
|
/** "signed" (steps 1-6) or "recomputed" (steps 1-7) when ok, else null. */
|
|
10
18
|
level: 'signed' | 'recomputed' | null;
|
|
@@ -21,12 +29,21 @@ export interface VerifyResult {
|
|
|
21
29
|
}
|
|
22
30
|
/** Input for SPEC 10.1 step 7. The library does not evaluate functions; `value` is the result. */
|
|
23
31
|
export interface Recompute {
|
|
24
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The function descriptor for cell.fn_hash: a parsed JSON value, or JSON text / UTF-8 bytes,
|
|
34
|
+
* parsed strictly first. Absent (undefined): E_FN_UNAVAILABLE. Anything else obtained, `null`
|
|
35
|
+
* included, is a descriptor offered: if it is not the descriptor it fails the hash check
|
|
36
|
+
* (E_FN_HASH_MISMATCH; SPEC 10.1 step 7.1). A JS string is always JSON text.
|
|
37
|
+
*/
|
|
25
38
|
descriptor?: unknown;
|
|
26
39
|
/** Input receipts by receipt hash, as JSON text (or bytes, or parsed objects). */
|
|
27
40
|
inputs?: Record<string, unknown>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
41
|
+
/**
|
|
42
|
+
* What evaluating the function produced: a typed scalar. Absent (undefined) when the function
|
|
43
|
+
* could not be evaluated: once 7.1-7.5 pass, the result is success at level "signed" with code
|
|
44
|
+
* E_EVAL_UNAVAILABLE (SPEC 10.1 step 7.6).
|
|
45
|
+
*/
|
|
46
|
+
value?: {
|
|
30
47
|
type: string;
|
|
31
48
|
value: string | boolean;
|
|
32
49
|
};
|
|
@@ -40,7 +57,9 @@ declare class Stop extends Error {
|
|
|
40
57
|
/**
|
|
41
58
|
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
42
59
|
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
43
|
-
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too
|
|
60
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too; without
|
|
61
|
+
* its `value`, steps 7.1-7.5 run and the result is success at level "signed" with code
|
|
62
|
+
* E_EVAL_UNAVAILABLE (path "$.value") unless they fail. With
|
|
44
63
|
* `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
|
|
45
64
|
* otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
|
|
46
65
|
* content does not match. It does not change `level`; `blobChecked` reports it.
|
package/dist/cjs/verify.js
CHANGED
|
@@ -69,11 +69,12 @@ function verifySigned(input, sources, setHash) {
|
|
|
69
69
|
}
|
|
70
70
|
return { receipt, hash };
|
|
71
71
|
}
|
|
72
|
+
/** Step 7. Throws Stop; returns false when 7.6 could not run (no value: E_EVAL_UNAVAILABLE). */
|
|
72
73
|
function recompute(receipt, rc, sources) {
|
|
73
74
|
const cell = receipt.cell;
|
|
74
75
|
const cellArgs = cell.args;
|
|
75
|
-
// 7.1 Descriptor.
|
|
76
|
-
if (rc.descriptor === undefined
|
|
76
|
+
// 7.1 Descriptor. Only obtaining nothing is unavailability; null is a descriptor offered (D35).
|
|
77
|
+
if (rc.descriptor === undefined)
|
|
77
78
|
stop('E_FN_UNAVAILABLE', `no descriptor for ${cell.fn_hash}`, '$.cell.fn_hash');
|
|
78
79
|
let desc;
|
|
79
80
|
try {
|
|
@@ -136,18 +137,23 @@ function recompute(receipt, rc, sources) {
|
|
|
136
137
|
if (!declaredInputs.includes(`${ic.ns}/${ic.fn}`)) {
|
|
137
138
|
stop('E_INPUT_UNDECLARED', `input ${h} is a cell of an undeclared function`, '$.inputs');
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
// At or before the receipt's asof (D17, revised in spec-0.3.0); instants are fixed width, so
|
|
141
|
+
// string order is time order (SPEC 3.3).
|
|
142
|
+
if (!(inner.receipt.asof <= receipt.asof))
|
|
143
|
+
stop('E_TIME_ORDER', `input ${h} has asof ${inner.receipt.asof}, after the receipt's asof`, '$.inputs');
|
|
141
144
|
const itx = inner.receipt.tx;
|
|
142
145
|
if (latestTx === null || itx > latestTx)
|
|
143
146
|
latestTx = itx;
|
|
144
147
|
}
|
|
145
148
|
if (latestTx !== null && receipt.tx !== latestTx)
|
|
146
149
|
stop('E_TIME_ORDER', 'tx must be the latest input tx', '$.tx');
|
|
147
|
-
// 7.6 Value.
|
|
150
|
+
// 7.6 Value. No value: the function could not be evaluated (E_EVAL_UNAVAILABLE, not a failure).
|
|
148
151
|
const v = rc.value;
|
|
152
|
+
if (v === undefined)
|
|
153
|
+
return false;
|
|
149
154
|
if (v.type !== receipt.type || v.value !== receipt.value)
|
|
150
155
|
stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
|
|
156
|
+
return true;
|
|
151
157
|
}
|
|
152
158
|
/**
|
|
153
159
|
* Step 7.7: content is only for a blob value (E_SCHEMA otherwise); it has exactly `length`
|
|
@@ -177,8 +183,8 @@ function checkRecomputeArg(rc) {
|
|
|
177
183
|
if (typeof rc !== 'object' || Array.isArray(rc))
|
|
178
184
|
throw new TypeError('recompute must be an object {descriptor, inputs, value}');
|
|
179
185
|
const r = rc;
|
|
180
|
-
if (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean')) {
|
|
181
|
-
throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}');
|
|
186
|
+
if (r.value !== undefined && (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean'))) {
|
|
187
|
+
throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}, or absent when the function could not be evaluated');
|
|
182
188
|
}
|
|
183
189
|
if (r.inputs !== undefined && (typeof r.inputs !== 'object' || r.inputs === null || Array.isArray(r.inputs))) {
|
|
184
190
|
throw new TypeError('recompute.inputs must be an object mapping receipt hash to receipt JSON');
|
|
@@ -188,7 +194,9 @@ function checkRecomputeArg(rc) {
|
|
|
188
194
|
/**
|
|
189
195
|
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
190
196
|
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
191
|
-
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too
|
|
197
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too; without
|
|
198
|
+
* its `value`, steps 7.1-7.5 run and the result is success at level "signed" with code
|
|
199
|
+
* E_EVAL_UNAVAILABLE (path "$.value") unless they fail. With
|
|
192
200
|
* `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
|
|
193
201
|
* otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
|
|
194
202
|
* content does not match. It does not change `level`; `blobChecked` reports it.
|
|
@@ -203,20 +211,24 @@ function verify(receipt, keys, recomputeArg, blob) {
|
|
|
203
211
|
let hash = null;
|
|
204
212
|
try {
|
|
205
213
|
const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
|
|
206
|
-
|
|
207
|
-
recompute(r, rc, sources);
|
|
214
|
+
const evaluated = rc ? recompute(r, rc, sources) : false;
|
|
208
215
|
const checked = content !== null;
|
|
209
216
|
if (checked)
|
|
210
|
-
checkContent(r, content);
|
|
211
|
-
const
|
|
217
|
+
checkContent(r, content); // 7.7 also after E_EVAL_UNAVAILABLE; its failure replaces the code
|
|
218
|
+
const unavailable = rc !== null && !evaluated;
|
|
219
|
+
const what = evaluated
|
|
220
|
+
? 'fully verified: signature valid and value recomputed'
|
|
221
|
+
: unavailable
|
|
222
|
+
? `signature-valid: signed by ${r.computed_by}; descriptor, arguments and inputs check out, but the function could not be evaluated, so the value was not recomputed`
|
|
223
|
+
: `signature-valid: signed by ${r.computed_by}`;
|
|
212
224
|
return {
|
|
213
225
|
ok: true,
|
|
214
|
-
code: 'OK',
|
|
215
|
-
level:
|
|
226
|
+
code: unavailable ? 'E_EVAL_UNAVAILABLE' : 'OK',
|
|
227
|
+
level: evaluated ? 'recomputed' : 'signed',
|
|
216
228
|
hash,
|
|
217
229
|
key: r.computed_by,
|
|
218
230
|
detail: checked ? `${what}; blob content matches` : what,
|
|
219
|
-
path: null,
|
|
231
|
+
path: unavailable ? '$.value' : null,
|
|
220
232
|
blobChecked: checked,
|
|
221
233
|
};
|
|
222
234
|
}
|
package/dist/esm/ed25519.js
CHANGED
|
@@ -56,6 +56,8 @@ export function verifyEd25519Strict(sig, msg, pub) {
|
|
|
56
56
|
const h = sha512(concat(rBytes, pub, msg));
|
|
57
57
|
const k = le(h) % L;
|
|
58
58
|
// R' = [S]B - [k]A (multiplyUnsafe accepts 0; inputs are public, so variable time is fine).
|
|
59
|
+
// subtract() adds the negation of the point [k]A, as SPEC 10.1 step 6 requires; it is not
|
|
60
|
+
// [L - k]A, which differs for a mixed-order A (receipt/ed25519-mixed-order-key).
|
|
59
61
|
const SB = s === 0n ? Point.ZERO : Point.BASE.multiplyUnsafe(s);
|
|
60
62
|
const kA = k === 0n ? Point.ZERO : A.multiplyUnsafe(k);
|
|
61
63
|
const rCheck = SB.subtract(kA).toBytes();
|
package/dist/esm/envelope.js
CHANGED
|
@@ -220,12 +220,11 @@ export function checkEnvelope(status, body, keys, request) {
|
|
|
220
220
|
const cellId = sha256Hex(jcsBytes(r.receipt.cell));
|
|
221
221
|
if (cellId !== request.cell_id)
|
|
222
222
|
return fail('E_ENVELOPE', `the receipt is for cell ${cellId}, not the requested ${request.cell_id}`, '$.receipt.cell', r.hash);
|
|
223
|
-
// With a requested asof:
|
|
223
|
+
// With a requested asof: at or before it, for ok and stale alike (D31, revised in spec-0.3.0). Instants are fixed
|
|
224
224
|
// width, so string order is time order (SPEC 3.3).
|
|
225
225
|
const asof = r.receipt.asof;
|
|
226
|
-
if (request.asof !== undefined &&
|
|
227
|
-
|
|
228
|
-
return fail('E_ENVELOPE', `the receipt answers asof ${asof}, not ${want}`, '$.receipt.asof', r.hash);
|
|
226
|
+
if (request.asof !== undefined && !(asof <= request.asof)) {
|
|
227
|
+
return fail('E_ENVELOPE', `the receipt answers asof ${asof}, later than the requested ${request.asof}`, '$.receipt.asof', r.hash);
|
|
229
228
|
}
|
|
230
229
|
// Step 6.
|
|
231
230
|
const trust = env.trust;
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** The spec revision this library conforms to. */
|
|
2
|
-
export declare const SPEC_VERSION = "spec-0.
|
|
3
|
-
/**
|
|
4
|
-
|
|
2
|
+
export declare const SPEC_VERSION = "spec-0.3.0";
|
|
3
|
+
/**
|
|
4
|
+
* Every error code defined by SPEC.md 10.2, in the order of that table. `E_EVAL_UNAVAILABLE` is
|
|
5
|
+
* the one code that is not a failure: `verify` reports it with success (10.1 step 7.6).
|
|
6
|
+
*/
|
|
7
|
+
export declare const ERROR_CODES: readonly ["E_PARSE_JSON", "E_PARSE_DUPLICATE_KEY", "E_PARSE_URI", "E_SCHEMA", "E_CANON_NAME", "E_CANON_ARG_NAME", "E_CANON_ARG_DUPLICATE", "E_CANON_ARG_ORDER", "E_CANON_PERCENT", "E_CANON_TYPE", "E_CANON_NUMBER", "E_CANON_UNIT", "E_CANON_DATE", "E_CANON_INSTANT", "E_CANON_STRING", "E_CANON_BLOB", "E_CANON_HEX", "E_CANON_LICENCE", "E_CANON_SET", "E_CANON_LIMIT", "E_SLICE", "E_PUBLISHER_MISMATCH", "E_VALID_MISMATCH", "E_TIME_ORDER", "E_ARG_UNKNOWN", "E_ARG_MISSING", "E_KEY_UNAVAILABLE", "E_KEY_DOC", "E_KEY_UNKNOWN", "E_KEY_NOT_VALID", "E_SIGNATURE", "E_FN_UNAVAILABLE", "E_FN_HASH_MISMATCH", "E_ARG_TYPE", "E_RETURN_TYPE", "E_INPUT_UNAVAILABLE", "E_INPUT_INVALID", "E_INPUT_UNDECLARED", "E_VALUE_MISMATCH", "E_EVAL_UNAVAILABLE", "E_BLOB_MISMATCH", "E_ENVELOPE"];
|
|
5
8
|
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
6
9
|
/** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
|
|
7
10
|
export declare class DatumeshError extends Error {
|
package/dist/esm/errors.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** The spec revision this library conforms to. */
|
|
2
|
-
export const SPEC_VERSION = 'spec-0.
|
|
3
|
-
/**
|
|
2
|
+
export const SPEC_VERSION = 'spec-0.3.0';
|
|
3
|
+
/**
|
|
4
|
+
* Every error code defined by SPEC.md 10.2, in the order of that table. `E_EVAL_UNAVAILABLE` is
|
|
5
|
+
* the one code that is not a failure: `verify` reports it with success (10.1 step 7.6).
|
|
6
|
+
*/
|
|
4
7
|
export const ERROR_CODES = [
|
|
5
8
|
'E_PARSE_JSON',
|
|
6
9
|
'E_PARSE_DUPLICATE_KEY',
|
|
@@ -41,6 +44,7 @@ export const ERROR_CODES = [
|
|
|
41
44
|
'E_INPUT_INVALID',
|
|
42
45
|
'E_INPUT_UNDECLARED',
|
|
43
46
|
'E_VALUE_MISMATCH',
|
|
47
|
+
'E_EVAL_UNAVAILABLE',
|
|
44
48
|
'E_BLOB_MISMATCH',
|
|
45
49
|
'E_ENVELOPE',
|
|
46
50
|
];
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ export { DatumeshError, ERROR_CODES, SPEC_VERSION, type ErrorCode } from './erro
|
|
|
3
3
|
export { verify, type VerifyResult, type Recompute } from './verify.js';
|
|
4
4
|
export { checkEnvelope, type EnvelopeResult, type EnvelopeRequest, type Outcome, type Trust } from './envelope.js';
|
|
5
5
|
export { parseUri, formatUri, httpsUrl, type CellRef, type TypedScalar, type Signature, type Unit, type UnitRef } from './uri.js';
|
|
6
|
-
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, type KeysResult, type KeyEntry, type KeyMap, type KeySource } from './keys.js';
|
|
6
|
+
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, FETCH_MAX_BYTES, type KeysResult, type KeyEntry, type KeyMap, type KeySource } from './keys.js';
|
|
7
7
|
export type { Kind } from './validate.js';
|
|
8
8
|
export type { JsonValue } from './json.js';
|
|
9
9
|
/** Library version. */
|
|
10
|
-
export declare const VERSION = "0.
|
|
10
|
+
export declare const VERSION = "0.3.0";
|
|
11
11
|
/**
|
|
12
12
|
* Validate `input` as `kind` and return its RFC 8785 canonical bytes. `input` is JSON text,
|
|
13
13
|
* UTF-8 bytes, or an already-parsed value. `kind` is "scalar", "cell", "ref", "function" or
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.
|
|
2
|
+
* @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.3.0).
|
|
3
3
|
*
|
|
4
4
|
* Everything here is synchronous and offline except fetchKeys, the only function that uses the
|
|
5
5
|
* network.
|
|
@@ -14,9 +14,9 @@ export { DatumeshError, ERROR_CODES, SPEC_VERSION } from './errors.js';
|
|
|
14
14
|
export { verify } from './verify.js';
|
|
15
15
|
export { checkEnvelope } from './envelope.js';
|
|
16
16
|
export { parseUri, formatUri, httpsUrl } from './uri.js';
|
|
17
|
-
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS } from './keys.js';
|
|
17
|
+
export { keysFromResponse, keysFromDocument, fetchKeys, FETCH_TIMEOUT_MS, FETCH_MAX_BYTES } from './keys.js';
|
|
18
18
|
/** Library version. */
|
|
19
|
-
export const VERSION = '0.
|
|
19
|
+
export const VERSION = '0.3.0';
|
|
20
20
|
function parseAndValidate(input, kind) {
|
|
21
21
|
if (kind !== undefined && kind !== null && !KINDS.includes(kind))
|
|
22
22
|
throw new TypeError(`unknown kind ${JSON.stringify(kind)}; expected one of ${KINDS.join(', ')}`);
|
package/dist/esm/json.d.ts
CHANGED
|
@@ -3,11 +3,10 @@ export interface JsonObject {
|
|
|
3
3
|
[key: string]: JsonValue;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* canonicalisation from exhausting the call stack.
|
|
6
|
+
* SPEC 6.1: at most 64 levels of nesting ([ and { open at once, counted outside strings), checked
|
|
7
|
+
* before anything else in the text. Datumesh objects nest at most five levels.
|
|
9
8
|
*/
|
|
10
|
-
export declare const MAX_DEPTH =
|
|
9
|
+
export declare const MAX_DEPTH = 64;
|
|
11
10
|
export declare function isObject(v: unknown): v is JsonObject;
|
|
12
11
|
export declare function newObject(): JsonObject;
|
|
13
12
|
export declare function has(o: JsonObject, k: string): boolean;
|
package/dist/esm/json.js
CHANGED
|
@@ -4,11 +4,33 @@ import { fail } from './errors.js';
|
|
|
4
4
|
import { hasLoneSurrogate } from './unicode.js';
|
|
5
5
|
import { utf8Decode } from './utf8.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* canonicalisation from exhausting the call stack.
|
|
7
|
+
* SPEC 6.1: at most 64 levels of nesting ([ and { open at once, counted outside strings), checked
|
|
8
|
+
* before anything else in the text. Datumesh objects nest at most five levels.
|
|
10
9
|
*/
|
|
11
|
-
export const MAX_DEPTH =
|
|
10
|
+
export const MAX_DEPTH = 64;
|
|
11
|
+
/** SPEC 6.1 order step 3: true when more than MAX_DEPTH brackets are open at once. */
|
|
12
|
+
function tooDeep(s) {
|
|
13
|
+
let depth = 0;
|
|
14
|
+
let inString = false;
|
|
15
|
+
for (let i = 0; i < s.length; i++) {
|
|
16
|
+
const c = s.charCodeAt(i);
|
|
17
|
+
if (inString) {
|
|
18
|
+
if (c === 0x5c)
|
|
19
|
+
i++; // skip the escaped character
|
|
20
|
+
else if (c === 0x22)
|
|
21
|
+
inString = false;
|
|
22
|
+
}
|
|
23
|
+
else if (c === 0x22)
|
|
24
|
+
inString = true;
|
|
25
|
+
else if (c === 0x5b || c === 0x7b) {
|
|
26
|
+
if (++depth > MAX_DEPTH)
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
else if (c === 0x5d || c === 0x7d)
|
|
30
|
+
depth--;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
12
34
|
export function isObject(v) {
|
|
13
35
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
14
36
|
}
|
|
@@ -58,7 +80,7 @@ class Parser {
|
|
|
58
80
|
}
|
|
59
81
|
value(path, depth) {
|
|
60
82
|
if (depth > MAX_DEPTH)
|
|
61
|
-
this.err(`nesting deeper than ${MAX_DEPTH} levels
|
|
83
|
+
this.err(`nesting deeper than ${MAX_DEPTH} levels`); // tooDeep found it first
|
|
62
84
|
const s = this.s;
|
|
63
85
|
const c = s[this.i];
|
|
64
86
|
if (c === '{')
|
|
@@ -244,12 +266,16 @@ class Parser {
|
|
|
244
266
|
}
|
|
245
267
|
/** Parse JSON text strictly. */
|
|
246
268
|
export function parseJsonText(text, numbers) {
|
|
269
|
+
// SPEC 6.1 order: BOM, (invalid UTF-8: parseJsonBytes), nesting, the first of syntax / NaN /
|
|
270
|
+
// overflow / duplicate in text order, then lone surrogates.
|
|
247
271
|
if (text.charCodeAt(0) === 0xfeff)
|
|
248
272
|
fail('E_PARSE_JSON', 'byte order mark', '$');
|
|
249
|
-
if (
|
|
250
|
-
fail('E_PARSE_JSON',
|
|
273
|
+
if (tooDeep(text))
|
|
274
|
+
fail('E_PARSE_JSON', `nesting deeper than ${MAX_DEPTH} levels`, '$');
|
|
251
275
|
const p = new Parser(text, numbers);
|
|
252
276
|
const v = p.parseDocument();
|
|
277
|
+
if (hasLoneSurrogate(text))
|
|
278
|
+
fail('E_PARSE_JSON', 'lone surrogate in the text', '$');
|
|
253
279
|
if (p.surrogatePath !== null)
|
|
254
280
|
fail('E_PARSE_JSON', 'lone surrogate', p.surrogatePath);
|
|
255
281
|
return v;
|
|
@@ -271,7 +297,8 @@ export function parseJsonBytes(bytes, numbers) {
|
|
|
271
297
|
* or bytes for that.
|
|
272
298
|
*/
|
|
273
299
|
export function normalizeValue(v, path = '$', depth = 0) {
|
|
274
|
-
|
|
300
|
+
const container = typeof v === 'object' && v !== null;
|
|
301
|
+
if (container && depth >= MAX_DEPTH)
|
|
275
302
|
fail('E_PARSE_JSON', `nesting deeper than ${MAX_DEPTH} levels (or a cycle)`, path);
|
|
276
303
|
if (v === null || typeof v === 'boolean')
|
|
277
304
|
return v;
|
package/dist/esm/keys.d.ts
CHANGED
|
@@ -23,10 +23,11 @@ export type KeyMap = Record<string, string>;
|
|
|
23
23
|
export type KeySource = KeysResult | KeyMap | Record<string, unknown> | KeySource[];
|
|
24
24
|
/**
|
|
25
25
|
* Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
|
|
26
|
-
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure.
|
|
27
|
-
*
|
|
26
|
+
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. Only the
|
|
27
|
+
* integer 200 is a retrieved document: any other value (a string "200" included, or undefined)
|
|
28
|
+
* is E_KEY_UNAVAILABLE. `body` is the response body as text or bytes. Never throws.
|
|
28
29
|
*/
|
|
29
|
-
export declare function keysFromResponse(host: string, status:
|
|
30
|
+
export declare function keysFromResponse(host: string, status: unknown, body?: string | Uint8Array | null): KeysResult;
|
|
30
31
|
/**
|
|
31
32
|
* Validate an already-parsed key document (its `host` member names the host it covers).
|
|
32
33
|
* Never throws.
|
|
@@ -34,6 +35,8 @@ export declare function keysFromResponse(host: string, status: number | null, bo
|
|
|
34
35
|
export declare function keysFromDocument(doc: unknown): KeysResult;
|
|
35
36
|
/** Default timeout of fetchKeys, in milliseconds. */
|
|
36
37
|
export declare const FETCH_TIMEOUT_MS = 10000;
|
|
38
|
+
/** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
|
|
39
|
+
export declare const FETCH_MAX_BYTES: number;
|
|
37
40
|
/**
|
|
38
41
|
* Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
|
|
39
42
|
* redirects followed and a timeout, then interpret it with keysFromResponse. The only function
|
package/dist/esm/keys.js
CHANGED
|
@@ -3,6 +3,15 @@ import { DatumeshError } from './errors.js';
|
|
|
3
3
|
import { fromHex, keyIdOf } from './hash.js';
|
|
4
4
|
import { has, isObject, memberPath, sortedKeys, toJson } from './json.js';
|
|
5
5
|
import { LIMITS, checkDnsName, isHex, isInstant } from './lexical.js';
|
|
6
|
+
function isDnsName(s) {
|
|
7
|
+
try {
|
|
8
|
+
checkDnsName(s, '$');
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
6
15
|
class KeyDocError extends Error {
|
|
7
16
|
constructor(msg, path) {
|
|
8
17
|
super(msg);
|
|
@@ -24,6 +33,9 @@ function validateKeyDocument(doc, host) {
|
|
|
24
33
|
bad(`missing member ${JSON.stringify(k)}`, '$');
|
|
25
34
|
if (doc.proto !== 'datumesh/1')
|
|
26
35
|
bad("proto must be 'datumesh/1'", '$.proto');
|
|
36
|
+
// 9.2 / 9.3: `host` is a DNS name (2.1); a document for any other host cannot satisfy 9.2.
|
|
37
|
+
if (typeof doc.host !== 'string' || !isDnsName(doc.host))
|
|
38
|
+
bad('host must be a DNS name (SPEC 2.1); keys are resolved only for DNS names', '$.host');
|
|
27
39
|
if (doc.host !== host)
|
|
28
40
|
bad(`host must be ${JSON.stringify(host)}`, '$.host');
|
|
29
41
|
const keys = doc.keys;
|
|
@@ -90,15 +102,21 @@ function docResult(host, parse) {
|
|
|
90
102
|
}
|
|
91
103
|
/**
|
|
92
104
|
* Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
|
|
93
|
-
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure.
|
|
94
|
-
*
|
|
105
|
+
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. Only the
|
|
106
|
+
* integer 200 is a retrieved document: any other value (a string "200" included, or undefined)
|
|
107
|
+
* is E_KEY_UNAVAILABLE. `body` is the response body as text or bytes. Never throws.
|
|
95
108
|
*/
|
|
96
109
|
export function keysFromResponse(host, status, body) {
|
|
97
110
|
const h = typeof host === 'string' ? host : String(host);
|
|
98
111
|
try {
|
|
99
|
-
if (status === null
|
|
112
|
+
if (status === null) {
|
|
100
113
|
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'network, DNS, TLS or timeout failure' };
|
|
101
114
|
}
|
|
115
|
+
if (status === undefined)
|
|
116
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'no HTTP status' };
|
|
117
|
+
if (typeof status !== 'number') {
|
|
118
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${JSON.stringify(String(status))} is not an integer (only the integer 200 is a retrieved document)` };
|
|
119
|
+
}
|
|
102
120
|
if (status !== 200) {
|
|
103
121
|
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${String(status)} (only 200 is accepted; redirects are not followed)` };
|
|
104
122
|
}
|
|
@@ -126,6 +144,45 @@ export function keysFromDocument(doc) {
|
|
|
126
144
|
}
|
|
127
145
|
/** Default timeout of fetchKeys, in milliseconds. */
|
|
128
146
|
export const FETCH_TIMEOUT_MS = 10000;
|
|
147
|
+
/** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
|
|
148
|
+
export const FETCH_MAX_BYTES = 256 * 1024;
|
|
149
|
+
class TooLarge extends Error {
|
|
150
|
+
}
|
|
151
|
+
/** Read a response body, abandoning it once it exceeds FETCH_MAX_BYTES. */
|
|
152
|
+
async function readBounded(res) {
|
|
153
|
+
const reader = res.body && typeof res.body.getReader === 'function' ? res.body.getReader() : null;
|
|
154
|
+
if (!reader) {
|
|
155
|
+
const b = new Uint8Array(await res.arrayBuffer());
|
|
156
|
+
if (b.length > FETCH_MAX_BYTES)
|
|
157
|
+
throw new TooLarge();
|
|
158
|
+
return b;
|
|
159
|
+
}
|
|
160
|
+
const parts = [];
|
|
161
|
+
let n = 0;
|
|
162
|
+
for (;;) {
|
|
163
|
+
const { done, value } = await reader.read();
|
|
164
|
+
if (done)
|
|
165
|
+
break;
|
|
166
|
+
n += value.length;
|
|
167
|
+
if (n > FETCH_MAX_BYTES) {
|
|
168
|
+
try {
|
|
169
|
+
await reader.cancel();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// abandoned anyway
|
|
173
|
+
}
|
|
174
|
+
throw new TooLarge();
|
|
175
|
+
}
|
|
176
|
+
parts.push(value);
|
|
177
|
+
}
|
|
178
|
+
const out = new Uint8Array(n);
|
|
179
|
+
let o = 0;
|
|
180
|
+
for (const p of parts) {
|
|
181
|
+
out.set(p, o);
|
|
182
|
+
o += p.length;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
129
186
|
/**
|
|
130
187
|
* Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
|
|
131
188
|
* redirects followed and a timeout, then interpret it with keysFromResponse. The only function
|
|
@@ -157,9 +214,11 @@ export async function fetchKeys(host, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
|
157
214
|
status = 0;
|
|
158
215
|
if (status !== 200)
|
|
159
216
|
return keysFromResponse(host, status);
|
|
160
|
-
body =
|
|
217
|
+
body = await readBounded(res);
|
|
161
218
|
}
|
|
162
219
|
catch (e) {
|
|
220
|
+
if (e instanceof TooLarge)
|
|
221
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `key document larger than 256 KiB; retrieval abandoned` };
|
|
163
222
|
return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `retrieval failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
164
223
|
}
|
|
165
224
|
return keysFromResponse(host, status, body);
|
package/dist/esm/uri.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Cell URIs (SPEC.md section 5).
|
|
2
2
|
import { fail } from './errors.js';
|
|
3
|
-
import { isObject, sortedKeys, toJson } from './json.js';
|
|
4
|
-
import { LIMITS, checkArgName, checkInstant, checkName, checkNamespace, checkTypedValue, isTypeName } from './lexical.js';
|
|
3
|
+
import { isObject, memberPath, normalizeValue, sortedKeys, toJson } from './json.js';
|
|
4
|
+
import { LIMITS, checkArgName, checkDnsName, checkInstant, checkName, checkNamespace, checkTypedValue, isTypeName } from './lexical.js';
|
|
5
5
|
import { utf8Decode, utf8Encode } from './utf8.js';
|
|
6
6
|
import { validate } from './validate.js';
|
|
7
7
|
const SCHEME = 'cell://';
|
|
@@ -45,20 +45,29 @@ function encodeValue(s) {
|
|
|
45
45
|
return out;
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* SPEC 5.4: the signature is taken as given (it comes from a descriptor already checked
|
|
49
|
-
* section 7)
|
|
50
|
-
*
|
|
48
|
+
* SPEC 5.4 step 5: the signature is taken as given (it comes from a descriptor already checked
|
|
49
|
+
* under section 7), but it must be readable: an object with an `args` object whose members are
|
|
50
|
+
* objects with a string `type` (E_SCHEMA otherwise, in JCS order of names). Nothing else in it is
|
|
51
|
+
* examined; an unknown type name fails only when a value of that argument is read (step 6).
|
|
51
52
|
*/
|
|
52
53
|
function readSignature(signature) {
|
|
53
54
|
let sig;
|
|
54
55
|
try {
|
|
55
|
-
sig =
|
|
56
|
+
sig = normalizeValue(signature, 'signature');
|
|
56
57
|
}
|
|
57
58
|
catch {
|
|
58
|
-
|
|
59
|
+
fail('E_SCHEMA', 'the signature is not JSON data', 'signature');
|
|
60
|
+
}
|
|
61
|
+
if (!isObject(sig))
|
|
62
|
+
fail('E_SCHEMA', 'the signature is not an object', 'signature');
|
|
63
|
+
const args = sig.args;
|
|
64
|
+
if (!isObject(args))
|
|
65
|
+
fail('E_SCHEMA', 'the signature has no args object', 'signature.args');
|
|
66
|
+
for (const k of sortedKeys(args)) {
|
|
67
|
+
const d = args[k];
|
|
68
|
+
if (!isObject(d) || typeof d.type !== 'string')
|
|
69
|
+
fail('E_SCHEMA', `the declaration of ${JSON.stringify(k)} is not an object with a string type`, memberPath('signature.args', k));
|
|
59
70
|
}
|
|
60
|
-
if (!isObject(sig) || !isObject(sig.args))
|
|
61
|
-
throw new TypeError('parseUri needs the function signature (descriptor.signature): an object with args');
|
|
62
71
|
return sig;
|
|
63
72
|
}
|
|
64
73
|
/**
|
|
@@ -69,8 +78,6 @@ function readSignature(signature) {
|
|
|
69
78
|
export function parseUri(uri, signature) {
|
|
70
79
|
if (typeof uri !== 'string')
|
|
71
80
|
throw new TypeError('uri must be a string');
|
|
72
|
-
const sig = readSignature(signature);
|
|
73
|
-
const declared = sig.args;
|
|
74
81
|
// 1. Outer grammar.
|
|
75
82
|
if (!uri.startsWith(SCHEME) || uri.includes('#'))
|
|
76
83
|
fail('E_PARSE_URI', 'not cell://<namespace>/<function>[?<query>] without fragment', '$');
|
|
@@ -80,8 +87,7 @@ export function parseUri(uri, signature) {
|
|
|
80
87
|
const query = q < 0 ? null : rest.slice(q + 1);
|
|
81
88
|
if (query === '')
|
|
82
89
|
fail('E_PARSE_URI', 'empty query', '$');
|
|
83
|
-
// SPEC 5.1
|
|
84
|
-
// reference tool reports them as E_CANON_NAME; see DEFECTS.md.)
|
|
90
|
+
// SPEC 5.1: an @ or : in the authority is userinfo or a port (E_PARSE_URI).
|
|
85
91
|
const authority = path.split('/')[0];
|
|
86
92
|
if (authority.includes('@'))
|
|
87
93
|
fail('E_PARSE_URI', 'a cell URI has no userinfo', '$');
|
|
@@ -130,7 +136,9 @@ export function parseUri(uri, signature) {
|
|
|
130
136
|
// 4. Argument count.
|
|
131
137
|
if (argParams.length > LIMITS.args)
|
|
132
138
|
fail('E_CANON_LIMIT', `more than ${LIMITS.args} arguments`, 'uri.query');
|
|
133
|
-
// 5.
|
|
139
|
+
// 5. The signature (readable, E_SCHEMA), then the arguments against it: unknown arguments,
|
|
140
|
+
// then missing ones.
|
|
141
|
+
const declared = readSignature(signature).args;
|
|
134
142
|
for (const p of argParams) {
|
|
135
143
|
if (!Object.prototype.hasOwnProperty.call(declared, p.name))
|
|
136
144
|
fail('E_ARG_UNKNOWN', `function declares no argument ${JSON.stringify(p.name)}`, at(p.name));
|
|
@@ -147,13 +155,8 @@ export function parseUri(uri, signature) {
|
|
|
147
155
|
args[p.name] = '*';
|
|
148
156
|
continue;
|
|
149
157
|
}
|
|
150
|
-
const
|
|
151
|
-
const type = (isObject(decl) ? decl.type : undefined);
|
|
158
|
+
const type = declared[p.name].type;
|
|
152
159
|
const s = decodeValue(p.value, at(p.name));
|
|
153
|
-
// The signature is not validated (5.4), but a value is read by its declared type: a
|
|
154
|
-
// declaration without a string type is E_SCHEMA, an unknown type name E_CANON_TYPE.
|
|
155
|
-
if (typeof type !== 'string')
|
|
156
|
-
fail('E_SCHEMA', `the signature declares ${p.name} without a string type`, `signature.args.${p.name}`);
|
|
157
160
|
if (!isTypeName(type))
|
|
158
161
|
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(type)} in the signature`, `signature.args.${p.name}.type`);
|
|
159
162
|
if (type === 'boolean') {
|
|
@@ -200,5 +203,7 @@ export function formatUri(ref) {
|
|
|
200
203
|
export function httpsUrl(uri, mesh) {
|
|
201
204
|
if (!uri.startsWith(SCHEME))
|
|
202
205
|
fail('E_PARSE_URI', 'not a cell URI', '$');
|
|
206
|
+
// SPEC 5.3: the mesh is a DNS name on the default HTTPS port (no port, IP address or userinfo).
|
|
207
|
+
checkDnsName(mesh, 'mesh');
|
|
203
208
|
return `https://${mesh}/cell/${uri.slice(SCHEME.length)}`;
|
|
204
209
|
}
|
package/dist/esm/verify.d.ts
CHANGED
|
@@ -2,9 +2,17 @@ import { type ErrorCode } from './errors.js';
|
|
|
2
2
|
import { type JsonObject, type JsonValue } from './json.js';
|
|
3
3
|
import { type Sources } from './keys.js';
|
|
4
4
|
export interface VerifyResult {
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* True when the receipt is signature-valid (and, if recompute was given with a value, fully
|
|
7
|
+
* verified).
|
|
8
|
+
*/
|
|
6
9
|
ok: boolean;
|
|
7
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* "OK" or a SPEC 10.2 error code. With `ok` true it is "OK", or `E_EVAL_UNAVAILABLE` when
|
|
12
|
+
* recompute was given without a value: steps 7.1-7.5 passed but the function was not evaluated
|
|
13
|
+
* (SPEC 10.1 step 7.6), so `level` is "signed" and `path` is "$.value". That is the one code
|
|
14
|
+
* reported with success.
|
|
15
|
+
*/
|
|
8
16
|
code: 'OK' | ErrorCode;
|
|
9
17
|
/** "signed" (steps 1-6) or "recomputed" (steps 1-7) when ok, else null. */
|
|
10
18
|
level: 'signed' | 'recomputed' | null;
|
|
@@ -21,12 +29,21 @@ export interface VerifyResult {
|
|
|
21
29
|
}
|
|
22
30
|
/** Input for SPEC 10.1 step 7. The library does not evaluate functions; `value` is the result. */
|
|
23
31
|
export interface Recompute {
|
|
24
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The function descriptor for cell.fn_hash: a parsed JSON value, or JSON text / UTF-8 bytes,
|
|
34
|
+
* parsed strictly first. Absent (undefined): E_FN_UNAVAILABLE. Anything else obtained, `null`
|
|
35
|
+
* included, is a descriptor offered: if it is not the descriptor it fails the hash check
|
|
36
|
+
* (E_FN_HASH_MISMATCH; SPEC 10.1 step 7.1). A JS string is always JSON text.
|
|
37
|
+
*/
|
|
25
38
|
descriptor?: unknown;
|
|
26
39
|
/** Input receipts by receipt hash, as JSON text (or bytes, or parsed objects). */
|
|
27
40
|
inputs?: Record<string, unknown>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
41
|
+
/**
|
|
42
|
+
* What evaluating the function produced: a typed scalar. Absent (undefined) when the function
|
|
43
|
+
* could not be evaluated: once 7.1-7.5 pass, the result is success at level "signed" with code
|
|
44
|
+
* E_EVAL_UNAVAILABLE (SPEC 10.1 step 7.6).
|
|
45
|
+
*/
|
|
46
|
+
value?: {
|
|
30
47
|
type: string;
|
|
31
48
|
value: string | boolean;
|
|
32
49
|
};
|
|
@@ -40,7 +57,9 @@ declare class Stop extends Error {
|
|
|
40
57
|
/**
|
|
41
58
|
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
42
59
|
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
43
|
-
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too
|
|
60
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too; without
|
|
61
|
+
* its `value`, steps 7.1-7.5 run and the result is success at level "signed" with code
|
|
62
|
+
* E_EVAL_UNAVAILABLE (path "$.value") unless they fail. With
|
|
44
63
|
* `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
|
|
45
64
|
* otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
|
|
46
65
|
* content does not match. It does not change `level`; `blobChecked` reports it.
|
package/dist/esm/verify.js
CHANGED
|
@@ -63,11 +63,12 @@ function verifySigned(input, sources, setHash) {
|
|
|
63
63
|
}
|
|
64
64
|
return { receipt, hash };
|
|
65
65
|
}
|
|
66
|
+
/** Step 7. Throws Stop; returns false when 7.6 could not run (no value: E_EVAL_UNAVAILABLE). */
|
|
66
67
|
function recompute(receipt, rc, sources) {
|
|
67
68
|
const cell = receipt.cell;
|
|
68
69
|
const cellArgs = cell.args;
|
|
69
|
-
// 7.1 Descriptor.
|
|
70
|
-
if (rc.descriptor === undefined
|
|
70
|
+
// 7.1 Descriptor. Only obtaining nothing is unavailability; null is a descriptor offered (D35).
|
|
71
|
+
if (rc.descriptor === undefined)
|
|
71
72
|
stop('E_FN_UNAVAILABLE', `no descriptor for ${cell.fn_hash}`, '$.cell.fn_hash');
|
|
72
73
|
let desc;
|
|
73
74
|
try {
|
|
@@ -130,18 +131,23 @@ function recompute(receipt, rc, sources) {
|
|
|
130
131
|
if (!declaredInputs.includes(`${ic.ns}/${ic.fn}`)) {
|
|
131
132
|
stop('E_INPUT_UNDECLARED', `input ${h} is a cell of an undeclared function`, '$.inputs');
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// At or before the receipt's asof (D17, revised in spec-0.3.0); instants are fixed width, so
|
|
135
|
+
// string order is time order (SPEC 3.3).
|
|
136
|
+
if (!(inner.receipt.asof <= receipt.asof))
|
|
137
|
+
stop('E_TIME_ORDER', `input ${h} has asof ${inner.receipt.asof}, after the receipt's asof`, '$.inputs');
|
|
135
138
|
const itx = inner.receipt.tx;
|
|
136
139
|
if (latestTx === null || itx > latestTx)
|
|
137
140
|
latestTx = itx;
|
|
138
141
|
}
|
|
139
142
|
if (latestTx !== null && receipt.tx !== latestTx)
|
|
140
143
|
stop('E_TIME_ORDER', 'tx must be the latest input tx', '$.tx');
|
|
141
|
-
// 7.6 Value.
|
|
144
|
+
// 7.6 Value. No value: the function could not be evaluated (E_EVAL_UNAVAILABLE, not a failure).
|
|
142
145
|
const v = rc.value;
|
|
146
|
+
if (v === undefined)
|
|
147
|
+
return false;
|
|
143
148
|
if (v.type !== receipt.type || v.value !== receipt.value)
|
|
144
149
|
stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
|
|
150
|
+
return true;
|
|
145
151
|
}
|
|
146
152
|
/**
|
|
147
153
|
* Step 7.7: content is only for a blob value (E_SCHEMA otherwise); it has exactly `length`
|
|
@@ -171,8 +177,8 @@ function checkRecomputeArg(rc) {
|
|
|
171
177
|
if (typeof rc !== 'object' || Array.isArray(rc))
|
|
172
178
|
throw new TypeError('recompute must be an object {descriptor, inputs, value}');
|
|
173
179
|
const r = rc;
|
|
174
|
-
if (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean')) {
|
|
175
|
-
throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}');
|
|
180
|
+
if (r.value !== undefined && (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean'))) {
|
|
181
|
+
throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}, or absent when the function could not be evaluated');
|
|
176
182
|
}
|
|
177
183
|
if (r.inputs !== undefined && (typeof r.inputs !== 'object' || r.inputs === null || Array.isArray(r.inputs))) {
|
|
178
184
|
throw new TypeError('recompute.inputs must be an object mapping receipt hash to receipt JSON');
|
|
@@ -182,7 +188,9 @@ function checkRecomputeArg(rc) {
|
|
|
182
188
|
/**
|
|
183
189
|
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
184
190
|
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
185
|
-
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too
|
|
191
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too; without
|
|
192
|
+
* its `value`, steps 7.1-7.5 run and the result is success at level "signed" with code
|
|
193
|
+
* E_EVAL_UNAVAILABLE (path "$.value") unless they fail. With
|
|
186
194
|
* `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
|
|
187
195
|
* otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
|
|
188
196
|
* content does not match. It does not change `level`; `blobChecked` reports it.
|
|
@@ -197,20 +205,24 @@ export function verify(receipt, keys, recomputeArg, blob) {
|
|
|
197
205
|
let hash = null;
|
|
198
206
|
try {
|
|
199
207
|
const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
|
|
200
|
-
|
|
201
|
-
recompute(r, rc, sources);
|
|
208
|
+
const evaluated = rc ? recompute(r, rc, sources) : false;
|
|
202
209
|
const checked = content !== null;
|
|
203
210
|
if (checked)
|
|
204
|
-
checkContent(r, content);
|
|
205
|
-
const
|
|
211
|
+
checkContent(r, content); // 7.7 also after E_EVAL_UNAVAILABLE; its failure replaces the code
|
|
212
|
+
const unavailable = rc !== null && !evaluated;
|
|
213
|
+
const what = evaluated
|
|
214
|
+
? 'fully verified: signature valid and value recomputed'
|
|
215
|
+
: unavailable
|
|
216
|
+
? `signature-valid: signed by ${r.computed_by}; descriptor, arguments and inputs check out, but the function could not be evaluated, so the value was not recomputed`
|
|
217
|
+
: `signature-valid: signed by ${r.computed_by}`;
|
|
206
218
|
return {
|
|
207
219
|
ok: true,
|
|
208
|
-
code: 'OK',
|
|
209
|
-
level:
|
|
220
|
+
code: unavailable ? 'E_EVAL_UNAVAILABLE' : 'OK',
|
|
221
|
+
level: evaluated ? 'recomputed' : 'signed',
|
|
210
222
|
hash,
|
|
211
223
|
key: r.computed_by,
|
|
212
224
|
detail: checked ? `${what}; blob content matches` : what,
|
|
213
|
-
path: null,
|
|
225
|
+
path: unavailable ? '$.value' : null,
|
|
214
226
|
blobChecked: checked,
|
|
215
227
|
};
|
|
216
228
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datumesh/verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Verifier for Datumesh receipts: strict canonical JSON (RFC 8785), cell URIs, units, blobs, response envelopes, key documents and Ed25519 signatures.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"homepage": "https://datumesh.com",
|
|
11
11
|
"keywords": ["datumesh", "receipt", "verify", "ed25519", "jcs", "rfc8785", "canonical-json"],
|
|
12
12
|
"datumesh": {
|
|
13
|
-
"spec": "spec-0.
|
|
13
|
+
"spec": "spec-0.3.0"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "./dist/cjs/index.js",
|