@datumesh/verify 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +54 -2
- package/dist/cjs/ed25519.d.ts +2 -0
- package/dist/cjs/ed25519.js +82 -0
- package/dist/cjs/errors.d.ts +14 -0
- package/dist/cjs/errors.js +61 -0
- package/dist/cjs/hash.d.ts +6 -0
- package/dist/cjs/hash.js +27 -0
- package/dist/cjs/index.d.ts +27 -0
- package/dist/cjs/index.js +83 -0
- package/dist/cjs/jcs.d.ts +5 -0
- package/dist/cjs/jcs.js +25 -0
- package/dist/cjs/json.d.ts +29 -0
- package/dist/cjs/json.js +321 -0
- package/dist/cjs/keys.d.ts +73 -0
- package/dist/cjs/keys.js +257 -0
- package/dist/cjs/lexical.d.ts +59 -0
- package/dist/cjs/lexical.js +207 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/unicode-data.d.ts +8 -0
- package/dist/cjs/unicode-data.js +14 -0
- package/dist/cjs/unicode.d.ts +12 -0
- package/dist/cjs/unicode.js +192 -0
- package/dist/cjs/uri.d.ts +31 -0
- package/dist/cjs/uri.js +201 -0
- package/dist/cjs/utf8.d.ts +10 -0
- package/dist/cjs/utf8.js +101 -0
- package/dist/cjs/validate.d.ts +17 -0
- package/dist/cjs/validate.js +349 -0
- package/dist/cjs/verify.d.ts +38 -0
- package/dist/cjs/verify.js +192 -0
- package/dist/esm/ed25519.d.ts +2 -0
- package/dist/esm/ed25519.js +79 -0
- package/dist/esm/errors.d.ts +14 -0
- package/dist/esm/errors.js +56 -0
- package/dist/esm/hash.d.ts +6 -0
- package/dist/esm/hash.js +21 -0
- package/dist/esm/index.d.ts +27 -0
- package/dist/esm/index.js +63 -0
- package/dist/esm/jcs.d.ts +5 -0
- package/dist/esm/jcs.js +18 -0
- package/dist/esm/json.d.ts +29 -0
- package/dist/esm/json.js +309 -0
- package/dist/esm/keys.d.ts +73 -0
- package/dist/esm/keys.js +249 -0
- package/dist/esm/lexical.d.ts +59 -0
- package/dist/esm/lexical.js +187 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/unicode-data.d.ts +8 -0
- package/dist/esm/unicode-data.js +11 -0
- package/dist/esm/unicode.d.ts +12 -0
- package/dist/esm/unicode.js +184 -0
- package/dist/esm/uri.d.ts +31 -0
- package/dist/esm/uri.js +196 -0
- package/dist/esm/utf8.d.ts +10 -0
- package/dist/esm/utf8.js +96 -0
- package/dist/esm/validate.d.ts +17 -0
- package/dist/esm/validate.js +340 -0
- package/dist/esm/verify.d.ts +38 -0
- package/dist/esm/verify.js +189 -0
- package/package.json +54 -4
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type JsonObject, type JsonValue } from './json.js';
|
|
2
|
+
export type Kind = 'scalar' | 'cell' | 'ref' | 'function' | 'receipt' | 'jcs';
|
|
3
|
+
export declare const KINDS: readonly Kind[];
|
|
4
|
+
export declare function structSignature(v: JsonValue, path: string): void;
|
|
5
|
+
export declare function canonSignature(sig: JsonObject, path: string): void;
|
|
6
|
+
/** Validate `v` as `kind` (steps 2-3; for receipts also step 4). Throws DatumeshError. */
|
|
7
|
+
export declare function validate(v: JsonValue, kind: Kind, path?: string): void;
|
|
8
|
+
/** Receipt step 2 only: true when the receipt has the right members and JSON types. */
|
|
9
|
+
export declare function receiptStructure(v: JsonValue): void;
|
|
10
|
+
/** Receipt steps 3 and 4, after receiptStructure has passed. */
|
|
11
|
+
export declare function receiptCanonicalAndConsistent(v: JsonValue): void;
|
|
12
|
+
/**
|
|
13
|
+
* Infer the kind of an object from its members when the caller gives none: a receipt has
|
|
14
|
+
* `sig`, `proto` or `computed_by`; a descriptor `signature` or `code`; a cell `fn_hash`; a cell
|
|
15
|
+
* reference `fn`, `ns` or `args`; a typed scalar `type` or `value`. Anything else is E_SCHEMA.
|
|
16
|
+
*/
|
|
17
|
+
export declare function inferKind(v: JsonValue): Exclude<Kind, 'jcs'>;
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// Structure (SPEC 10.1 step 2, E_SCHEMA), canonical form (step 3) and receipt consistency
|
|
2
|
+
// (step 4) for every kind of Datumesh object. Within a step members are examined in JCS order,
|
|
3
|
+
// depth first; at each object its member set is checked before its members.
|
|
4
|
+
import { fail } from './errors.js';
|
|
5
|
+
import { has, isObject, memberPath, sortedKeys } from './json.js';
|
|
6
|
+
import { LIMITS, VALID_TIME_TYPES, checkArgName, checkFunctionName, checkHex, checkInstant, checkKeyRef, checkLicence, checkName, checkNamespace, checkString, checkTypedValue, isTypeName, } from './lexical.js';
|
|
7
|
+
export const KINDS = ['scalar', 'cell', 'ref', 'function', 'receipt', 'jcs'];
|
|
8
|
+
const SLICE = '*';
|
|
9
|
+
// ---------------------------------------------------------------- step 2: structure
|
|
10
|
+
function expectObject(v, path) {
|
|
11
|
+
if (!isObject(v))
|
|
12
|
+
fail('E_SCHEMA', 'expected an object', path);
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
function members(o, required, optional, path) {
|
|
16
|
+
const names = new Set([...Object.keys(o), ...required]);
|
|
17
|
+
for (const n of [...names].sort()) {
|
|
18
|
+
if (!has(o, n))
|
|
19
|
+
fail('E_SCHEMA', `missing member ${JSON.stringify(n)}`, path);
|
|
20
|
+
if (!required.includes(n) && !optional.includes(n))
|
|
21
|
+
fail('E_SCHEMA', `unexpected member ${JSON.stringify(n)}`, path);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function expectString(o, k, path) {
|
|
25
|
+
if (typeof o[k] !== 'string')
|
|
26
|
+
fail('E_SCHEMA', `${k} must be a string`, memberPath(path, k));
|
|
27
|
+
}
|
|
28
|
+
function structScalar(v, path) {
|
|
29
|
+
const o = expectObject(v, path);
|
|
30
|
+
members(o, ['type', 'value'], [], path);
|
|
31
|
+
expectString(o, 'type', path);
|
|
32
|
+
if (typeof o.value !== 'string' && typeof o.value !== 'boolean') {
|
|
33
|
+
fail('E_SCHEMA', 'value must be a string or a boolean', memberPath(path, 'value'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function structArgs(v, path) {
|
|
37
|
+
const o = expectObject(v, path);
|
|
38
|
+
for (const k of sortedKeys(o)) {
|
|
39
|
+
if (o[k] === SLICE)
|
|
40
|
+
continue;
|
|
41
|
+
structScalar(o[k], memberPath(path, k));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function structCell(v, path, ref) {
|
|
45
|
+
const o = expectObject(v, path);
|
|
46
|
+
if (ref)
|
|
47
|
+
members(o, ['args', 'fn', 'ns'], ['asof'], path);
|
|
48
|
+
else
|
|
49
|
+
members(o, ['args', 'fn', 'fn_hash', 'ns'], [], path);
|
|
50
|
+
for (const k of sortedKeys(o)) {
|
|
51
|
+
if (k === 'args')
|
|
52
|
+
structArgs(o.args, memberPath(path, 'args'));
|
|
53
|
+
else
|
|
54
|
+
expectString(o, k, path);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function structFunction(v, path) {
|
|
58
|
+
const o = expectObject(v, path);
|
|
59
|
+
members(o, ['code', 'inputs', 'signature'], [], path);
|
|
60
|
+
const codePath = memberPath(path, 'code');
|
|
61
|
+
const code = expectObject(o.code, codePath);
|
|
62
|
+
members(code, ['body', 'lang'], [], codePath);
|
|
63
|
+
expectString(code, 'body', codePath);
|
|
64
|
+
expectString(code, 'lang', codePath);
|
|
65
|
+
const inputs = o.inputs;
|
|
66
|
+
if (!Array.isArray(inputs) || inputs.some((x) => typeof x !== 'string')) {
|
|
67
|
+
fail('E_SCHEMA', 'inputs must be an array of strings', memberPath(path, 'inputs'));
|
|
68
|
+
}
|
|
69
|
+
structSignature(o.signature, memberPath(path, 'signature'));
|
|
70
|
+
}
|
|
71
|
+
export function structSignature(v, path) {
|
|
72
|
+
const sig = expectObject(v, path);
|
|
73
|
+
members(sig, ['args', 'returns'], [], path);
|
|
74
|
+
const argsPath = memberPath(path, 'args');
|
|
75
|
+
const args = expectObject(sig.args, argsPath);
|
|
76
|
+
for (const k of sortedKeys(args)) {
|
|
77
|
+
const p = memberPath(argsPath, k);
|
|
78
|
+
const d = expectObject(args[k], p);
|
|
79
|
+
members(d, ['type', 'valid'], [], p);
|
|
80
|
+
expectString(d, 'type', p);
|
|
81
|
+
if (typeof d.valid !== 'boolean')
|
|
82
|
+
fail('E_SCHEMA', 'valid must be a boolean', memberPath(p, 'valid'));
|
|
83
|
+
}
|
|
84
|
+
expectString(sig, 'returns', path);
|
|
85
|
+
}
|
|
86
|
+
const RECEIPT_MEMBERS = [
|
|
87
|
+
'asof',
|
|
88
|
+
'cell',
|
|
89
|
+
'computed_by',
|
|
90
|
+
'inputs',
|
|
91
|
+
'issued',
|
|
92
|
+
'licence',
|
|
93
|
+
'proto',
|
|
94
|
+
'publisher',
|
|
95
|
+
'sig',
|
|
96
|
+
'summary',
|
|
97
|
+
'tx',
|
|
98
|
+
'type',
|
|
99
|
+
'valid',
|
|
100
|
+
'value',
|
|
101
|
+
];
|
|
102
|
+
function structReceipt(v, path) {
|
|
103
|
+
const o = expectObject(v, path);
|
|
104
|
+
members(o, RECEIPT_MEMBERS, [], path);
|
|
105
|
+
for (const k of sortedKeys(o)) {
|
|
106
|
+
const p = memberPath(path, k);
|
|
107
|
+
switch (k) {
|
|
108
|
+
case 'cell':
|
|
109
|
+
structCell(o.cell, p, false);
|
|
110
|
+
break;
|
|
111
|
+
case 'inputs': {
|
|
112
|
+
const a = o.inputs;
|
|
113
|
+
if (!Array.isArray(a) || a.some((x) => typeof x !== 'string'))
|
|
114
|
+
fail('E_SCHEMA', 'inputs must be an array of strings', p);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'valid':
|
|
118
|
+
structArgs(o.valid, p);
|
|
119
|
+
break;
|
|
120
|
+
case 'value':
|
|
121
|
+
if (typeof o.value !== 'string' && typeof o.value !== 'boolean')
|
|
122
|
+
fail('E_SCHEMA', 'value must be a string or a boolean', p);
|
|
123
|
+
break;
|
|
124
|
+
case 'proto':
|
|
125
|
+
expectString(o, k, path);
|
|
126
|
+
if (o.proto !== 'datumesh/1')
|
|
127
|
+
fail('E_SCHEMA', "proto must be 'datumesh/1'", p);
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
expectString(o, k, path);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------- step 3: canonical form
|
|
135
|
+
function canonScalar(o, path) {
|
|
136
|
+
const t = o.type;
|
|
137
|
+
if (!isTypeName(t))
|
|
138
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(t)}`, memberPath(path, 'type'));
|
|
139
|
+
checkTypedValue(t, o.value, memberPath(path, 'value'));
|
|
140
|
+
}
|
|
141
|
+
function canonArgs(o, path) {
|
|
142
|
+
if (Object.keys(o).length > LIMITS.args)
|
|
143
|
+
fail('E_CANON_LIMIT', `more than ${LIMITS.args} arguments`, path);
|
|
144
|
+
for (const k of sortedKeys(o)) {
|
|
145
|
+
const p = memberPath(path, k);
|
|
146
|
+
checkArgName(k, p);
|
|
147
|
+
if (o[k] !== SLICE)
|
|
148
|
+
canonScalar(o[k], p);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function canonCell(o, path) {
|
|
152
|
+
for (const k of sortedKeys(o)) {
|
|
153
|
+
const p = memberPath(path, k);
|
|
154
|
+
const v = o[k];
|
|
155
|
+
if (k === 'args')
|
|
156
|
+
canonArgs(o.args, p);
|
|
157
|
+
else if (k === 'asof')
|
|
158
|
+
checkInstant(v, p);
|
|
159
|
+
else if (k === 'fn')
|
|
160
|
+
checkName(v, p);
|
|
161
|
+
else if (k === 'fn_hash')
|
|
162
|
+
checkHex(v, 64, p);
|
|
163
|
+
else if (k === 'ns')
|
|
164
|
+
checkNamespace(v, p);
|
|
165
|
+
}
|
|
166
|
+
if (o.ns.length + 1 + o.fn.length > LIMITS.function_name) {
|
|
167
|
+
fail('E_CANON_LIMIT', 'function name too long', path);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function canonStringSet(a, path, each) {
|
|
171
|
+
a.forEach((s, i) => each(s, `${path}[${i}]`));
|
|
172
|
+
for (let i = 1; i < a.length; i++) {
|
|
173
|
+
if (!(a[i - 1] < a[i]))
|
|
174
|
+
fail('E_CANON_SET', 'entries must be strictly ascending', path);
|
|
175
|
+
}
|
|
176
|
+
if (a.length > LIMITS.inputs)
|
|
177
|
+
fail('E_CANON_LIMIT', `more than ${LIMITS.inputs} inputs`, path);
|
|
178
|
+
}
|
|
179
|
+
export function canonSignature(sig, path) {
|
|
180
|
+
const argsPath = memberPath(path, 'args');
|
|
181
|
+
const args = sig.args;
|
|
182
|
+
if (Object.keys(args).length > LIMITS.args)
|
|
183
|
+
fail('E_CANON_LIMIT', `more than ${LIMITS.args} arguments`, argsPath);
|
|
184
|
+
for (const k of sortedKeys(args)) {
|
|
185
|
+
const p = memberPath(argsPath, k);
|
|
186
|
+
checkArgName(k, p);
|
|
187
|
+
const d = args[k];
|
|
188
|
+
if (!isTypeName(d.type))
|
|
189
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(d.type)}`, memberPath(p, 'type'));
|
|
190
|
+
if (d.valid === true && !VALID_TIME_TYPES.includes(d.type)) {
|
|
191
|
+
fail('E_CANON_TYPE', 'a valid-time argument must be a date, instant or string', p);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!isTypeName(sig.returns))
|
|
195
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(sig.returns)}`, memberPath(path, 'returns'));
|
|
196
|
+
}
|
|
197
|
+
function canonFunction(o, path) {
|
|
198
|
+
const code = o.code;
|
|
199
|
+
const codePath = memberPath(path, 'code');
|
|
200
|
+
checkString(code.body, memberPath(codePath, 'body'), { body: true });
|
|
201
|
+
checkName(code.lang, memberPath(codePath, 'lang'));
|
|
202
|
+
canonStringSet(o.inputs, memberPath(path, 'inputs'), checkFunctionName);
|
|
203
|
+
canonSignature(o.signature, memberPath(path, 'signature'));
|
|
204
|
+
}
|
|
205
|
+
function canonReceipt(o, path) {
|
|
206
|
+
for (const k of sortedKeys(o)) {
|
|
207
|
+
const p = memberPath(path, k);
|
|
208
|
+
const v = o[k];
|
|
209
|
+
switch (k) {
|
|
210
|
+
case 'asof':
|
|
211
|
+
case 'issued':
|
|
212
|
+
case 'tx':
|
|
213
|
+
checkInstant(v, p);
|
|
214
|
+
break;
|
|
215
|
+
case 'cell':
|
|
216
|
+
canonCell(v, p);
|
|
217
|
+
break;
|
|
218
|
+
case 'computed_by':
|
|
219
|
+
checkKeyRef(v, p);
|
|
220
|
+
break;
|
|
221
|
+
case 'inputs':
|
|
222
|
+
canonStringSet(v, p, (s, q) => checkHex(s, 64, q));
|
|
223
|
+
break;
|
|
224
|
+
case 'licence':
|
|
225
|
+
checkLicence(v, p);
|
|
226
|
+
break;
|
|
227
|
+
case 'publisher':
|
|
228
|
+
checkNamespace(v, p);
|
|
229
|
+
break;
|
|
230
|
+
case 'sig':
|
|
231
|
+
checkHex(v, 128, p);
|
|
232
|
+
break;
|
|
233
|
+
case 'summary':
|
|
234
|
+
if (v === '')
|
|
235
|
+
fail('E_CANON_STRING', 'summary must not be empty', p);
|
|
236
|
+
checkString(v, p, { limit: LIMITS.summary });
|
|
237
|
+
break;
|
|
238
|
+
case 'type':
|
|
239
|
+
if (!isTypeName(v))
|
|
240
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(v)}`, p);
|
|
241
|
+
break;
|
|
242
|
+
case 'valid':
|
|
243
|
+
canonArgs(v, p);
|
|
244
|
+
break;
|
|
245
|
+
case 'value':
|
|
246
|
+
checkTypedValue(o.type, v, p);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// ---------------------------------------------------------------- step 4: consistency
|
|
252
|
+
function consistency(o, path) {
|
|
253
|
+
const cell = o.cell;
|
|
254
|
+
const args = cell.args;
|
|
255
|
+
const valid = o.valid;
|
|
256
|
+
const argsPath = memberPath(memberPath(path, 'cell'), 'args');
|
|
257
|
+
const validPath = memberPath(path, 'valid');
|
|
258
|
+
for (const k of sortedKeys(args)) {
|
|
259
|
+
if (args[k] === SLICE)
|
|
260
|
+
fail('E_SLICE', 'a receipt is for one cell, not a slice', memberPath(argsPath, k));
|
|
261
|
+
}
|
|
262
|
+
for (const k of sortedKeys(valid)) {
|
|
263
|
+
if (valid[k] === SLICE)
|
|
264
|
+
fail('E_SLICE', 'a receipt is for one cell, not a slice', memberPath(validPath, k));
|
|
265
|
+
}
|
|
266
|
+
if (o.publisher !== cell.ns)
|
|
267
|
+
fail('E_PUBLISHER_MISMATCH', 'publisher must equal cell.ns', memberPath(path, 'publisher'));
|
|
268
|
+
for (const k of sortedKeys(valid)) {
|
|
269
|
+
const v = valid[k];
|
|
270
|
+
const a = has(args, k) ? args[k] : null;
|
|
271
|
+
if (!a || a.type !== v.type || a.value !== v.value) {
|
|
272
|
+
fail('E_VALID_MISMATCH', `valid.${k} does not echo cell.args.${k}`, validPath);
|
|
273
|
+
}
|
|
274
|
+
if (!VALID_TIME_TYPES.includes(v.type)) {
|
|
275
|
+
fail('E_VALID_MISMATCH', 'valid-time arguments are dates, instants or strings', validPath);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const tx = o.tx;
|
|
279
|
+
const asof = o.asof;
|
|
280
|
+
const issued = o.issued;
|
|
281
|
+
// Instants are fixed width, so string order is time order (SPEC 3.3).
|
|
282
|
+
if (!(tx <= asof && asof <= issued))
|
|
283
|
+
fail('E_TIME_ORDER', 'requires tx <= asof <= issued', path);
|
|
284
|
+
}
|
|
285
|
+
// ---------------------------------------------------------------- entry points
|
|
286
|
+
/** Validate `v` as `kind` (steps 2-3; for receipts also step 4). Throws DatumeshError. */
|
|
287
|
+
export function validate(v, kind, path = '$') {
|
|
288
|
+
switch (kind) {
|
|
289
|
+
case 'jcs':
|
|
290
|
+
return;
|
|
291
|
+
case 'scalar':
|
|
292
|
+
structScalar(v, path);
|
|
293
|
+
canonScalar(v, path);
|
|
294
|
+
return;
|
|
295
|
+
case 'cell':
|
|
296
|
+
case 'ref':
|
|
297
|
+
structCell(v, path, kind === 'ref');
|
|
298
|
+
canonCell(v, path);
|
|
299
|
+
return;
|
|
300
|
+
case 'function':
|
|
301
|
+
structFunction(v, path);
|
|
302
|
+
canonFunction(v, path);
|
|
303
|
+
return;
|
|
304
|
+
case 'receipt':
|
|
305
|
+
structReceipt(v, path);
|
|
306
|
+
canonReceipt(v, path);
|
|
307
|
+
consistency(v, path);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Receipt step 2 only: true when the receipt has the right members and JSON types. */
|
|
312
|
+
export function receiptStructure(v) {
|
|
313
|
+
structReceipt(v, '$');
|
|
314
|
+
}
|
|
315
|
+
/** Receipt steps 3 and 4, after receiptStructure has passed. */
|
|
316
|
+
export function receiptCanonicalAndConsistent(v) {
|
|
317
|
+
canonReceipt(v, '$');
|
|
318
|
+
consistency(v, '$');
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Infer the kind of an object from its members when the caller gives none: a receipt has
|
|
322
|
+
* `sig`, `proto` or `computed_by`; a descriptor `signature` or `code`; a cell `fn_hash`; a cell
|
|
323
|
+
* reference `fn`, `ns` or `args`; a typed scalar `type` or `value`. Anything else is E_SCHEMA.
|
|
324
|
+
*/
|
|
325
|
+
export function inferKind(v) {
|
|
326
|
+
if (!isObject(v))
|
|
327
|
+
fail('E_SCHEMA', 'cannot infer the kind of a non-object; pass kind', '$');
|
|
328
|
+
const h = (k) => has(v, k);
|
|
329
|
+
if (h('sig') || h('proto') || h('computed_by'))
|
|
330
|
+
return 'receipt';
|
|
331
|
+
if (h('signature') || h('code'))
|
|
332
|
+
return 'function';
|
|
333
|
+
if (h('fn_hash'))
|
|
334
|
+
return 'cell';
|
|
335
|
+
if (h('fn') || h('ns') || h('args'))
|
|
336
|
+
return 'ref';
|
|
337
|
+
if (h('type') || h('value'))
|
|
338
|
+
return 'scalar';
|
|
339
|
+
fail('E_SCHEMA', 'cannot infer the kind from the members; pass kind', '$');
|
|
340
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ErrorCode } from './errors.js';
|
|
2
|
+
export interface VerifyResult {
|
|
3
|
+
/** True when the receipt is signature-valid (and, if recompute was given, fully verified). */
|
|
4
|
+
ok: boolean;
|
|
5
|
+
/** "OK" or a SPEC 10.2 error code. */
|
|
6
|
+
code: 'OK' | ErrorCode;
|
|
7
|
+
/** "signed" (steps 1-6) or "recomputed" (steps 1-7) when ok, else null. */
|
|
8
|
+
level: 'signed' | 'recomputed' | null;
|
|
9
|
+
/** The receipt hash, once the receipt has the structure of a receipt (step 2), else null. */
|
|
10
|
+
hash: string | null;
|
|
11
|
+
/** The `computed_by` key reference when ok, else null. */
|
|
12
|
+
key: string | null;
|
|
13
|
+
/** Human-readable explanation. */
|
|
14
|
+
detail: string;
|
|
15
|
+
/** Where the failure was found (a JSON path like `$.cell.args.base`), or null. */
|
|
16
|
+
path: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** Input for SPEC 10.1 step 7. The library does not evaluate functions; `value` is the result. */
|
|
19
|
+
export interface Recompute {
|
|
20
|
+
/** The function descriptor for cell.fn_hash (object or JSON text); absent: E_FN_UNAVAILABLE. */
|
|
21
|
+
descriptor?: unknown;
|
|
22
|
+
/** Input receipts by receipt hash, as JSON text (or bytes, or parsed objects). */
|
|
23
|
+
inputs?: Record<string, unknown>;
|
|
24
|
+
/** What evaluating the function produced: a typed scalar. */
|
|
25
|
+
value: {
|
|
26
|
+
type: string;
|
|
27
|
+
value: string | boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
32
|
+
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
33
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too.
|
|
34
|
+
*
|
|
35
|
+
* Never throws for an invalid receipt or bad key material; throws TypeError only when an
|
|
36
|
+
* argument is of the wrong kind altogether.
|
|
37
|
+
*/
|
|
38
|
+
export declare function verify(receipt: unknown, keys: unknown, recomputeArg?: Recompute | null): VerifyResult;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Receipt verification (SPEC.md 10.1). Synchronous, offline, no global state, no logging.
|
|
2
|
+
import { DatumeshError } from './errors.js';
|
|
3
|
+
import { verifyEd25519Strict } from './ed25519.js';
|
|
4
|
+
import { fromHex, sha256Hex } from './hash.js';
|
|
5
|
+
import { has, newObject, sortedKeys, toJson } from './json.js';
|
|
6
|
+
import { jcsBytes } from './jcs.js';
|
|
7
|
+
import { collectSources, lookupKey } from './keys.js';
|
|
8
|
+
import { receiptCanonicalAndConsistent, receiptStructure, validate } from './validate.js';
|
|
9
|
+
class Stop extends Error {
|
|
10
|
+
constructor(code, detail, path) {
|
|
11
|
+
super(detail);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.detail = detail;
|
|
14
|
+
this.path = path;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function stop(code, detail, path = '$') {
|
|
18
|
+
throw new Stop(code, detail, path);
|
|
19
|
+
}
|
|
20
|
+
function withoutSig(r) {
|
|
21
|
+
const o = newObject();
|
|
22
|
+
for (const k of Object.keys(r))
|
|
23
|
+
if (k !== 'sig')
|
|
24
|
+
o[k] = r[k];
|
|
25
|
+
return o;
|
|
26
|
+
}
|
|
27
|
+
/** Steps 1-6. Throws Stop; returns the parsed receipt and its hash. */
|
|
28
|
+
function verifySigned(input, sources, setHash) {
|
|
29
|
+
let r;
|
|
30
|
+
try {
|
|
31
|
+
r = toJson(input); // step 1
|
|
32
|
+
receiptStructure(r); // step 2
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
if (e instanceof DatumeshError)
|
|
36
|
+
stop(e.code, e.detail, e.path);
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
const receipt = r;
|
|
40
|
+
const payload = jcsBytes(withoutSig(receipt));
|
|
41
|
+
const hash = sha256Hex(payload);
|
|
42
|
+
setHash(hash);
|
|
43
|
+
try {
|
|
44
|
+
receiptCanonicalAndConsistent(receipt); // steps 3-4
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
if (e instanceof DatumeshError)
|
|
48
|
+
stop(e.code, e.detail, e.path);
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
// Step 5.
|
|
52
|
+
const ref = receipt.computed_by;
|
|
53
|
+
const hashAt = ref.indexOf('#');
|
|
54
|
+
const host = ref.slice(0, hashAt);
|
|
55
|
+
const id = ref.slice(hashAt + 1);
|
|
56
|
+
const found = lookupKey(sources, host, id, receipt.issued);
|
|
57
|
+
if (!found.ok)
|
|
58
|
+
stop(found.code, found.detail, found.code === 'E_KEY_NOT_VALID' ? '$.issued' : '$.computed_by');
|
|
59
|
+
// Step 6.
|
|
60
|
+
if (!verifyEd25519Strict(fromHex(receipt.sig), payload, fromHex(found.key.public))) {
|
|
61
|
+
stop('E_SIGNATURE', 'Ed25519 verification failed', '$.sig');
|
|
62
|
+
}
|
|
63
|
+
return { receipt, hash };
|
|
64
|
+
}
|
|
65
|
+
function recompute(receipt, rc, sources) {
|
|
66
|
+
const cell = receipt.cell;
|
|
67
|
+
const cellArgs = cell.args;
|
|
68
|
+
// 7.1 Descriptor.
|
|
69
|
+
if (rc.descriptor === undefined || rc.descriptor === null)
|
|
70
|
+
stop('E_FN_UNAVAILABLE', `no descriptor for ${cell.fn_hash}`, '$.cell.fn_hash');
|
|
71
|
+
let desc;
|
|
72
|
+
try {
|
|
73
|
+
const d = toJson(rc.descriptor);
|
|
74
|
+
if (sha256Hex(jcsBytes(d)) !== cell.fn_hash)
|
|
75
|
+
stop('E_FN_HASH_MISMATCH', 'descriptor does not hash to cell.fn_hash', '$.cell.fn_hash');
|
|
76
|
+
validate(d, 'function', 'descriptor');
|
|
77
|
+
desc = d;
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (e instanceof DatumeshError)
|
|
81
|
+
stop(e.code, `descriptor: ${e.detail}`, e.path);
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
const sig = desc.signature;
|
|
85
|
+
const declared = sig.args;
|
|
86
|
+
// 7.2 Arguments: unknown, then missing, then types.
|
|
87
|
+
for (const k of sortedKeys(cellArgs)) {
|
|
88
|
+
if (!has(declared, k))
|
|
89
|
+
stop('E_ARG_UNKNOWN', `function declares no argument ${JSON.stringify(k)}`, `$.cell.args.${k}`);
|
|
90
|
+
}
|
|
91
|
+
for (const k of sortedKeys(declared)) {
|
|
92
|
+
if (!has(cellArgs, k))
|
|
93
|
+
stop('E_ARG_MISSING', `argument ${JSON.stringify(k)} is not bound`, '$.cell.args');
|
|
94
|
+
}
|
|
95
|
+
for (const k of sortedKeys(cellArgs)) {
|
|
96
|
+
const want = declared[k].type;
|
|
97
|
+
if (cellArgs[k].type !== want)
|
|
98
|
+
stop('E_ARG_TYPE', `${k} is declared ${String(want)}`, `$.cell.args.${k}`);
|
|
99
|
+
}
|
|
100
|
+
// 7.3 valid holds exactly the declared valid-time arguments.
|
|
101
|
+
const validNames = sortedKeys(receipt.valid);
|
|
102
|
+
const declaredValid = sortedKeys(declared).filter((k) => declared[k].valid === true);
|
|
103
|
+
if (validNames.join('\u0000') !== declaredValid.join('\u0000')) {
|
|
104
|
+
stop('E_VALID_MISMATCH', 'valid must echo exactly the declared valid-time arguments', '$.valid');
|
|
105
|
+
}
|
|
106
|
+
// 7.4 Return type.
|
|
107
|
+
if (receipt.type !== sig.returns)
|
|
108
|
+
stop('E_RETURN_TYPE', `function returns ${String(sig.returns)}`, '$.type');
|
|
109
|
+
// 7.5 Inputs.
|
|
110
|
+
const declaredInputs = desc.inputs;
|
|
111
|
+
const supplied = rc.inputs ?? {};
|
|
112
|
+
let latestTx = null;
|
|
113
|
+
for (const h of receipt.inputs) {
|
|
114
|
+
if (!Object.prototype.hasOwnProperty.call(supplied, h))
|
|
115
|
+
stop('E_INPUT_UNAVAILABLE', `input receipt ${h} not available`, '$.inputs');
|
|
116
|
+
let inner;
|
|
117
|
+
try {
|
|
118
|
+
inner = verifySigned(supplied[h], sources, () => undefined);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
if (e instanceof Stop)
|
|
122
|
+
stop('E_INPUT_INVALID', `input ${h}: ${e.code}: ${e.detail}`, '$.inputs');
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
if (inner.hash !== h)
|
|
126
|
+
stop('E_INPUT_UNAVAILABLE', `supplied receipt does not hash to ${h}`, '$.inputs');
|
|
127
|
+
const ic = inner.receipt.cell;
|
|
128
|
+
if (!declaredInputs.includes(`${ic.ns}/${ic.fn}`)) {
|
|
129
|
+
stop('E_INPUT_UNDECLARED', `input ${h} is a cell of an undeclared function`, '$.inputs');
|
|
130
|
+
}
|
|
131
|
+
if (inner.receipt.asof !== receipt.asof)
|
|
132
|
+
stop('E_TIME_ORDER', "input receipts must share the receipt's asof", '$.inputs');
|
|
133
|
+
const itx = inner.receipt.tx;
|
|
134
|
+
if (latestTx === null || itx > latestTx)
|
|
135
|
+
latestTx = itx;
|
|
136
|
+
}
|
|
137
|
+
if (latestTx !== null && receipt.tx !== latestTx)
|
|
138
|
+
stop('E_TIME_ORDER', 'tx must be the latest input tx', '$.tx');
|
|
139
|
+
// 7.6 Value.
|
|
140
|
+
const v = rc.value;
|
|
141
|
+
if (v.type !== receipt.type || v.value !== receipt.value)
|
|
142
|
+
stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
|
|
143
|
+
}
|
|
144
|
+
function checkRecomputeArg(rc) {
|
|
145
|
+
if (rc === undefined || rc === null)
|
|
146
|
+
return null;
|
|
147
|
+
if (typeof rc !== 'object' || Array.isArray(rc))
|
|
148
|
+
throw new TypeError('recompute must be an object {descriptor, inputs, value}');
|
|
149
|
+
const r = rc;
|
|
150
|
+
if (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean')) {
|
|
151
|
+
throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}');
|
|
152
|
+
}
|
|
153
|
+
if (r.inputs !== undefined && (typeof r.inputs !== 'object' || r.inputs === null || Array.isArray(r.inputs))) {
|
|
154
|
+
throw new TypeError('recompute.inputs must be an object mapping receipt hash to receipt JSON');
|
|
155
|
+
}
|
|
156
|
+
return r;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
160
|
+
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
161
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too.
|
|
162
|
+
*
|
|
163
|
+
* Never throws for an invalid receipt or bad key material; throws TypeError only when an
|
|
164
|
+
* argument is of the wrong kind altogether.
|
|
165
|
+
*/
|
|
166
|
+
export function verify(receipt, keys, recomputeArg) {
|
|
167
|
+
const sources = collectSources(keys);
|
|
168
|
+
const rc = checkRecomputeArg(recomputeArg);
|
|
169
|
+
let hash = null;
|
|
170
|
+
try {
|
|
171
|
+
const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
|
|
172
|
+
if (rc)
|
|
173
|
+
recompute(r, rc, sources);
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
code: 'OK',
|
|
177
|
+
level: rc ? 'recomputed' : 'signed',
|
|
178
|
+
hash,
|
|
179
|
+
key: r.computed_by,
|
|
180
|
+
detail: rc ? 'fully verified: signature valid and value recomputed' : `signature-valid: signed by ${r.computed_by}`,
|
|
181
|
+
path: null,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
if (e instanceof Stop)
|
|
186
|
+
return { ok: false, code: e.code, level: null, hash, key: null, detail: e.detail, path: e.path };
|
|
187
|
+
throw e; // a bug in this library, not a property of the receipt
|
|
188
|
+
}
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datumesh/verify",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Verifier for Datumesh receipts: strict canonical JSON (RFC 8785), cell URIs, key documents and Ed25519 signatures.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/datumesh-com/datumesh-verify-js.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://datumesh.com",
|
|
11
|
+
"keywords": ["datumesh", "receipt", "verify", "ed25519", "jcs", "rfc8785", "canonical-json"],
|
|
12
|
+
"datumesh": {
|
|
13
|
+
"spec": "spec-0.1.1"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/cjs/index.js",
|
|
17
|
+
"module": "./dist/esm/index.js",
|
|
18
|
+
"types": "./dist/esm/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/esm/index.d.ts",
|
|
23
|
+
"default": "./dist/esm/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/cjs/index.d.ts",
|
|
27
|
+
"default": "./dist/cjs/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "node scripts/build.mjs",
|
|
43
|
+
"test": "npm run build && node scripts/run-tests.mjs",
|
|
44
|
+
"conformance": "node tools/check-vectors.mjs",
|
|
45
|
+
"prepack": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@noble/curves": "1.9.7",
|
|
49
|
+
"@noble/hashes": "1.8.0",
|
|
50
|
+
"canonicalize": "2.1.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"esbuild": "0.25.10",
|
|
54
|
+
"typescript": "5.9.3"
|
|
55
|
+
}
|
|
56
|
+
}
|