@datumesh/verify 0.2.0 → 0.2.1

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 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.2.0` and passing all of its test vectors.
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.2.1` and passing all of its test vectors.
4
4
 
5
5
  ```sh
6
6
  npm install @datumesh/verify
@@ -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();
@@ -1,5 +1,5 @@
1
1
  /** The spec revision this library conforms to. */
2
- export declare const SPEC_VERSION = "spec-0.2.0";
2
+ export declare const SPEC_VERSION = "spec-0.2.1";
3
3
  /** Every error code defined by SPEC.md 10.2, in the order of that table. */
4
4
  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_BLOB_MISMATCH", "E_ENVELOPE"];
5
5
  export type ErrorCode = (typeof ERROR_CODES)[number];
@@ -3,7 +3,7 @@ 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.2.0';
6
+ exports.SPEC_VERSION = 'spec-0.2.1';
7
7
  /** Every error code defined by SPEC.md 10.2, in the order of that table. */
8
8
  exports.ERROR_CODES = [
9
9
  'E_PARSE_JSON',
@@ -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.2.0";
10
+ export declare const VERSION = "0.2.1";
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.2.0).
12
+ * @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.2.1).
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.2.0';
42
+ exports.VERSION = '0.2.1';
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(', ')}`);
@@ -3,11 +3,10 @@ export interface JsonObject {
3
3
  [key: string]: JsonValue;
4
4
  }
5
5
  /**
6
- * Nesting deeper than this is rejected with E_PARSE_JSON. SPEC.md sets no depth limit; this is
7
- * an implementation limit (Datumesh objects nest at most five levels) that keeps parsing and
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 = 1000;
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
- * Nesting deeper than this is rejected with E_PARSE_JSON. SPEC.md sets no depth limit; this is
20
- * an implementation limit (Datumesh objects nest at most five levels) that keeps parsing and
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 = 1000;
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 (implementation limit)`);
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 ((0, unicode_js_1.hasLoneSurrogate)(text))
262
- (0, errors_js_1.fail)('E_PARSE_JSON', 'lone surrogate in the text', '$');
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
- if (depth > exports.MAX_DEPTH)
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;
@@ -34,6 +34,8 @@ export declare function keysFromResponse(host: string, status: number | null, bo
34
34
  export declare function keysFromDocument(doc: unknown): KeysResult;
35
35
  /** Default timeout of fetchKeys, in milliseconds. */
36
36
  export declare const FETCH_TIMEOUT_MS = 10000;
37
+ /** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
38
+ export declare const FETCH_MAX_BYTES: number;
37
39
  /**
38
40
  * Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
39
41
  * 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;
@@ -134,6 +146,45 @@ function keysFromDocument(doc) {
134
146
  }
135
147
  /** Default timeout of fetchKeys, in milliseconds. */
136
148
  exports.FETCH_TIMEOUT_MS = 10000;
149
+ /** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
150
+ exports.FETCH_MAX_BYTES = 256 * 1024;
151
+ class TooLarge extends Error {
152
+ }
153
+ /** Read a response body, abandoning it once it exceeds FETCH_MAX_BYTES. */
154
+ async function readBounded(res) {
155
+ const reader = res.body && typeof res.body.getReader === 'function' ? res.body.getReader() : null;
156
+ if (!reader) {
157
+ const b = new Uint8Array(await res.arrayBuffer());
158
+ if (b.length > exports.FETCH_MAX_BYTES)
159
+ throw new TooLarge();
160
+ return b;
161
+ }
162
+ const parts = [];
163
+ let n = 0;
164
+ for (;;) {
165
+ const { done, value } = await reader.read();
166
+ if (done)
167
+ break;
168
+ n += value.length;
169
+ if (n > exports.FETCH_MAX_BYTES) {
170
+ try {
171
+ await reader.cancel();
172
+ }
173
+ catch {
174
+ // abandoned anyway
175
+ }
176
+ throw new TooLarge();
177
+ }
178
+ parts.push(value);
179
+ }
180
+ const out = new Uint8Array(n);
181
+ let o = 0;
182
+ for (const p of parts) {
183
+ out.set(p, o);
184
+ o += p.length;
185
+ }
186
+ return out;
187
+ }
137
188
  /**
138
189
  * Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
139
190
  * redirects followed and a timeout, then interpret it with keysFromResponse. The only function
@@ -165,9 +216,11 @@ async function fetchKeys(host, timeoutMs = exports.FETCH_TIMEOUT_MS) {
165
216
  status = 0;
166
217
  if (status !== 200)
167
218
  return keysFromResponse(host, status);
168
- body = new Uint8Array(await res.arrayBuffer());
219
+ body = await readBounded(res);
169
220
  }
170
221
  catch (e) {
222
+ if (e instanceof TooLarge)
223
+ return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `key document larger than 256 KiB; retrieval abandoned` };
171
224
  return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `retrieval failed: ${e instanceof Error ? e.message : String(e)}` };
172
225
  }
173
226
  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 under
54
- * section 7); only the declared argument names and types are used. Something that is not a
55
- * signature at all is a programmer error.
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.toJson)(signature);
61
+ sig = (0, json_js_1.normalizeValue)(signature, 'signature');
61
62
  }
62
63
  catch {
63
- throw new TypeError('parseUri needs the function signature (descriptor.signature) as JSON');
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 lists userinfo and a port among outer-grammar failures (E_PARSE_URI). (The
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. Against the signature: unknown arguments, then missing ones.
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 decl = declared[p.name];
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
  }
@@ -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();
@@ -1,5 +1,5 @@
1
1
  /** The spec revision this library conforms to. */
