@datumesh/verify 0.0.0-stage → 0.2.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/LICENSE +202 -0
- package/README.md +56 -2
- package/dist/cjs/ed25519.d.ts +2 -0
- package/dist/cjs/ed25519.js +82 -0
- package/dist/cjs/envelope.d.ts +38 -0
- package/dist/cjs/envelope.js +243 -0
- package/dist/cjs/errors.d.ts +14 -0
- package/dist/cjs/errors.js +65 -0
- package/dist/cjs/hash.d.ts +6 -0
- package/dist/cjs/hash.js +27 -0
- package/dist/cjs/index.d.ts +37 -0
- package/dist/cjs/index.js +101 -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 +325 -0
- package/dist/cjs/keys.d.ts +73 -0
- package/dist/cjs/keys.js +257 -0
- package/dist/cjs/lexical.d.ts +69 -0
- package/dist/cjs/lexical.js +231 -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/units-data.d.ts +4 -0
- package/dist/cjs/units-data.js +11 -0
- package/dist/cjs/units.d.ts +24 -0
- package/dist/cjs/units.js +131 -0
- package/dist/cjs/uri.d.ts +43 -0
- package/dist/cjs/uri.js +209 -0
- package/dist/cjs/utf8.d.ts +10 -0
- package/dist/cjs/utf8.js +101 -0
- package/dist/cjs/validate.d.ts +25 -0
- package/dist/cjs/validate.js +408 -0
- package/dist/cjs/verify.d.ts +57 -0
- package/dist/cjs/verify.js +232 -0
- package/dist/esm/ed25519.d.ts +2 -0
- package/dist/esm/ed25519.js +79 -0
- package/dist/esm/envelope.d.ts +38 -0
- package/dist/esm/envelope.js +240 -0
- package/dist/esm/errors.d.ts +14 -0
- package/dist/esm/errors.js +60 -0
- package/dist/esm/hash.d.ts +6 -0
- package/dist/esm/hash.js +21 -0
- package/dist/esm/index.d.ts +37 -0
- package/dist/esm/index.js +79 -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 +313 -0
- package/dist/esm/keys.d.ts +73 -0
- package/dist/esm/keys.js +249 -0
- package/dist/esm/lexical.d.ts +69 -0
- package/dist/esm/lexical.js +209 -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/units-data.d.ts +4 -0
- package/dist/esm/units-data.js +8 -0
- package/dist/esm/units.d.ts +24 -0
- package/dist/esm/units.js +123 -0
- package/dist/esm/uri.d.ts +43 -0
- package/dist/esm/uri.js +204 -0
- package/dist/esm/utf8.d.ts +10 -0
- package/dist/esm/utf8.js +96 -0
- package/dist/esm/validate.d.ts +25 -0
- package/dist/esm/validate.js +397 -0
- package/dist/esm/verify.d.ts +57 -0
- package/dist/esm/verify.js +227 -0
- package/package.json +54 -4
|
@@ -0,0 +1,397 @@
|
|
|
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
|
+
import { canonUnit, resolveUnit, structUnit } from './units.js';
|
|
8
|
+
export const KINDS = ['scalar', 'cell', 'ref', 'function', 'receipt', 'jcs'];
|
|
9
|
+
const SLICE = '*';
|
|
10
|
+
// ---------------------------------------------------------------- step 2: structure
|
|
11
|
+
function expectObject(v, path) {
|
|
12
|
+
if (!isObject(v))
|
|
13
|
+
fail('E_SCHEMA', 'expected an object', path);
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
function members(o, required, optional, path) {
|
|
17
|
+
const names = new Set([...Object.keys(o), ...required]);
|
|
18
|
+
for (const n of [...names].sort()) {
|
|
19
|
+
if (!has(o, n))
|
|
20
|
+
fail('E_SCHEMA', `missing member ${JSON.stringify(n)}`, path);
|
|
21
|
+
if (!required.includes(n) && !optional.includes(n))
|
|
22
|
+
fail('E_SCHEMA', `unexpected member ${JSON.stringify(n)}`, path);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function expectString(o, k, path) {
|
|
26
|
+
if (typeof o[k] !== 'string')
|
|
27
|
+
fail('E_SCHEMA', `${k} must be a string`, memberPath(path, k));
|
|
28
|
+
}
|
|
29
|
+
function structScalar(v, path) {
|
|
30
|
+
const o = expectObject(v, path);
|
|
31
|
+
members(o, ['type', 'value'], [], path);
|
|
32
|
+
expectString(o, 'type', path);
|
|
33
|
+
if (typeof o.value !== 'string' && typeof o.value !== 'boolean') {
|
|
34
|
+
fail('E_SCHEMA', 'value must be a string or a boolean', memberPath(path, 'value'));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function structArgs(v, path) {
|
|
38
|
+
const o = expectObject(v, path);
|
|
39
|
+
for (const k of sortedKeys(o)) {
|
|
40
|
+
if (o[k] === SLICE)
|
|
41
|
+
continue;
|
|
42
|
+
structScalar(o[k], memberPath(path, k));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function structCell(v, path, ref) {
|
|
46
|
+
const o = expectObject(v, path);
|
|
47
|
+
if (ref)
|
|
48
|
+
members(o, ['args', 'fn', 'ns'], ['asof'], path);
|
|
49
|
+
else
|
|
50
|
+
members(o, ['args', 'fn', 'fn_hash', 'ns'], [], path);
|
|
51
|
+
for (const k of sortedKeys(o)) {
|
|
52
|
+
if (k === 'args')
|
|
53
|
+
structArgs(o.args, memberPath(path, 'args'));
|
|
54
|
+
else
|
|
55
|
+
expectString(o, k, path);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function structFunction(v, path) {
|
|
59
|
+
const o = expectObject(v, path);
|
|
60
|
+
members(o, ['code', 'inputs', 'signature'], [], path);
|
|
61
|
+
const codePath = memberPath(path, 'code');
|
|
62
|
+
const code = expectObject(o.code, codePath);
|
|
63
|
+
members(code, ['body', 'lang'], [], codePath);
|
|
64
|
+
expectString(code, 'body', codePath);
|
|
65
|
+
expectString(code, 'lang', codePath);
|
|
66
|
+
const inputs = o.inputs;
|
|
67
|
+
if (!Array.isArray(inputs) || inputs.some((x) => typeof x !== 'string')) {
|
|
68
|
+
fail('E_SCHEMA', 'inputs must be an array of strings', memberPath(path, 'inputs'));
|
|
69
|
+
}
|
|
70
|
+
structSignature(o.signature, memberPath(path, 'signature'));
|
|
71
|
+
}
|
|
72
|
+
export function structSignature(v, path) {
|
|
73
|
+
const sig = expectObject(v, path);
|
|
74
|
+
members(sig, ['args', 'returns'], [], path);
|
|
75
|
+
const argsPath = memberPath(path, 'args');
|
|
76
|
+
const args = expectObject(sig.args, argsPath);
|
|
77
|
+
for (const k of sortedKeys(args)) {
|
|
78
|
+
const p = memberPath(argsPath, k);
|
|
79
|
+
const d = expectObject(args[k], p);
|
|
80
|
+
members(d, ['type', 'valid'], ['unit'], p);
|
|
81
|
+
expectString(d, 'type', p);
|
|
82
|
+
if (has(d, 'unit'))
|
|
83
|
+
structUnit(d.unit, memberPath(p, 'unit'));
|
|
84
|
+
if (typeof d.valid !== 'boolean')
|
|
85
|
+
fail('E_SCHEMA', 'valid must be a boolean', memberPath(p, 'valid'));
|
|
86
|
+
}
|
|
87
|
+
// SPEC 7: a type name, or {"type", "unit"} (the unit's absence is E_CANON_UNIT, in step 3).
|
|
88
|
+
const rp = memberPath(path, 'returns');
|
|
89
|
+
const r = sig.returns;
|
|
90
|
+
if (typeof r === 'string')
|
|
91
|
+
return;
|
|
92
|
+
if (!isObject(r))
|
|
93
|
+
fail('E_SCHEMA', 'returns must be a type name or an object', rp);
|
|
94
|
+
members(r, ['type'], ['unit'], rp);
|
|
95
|
+
expectString(r, 'type', rp);
|
|
96
|
+
if (has(r, 'unit'))
|
|
97
|
+
structUnit(r.unit, memberPath(rp, 'unit'));
|
|
98
|
+
}
|
|
99
|
+
const RECEIPT_MEMBERS = [
|
|
100
|
+
'asof',
|
|
101
|
+
'cell',
|
|
102
|
+
'computed_by',
|
|
103
|
+
'inputs',
|
|
104
|
+
'issued',
|
|
105
|
+
'licence',
|
|
106
|
+
'proto',
|
|
107
|
+
'publisher',
|
|
108
|
+
'sig',
|
|
109
|
+
'summary',
|
|
110
|
+
'tx',
|
|
111
|
+
'type',
|
|
112
|
+
'valid',
|
|
113
|
+
'value',
|
|
114
|
+
];
|
|
115
|
+
function structReceipt(v, path) {
|
|
116
|
+
const o = expectObject(v, path);
|
|
117
|
+
members(o, RECEIPT_MEMBERS, [], path);
|
|
118
|
+
for (const k of sortedKeys(o)) {
|
|
119
|
+
const p = memberPath(path, k);
|
|
120
|
+
switch (k) {
|
|
121
|
+
case 'cell':
|
|
122
|
+
structCell(o.cell, p, false);
|
|
123
|
+
break;
|
|
124
|
+
case 'inputs': {
|
|
125
|
+
const a = o.inputs;
|
|
126
|
+
if (!Array.isArray(a) || a.some((x) => typeof x !== 'string'))
|
|
127
|
+
fail('E_SCHEMA', 'inputs must be an array of strings', p);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case 'valid':
|
|
131
|
+
structArgs(o.valid, p);
|
|
132
|
+
break;
|
|
133
|
+
case 'value':
|
|
134
|
+
if (typeof o.value !== 'string' && typeof o.value !== 'boolean')
|
|
135
|
+
fail('E_SCHEMA', 'value must be a string or a boolean', p);
|
|
136
|
+
break;
|
|
137
|
+
case 'proto':
|
|
138
|
+
expectString(o, k, path);
|
|
139
|
+
if (o.proto !== 'datumesh/1')
|
|
140
|
+
fail('E_SCHEMA', "proto must be 'datumesh/1'", p);
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
expectString(o, k, path);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------- step 3: canonical form
|
|
148
|
+
function canonScalar(o, path) {
|
|
149
|
+
const t = o.type;
|
|
150
|
+
if (!isTypeName(t))
|
|
151
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(t)}`, memberPath(path, 'type'));
|
|
152
|
+
checkTypedValue(t, o.value, memberPath(path, 'value'));
|
|
153
|
+
}
|
|
154
|
+
function canonArgs(o, path) {
|
|
155
|
+
if (Object.keys(o).length > LIMITS.args)
|
|
156
|
+
fail('E_CANON_LIMIT', `more than ${LIMITS.args} arguments`, path);
|
|
157
|
+
for (const k of sortedKeys(o)) {
|
|
158
|
+
const p = memberPath(path, k);
|
|
159
|
+
checkArgName(k, p);
|
|
160
|
+
if (o[k] !== SLICE)
|
|
161
|
+
canonScalar(o[k], p);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function canonCell(o, path) {
|
|
165
|
+
for (const k of sortedKeys(o)) {
|
|
166
|
+
const p = memberPath(path, k);
|
|
167
|
+
const v = o[k];
|
|
168
|
+
if (k === 'args')
|
|
169
|
+
canonArgs(o.args, p);
|
|
170
|
+
else if (k === 'asof')
|
|
171
|
+
checkInstant(v, p);
|
|
172
|
+
else if (k === 'fn')
|
|
173
|
+
checkName(v, p);
|
|
174
|
+
else if (k === 'fn_hash')
|
|
175
|
+
checkHex(v, 64, p);
|
|
176
|
+
else if (k === 'ns')
|
|
177
|
+
checkNamespace(v, p);
|
|
178
|
+
}
|
|
179
|
+
if (o.ns.length + 1 + o.fn.length > LIMITS.function_name) {
|
|
180
|
+
fail('E_CANON_LIMIT', 'function name too long', path);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function canonStringSet(a, path, each) {
|
|
184
|
+
a.forEach((s, i) => each(s, `${path}[${i}]`));
|
|
185
|
+
for (let i = 1; i < a.length; i++) {
|
|
186
|
+
if (!(a[i - 1] < a[i]))
|
|
187
|
+
fail('E_CANON_SET', 'entries must be strictly ascending', path);
|
|
188
|
+
}
|
|
189
|
+
if (a.length > LIMITS.inputs)
|
|
190
|
+
fail('E_CANON_LIMIT', `more than ${LIMITS.inputs} inputs`, path);
|
|
191
|
+
}
|
|
192
|
+
export function canonSignature(sig, path) {
|
|
193
|
+
const argsPath = memberPath(path, 'args');
|
|
194
|
+
const args = sig.args;
|
|
195
|
+
if (Object.keys(args).length > LIMITS.args)
|
|
196
|
+
fail('E_CANON_LIMIT', `more than ${LIMITS.args} arguments`, argsPath);
|
|
197
|
+
for (const k of sortedKeys(args)) {
|
|
198
|
+
const p = memberPath(argsPath, k);
|
|
199
|
+
checkArgName(k, p);
|
|
200
|
+
const d = args[k];
|
|
201
|
+
// SPEC 7: type, then unit, then valid.
|
|
202
|
+
if (!isTypeName(d.type))
|
|
203
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(d.type)}`, memberPath(p, 'type'));
|
|
204
|
+
if (d.type === 'number') {
|
|
205
|
+
if (!has(d, 'unit'))
|
|
206
|
+
fail('E_CANON_UNIT', 'a number argument declares its unit', p);
|
|
207
|
+
canonUnit(d.unit, args, memberPath(p, 'unit'));
|
|
208
|
+
}
|
|
209
|
+
else if (has(d, 'unit')) {
|
|
210
|
+
fail('E_CANON_UNIT', 'only number arguments have a unit', memberPath(p, 'unit'));
|
|
211
|
+
}
|
|
212
|
+
if (d.valid === true && !VALID_TIME_TYPES.includes(d.type)) {
|
|
213
|
+
fail('E_CANON_TYPE', 'a valid-time argument must be a date, instant or string', p);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const rp = memberPath(path, 'returns');
|
|
217
|
+
const r = sig.returns;
|
|
218
|
+
if (typeof r === 'string') {
|
|
219
|
+
if (!isTypeName(r))
|
|
220
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(r)}`, rp);
|
|
221
|
+
if (r === 'number')
|
|
222
|
+
fail('E_CANON_UNIT', 'a number return is {"type": "number", "unit": ...}, not the bare name', rp);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const o = r;
|
|
226
|
+
if (!isTypeName(o.type))
|
|
227
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(o.type)}`, memberPath(rp, 'type'));
|
|
228
|
+
if (o.type !== 'number')
|
|
229
|
+
fail('E_CANON_UNIT', 'only a number return is written as an object', rp);
|
|
230
|
+
if (!has(o, 'unit'))
|
|
231
|
+
fail('E_CANON_UNIT', 'a number return declares its unit', rp);
|
|
232
|
+
canonUnit(o.unit, args, memberPath(rp, 'unit'));
|
|
233
|
+
}
|
|
234
|
+
/** The return type named by a valid signature's `returns` (SPEC 10.1 step 7.4). */
|
|
235
|
+
export function returnType(sig) {
|
|
236
|
+
const r = sig.returns;
|
|
237
|
+
return typeof r === 'string' ? r : r.type;
|
|
238
|
+
}
|
|
239
|
+
function canonFunction(o, path) {
|
|
240
|
+
const code = o.code;
|
|
241
|
+
const codePath = memberPath(path, 'code');
|
|
242
|
+
checkString(code.body, memberPath(codePath, 'body'), { body: true });
|
|
243
|
+
checkName(code.lang, memberPath(codePath, 'lang'));
|
|
244
|
+
canonStringSet(o.inputs, memberPath(path, 'inputs'), checkFunctionName);
|
|
245
|
+
canonSignature(o.signature, memberPath(path, 'signature'));
|
|
246
|
+
}
|
|
247
|
+
function canonReceipt(o, path) {
|
|
248
|
+
for (const k of sortedKeys(o)) {
|
|
249
|
+
const p = memberPath(path, k);
|
|
250
|
+
const v = o[k];
|
|
251
|
+
switch (k) {
|
|
252
|
+
case 'asof':
|
|
253
|
+
case 'issued':
|
|
254
|
+
case 'tx':
|
|
255
|
+
checkInstant(v, p);
|
|
256
|
+
break;
|
|
257
|
+
case 'cell':
|
|
258
|
+
canonCell(v, p);
|
|
259
|
+
break;
|
|
260
|
+
case 'computed_by':
|
|
261
|
+
checkKeyRef(v, p);
|
|
262
|
+
break;
|
|
263
|
+
case 'inputs':
|
|
264
|
+
canonStringSet(v, p, (s, q) => checkHex(s, 64, q));
|
|
265
|
+
break;
|
|
266
|
+
case 'licence':
|
|
267
|
+
checkLicence(v, p);
|
|
268
|
+
break;
|
|
269
|
+
case 'publisher':
|
|
270
|
+
checkNamespace(v, p);
|
|
271
|
+
break;
|
|
272
|
+
case 'sig':
|
|
273
|
+
checkHex(v, 128, p);
|
|
274
|
+
break;
|
|
275
|
+
case 'summary':
|
|
276
|
+
if (v === '')
|
|
277
|
+
fail('E_CANON_STRING', 'summary must not be empty', p);
|
|
278
|
+
checkString(v, p, { limit: LIMITS.summary });
|
|
279
|
+
break;
|
|
280
|
+
case 'type':
|
|
281
|
+
if (!isTypeName(v))
|
|
282
|
+
fail('E_CANON_TYPE', `unknown type ${JSON.stringify(v)}`, p);
|
|
283
|
+
break;
|
|
284
|
+
case 'valid':
|
|
285
|
+
canonArgs(v, p);
|
|
286
|
+
break;
|
|
287
|
+
case 'value':
|
|
288
|
+
checkTypedValue(o.type, v, p);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// ---------------------------------------------------------------- step 4: consistency
|
|
294
|
+
function consistency(o, path) {
|
|
295
|
+
const cell = o.cell;
|
|
296
|
+
const args = cell.args;
|
|
297
|
+
const valid = o.valid;
|
|
298
|
+
const argsPath = memberPath(memberPath(path, 'cell'), 'args');
|
|
299
|
+
const validPath = memberPath(path, 'valid');
|
|
300
|
+
for (const k of sortedKeys(args)) {
|
|
301
|
+
if (args[k] === SLICE)
|
|
302
|
+
fail('E_SLICE', 'a receipt is for one cell, not a slice', memberPath(argsPath, k));
|
|
303
|
+
}
|
|
304
|
+
for (const k of sortedKeys(valid)) {
|
|
305
|
+
if (valid[k] === SLICE)
|
|
306
|
+
fail('E_SLICE', 'a receipt is for one cell, not a slice', memberPath(validPath, k));
|
|
307
|
+
}
|
|
308
|
+
if (o.publisher !== cell.ns)
|
|
309
|
+
fail('E_PUBLISHER_MISMATCH', 'publisher must equal cell.ns', memberPath(path, 'publisher'));
|
|
310
|
+
for (const k of sortedKeys(valid)) {
|
|
311
|
+
const v = valid[k];
|
|
312
|
+
const a = has(args, k) ? args[k] : null;
|
|
313
|
+
if (!a || a.type !== v.type || a.value !== v.value) {
|
|
314
|
+
fail('E_VALID_MISMATCH', `valid.${k} does not echo cell.args.${k}`, validPath);
|
|
315
|
+
}
|
|
316
|
+
if (!VALID_TIME_TYPES.includes(v.type)) {
|
|
317
|
+
fail('E_VALID_MISMATCH', 'valid-time arguments are dates, instants or strings', validPath);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const tx = o.tx;
|
|
321
|
+
const asof = o.asof;
|
|
322
|
+
const issued = o.issued;
|
|
323
|
+
// Instants are fixed width, so string order is time order (SPEC 3.3).
|
|
324
|
+
if (!(tx <= asof && asof <= issued))
|
|
325
|
+
fail('E_TIME_ORDER', 'requires tx <= asof <= issued', path);
|
|
326
|
+
}
|
|
327
|
+
// ---------------------------------------------------------------- entry points
|
|
328
|
+
/** Validate `v` as `kind` (steps 2-3; for receipts also step 4). Throws DatumeshError. */
|
|
329
|
+
export function validate(v, kind, path = '$') {
|
|
330
|
+
switch (kind) {
|
|
331
|
+
case 'jcs':
|
|
332
|
+
return;
|
|
333
|
+
case 'scalar':
|
|
334
|
+
structScalar(v, path);
|
|
335
|
+
canonScalar(v, path);
|
|
336
|
+
return;
|
|
337
|
+
case 'cell':
|
|
338
|
+
case 'ref':
|
|
339
|
+
structCell(v, path, kind === 'ref');
|
|
340
|
+
canonCell(v, path);
|
|
341
|
+
return;
|
|
342
|
+
case 'function':
|
|
343
|
+
structFunction(v, path);
|
|
344
|
+
canonFunction(v, path);
|
|
345
|
+
return;
|
|
346
|
+
case 'receipt':
|
|
347
|
+
structReceipt(v, path);
|
|
348
|
+
canonReceipt(v, path);
|
|
349
|
+
consistency(v, path);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The unit of a cell's value (SPEC 7.1, units.md 3). Order: the cell (structure, canonical
|
|
355
|
+
* form), fn_hash against the descriptor (E_FN_HASH_MISMATCH), the descriptor (structure,
|
|
356
|
+
* canonical form), then substitution.
|
|
357
|
+
*/
|
|
358
|
+
export function cellUnit(desc, cell, fnHash) {
|
|
359
|
+
structCell(cell, 'cell', false);
|
|
360
|
+
canonCell(cell, 'cell');
|
|
361
|
+
if (fnHash(desc) !== cell.fn_hash)
|
|
362
|
+
fail('E_FN_HASH_MISMATCH', 'the descriptor does not hash to cell.fn_hash', 'descriptor');
|
|
363
|
+
structFunction(desc, 'descriptor');
|
|
364
|
+
canonFunction(desc, 'descriptor');
|
|
365
|
+
const sig = desc.signature;
|
|
366
|
+
return resolveUnit(sig.returns, cell.args, 'descriptor.signature.returns', 'cell.args');
|
|
367
|
+
}
|
|
368
|
+
/** Receipt step 2 only: true when the receipt has the right members and JSON types. */
|
|
369
|
+
export function receiptStructure(v) {
|
|
370
|
+
structReceipt(v, '$');
|
|
371
|
+
}
|
|
372
|
+
/** Receipt steps 3 and 4, after receiptStructure has passed. */
|
|
373
|
+
export function receiptCanonicalAndConsistent(v) {
|
|
374
|
+
canonReceipt(v, '$');
|
|
375
|
+
consistency(v, '$');
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Infer the kind of an object from its members when the caller gives none: a receipt has
|
|
379
|
+
* `sig`, `proto` or `computed_by`; a descriptor `signature` or `code`; a cell `fn_hash`; a cell
|
|
380
|
+
* reference `fn`, `ns` or `args`; a typed scalar `type` or `value`. Anything else is E_SCHEMA.
|
|
381
|
+
*/
|
|
382
|
+
export function inferKind(v) {
|
|
383
|
+
if (!isObject(v))
|
|
384
|
+
fail('E_SCHEMA', 'cannot infer the kind of a non-object; pass kind', '$');
|
|
385
|
+
const h = (k) => has(v, k);
|
|
386
|
+
if (h('sig') || h('proto') || h('computed_by'))
|
|
387
|
+
return 'receipt';
|
|
388
|
+
if (h('signature') || h('code'))
|
|
389
|
+
return 'function';
|
|
390
|
+
if (h('fn_hash'))
|
|
391
|
+
return 'cell';
|
|
392
|
+
if (h('fn') || h('ns') || h('args'))
|
|
393
|
+
return 'ref';
|
|
394
|
+
if (h('type') || h('value'))
|
|
395
|
+
return 'scalar';
|
|
396
|
+
fail('E_SCHEMA', 'cannot infer the kind from the members; pass kind', '$');
|
|
397
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type ErrorCode } from './errors.js';
|
|
2
|
+
import { type JsonObject, type JsonValue } from './json.js';
|
|
3
|
+
import { type Sources } from './keys.js';
|
|
4
|
+
export interface VerifyResult {
|
|
5
|
+
/** True when the receipt is signature-valid (and, if recompute was given, fully verified). */
|
|
6
|
+
ok: boolean;
|
|
7
|
+
/** "OK" or a SPEC 10.2 error code. */
|
|
8
|
+
code: 'OK' | ErrorCode;
|
|
9
|
+
/** "signed" (steps 1-6) or "recomputed" (steps 1-7) when ok, else null. */
|
|
10
|
+
level: 'signed' | 'recomputed' | null;
|
|
11
|
+
/** The receipt hash, once the receipt has the structure of a receipt (step 2), else null. */
|
|
12
|
+
hash: string | null;
|
|
13
|
+
/** The `computed_by` key reference when ok, else null. */
|
|
14
|
+
key: string | null;
|
|
15
|
+
/** Human-readable explanation. */
|
|
16
|
+
detail: string;
|
|
17
|
+
/** Where the failure was found (a JSON path like `$.cell.args.base`), or null. */
|
|
18
|
+
path: string | null;
|
|
19
|
+
/** True only when blob content was supplied and step 7.7 ran and passed. Does not change `level`. */
|
|
20
|
+
blobChecked: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Input for SPEC 10.1 step 7. The library does not evaluate functions; `value` is the result. */
|
|
23
|
+
export interface Recompute {
|
|
24
|
+
/** The function descriptor for cell.fn_hash (object or JSON text); absent: E_FN_UNAVAILABLE. */
|
|
25
|
+
descriptor?: unknown;
|
|
26
|
+
/** Input receipts by receipt hash, as JSON text (or bytes, or parsed objects). */
|
|
27
|
+
inputs?: Record<string, unknown>;
|
|
28
|
+
/** What evaluating the function produced: a typed scalar. */
|
|
29
|
+
value: {
|
|
30
|
+
type: string;
|
|
31
|
+
value: string | boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
declare class Stop extends Error {
|
|
35
|
+
readonly code: ErrorCode;
|
|
36
|
+
readonly detail: string;
|
|
37
|
+
readonly path: string | null;
|
|
38
|
+
constructor(code: ErrorCode, detail: string, path: string | null);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
42
|
+
* 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. With
|
|
44
|
+
* `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
|
|
45
|
+
* otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
|
|
46
|
+
* content does not match. It does not change `level`; `blobChecked` reports it.
|
|
47
|
+
*
|
|
48
|
+
* Never throws for an invalid receipt or bad key material; throws TypeError only when an
|
|
49
|
+
* argument is of the wrong kind altogether.
|
|
50
|
+
*/
|
|
51
|
+
export declare function verify(receipt: unknown, keys: unknown, recomputeArg?: Recompute | null, blob?: Uint8Array | ArrayBuffer | null): VerifyResult;
|
|
52
|
+
/** Steps 1-6 for an already-parsed receipt (used by checkEnvelope). Throws a Stop. */
|
|
53
|
+
export declare function verifyEmbedded(receipt: JsonValue, sources: Sources): {
|
|
54
|
+
receipt: JsonObject;
|
|
55
|
+
hash: string;
|
|
56
|
+
};
|
|
57
|
+
export { Stop };
|
|
@@ -0,0 +1,227 @@
|
|
|
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 { checkBlob } from './lexical.js';
|
|
9
|
+
import { receiptCanonicalAndConsistent, receiptStructure, returnType, validate } from './validate.js';
|
|
10
|
+
class Stop extends Error {
|
|
11
|
+
constructor(code, detail, path) {
|
|
12
|
+
super(detail);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.detail = detail;
|
|
15
|
+
this.path = path;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function stop(code, detail, path = '$') {
|
|
19
|
+
throw new Stop(code, detail, path);
|
|
20
|
+
}
|
|
21
|
+
function withoutSig(r) {
|
|
22
|
+
const o = newObject();
|
|
23
|
+
for (const k of Object.keys(r))
|
|
24
|
+
if (k !== 'sig')
|
|
25
|
+
o[k] = r[k];
|
|
26
|
+
return o;
|
|
27
|
+
}
|
|
28
|
+
/** Steps 1-6. Throws Stop; returns the parsed receipt and its hash. */
|
|
29
|
+
function verifySigned(input, sources, setHash) {
|
|
30
|
+
let r;
|
|
31
|
+
try {
|
|
32
|
+
r = toJson(input); // step 1
|
|
33
|
+
receiptStructure(r); // step 2
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
if (e instanceof DatumeshError)
|
|
37
|
+
stop(e.code, e.detail, e.path);
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
const receipt = r;
|
|
41
|
+
const payload = jcsBytes(withoutSig(receipt));
|
|
42
|
+
const hash = sha256Hex(payload);
|
|
43
|
+
setHash(hash);
|
|
44
|
+
try {
|
|
45
|
+
receiptCanonicalAndConsistent(receipt); // steps 3-4
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (e instanceof DatumeshError)
|
|
49
|
+
stop(e.code, e.detail, e.path);
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
// Step 5.
|
|
53
|
+
const ref = receipt.computed_by;
|
|
54
|
+
const hashAt = ref.indexOf('#');
|
|
55
|
+
const host = ref.slice(0, hashAt);
|
|
56
|
+
const id = ref.slice(hashAt + 1);
|
|
57
|
+
const found = lookupKey(sources, host, id, receipt.issued);
|
|
58
|
+
if (!found.ok)
|
|
59
|
+
stop(found.code, found.detail, found.code === 'E_KEY_NOT_VALID' ? '$.issued' : '$.computed_by');
|
|
60
|
+
// Step 6.
|
|
61
|
+
if (!verifyEd25519Strict(fromHex(receipt.sig), payload, fromHex(found.key.public))) {
|
|
62
|
+
stop('E_SIGNATURE', 'Ed25519 verification failed', '$.sig');
|
|
63
|
+
}
|
|
64
|
+
return { receipt, hash };
|
|
65
|
+
}
|
|
66
|
+
function recompute(receipt, rc, sources) {
|
|
67
|
+
const cell = receipt.cell;
|
|
68
|
+
const cellArgs = cell.args;
|
|
69
|
+
// 7.1 Descriptor.
|
|
70
|
+
if (rc.descriptor === undefined || rc.descriptor === null)
|
|
71
|
+
stop('E_FN_UNAVAILABLE', `no descriptor for ${cell.fn_hash}`, '$.cell.fn_hash');
|
|
72
|
+
let desc;
|
|
73
|
+
try {
|
|
74
|
+
const d = toJson(rc.descriptor);
|
|
75
|
+
if (sha256Hex(jcsBytes(d)) !== cell.fn_hash)
|
|
76
|
+
stop('E_FN_HASH_MISMATCH', 'descriptor does not hash to cell.fn_hash', '$.cell.fn_hash');
|
|
77
|
+
validate(d, 'function', 'descriptor');
|
|
78
|
+
desc = d;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e instanceof DatumeshError)
|
|
82
|
+
stop(e.code, `descriptor: ${e.detail}`, e.path);
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
const sig = desc.signature;
|
|
86
|
+
const declared = sig.args;
|
|
87
|
+
// 7.2 Arguments: unknown, then missing, then types.
|
|
88
|
+
for (const k of sortedKeys(cellArgs)) {
|
|
89
|
+
if (!has(declared, k))
|
|
90
|
+
stop('E_ARG_UNKNOWN', `function declares no argument ${JSON.stringify(k)}`, `$.cell.args.${k}`);
|
|
91
|
+
}
|
|
92
|
+
for (const k of sortedKeys(declared)) {
|
|
93
|
+
if (!has(cellArgs, k))
|
|
94
|
+
stop('E_ARG_MISSING', `argument ${JSON.stringify(k)} is not bound`, '$.cell.args');
|
|
95
|
+
}
|
|
96
|
+
for (const k of sortedKeys(cellArgs)) {
|
|
97
|
+
const want = declared[k].type;
|
|
98
|
+
if (cellArgs[k].type !== want)
|
|
99
|
+
stop('E_ARG_TYPE', `${k} is declared ${String(want)}`, `$.cell.args.${k}`);
|
|
100
|
+
}
|
|
101
|
+
// 7.3 valid holds exactly the declared valid-time arguments.
|
|
102
|
+
const validNames = sortedKeys(receipt.valid);
|
|
103
|
+
const declaredValid = sortedKeys(declared).filter((k) => declared[k].valid === true);
|
|
104
|
+
if (validNames.join('\u0000') !== declaredValid.join('\u0000')) {
|
|
105
|
+
stop('E_VALID_MISMATCH', 'valid must echo exactly the declared valid-time arguments', '$.valid');
|
|
106
|
+
}
|
|
107
|
+
// 7.4 Return type: `returns`, or its `type` member when it is an object.
|
|
108
|
+
const rt = returnType(sig);
|
|
109
|
+
if (receipt.type !== rt)
|
|
110
|
+
stop('E_RETURN_TYPE', `function returns ${rt}`, '$.type');
|
|
111
|
+
// 7.5 Inputs.
|
|
112
|
+
const declaredInputs = desc.inputs;
|
|
113
|
+
const supplied = rc.inputs ?? {};
|
|
114
|
+
let latestTx = null;
|
|
115
|
+
for (const h of receipt.inputs) {
|
|
116
|
+
if (!Object.prototype.hasOwnProperty.call(supplied, h))
|
|
117
|
+
stop('E_INPUT_UNAVAILABLE', `input receipt ${h} not available`, '$.inputs');
|
|
118
|
+
let inner;
|
|
119
|
+
try {
|
|
120
|
+
inner = verifySigned(supplied[h], sources, () => undefined);
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
if (e instanceof Stop)
|
|
124
|
+
stop('E_INPUT_INVALID', `input ${h}: ${e.code}: ${e.detail}`, '$.inputs');
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
if (inner.hash !== h)
|
|
128
|
+
stop('E_INPUT_UNAVAILABLE', `supplied receipt does not hash to ${h}`, '$.inputs');
|
|
129
|
+
const ic = inner.receipt.cell;
|
|
130
|
+
if (!declaredInputs.includes(`${ic.ns}/${ic.fn}`)) {
|
|
131
|
+
stop('E_INPUT_UNDECLARED', `input ${h} is a cell of an undeclared function`, '$.inputs');
|
|
132
|
+
}
|
|
133
|
+
if (inner.receipt.asof !== receipt.asof)
|
|
134
|
+
stop('E_TIME_ORDER', "input receipts must share the receipt's asof", '$.inputs');
|
|
135
|
+
const itx = inner.receipt.tx;
|
|
136
|
+
if (latestTx === null || itx > latestTx)
|
|
137
|
+
latestTx = itx;
|
|
138
|
+
}
|
|
139
|
+
if (latestTx !== null && receipt.tx !== latestTx)
|
|
140
|
+
stop('E_TIME_ORDER', 'tx must be the latest input tx', '$.tx');
|
|
141
|
+
// 7.6 Value.
|
|
142
|
+
const v = rc.value;
|
|
143
|
+
if (v.type !== receipt.type || v.value !== receipt.value)
|
|
144
|
+
stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Step 7.7: content is only for a blob value (E_SCHEMA otherwise); it has exactly `length`
|
|
148
|
+
* bytes, then a SHA-256 equal to the blob's hash (E_BLOB_MISMATCH).
|
|
149
|
+
*/
|
|
150
|
+
function checkContent(receipt, content) {
|
|
151
|
+
if (receipt.type !== 'blob')
|
|
152
|
+
stop('E_SCHEMA', `content was supplied, but the value is a ${String(receipt.type)}, not a blob`, '$.type');
|
|
153
|
+
const blob = checkBlob(receipt.value, '$.value'); // already checked in step 3
|
|
154
|
+
if (content.length !== blob.length)
|
|
155
|
+
stop('E_BLOB_MISMATCH', `content is ${content.length} bytes, the blob says ${blob.length}`, '$.value');
|
|
156
|
+
if (sha256Hex(content) !== blob.sha256)
|
|
157
|
+
stop('E_BLOB_MISMATCH', "the content's SHA-256 is not the blob's hash", '$.value');
|
|
158
|
+
}
|
|
159
|
+
function checkBlobArg(b) {
|
|
160
|
+
if (b === undefined || b === null)
|
|
161
|
+
return null;
|
|
162
|
+
if (b instanceof Uint8Array)
|
|
163
|
+
return b;
|
|
164
|
+
if (b instanceof ArrayBuffer)
|
|
165
|
+
return new Uint8Array(b);
|
|
166
|
+
throw new TypeError('blob must be the content bytes (Uint8Array or ArrayBuffer)');
|
|
167
|
+
}
|
|
168
|
+
function checkRecomputeArg(rc) {
|
|
169
|
+
if (rc === undefined || rc === null)
|
|
170
|
+
return null;
|
|
171
|
+
if (typeof rc !== 'object' || Array.isArray(rc))
|
|
172
|
+
throw new TypeError('recompute must be an object {descriptor, inputs, value}');
|
|
173
|
+
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}');
|
|
176
|
+
}
|
|
177
|
+
if (r.inputs !== undefined && (typeof r.inputs !== 'object' || r.inputs === null || Array.isArray(r.inputs))) {
|
|
178
|
+
throw new TypeError('recompute.inputs must be an object mapping receipt hash to receipt JSON');
|
|
179
|
+
}
|
|
180
|
+
return r;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
184
|
+
* 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. With
|
|
186
|
+
* `blob`, the content of a blob value, step 7.7 runs last (after step 7 when recomputing,
|
|
187
|
+
* otherwise right after step 6): E_SCHEMA if the value is not a blob, E_BLOB_MISMATCH if the
|
|
188
|
+
* content does not match. It does not change `level`; `blobChecked` reports it.
|
|
189
|
+
*
|
|
190
|
+
* Never throws for an invalid receipt or bad key material; throws TypeError only when an
|
|
191
|
+
* argument is of the wrong kind altogether.
|
|
192
|
+
*/
|
|
193
|
+
export function verify(receipt, keys, recomputeArg, blob) {
|
|
194
|
+
const sources = collectSources(keys);
|
|
195
|
+
const rc = checkRecomputeArg(recomputeArg);
|
|
196
|
+
const content = checkBlobArg(blob);
|
|
197
|
+
let hash = null;
|
|
198
|
+
try {
|
|
199
|
+
const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
|
|
200
|
+
if (rc)
|
|
201
|
+
recompute(r, rc, sources);
|
|
202
|
+
const checked = content !== null;
|
|
203
|
+
if (checked)
|
|
204
|
+
checkContent(r, content);
|
|
205
|
+
const what = rc ? 'fully verified: signature valid and value recomputed' : `signature-valid: signed by ${r.computed_by}`;
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
code: 'OK',
|
|
209
|
+
level: rc ? 'recomputed' : 'signed',
|
|
210
|
+
hash,
|
|
211
|
+
key: r.computed_by,
|
|
212
|
+
detail: checked ? `${what}; blob content matches` : what,
|
|
213
|
+
path: null,
|
|
214
|
+
blobChecked: checked,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
if (e instanceof Stop)
|
|
219
|
+
return { ok: false, code: e.code, level: null, hash, key: null, detail: e.detail, path: e.path, blobChecked: false };
|
|
220
|
+
throw e; // a bug in this library, not a property of the receipt
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Steps 1-6 for an already-parsed receipt (used by checkEnvelope). Throws a Stop. */
|
|
224
|
+
export function verifyEmbedded(receipt, sources) {
|
|
225
|
+
return verifySigned(receipt, sources, () => undefined);
|
|
226
|
+
}
|
|
227
|
+
export { Stop };
|