@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,349 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KINDS = void 0;
|
|
4
|
+
exports.structSignature = structSignature;
|
|
5
|
+
exports.canonSignature = canonSignature;
|
|
6
|
+
exports.validate = validate;
|
|
7
|
+
exports.receiptStructure = receiptStructure;
|
|
8
|
+
exports.receiptCanonicalAndConsistent = receiptCanonicalAndConsistent;
|
|
9
|
+
exports.inferKind = inferKind;
|
|
10
|
+
// Structure (SPEC 10.1 step 2, E_SCHEMA), canonical form (step 3) and receipt consistency
|
|
11
|
+
// (step 4) for every kind of Datumesh object. Within a step members are examined in JCS order,
|
|
12
|
+
// depth first; at each object its member set is checked before its members.
|
|
13
|
+
const errors_js_1 = require("./errors.js");
|
|
14
|
+
const json_js_1 = require("./json.js");
|
|
15
|
+
const lexical_js_1 = require("./lexical.js");
|
|
16
|
+
exports.KINDS = ['scalar', 'cell', 'ref', 'function', 'receipt', 'jcs'];
|
|
17
|
+
const SLICE = '*';
|
|
18
|
+
// ---------------------------------------------------------------- step 2: structure
|
|
19
|
+
function expectObject(v, path) {
|
|
20
|
+
if (!(0, json_js_1.isObject)(v))
|
|
21
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'expected an object', path);
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
function members(o, required, optional, path) {
|
|
25
|
+
const names = new Set([...Object.keys(o), ...required]);
|
|
26
|
+
for (const n of [...names].sort()) {
|
|
27
|
+
if (!(0, json_js_1.has)(o, n))
|
|
28
|
+
(0, errors_js_1.fail)('E_SCHEMA', `missing member ${JSON.stringify(n)}`, path);
|
|
29
|
+
if (!required.includes(n) && !optional.includes(n))
|
|
30
|
+
(0, errors_js_1.fail)('E_SCHEMA', `unexpected member ${JSON.stringify(n)}`, path);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function expectString(o, k, path) {
|
|
34
|
+
if (typeof o[k] !== 'string')
|
|
35
|
+
(0, errors_js_1.fail)('E_SCHEMA', `${k} must be a string`, (0, json_js_1.memberPath)(path, k));
|
|
36
|
+
}
|
|
37
|
+
function structScalar(v, path) {
|
|
38
|
+
const o = expectObject(v, path);
|
|
39
|
+
members(o, ['type', 'value'], [], path);
|
|
40
|
+
expectString(o, 'type', path);
|
|
41
|
+
if (typeof o.value !== 'string' && typeof o.value !== 'boolean') {
|
|
42
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'value must be a string or a boolean', (0, json_js_1.memberPath)(path, 'value'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function structArgs(v, path) {
|
|
46
|
+
const o = expectObject(v, path);
|
|
47
|
+
for (const k of (0, json_js_1.sortedKeys)(o)) {
|
|
48
|
+
if (o[k] === SLICE)
|
|
49
|
+
continue;
|
|
50
|
+
structScalar(o[k], (0, json_js_1.memberPath)(path, k));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function structCell(v, path, ref) {
|
|
54
|
+
const o = expectObject(v, path);
|
|
55
|
+
if (ref)
|
|
56
|
+
members(o, ['args', 'fn', 'ns'], ['asof'], path);
|
|
57
|
+
else
|
|
58
|
+
members(o, ['args', 'fn', 'fn_hash', 'ns'], [], path);
|
|
59
|
+
for (const k of (0, json_js_1.sortedKeys)(o)) {
|
|
60
|
+
if (k === 'args')
|
|
61
|
+
structArgs(o.args, (0, json_js_1.memberPath)(path, 'args'));
|
|
62
|
+
else
|
|
63
|
+
expectString(o, k, path);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function structFunction(v, path) {
|
|
67
|
+
const o = expectObject(v, path);
|
|
68
|
+
members(o, ['code', 'inputs', 'signature'], [], path);
|
|
69
|
+
const codePath = (0, json_js_1.memberPath)(path, 'code');
|
|
70
|
+
const code = expectObject(o.code, codePath);
|
|
71
|
+
members(code, ['body', 'lang'], [], codePath);
|
|
72
|
+
expectString(code, 'body', codePath);
|
|
73
|
+
expectString(code, 'lang', codePath);
|
|
74
|
+
const inputs = o.inputs;
|
|
75
|
+
if (!Array.isArray(inputs) || inputs.some((x) => typeof x !== 'string')) {
|
|
76
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'inputs must be an array of strings', (0, json_js_1.memberPath)(path, 'inputs'));
|
|
77
|
+
}
|
|
78
|
+
structSignature(o.signature, (0, json_js_1.memberPath)(path, 'signature'));
|
|
79
|
+
}
|
|
80
|
+
function structSignature(v, path) {
|
|
81
|
+
const sig = expectObject(v, path);
|
|
82
|
+
members(sig, ['args', 'returns'], [], path);
|
|
83
|
+
const argsPath = (0, json_js_1.memberPath)(path, 'args');
|
|
84
|
+
const args = expectObject(sig.args, argsPath);
|
|
85
|
+
for (const k of (0, json_js_1.sortedKeys)(args)) {
|
|
86
|
+
const p = (0, json_js_1.memberPath)(argsPath, k);
|
|
87
|
+
const d = expectObject(args[k], p);
|
|
88
|
+
members(d, ['type', 'valid'], [], p);
|
|
89
|
+
expectString(d, 'type', p);
|
|
90
|
+
if (typeof d.valid !== 'boolean')
|
|
91
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'valid must be a boolean', (0, json_js_1.memberPath)(p, 'valid'));
|
|
92
|
+
}
|
|
93
|
+
expectString(sig, 'returns', path);
|
|
94
|
+
}
|
|
95
|
+
const RECEIPT_MEMBERS = [
|
|
96
|
+
'asof',
|
|
97
|
+
'cell',
|
|
98
|
+
'computed_by',
|
|
99
|
+
'inputs',
|
|
100
|
+
'issued',
|
|
101
|
+
'licence',
|
|
102
|
+
'proto',
|
|
103
|
+
'publisher',
|
|
104
|
+
'sig',
|
|
105
|
+
'summary',
|
|
106
|
+
'tx',
|
|
107
|
+
'type',
|
|
108
|
+
'valid',
|
|
109
|
+
'value',
|
|
110
|
+
];
|
|
111
|
+
function structReceipt(v, path) {
|
|
112
|
+
const o = expectObject(v, path);
|
|
113
|
+
members(o, RECEIPT_MEMBERS, [], path);
|
|
114
|
+
for (const k of (0, json_js_1.sortedKeys)(o)) {
|
|
115
|
+
const p = (0, json_js_1.memberPath)(path, k);
|
|
116
|
+
switch (k) {
|
|
117
|
+
case 'cell':
|
|
118
|
+
structCell(o.cell, p, false);
|
|
119
|
+
break;
|
|
120
|
+
case 'inputs': {
|
|
121
|
+
const a = o.inputs;
|
|
122
|
+
if (!Array.isArray(a) || a.some((x) => typeof x !== 'string'))
|
|
123
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'inputs must be an array of strings', p);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'valid':
|
|
127
|
+
structArgs(o.valid, p);
|
|
128
|
+
break;
|
|
129
|
+
case 'value':
|
|
130
|
+
if (typeof o.value !== 'string' && typeof o.value !== 'boolean')
|
|
131
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'value must be a string or a boolean', p);
|
|
132
|
+
break;
|
|
133
|
+
case 'proto':
|
|
134
|
+
expectString(o, k, path);
|
|
135
|
+
if (o.proto !== 'datumesh/1')
|
|
136
|
+
(0, errors_js_1.fail)('E_SCHEMA', "proto must be 'datumesh/1'", p);
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
expectString(o, k, path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------- step 3: canonical form
|
|
144
|
+
function canonScalar(o, path) {
|
|
145
|
+
const t = o.type;
|
|
146
|
+
if (!(0, lexical_js_1.isTypeName)(t))
|
|
147
|
+
(0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(t)}`, (0, json_js_1.memberPath)(path, 'type'));
|
|
148
|
+
(0, lexical_js_1.checkTypedValue)(t, o.value, (0, json_js_1.memberPath)(path, 'value'));
|
|
149
|
+
}
|
|
150
|
+
function canonArgs(o, path) {
|
|
151
|
+
if (Object.keys(o).length > lexical_js_1.LIMITS.args)
|
|
152
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', `more than ${lexical_js_1.LIMITS.args} arguments`, path);
|
|
153
|
+
for (const k of (0, json_js_1.sortedKeys)(o)) {
|
|
154
|
+
const p = (0, json_js_1.memberPath)(path, k);
|
|
155
|
+
(0, lexical_js_1.checkArgName)(k, p);
|
|
156
|
+
if (o[k] !== SLICE)
|
|
157
|
+
canonScalar(o[k], p);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function canonCell(o, path) {
|
|
161
|
+
for (const k of (0, json_js_1.sortedKeys)(o)) {
|
|
162
|
+
const p = (0, json_js_1.memberPath)(path, k);
|
|
163
|
+
const v = o[k];
|
|
164
|
+
if (k === 'args')
|
|
165
|
+
canonArgs(o.args, p);
|
|
166
|
+
else if (k === 'asof')
|
|
167
|
+
(0, lexical_js_1.checkInstant)(v, p);
|
|
168
|
+
else if (k === 'fn')
|
|
169
|
+
(0, lexical_js_1.checkName)(v, p);
|
|
170
|
+
else if (k === 'fn_hash')
|
|
171
|
+
(0, lexical_js_1.checkHex)(v, 64, p);
|
|
172
|
+
else if (k === 'ns')
|
|
173
|
+
(0, lexical_js_1.checkNamespace)(v, p);
|
|
174
|
+
}
|
|
175
|
+
if (o.ns.length + 1 + o.fn.length > lexical_js_1.LIMITS.function_name) {
|
|
176
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', 'function name too long', path);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function canonStringSet(a, path, each) {
|
|
180
|
+
a.forEach((s, i) => each(s, `${path}[${i}]`));
|
|
181
|
+
for (let i = 1; i < a.length; i++) {
|
|
182
|
+
if (!(a[i - 1] < a[i]))
|
|
183
|
+
(0, errors_js_1.fail)('E_CANON_SET', 'entries must be strictly ascending', path);
|
|
184
|
+
}
|
|
185
|
+
if (a.length > lexical_js_1.LIMITS.inputs)
|
|
186
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', `more than ${lexical_js_1.LIMITS.inputs} inputs`, path);
|
|
187
|
+
}
|
|
188
|
+
function canonSignature(sig, path) {
|
|
189
|
+
const argsPath = (0, json_js_1.memberPath)(path, 'args');
|
|
190
|
+
const args = sig.args;
|
|
191
|
+
if (Object.keys(args).length > lexical_js_1.LIMITS.args)
|
|
192
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', `more than ${lexical_js_1.LIMITS.args} arguments`, argsPath);
|
|
193
|
+
for (const k of (0, json_js_1.sortedKeys)(args)) {
|
|
194
|
+
const p = (0, json_js_1.memberPath)(argsPath, k);
|
|
195
|
+
(0, lexical_js_1.checkArgName)(k, p);
|
|
196
|
+
const d = args[k];
|
|
197
|
+
if (!(0, lexical_js_1.isTypeName)(d.type))
|
|
198
|
+
(0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(d.type)}`, (0, json_js_1.memberPath)(p, 'type'));
|
|
199
|
+
if (d.valid === true && !lexical_js_1.VALID_TIME_TYPES.includes(d.type)) {
|
|
200
|
+
(0, errors_js_1.fail)('E_CANON_TYPE', 'a valid-time argument must be a date, instant or string', p);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!(0, lexical_js_1.isTypeName)(sig.returns))
|
|
204
|
+
(0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(sig.returns)}`, (0, json_js_1.memberPath)(path, 'returns'));
|
|
205
|
+
}
|
|
206
|
+
function canonFunction(o, path) {
|
|
207
|
+
const code = o.code;
|
|
208
|
+
const codePath = (0, json_js_1.memberPath)(path, 'code');
|
|
209
|
+
(0, lexical_js_1.checkString)(code.body, (0, json_js_1.memberPath)(codePath, 'body'), { body: true });
|
|
210
|
+
(0, lexical_js_1.checkName)(code.lang, (0, json_js_1.memberPath)(codePath, 'lang'));
|
|
211
|
+
canonStringSet(o.inputs, (0, json_js_1.memberPath)(path, 'inputs'), lexical_js_1.checkFunctionName);
|
|
212
|
+
canonSignature(o.signature, (0, json_js_1.memberPath)(path, 'signature'));
|
|
213
|
+
}
|
|
214
|
+
function canonReceipt(o, path) {
|
|
215
|
+
for (const k of (0, json_js_1.sortedKeys)(o)) {
|
|
216
|
+
const p = (0, json_js_1.memberPath)(path, k);
|
|
217
|
+
const v = o[k];
|
|
218
|
+
switch (k) {
|
|
219
|
+
case 'asof':
|
|
220
|
+
case 'issued':
|
|
221
|
+
case 'tx':
|
|
222
|
+
(0, lexical_js_1.checkInstant)(v, p);
|
|
223
|
+
break;
|
|
224
|
+
case 'cell':
|
|
225
|
+
canonCell(v, p);
|
|
226
|
+
break;
|
|
227
|
+
case 'computed_by':
|
|
228
|
+
(0, lexical_js_1.checkKeyRef)(v, p);
|
|
229
|
+
break;
|
|
230
|
+
case 'inputs':
|
|
231
|
+
canonStringSet(v, p, (s, q) => (0, lexical_js_1.checkHex)(s, 64, q));
|
|
232
|
+
break;
|
|
233
|
+
case 'licence':
|
|
234
|
+
(0, lexical_js_1.checkLicence)(v, p);
|
|
235
|
+
break;
|
|
236
|
+
case 'publisher':
|
|
237
|
+
(0, lexical_js_1.checkNamespace)(v, p);
|
|
238
|
+
break;
|
|
239
|
+
case 'sig':
|
|
240
|
+
(0, lexical_js_1.checkHex)(v, 128, p);
|
|
241
|
+
break;
|
|
242
|
+
case 'summary':
|
|
243
|
+
if (v === '')
|
|
244
|
+
(0, errors_js_1.fail)('E_CANON_STRING', 'summary must not be empty', p);
|
|
245
|
+
(0, lexical_js_1.checkString)(v, p, { limit: lexical_js_1.LIMITS.summary });
|
|
246
|
+
break;
|
|
247
|
+
case 'type':
|
|
248
|
+
if (!(0, lexical_js_1.isTypeName)(v))
|
|
249
|
+
(0, errors_js_1.fail)('E_CANON_TYPE', `unknown type ${JSON.stringify(v)}`, p);
|
|
250
|
+
break;
|
|
251
|
+
case 'valid':
|
|
252
|
+
canonArgs(v, p);
|
|
253
|
+
break;
|
|
254
|
+
case 'value':
|
|
255
|
+
(0, lexical_js_1.checkTypedValue)(o.type, v, p);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// ---------------------------------------------------------------- step 4: consistency
|
|
261
|
+
function consistency(o, path) {
|
|
262
|
+
const cell = o.cell;
|
|
263
|
+
const args = cell.args;
|
|
264
|
+
const valid = o.valid;
|
|
265
|
+
const argsPath = (0, json_js_1.memberPath)((0, json_js_1.memberPath)(path, 'cell'), 'args');
|
|
266
|
+
const validPath = (0, json_js_1.memberPath)(path, 'valid');
|
|
267
|
+
for (const k of (0, json_js_1.sortedKeys)(args)) {
|
|
268
|
+
if (args[k] === SLICE)
|
|
269
|
+
(0, errors_js_1.fail)('E_SLICE', 'a receipt is for one cell, not a slice', (0, json_js_1.memberPath)(argsPath, k));
|
|
270
|
+
}
|
|
271
|
+
for (const k of (0, json_js_1.sortedKeys)(valid)) {
|
|
272
|
+
if (valid[k] === SLICE)
|
|
273
|
+
(0, errors_js_1.fail)('E_SLICE', 'a receipt is for one cell, not a slice', (0, json_js_1.memberPath)(validPath, k));
|
|
274
|
+
}
|
|
275
|
+
if (o.publisher !== cell.ns)
|
|
276
|
+
(0, errors_js_1.fail)('E_PUBLISHER_MISMATCH', 'publisher must equal cell.ns', (0, json_js_1.memberPath)(path, 'publisher'));
|
|
277
|
+
for (const k of (0, json_js_1.sortedKeys)(valid)) {
|
|
278
|
+
const v = valid[k];
|
|
279
|
+
const a = (0, json_js_1.has)(args, k) ? args[k] : null;
|
|
280
|
+
if (!a || a.type !== v.type || a.value !== v.value) {
|
|
281
|
+
(0, errors_js_1.fail)('E_VALID_MISMATCH', `valid.${k} does not echo cell.args.${k}`, validPath);
|
|
282
|
+
}
|
|
283
|
+
if (!lexical_js_1.VALID_TIME_TYPES.includes(v.type)) {
|
|
284
|
+
(0, errors_js_1.fail)('E_VALID_MISMATCH', 'valid-time arguments are dates, instants or strings', validPath);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const tx = o.tx;
|
|
288
|
+
const asof = o.asof;
|
|
289
|
+
const issued = o.issued;
|
|
290
|
+
// Instants are fixed width, so string order is time order (SPEC 3.3).
|
|
291
|
+
if (!(tx <= asof && asof <= issued))
|
|
292
|
+
(0, errors_js_1.fail)('E_TIME_ORDER', 'requires tx <= asof <= issued', path);
|
|
293
|
+
}
|
|
294
|
+
// ---------------------------------------------------------------- entry points
|
|
295
|
+
/** Validate `v` as `kind` (steps 2-3; for receipts also step 4). Throws DatumeshError. */
|
|
296
|
+
function validate(v, kind, path = '$') {
|
|
297
|
+
switch (kind) {
|
|
298
|
+
case 'jcs':
|
|
299
|
+
return;
|
|
300
|
+
case 'scalar':
|
|
301
|
+
structScalar(v, path);
|
|
302
|
+
canonScalar(v, path);
|
|
303
|
+
return;
|
|
304
|
+
case 'cell':
|
|
305
|
+
case 'ref':
|
|
306
|
+
structCell(v, path, kind === 'ref');
|
|
307
|
+
canonCell(v, path);
|
|
308
|
+
return;
|
|
309
|
+
case 'function':
|
|
310
|
+
structFunction(v, path);
|
|
311
|
+
canonFunction(v, path);
|
|
312
|
+
return;
|
|
313
|
+
case 'receipt':
|
|
314
|
+
structReceipt(v, path);
|
|
315
|
+
canonReceipt(v, path);
|
|
316
|
+
consistency(v, path);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Receipt step 2 only: true when the receipt has the right members and JSON types. */
|
|
321
|
+
function receiptStructure(v) {
|
|
322
|
+
structReceipt(v, '$');
|
|
323
|
+
}
|
|
324
|
+
/** Receipt steps 3 and 4, after receiptStructure has passed. */
|
|
325
|
+
function receiptCanonicalAndConsistent(v) {
|
|
326
|
+
canonReceipt(v, '$');
|
|
327
|
+
consistency(v, '$');
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Infer the kind of an object from its members when the caller gives none: a receipt has
|
|
331
|
+
* `sig`, `proto` or `computed_by`; a descriptor `signature` or `code`; a cell `fn_hash`; a cell
|
|
332
|
+
* reference `fn`, `ns` or `args`; a typed scalar `type` or `value`. Anything else is E_SCHEMA.
|
|
333
|
+
*/
|
|
334
|
+
function inferKind(v) {
|
|
335
|
+
if (!(0, json_js_1.isObject)(v))
|
|
336
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'cannot infer the kind of a non-object; pass kind', '$');
|
|
337
|
+
const h = (k) => (0, json_js_1.has)(v, k);
|
|
338
|
+
if (h('sig') || h('proto') || h('computed_by'))
|
|
339
|
+
return 'receipt';
|
|
340
|
+
if (h('signature') || h('code'))
|
|
341
|
+
return 'function';
|
|
342
|
+
if (h('fn_hash'))
|
|
343
|
+
return 'cell';
|
|
344
|
+
if (h('fn') || h('ns') || h('args'))
|
|
345
|
+
return 'ref';
|
|
346
|
+
if (h('type') || h('value'))
|
|
347
|
+
return 'scalar';
|
|
348
|
+
(0, errors_js_1.fail)('E_SCHEMA', 'cannot infer the kind from the members; pass kind', '$');
|
|
349
|
+
}
|
|
@@ -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,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verify = verify;
|
|
4
|
+
// Receipt verification (SPEC.md 10.1). Synchronous, offline, no global state, no logging.
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
6
|
+
const ed25519_js_1 = require("./ed25519.js");
|
|
7
|
+
const hash_js_1 = require("./hash.js");
|
|
8
|
+
const json_js_1 = require("./json.js");
|
|
9
|
+
const jcs_js_1 = require("./jcs.js");
|
|
10
|
+
const keys_js_1 = require("./keys.js");
|
|
11
|
+
const validate_js_1 = require("./validate.js");
|
|
12
|
+
class Stop extends Error {
|
|
13
|
+
constructor(code, detail, path) {
|
|
14
|
+
super(detail);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.detail = detail;
|
|
17
|
+
this.path = path;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function stop(code, detail, path = '$') {
|
|
21
|
+
throw new Stop(code, detail, path);
|
|
22
|
+
}
|
|
23
|
+
function withoutSig(r) {
|
|
24
|
+
const o = (0, json_js_1.newObject)();
|
|
25
|
+
for (const k of Object.keys(r))
|
|
26
|
+
if (k !== 'sig')
|
|
27
|
+
o[k] = r[k];
|
|
28
|
+
return o;
|
|
29
|
+
}
|
|
30
|
+
/** Steps 1-6. Throws Stop; returns the parsed receipt and its hash. */
|
|
31
|
+
function verifySigned(input, sources, setHash) {
|
|
32
|
+
let r;
|
|
33
|
+
try {
|
|
34
|
+
r = (0, json_js_1.toJson)(input); // step 1
|
|
35
|
+
(0, validate_js_1.receiptStructure)(r); // step 2
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof errors_js_1.DatumeshError)
|
|
39
|
+
stop(e.code, e.detail, e.path);
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
const receipt = r;
|
|
43
|
+
const payload = (0, jcs_js_1.jcsBytes)(withoutSig(receipt));
|
|
44
|
+
const hash = (0, hash_js_1.sha256Hex)(payload);
|
|
45
|
+
setHash(hash);
|
|
46
|
+
try {
|
|
47
|
+
(0, validate_js_1.receiptCanonicalAndConsistent)(receipt); // steps 3-4
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
if (e instanceof errors_js_1.DatumeshError)
|
|
51
|
+
stop(e.code, e.detail, e.path);
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
// Step 5.
|
|
55
|
+
const ref = receipt.computed_by;
|
|
56
|
+
const hashAt = ref.indexOf('#');
|
|
57
|
+
const host = ref.slice(0, hashAt);
|
|
58
|
+
const id = ref.slice(hashAt + 1);
|
|
59
|
+
const found = (0, keys_js_1.lookupKey)(sources, host, id, receipt.issued);
|
|
60
|
+
if (!found.ok)
|
|
61
|
+
stop(found.code, found.detail, found.code === 'E_KEY_NOT_VALID' ? '$.issued' : '$.computed_by');
|
|
62
|
+
// Step 6.
|
|
63
|
+
if (!(0, ed25519_js_1.verifyEd25519Strict)((0, hash_js_1.fromHex)(receipt.sig), payload, (0, hash_js_1.fromHex)(found.key.public))) {
|
|
64
|
+
stop('E_SIGNATURE', 'Ed25519 verification failed', '$.sig');
|
|
65
|
+
}
|
|
66
|
+
return { receipt, hash };
|
|
67
|
+
}
|
|
68
|
+
function recompute(receipt, rc, sources) {
|
|
69
|
+
const cell = receipt.cell;
|
|
70
|
+
const cellArgs = cell.args;
|
|
71
|
+
// 7.1 Descriptor.
|
|
72
|
+
if (rc.descriptor === undefined || rc.descriptor === null)
|
|
73
|
+
stop('E_FN_UNAVAILABLE', `no descriptor for ${cell.fn_hash}`, '$.cell.fn_hash');
|
|
74
|
+
let desc;
|
|
75
|
+
try {
|
|
76
|
+
const d = (0, json_js_1.toJson)(rc.descriptor);
|
|
77
|
+
if ((0, hash_js_1.sha256Hex)((0, jcs_js_1.jcsBytes)(d)) !== cell.fn_hash)
|
|
78
|
+
stop('E_FN_HASH_MISMATCH', 'descriptor does not hash to cell.fn_hash', '$.cell.fn_hash');
|
|
79
|
+
(0, validate_js_1.validate)(d, 'function', 'descriptor');
|
|
80
|
+
desc = d;
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
if (e instanceof errors_js_1.DatumeshError)
|
|
84
|
+
stop(e.code, `descriptor: ${e.detail}`, e.path);
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
const sig = desc.signature;
|
|
88
|
+
const declared = sig.args;
|
|
89
|
+
// 7.2 Arguments: unknown, then missing, then types.
|
|
90
|
+
for (const k of (0, json_js_1.sortedKeys)(cellArgs)) {
|
|
91
|
+
if (!(0, json_js_1.has)(declared, k))
|
|
92
|
+
stop('E_ARG_UNKNOWN', `function declares no argument ${JSON.stringify(k)}`, `$.cell.args.${k}`);
|
|
93
|
+
}
|
|
94
|
+
for (const k of (0, json_js_1.sortedKeys)(declared)) {
|
|
95
|
+
if (!(0, json_js_1.has)(cellArgs, k))
|
|
96
|
+
stop('E_ARG_MISSING', `argument ${JSON.stringify(k)} is not bound`, '$.cell.args');
|
|
97
|
+
}
|
|
98
|
+
for (const k of (0, json_js_1.sortedKeys)(cellArgs)) {
|
|
99
|
+
const want = declared[k].type;
|
|
100
|
+
if (cellArgs[k].type !== want)
|
|
101
|
+
stop('E_ARG_TYPE', `${k} is declared ${String(want)}`, `$.cell.args.${k}`);
|
|
102
|
+
}
|
|
103
|
+
// 7.3 valid holds exactly the declared valid-time arguments.
|
|
104
|
+
const validNames = (0, json_js_1.sortedKeys)(receipt.valid);
|
|
105
|
+
const declaredValid = (0, json_js_1.sortedKeys)(declared).filter((k) => declared[k].valid === true);
|
|
106
|
+
if (validNames.join('\u0000') !== declaredValid.join('\u0000')) {
|
|
107
|
+
stop('E_VALID_MISMATCH', 'valid must echo exactly the declared valid-time arguments', '$.valid');
|
|
108
|
+
}
|
|
109
|
+
// 7.4 Return type.
|
|
110
|
+
if (receipt.type !== sig.returns)
|
|
111
|
+
stop('E_RETURN_TYPE', `function returns ${String(sig.returns)}`, '$.type');
|
|
112
|
+
// 7.5 Inputs.
|
|
113
|
+
const declaredInputs = desc.inputs;
|
|
114
|
+
const supplied = rc.inputs ?? {};
|
|
115
|
+
let latestTx = null;
|
|
116
|
+
for (const h of receipt.inputs) {
|
|
117
|
+
if (!Object.prototype.hasOwnProperty.call(supplied, h))
|
|
118
|
+
stop('E_INPUT_UNAVAILABLE', `input receipt ${h} not available`, '$.inputs');
|
|
119
|
+
let inner;
|
|
120
|
+
try {
|
|
121
|
+
inner = verifySigned(supplied[h], sources, () => undefined);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e instanceof Stop)
|
|
125
|
+
stop('E_INPUT_INVALID', `input ${h}: ${e.code}: ${e.detail}`, '$.inputs');
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
if (inner.hash !== h)
|
|
129
|
+
stop('E_INPUT_UNAVAILABLE', `supplied receipt does not hash to ${h}`, '$.inputs');
|
|
130
|
+
const ic = inner.receipt.cell;
|
|
131
|
+
if (!declaredInputs.includes(`${ic.ns}/${ic.fn}`)) {
|
|
132
|
+
stop('E_INPUT_UNDECLARED', `input ${h} is a cell of an undeclared function`, '$.inputs');
|
|
133
|
+
}
|
|
134
|
+
if (inner.receipt.asof !== receipt.asof)
|
|
135
|
+
stop('E_TIME_ORDER', "input receipts must share the receipt's asof", '$.inputs');
|
|
136
|
+
const itx = inner.receipt.tx;
|
|
137
|
+
if (latestTx === null || itx > latestTx)
|
|
138
|
+
latestTx = itx;
|
|
139
|
+
}
|
|
140
|
+
if (latestTx !== null && receipt.tx !== latestTx)
|
|
141
|
+
stop('E_TIME_ORDER', 'tx must be the latest input tx', '$.tx');
|
|
142
|
+
// 7.6 Value.
|
|
143
|
+
const v = rc.value;
|
|
144
|
+
if (v.type !== receipt.type || v.value !== receipt.value)
|
|
145
|
+
stop('E_VALUE_MISMATCH', 'recomputed value differs from the receipt', '$.value');
|
|
146
|
+
}
|
|
147
|
+
function checkRecomputeArg(rc) {
|
|
148
|
+
if (rc === undefined || rc === null)
|
|
149
|
+
return null;
|
|
150
|
+
if (typeof rc !== 'object' || Array.isArray(rc))
|
|
151
|
+
throw new TypeError('recompute must be an object {descriptor, inputs, value}');
|
|
152
|
+
const r = rc;
|
|
153
|
+
if (!r.value || typeof r.value !== 'object' || typeof r.value.type !== 'string' || (typeof r.value.value !== 'string' && typeof r.value.value !== 'boolean')) {
|
|
154
|
+
throw new TypeError('recompute.value must be the evaluated typed scalar {type, value}');
|
|
155
|
+
}
|
|
156
|
+
if (r.inputs !== undefined && (typeof r.inputs !== 'object' || r.inputs === null || Array.isArray(r.inputs))) {
|
|
157
|
+
throw new TypeError('recompute.inputs must be an object mapping receipt hash to receipt JSON');
|
|
158
|
+
}
|
|
159
|
+
return r;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Verify a receipt (SPEC 10.1). `receipt` is JSON text, UTF-8 bytes or a parsed object (only
|
|
163
|
+
* text and bytes allow duplicate-member detection). `keys` is a key document, a KeysResult, a
|
|
164
|
+
* {keyId: publicKeyHex} map, or an array of these. With `recompute`, step 7 runs too.
|
|
165
|
+
*
|
|
166
|
+
* Never throws for an invalid receipt or bad key material; throws TypeError only when an
|
|
167
|
+
* argument is of the wrong kind altogether.
|
|
168
|
+
*/
|
|
169
|
+
function verify(receipt, keys, recomputeArg) {
|
|
170
|
+
const sources = (0, keys_js_1.collectSources)(keys);
|
|
171
|
+
const rc = checkRecomputeArg(recomputeArg);
|
|
172
|
+
let hash = null;
|
|
173
|
+
try {
|
|
174
|
+
const { receipt: r } = verifySigned(receipt, sources, (h) => (hash = h));
|
|
175
|
+
if (rc)
|
|
176
|
+
recompute(r, rc, sources);
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
code: 'OK',
|
|
180
|
+
level: rc ? 'recomputed' : 'signed',
|
|
181
|
+
hash,
|
|
182
|
+
key: r.computed_by,
|
|
183
|
+
detail: rc ? 'fully verified: signature valid and value recomputed' : `signature-valid: signed by ${r.computed_by}`,
|
|
184
|
+
path: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
if (e instanceof Stop)
|
|
189
|
+
return { ok: false, code: e.code, level: null, hash, key: null, detail: e.detail, path: e.path };
|
|
190
|
+
throw e; // a bug in this library, not a property of the receipt
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Ed25519 verification as SPEC.md 10.1 step 6 requires (libsodium-strict, RFC 8032 cofactorless):
|
|
2
|
+
// - S < L;
|
|
3
|
+
// - A and R canonically encoded (y < p; x = 0 with the sign bit set is rejected);
|
|
4
|
+
// - A and R not of small order;
|
|
5
|
+
// - [S]B - [k]A, encoded, equals R byte for byte, with k = SHA-512(R || A || M) mod L.
|
|
6
|
+
//
|
|
7
|
+
// @noble/curves supplies the group arithmetic. Its own verify() with zip215:false enforces
|
|
8
|
+
// S < L, canonical A and R, and rejects small-order A, but it accepts small-order R and checks
|
|
9
|
+
// the cofactored equation [8]([S]B - R - [k]A) = 0, which accepts some signatures the
|
|
10
|
+
// cofactorless rule rejects. So the checks are done here, and noble's verify() is additionally
|
|
11
|
+
// required to succeed (it is implied by the cofactorless check; it is a second opinion only).
|
|
12
|
+
import { ed25519 } from '@noble/curves/ed25519';
|
|
13
|
+
import { sha512 } from '@noble/hashes/sha2';
|
|
14
|
+
const Point = ed25519.Point;
|
|
15
|
+
const L = 2n ** 252n + 27742317777372353535851937790883648493n;
|
|
16
|
+
const P = 2n ** 255n - 19n;
|
|
17
|
+
function le(b) {
|
|
18
|
+
let n = 0n;
|
|
19
|
+
for (let i = b.length - 1; i >= 0; i--)
|
|
20
|
+
n = (n << 8n) | BigInt(b[i]);
|
|
21
|
+
return n;
|
|
22
|
+
}
|
|
23
|
+
function decodeStrict(b) {
|
|
24
|
+
const y = le(b) & ((1n << 255n) - 1n);
|
|
25
|
+
if (y >= P)
|
|
26
|
+
return null; // non-canonical encoding
|
|
27
|
+
try {
|
|
28
|
+
return Point.fromBytes(b, false); // also rejects x = 0 with the sign bit set
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function equalBytes(a, b) {
|
|
35
|
+
if (a.length !== b.length)
|
|
36
|
+
return false;
|
|
37
|
+
let d = 0;
|
|
38
|
+
for (let i = 0; i < a.length; i++)
|
|
39
|
+
d |= a[i] ^ b[i];
|
|
40
|
+
return d === 0;
|
|
41
|
+
}
|
|
42
|
+
/** Strict Ed25519 verification of `sig` (64 bytes) over `msg` with public key `pub` (32 bytes). */
|
|
43
|
+
export function verifyEd25519Strict(sig, msg, pub) {
|
|
44
|
+
if (sig.length !== 64 || pub.length !== 32)
|
|
45
|
+
return false;
|
|
46
|
+
const rBytes = sig.subarray(0, 32);
|
|
47
|
+
const s = le(sig.subarray(32, 64));
|
|
48
|
+
if (s >= L)
|
|
49
|
+
return false;
|
|
50
|
+
const A = decodeStrict(pub);
|
|
51
|
+
const R = decodeStrict(rBytes);
|
|
52
|
+
if (!A || !R)
|
|
53
|
+
return false;
|
|
54
|
+
if (A.isSmallOrder() || R.isSmallOrder())
|
|
55
|
+
return false;
|
|
56
|
+
const h = sha512(concat(rBytes, pub, msg));
|
|
57
|
+
const k = le(h) % L;
|
|
58
|
+
// R' = [S]B - [k]A (multiplyUnsafe accepts 0; inputs are public, so variable time is fine).
|
|
59
|
+
const SB = s === 0n ? Point.ZERO : Point.BASE.multiplyUnsafe(s);
|
|
60
|
+
const kA = k === 0n ? Point.ZERO : A.multiplyUnsafe(k);
|
|
61
|
+
const rCheck = SB.subtract(kA).toBytes();
|
|
62
|
+
if (!equalBytes(rCheck, rBytes))
|
|
63
|
+
return false;
|
|
64
|
+
try {
|
|
65
|
+
return ed25519.verify(sig, msg, pub, { zip215: false });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function concat(...parts) {
|
|
72
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
73
|
+
let o = 0;
|
|
74
|
+
for (const p of parts) {
|
|
75
|
+
out.set(p, o);
|
|
76
|
+
o += p.length;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The spec revision this library conforms to. */
|
|
2
|
+
export declare const SPEC_VERSION = "spec-0.1.1";
|
|
3
|
+
/** Every error code defined by SPEC.md 10.2, in the order of that table. */
|
|
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_DATE", "E_CANON_INSTANT", "E_CANON_STRING", "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"];
|
|
5
|
+
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
6
|
+
/** A Datumesh rule was broken. `code` is a SPEC.md 10.2 code; `path` locates the failure. */
|
|
7
|
+
export declare class DatumeshError extends Error {
|
|
8
|
+
readonly code: ErrorCode;
|
|
9
|
+
readonly path: string | null;
|
|
10
|
+
readonly detail: string;
|
|
11
|
+
constructor(code: ErrorCode, detail: string, path?: string | null);
|
|
12
|
+
}
|
|
13
|
+
/** Shorthand used throughout: throw a DatumeshError. */
|
|
14
|
+
export declare function fail(code: ErrorCode, detail: string, path?: string | null): never;
|