2
- export declare const SPEC_VERSION = "spec-0.2.0";
2
+ export declare const SPEC_VERSION = "spec-0.2.1";
3
3
  /** Every error code defined by SPEC.md 10.2, in the order of that table. */
4
4
  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_BLOB_MISMATCH", "E_ENVELOPE"];
5
5
  export type ErrorCode = (typeof ERROR_CODES)[number];
@@ -1,5 +1,5 @@
1
1
  /** The spec revision this library conforms to. */
2
- export const SPEC_VERSION = 'spec-0.2.0';
2
+ export const SPEC_VERSION = 'spec-0.2.1';
3
3
  /** Every error code defined by SPEC.md 10.2, in the order of that table. */
4
4
  export const ERROR_CODES = [
5
5
  'E_PARSE_JSON',
@@ -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.2.0";
10
+ export declare const VERSION = "0.2.1";
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.0).
2
+ * @datumesh/verify: verify Datumesh receipts (SPEC.md, revision spec-0.2.1).
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.2.0';
19
+ export const VERSION = '0.2.1';
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(', ')}`);
@@ -3,11 +3,10 @@ export interface JsonObject {
3
3
  [key: string]: JsonValue;
4
4
  }
5
5
  /**
6
- * Nesting deeper than this is rejected with E_PARSE_JSON. SPEC.md sets no depth limit; this is
7
- * an implementation limit (Datumesh objects nest at most five levels) that keeps parsing and
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 = 1000;
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
- * Nesting deeper than this is rejected with E_PARSE_JSON. SPEC.md sets no depth limit; this is
8
- * an implementation limit (Datumesh objects nest at most five levels) that keeps parsing and
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 = 1000;
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 (implementation limit)`);
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 (hasLoneSurrogate(text))
250
- fail('E_PARSE_JSON', 'lone surrogate in the text', '$');
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
- if (depth > MAX_DEPTH)
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;
@@ -34,6 +34,8 @@ export declare function keysFromResponse(host: string, status: number | null, bo
34
34
  export declare function keysFromDocument(doc: unknown): KeysResult;
35
35
  /** Default timeout of fetchKeys, in milliseconds. */
36
36
  export declare const FETCH_TIMEOUT_MS = 10000;
37
+ /** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
38
+ export declare const FETCH_MAX_BYTES: number;
37
39
  /**
38
40
  * Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
39
41
  * 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;
@@ -126,6 +138,45 @@ export function keysFromDocument(doc) {
126
138
  }
127
139
  /** Default timeout of fetchKeys, in milliseconds. */
128
140
  export const FETCH_TIMEOUT_MS = 10000;
141
+ /** fetchKeys abandons a key document larger than this (SPEC 9.3: SHOULD bound retrieval). */
142
+ export const FETCH_MAX_BYTES = 256 * 1024;
143
+ class TooLarge extends Error {
144
+ }
145
+ /** Read a response body, abandoning it once it exceeds FETCH_MAX_BYTES. */
146
+ async function readBounded(res) {
147
+ const reader = res.body && typeof res.body.getReader === 'function' ? res.body.getReader() : null;
148
+ if (!reader) {
149
+ const b = new Uint8Array(await res.arrayBuffer());
150
+ if (b.length > FETCH_MAX_BYTES)
151
+ throw new TooLarge();
152
+ return b;
153
+ }
154
+ const parts = [];
155
+ let n = 0;
156
+ for (;;) {
157
+ const { done, value } = await reader.read();
158
+ if (done)
159
+ break;
160
+ n += value.length;
161
+ if (n > FETCH_MAX_BYTES) {
162
+ try {
163
+ await reader.cancel();
164
+ }
165
+ catch {
166
+ // abandoned anyway
167
+ }
168
+ throw new TooLarge();
169
+ }
170
+ parts.push(value);
171
+ }
172
+ const out = new Uint8Array(n);
173
+ let o = 0;
174
+ for (const p of parts) {
175
+ out.set(p, o);
176
+ o += p.length;
177
+ }
178
+ return out;
179
+ }
129
180
  /**
130
181
  * Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
131
182
  * redirects followed and a timeout, then interpret it with keysFromResponse. The only function
@@ -157,9 +208,11 @@ export async function fetchKeys(host, timeoutMs = FETCH_TIMEOUT_MS) {
157
208
  status = 0;
158
209
  if (status !== 200)
159
210
  return keysFromResponse(host, status);
160
- body = new Uint8Array(await res.arrayBuffer());
211
+ body = await readBounded(res);
161
212
  }
162
213
  catch (e) {
214
+ if (e instanceof TooLarge)
215
+ return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `key document larger than 256 KiB; retrieval abandoned` };
163
216
  return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `retrieval failed: ${e instanceof Error ? e.message : String(e)}` };
164
217
  }
165
218
  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 under
49
- * section 7); only the declared argument names and types are used. Something that is not a
50
- * signature at all is a programmer error.
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 = toJson(signature);
56
+ sig = normalizeValue(signature, 'signature');
56
57
  }
57
58
  catch {
58
- throw new TypeError('parseUri needs the function signature (descriptor.signature) as JSON');
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 lists userinfo and a port among outer-grammar failures (E_PARSE_URI). (The
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. Against the signature: unknown arguments, then missing ones.
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 decl = declared[p.name];
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datumesh/verify",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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.2.0"
13
+ "spec": "spec-0.2.1"
14
14
  },
15
15
  "type": "module",
16
16
  "main": "./dist/cjs/index.js",