@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,73 @@
|
|
|
1
|
+
import { type ErrorCode } from './errors.js';
|
|
2
|
+
/** One validated key from a key document. */
|
|
3
|
+
export interface KeyEntry {
|
|
4
|
+
alg: 'ed25519';
|
|
5
|
+
id: string;
|
|
6
|
+
public: string;
|
|
7
|
+
valid_from: string;
|
|
8
|
+
valid_to?: string;
|
|
9
|
+
}
|
|
10
|
+
/** The outcome of resolving one host's keys (SPEC 9.3). */
|
|
11
|
+
export interface KeysResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
/** "OK", "E_KEY_UNAVAILABLE" or "E_KEY_DOC". */
|
|
14
|
+
code: 'OK' | 'E_KEY_UNAVAILABLE' | 'E_KEY_DOC';
|
|
15
|
+
host: string;
|
|
16
|
+
detail: string;
|
|
17
|
+
/** The validated keys, in document order, when ok. */
|
|
18
|
+
keys?: KeyEntry[];
|
|
19
|
+
}
|
|
20
|
+
/** A mapping of key id to 64-hex public key: usable for any host, with no validity windows. */
|
|
21
|
+
export type KeyMap = Record<string, string>;
|
|
22
|
+
/** Anything `verify` accepts as keys. */
|
|
23
|
+
export type KeySource = KeysResult | KeyMap | Record<string, unknown> | KeySource[];
|
|
24
|
+
/**
|
|
25
|
+
* Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
|
|
26
|
+
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. `body` is the
|
|
27
|
+
* response body as text or bytes. Never throws.
|
|
28
|
+
*/
|
|
29
|
+
export declare function keysFromResponse(host: string, status: number | null, body?: string | Uint8Array | null): KeysResult;
|
|
30
|
+
/**
|
|
31
|
+
* Validate an already-parsed key document (its `host` member names the host it covers).
|
|
32
|
+
* Never throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare function keysFromDocument(doc: unknown): KeysResult;
|
|
35
|
+
/** Default timeout of fetchKeys, in milliseconds. */
|
|
36
|
+
export declare const FETCH_TIMEOUT_MS = 10000;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
|
|
39
|
+
* redirects followed and a timeout, then interpret it with keysFromResponse. The only function
|
|
40
|
+
* in this library that touches the network. Never throws.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fetchKeys(host: string, timeoutMs?: number): Promise<KeysResult>;
|
|
43
|
+
interface HostSource {
|
|
44
|
+
host: string;
|
|
45
|
+
result: KeysResult;
|
|
46
|
+
}
|
|
47
|
+
interface AnySource {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
code: ErrorCode | 'OK';
|
|
50
|
+
detail: string;
|
|
51
|
+
keys: KeyEntry[];
|
|
52
|
+
}
|
|
53
|
+
export interface Sources {
|
|
54
|
+
hosts: HostSource[];
|
|
55
|
+
any: AnySource[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Normalise the `keys` argument of verify. Throws TypeError only for arguments that are not key
|
|
59
|
+
* sources at all (programmer errors); malformed key material becomes an error source.
|
|
60
|
+
*/
|
|
61
|
+
export declare function collectSources(keys: unknown, into?: Sources, depth?: number): Sources;
|
|
62
|
+
export type KeyLookup = {
|
|
63
|
+
ok: true;
|
|
64
|
+
key: KeyEntry;
|
|
65
|
+
windows: boolean;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
code: ErrorCode;
|
|
69
|
+
detail: string;
|
|
70
|
+
};
|
|
71
|
+
/** SPEC 10.1 step 5 for key reference host#id at time `issued`. */
|
|
72
|
+
export declare function lookupKey(sources: Sources, host: string, id: string, issued: string): KeyLookup;
|
|
73
|
+
export {};
|
package/dist/cjs/keys.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FETCH_TIMEOUT_MS = void 0;
|
|
4
|
+
exports.keysFromResponse = keysFromResponse;
|
|
5
|
+
exports.keysFromDocument = keysFromDocument;
|
|
6
|
+
exports.fetchKeys = fetchKeys;
|
|
7
|
+
exports.collectSources = collectSources;
|
|
8
|
+
exports.lookupKey = lookupKey;
|
|
9
|
+
// Key documents (SPEC.md 9.2), their retrieval (9.3) and the key sources `verify` accepts.
|
|
10
|
+
const errors_js_1 = require("./errors.js");
|
|
11
|
+
const hash_js_1 = require("./hash.js");
|
|
12
|
+
const json_js_1 = require("./json.js");
|
|
13
|
+
const lexical_js_1 = require("./lexical.js");
|
|
14
|
+
class KeyDocError extends Error {
|
|
15
|
+
constructor(msg, path) {
|
|
16
|
+
super(msg);
|
|
17
|
+
this.path = path;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function bad(msg, path) {
|
|
21
|
+
throw new KeyDocError(msg, path);
|
|
22
|
+
}
|
|
23
|
+
/** Validate a parsed key document for `host` (SPEC 9.2). Throws KeyDocError. */
|
|
24
|
+
function validateKeyDocument(doc, host) {
|
|
25
|
+
if (!(0, json_js_1.isObject)(doc))
|
|
26
|
+
bad('expected an object', '$');
|
|
27
|
+
for (const k of (0, json_js_1.sortedKeys)(doc))
|
|
28
|
+
if (!['host', 'keys', 'proto'].includes(k))
|
|
29
|
+
bad(`unexpected member ${JSON.stringify(k)}`, '$');
|
|
30
|
+
for (const k of ['host', 'keys', 'proto'])
|
|
31
|
+
if (!(0, json_js_1.has)(doc, k))
|
|
32
|
+
bad(`missing member ${JSON.stringify(k)}`, '$');
|
|
33
|
+
if (doc.proto !== 'datumesh/1')
|
|
34
|
+
bad("proto must be 'datumesh/1'", '$.proto');
|
|
35
|
+
if (doc.host !== host)
|
|
36
|
+
bad(`host must be ${JSON.stringify(host)}`, '$.host');
|
|
37
|
+
const keys = doc.keys;
|
|
38
|
+
if (!Array.isArray(keys) || keys.length < 1 || keys.length > lexical_js_1.LIMITS.keys)
|
|
39
|
+
bad(`keys must be an array of 1 to ${lexical_js_1.LIMITS.keys} keys`, '$.keys');
|
|
40
|
+
const out = [];
|
|
41
|
+
const seenIds = new Set();
|
|
42
|
+
const seenPublic = new Set();
|
|
43
|
+
keys.forEach((k, i) => {
|
|
44
|
+
const p = `$.keys[${i}]`;
|
|
45
|
+
if (!(0, json_js_1.isObject)(k))
|
|
46
|
+
bad('expected an object', p);
|
|
47
|
+
for (const m of (0, json_js_1.sortedKeys)(k))
|
|
48
|
+
if (!['alg', 'id', 'public', 'valid_from', 'valid_to'].includes(m))
|
|
49
|
+
bad(`unexpected member ${JSON.stringify(m)}`, p);
|
|
50
|
+
for (const m of ['alg', 'id', 'public', 'valid_from'])
|
|
51
|
+
if (!(0, json_js_1.has)(k, m))
|
|
52
|
+
bad(`missing member ${JSON.stringify(m)}`, p);
|
|
53
|
+
if (k.alg !== 'ed25519')
|
|
54
|
+
bad('alg must be ed25519', (0, json_js_1.memberPath)(p, 'alg'));
|
|
55
|
+
if (typeof k.id !== 'string' || !(0, lexical_js_1.isHex)(k.id, 16))
|
|
56
|
+
bad('id must be 16 lowercase hex digits', (0, json_js_1.memberPath)(p, 'id'));
|
|
57
|
+
if (typeof k.public !== 'string' || !(0, lexical_js_1.isHex)(k.public, 64))
|
|
58
|
+
bad('public must be 64 lowercase hex digits', (0, json_js_1.memberPath)(p, 'public'));
|
|
59
|
+
if (typeof k.valid_from !== 'string' || !(0, lexical_js_1.isInstant)(k.valid_from))
|
|
60
|
+
bad('valid_from must be an instant', (0, json_js_1.memberPath)(p, 'valid_from'));
|
|
61
|
+
if ((0, json_js_1.has)(k, 'valid_to')) {
|
|
62
|
+
if (typeof k.valid_to !== 'string' || !(0, lexical_js_1.isInstant)(k.valid_to))
|
|
63
|
+
bad('valid_to must be an instant', (0, json_js_1.memberPath)(p, 'valid_to'));
|
|
64
|
+
if (!(k.valid_to > k.valid_from))
|
|
65
|
+
bad('valid_to must be later than valid_from', p);
|
|
66
|
+
}
|
|
67
|
+
if ((0, hash_js_1.keyIdOf)((0, hash_js_1.fromHex)(k.public)) !== k.id)
|
|
68
|
+
bad('id is not the key id of public', (0, json_js_1.memberPath)(p, 'id'));
|
|
69
|
+
if (seenIds.has(k.id) || seenPublic.has(k.public))
|
|
70
|
+
bad('key appears twice', p);
|
|
71
|
+
seenIds.add(k.id);
|
|
72
|
+
seenPublic.add(k.public);
|
|
73
|
+
const e = { alg: 'ed25519', id: k.id, public: k.public, valid_from: k.valid_from };
|
|
74
|
+
if ((0, json_js_1.has)(k, 'valid_to'))
|
|
75
|
+
e.valid_to = k.valid_to;
|
|
76
|
+
out.push(e);
|
|
77
|
+
});
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function docResult(host, parse) {
|
|
81
|
+
let doc;
|
|
82
|
+
try {
|
|
83
|
+
doc = parse();
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
const d = e instanceof errors_js_1.DatumeshError ? `${e.code}: ${e.detail}` : String(e);
|
|
87
|
+
return { ok: false, code: 'E_KEY_DOC', host, detail: `key document is not strict JSON (${d})` };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const keys = validateKeyDocument(doc, host);
|
|
91
|
+
return { ok: true, code: 'OK', host, detail: `${keys.length} key(s) for ${host}`, keys };
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (e instanceof KeyDocError)
|
|
95
|
+
return { ok: false, code: 'E_KEY_DOC', host, detail: `${e.message} at ${e.path}` };
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Interpret the HTTPS response for https://<host>/.well-known/datumesh.json (SPEC 9.3).
|
|
101
|
+
* `status` is the HTTP status, or null for a network, DNS, TLS or timeout failure. `body` is the
|
|
102
|
+
* response body as text or bytes. Never throws.
|
|
103
|
+
*/
|
|
104
|
+
function keysFromResponse(host, status, body) {
|
|
105
|
+
const h = typeof host === 'string' ? host : String(host);
|
|
106
|
+
try {
|
|
107
|
+
if (status === null || status === undefined) {
|
|
108
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: 'network, DNS, TLS or timeout failure' };
|
|
109
|
+
}
|
|
110
|
+
if (status !== 200) {
|
|
111
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `HTTP status ${String(status)} (only 200 is accepted; redirects are not followed)` };
|
|
112
|
+
}
|
|
113
|
+
if (typeof body !== 'string' && !(body instanceof Uint8Array)) {
|
|
114
|
+
return { ok: false, code: 'E_KEY_DOC', host: h, detail: '200 response without a body' };
|
|
115
|
+
}
|
|
116
|
+
return docResult(h, () => (0, json_js_1.toJson)(body));
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: h, detail: `could not interpret the response: ${String(e)}` };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Validate an already-parsed key document (its `host` member names the host it covers).
|
|
124
|
+
* Never throws.
|
|
125
|
+
*/
|
|
126
|
+
function keysFromDocument(doc) {
|
|
127
|
+
const host = (0, json_js_1.isObject)(doc) && typeof doc.host === 'string' ? doc.host : '';
|
|
128
|
+
try {
|
|
129
|
+
return docResult(host, () => (0, json_js_1.toJson)(doc));
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
return { ok: false, code: 'E_KEY_DOC', host, detail: String(e) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Default timeout of fetchKeys, in milliseconds. */
|
|
136
|
+
exports.FETCH_TIMEOUT_MS = 10000;
|
|
137
|
+
/**
|
|
138
|
+
* Fetch https://<host>/.well-known/datumesh.json once, with TLS verified by the platform, no
|
|
139
|
+
* redirects followed and a timeout, then interpret it with keysFromResponse. The only function
|
|
140
|
+
* in this library that touches the network. Never throws.
|
|
141
|
+
*/
|
|
142
|
+
async function fetchKeys(host, timeoutMs = exports.FETCH_TIMEOUT_MS) {
|
|
143
|
+
try {
|
|
144
|
+
(0, lexical_js_1.checkDnsName)(host, '$');
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host: String(host), detail: 'host is not a lowercase DNS name (SPEC 2.1); nothing was fetched' };
|
|
148
|
+
}
|
|
149
|
+
const f = globalThis.fetch;
|
|
150
|
+
if (typeof f !== 'function')
|
|
151
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: 'no global fetch in this runtime' };
|
|
152
|
+
let status;
|
|
153
|
+
let body;
|
|
154
|
+
try {
|
|
155
|
+
const res = await f(`https://${host}/.well-known/datumesh.json`, {
|
|
156
|
+
method: 'GET',
|
|
157
|
+
redirect: 'manual',
|
|
158
|
+
credentials: 'omit',
|
|
159
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
160
|
+
});
|
|
161
|
+
status = res.status;
|
|
162
|
+
// A redirect under redirect:"manual" is a 3xx (Node) or an opaque-redirect response with
|
|
163
|
+
// status 0 (browsers); either way it is not 200.
|
|
164
|
+
if (res.type === 'opaqueredirect')
|
|
165
|
+
status = 0;
|
|
166
|
+
if (status !== 200)
|
|
167
|
+
return keysFromResponse(host, status);
|
|
168
|
+
body = new Uint8Array(await res.arrayBuffer());
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', host, detail: `retrieval failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
172
|
+
}
|
|
173
|
+
return keysFromResponse(host, status, body);
|
|
174
|
+
}
|
|
175
|
+
function isKeysResult(v) {
|
|
176
|
+
return typeof v.ok === 'boolean' && typeof v.code === 'string' && typeof v.host === 'string';
|
|
177
|
+
}
|
|
178
|
+
function fromMap(m) {
|
|
179
|
+
const keys = [];
|
|
180
|
+
for (const id of Object.keys(m)) {
|
|
181
|
+
const pub = m[id];
|
|
182
|
+
if (!(0, lexical_js_1.isHex)(id, 16) || typeof pub !== 'string' || !(0, lexical_js_1.isHex)(pub, 64) || (0, hash_js_1.keyIdOf)((0, hash_js_1.fromHex)(pub)) !== id) {
|
|
183
|
+
return { ok: false, code: 'E_KEY_DOC', detail: `key map entry ${JSON.stringify(id)} is not <key id>: <64-hex public key> with a matching id`, keys: [] };
|
|
184
|
+
}
|
|
185
|
+
keys.push({ alg: 'ed25519', id, public: pub, valid_from: '0001-01-01T00:00:00.000000Z' });
|
|
186
|
+
}
|
|
187
|
+
return { ok: true, code: 'OK', detail: 'key map (any host, no validity windows)', keys };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Normalise the `keys` argument of verify. Throws TypeError only for arguments that are not key
|
|
191
|
+
* sources at all (programmer errors); malformed key material becomes an error source.
|
|
192
|
+
*/
|
|
193
|
+
function collectSources(keys, into = { hosts: [], any: [] }, depth = 0) {
|
|
194
|
+
if (Array.isArray(keys)) {
|
|
195
|
+
if (depth > 8)
|
|
196
|
+
throw new TypeError('keys: arrays nested too deeply');
|
|
197
|
+
for (const k of keys)
|
|
198
|
+
collectSources(k, into, depth + 1);
|
|
199
|
+
return into;
|
|
200
|
+
}
|
|
201
|
+
if (typeof keys !== 'object' || keys === null) {
|
|
202
|
+
throw new TypeError('keys must be a key document, a KeysResult, a {keyId: publicKeyHex} map, or an array of these');
|
|
203
|
+
}
|
|
204
|
+
const o = keys;
|
|
205
|
+
if (isKeysResult(o)) {
|
|
206
|
+
const r = o;
|
|
207
|
+
if (r.ok) {
|
|
208
|
+
// Re-check what a caller may have built by hand.
|
|
209
|
+
const ok = Array.isArray(r.keys) && r.keys.every((k) => k && (0, lexical_js_1.isHex)(k.id, 16) && (0, lexical_js_1.isHex)(k.public, 64) && (0, lexical_js_1.isInstant)(k.valid_from) && (k.valid_to === undefined || (0, lexical_js_1.isInstant)(k.valid_to)));
|
|
210
|
+
into.hosts.push({ host: r.host, result: ok ? r : { ok: false, code: 'E_KEY_DOC', host: r.host, detail: 'KeysResult has malformed keys' } });
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const code = r.code === 'E_KEY_DOC' ? 'E_KEY_DOC' : 'E_KEY_UNAVAILABLE';
|
|
214
|
+
into.hosts.push({ host: r.host, result: { ok: false, code, host: r.host, detail: String(r.detail ?? '') } });
|
|
215
|
+
}
|
|
216
|
+
return into;
|
|
217
|
+
}
|
|
218
|
+
if ('keys' in o || 'host' in o || 'proto' in o) {
|
|
219
|
+
const r = keysFromDocument(o);
|
|
220
|
+
if (r.host)
|
|
221
|
+
into.hosts.push({ host: r.host, result: r });
|
|
222
|
+
else
|
|
223
|
+
into.any.push({ ok: false, code: 'E_KEY_DOC', detail: `key document without a host: ${r.detail}`, keys: [] });
|
|
224
|
+
return into;
|
|
225
|
+
}
|
|
226
|
+
into.any.push(fromMap(o));
|
|
227
|
+
return into;
|
|
228
|
+
}
|
|
229
|
+
/** SPEC 10.1 step 5 for key reference host#id at time `issued`. */
|
|
230
|
+
function lookupKey(sources, host, id, issued) {
|
|
231
|
+
const h = sources.hosts.find((s) => s.host === host);
|
|
232
|
+
let keys;
|
|
233
|
+
let windows;
|
|
234
|
+
if (h) {
|
|
235
|
+
if (!h.result.ok)
|
|
236
|
+
return { ok: false, code: h.result.code, detail: `${host}: ${h.result.detail}` };
|
|
237
|
+
keys = h.result.keys ?? [];
|
|
238
|
+
windows = true;
|
|
239
|
+
}
|
|
240
|
+
else if (sources.any.length) {
|
|
241
|
+
const a = sources.any[0];
|
|
242
|
+
if (!a.ok)
|
|
243
|
+
return { ok: false, code: a.code, detail: a.detail };
|
|
244
|
+
keys = a.keys;
|
|
245
|
+
windows = false;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
return { ok: false, code: 'E_KEY_UNAVAILABLE', detail: `no key source covers ${host}` };
|
|
249
|
+
}
|
|
250
|
+
const key = keys.find((k) => k.id === id);
|
|
251
|
+
if (!key)
|
|
252
|
+
return { ok: false, code: 'E_KEY_UNKNOWN', detail: `${host} publishes no key ${id}` };
|
|
253
|
+
if (windows && !(key.valid_from <= issued && (key.valid_to === undefined || issued < key.valid_to))) {
|
|
254
|
+
return { ok: false, code: 'E_KEY_NOT_VALID', detail: `issued ${issued} is outside the window of key ${id}` };
|
|
255
|
+
}
|
|
256
|
+
return { ok: true, key, windows };
|
|
257
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export declare const LIMITS: {
|
|
2
|
+
readonly name: 64;
|
|
3
|
+
readonly function_name: 512;
|
|
4
|
+
readonly args: 32;
|
|
5
|
+
readonly number: 128;
|
|
6
|
+
readonly string: 1024;
|
|
7
|
+
readonly summary: 1024;
|
|
8
|
+
readonly body: 1048576;
|
|
9
|
+
readonly inputs: 4096;
|
|
10
|
+
readonly licence_spdx: 64;
|
|
11
|
+
readonly licence_url: 512;
|
|
12
|
+
readonly keys: 64;
|
|
13
|
+
};
|
|
14
|
+
export declare const TYPES: readonly ["blob", "boolean", "date", "instant", "number", "string"];
|
|
15
|
+
export type TypeName = (typeof TYPES)[number];
|
|
16
|
+
export declare const VALID_TIME_TYPES: readonly string[];
|
|
17
|
+
export declare function isTypeName(t: unknown): t is TypeName;
|
|
18
|
+
/** SPEC 2.2. Grammar first (`code`), then the 64-character limit (E_CANON_LIMIT). */
|
|
19
|
+
export declare function checkName(s: string, path: string, code?: 'E_CANON_NAME' | 'E_CANON_ARG_NAME'): void;
|
|
20
|
+
/** SPEC 2.4: a name that is not `asof` and does not start with `dm_`. */
|
|
21
|
+
export declare function checkArgName(s: string, path: string): void;
|
|
22
|
+
/** SPEC 2.1. Every failure is E_CANON_NAME. */
|
|
23
|
+
export declare function checkDnsName(s: string, path: string): void;
|
|
24
|
+
/** SPEC 2.3: dns-name *( "/" name ). */
|
|
25
|
+
export declare function checkNamespace(s: string, path: string): void;
|
|
26
|
+
/** SPEC 2.3: namespace "/" name, at most 512 characters. */
|
|
27
|
+
export declare function checkFunctionName(s: string, path: string): void;
|
|
28
|
+
export declare function isHex(s: string, digits: number): boolean;
|
|
29
|
+
export declare function checkHex(s: string, digits: number, path: string): void;
|
|
30
|
+
/** SPEC 9.1: `<host>#<key id>`. Host failures are E_CANON_NAME, key id failures E_CANON_HEX. */
|
|
31
|
+
export declare function checkKeyRef(s: string, path: string): {
|
|
32
|
+
host: string;
|
|
33
|
+
id: string;
|
|
34
|
+
};
|
|
35
|
+
/** SPEC 3.1: a canonical decimal of at most 128 characters. */
|
|
36
|
+
export declare function isNumber(s: string): boolean;
|
|
37
|
+
/** SPEC 3.1. Grammar (E_CANON_NUMBER) before the 128-character limit (E_CANON_LIMIT). */
|
|
38
|
+
export declare function checkNumber(s: string, path: string): void;
|
|
39
|
+
export declare function isDate(s: string): boolean;
|
|
40
|
+
export declare function isInstant(s: string): boolean;
|
|
41
|
+
/** SPEC 3.2. */
|
|
42
|
+
export declare function checkDate(s: string, path: string): void;
|
|
43
|
+
/** SPEC 3.3. */
|
|
44
|
+
export declare function checkInstant(s: string, path: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* SPEC 3.5, in its order: characters, then NFC, then the UTF-8 length limit. With `body`
|
|
47
|
+
* set, TAB and LF are allowed and the limit is 1 MiB (SPEC 7, `code.body`).
|
|
48
|
+
*/
|
|
49
|
+
export declare function checkString(s: string, path: string, opts?: {
|
|
50
|
+
body?: boolean;
|
|
51
|
+
limit?: number;
|
|
52
|
+
}): void;
|
|
53
|
+
export declare const MEDIA_TYPE_MAX = 128;
|
|
54
|
+
/** SPEC 3.7. Grammar, then length <= 2^53 - 1, then media type <= 128 characters; all E_CANON_BLOB. */
|
|
55
|
+
export declare function checkBlob(s: string, path: string): {
|
|
56
|
+
sha256: string;
|
|
57
|
+
length: number;
|
|
58
|
+
mediaType: string;
|
|
59
|
+
};
|
|
60
|
+
export declare const URL_RE: RegExp;
|
|
61
|
+
/**
|
|
62
|
+
* SPEC 8.1 `licence`: an SPDX licence identifier or an https:// URL. SPEC.md gives no grammar;
|
|
63
|
+
* this follows schemas/receipt.schema.json (and the reference tool): an identifier matches
|
|
64
|
+
* [A-Za-z0-9][A-Za-z0-9.+-]* (at most 64 characters), a URL is "https://" followed by one or more
|
|
65
|
+
* printable ASCII characters other than space (at most 512). Grammar before length.
|
|
66
|
+
*/
|
|
67
|
+
export declare function checkLicence(s: string, path: string): void;
|
|
68
|
+
/** Value of a typed scalar against its (known) type: JSON type first, then the lexical form. */
|
|
69
|
+
export declare function checkTypedValue(type: TypeName, value: unknown, path: string): void;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.URL_RE = exports.MEDIA_TYPE_MAX = exports.VALID_TIME_TYPES = exports.TYPES = exports.LIMITS = void 0;
|
|
4
|
+
exports.isTypeName = isTypeName;
|
|
5
|
+
exports.checkName = checkName;
|
|
6
|
+
exports.checkArgName = checkArgName;
|
|
7
|
+
exports.checkDnsName = checkDnsName;
|
|
8
|
+
exports.checkNamespace = checkNamespace;
|
|
9
|
+
exports.checkFunctionName = checkFunctionName;
|
|
10
|
+
exports.isHex = isHex;
|
|
11
|
+
exports.checkHex = checkHex;
|
|
12
|
+
exports.checkKeyRef = checkKeyRef;
|
|
13
|
+
exports.isNumber = isNumber;
|
|
14
|
+
exports.checkNumber = checkNumber;
|
|
15
|
+
exports.isDate = isDate;
|
|
16
|
+
exports.isInstant = isInstant;
|
|
17
|
+
exports.checkDate = checkDate;
|
|
18
|
+
exports.checkInstant = checkInstant;
|
|
19
|
+
exports.checkString = checkString;
|
|
20
|
+
exports.checkBlob = checkBlob;
|
|
21
|
+
exports.checkLicence = checkLicence;
|
|
22
|
+
exports.checkTypedValue = checkTypedValue;
|
|
23
|
+
// Lexical rules of SPEC.md sections 2-3 and 8.1. Each check throws a DatumeshError with the
|
|
24
|
+
// code the spec assigns; where the spec does not order two checks, the order chosen is noted
|
|
25
|
+
// (and recorded in DEFECTS.md).
|
|
26
|
+
const errors_js_1 = require("./errors.js");
|
|
27
|
+
const unicode_js_1 = require("./unicode.js");
|
|
28
|
+
const utf8_js_1 = require("./utf8.js");
|
|
29
|
+
exports.LIMITS = {
|
|
30
|
+
name: 64,
|
|
31
|
+
function_name: 512,
|
|
32
|
+
args: 32,
|
|
33
|
+
number: 128,
|
|
34
|
+
string: 1024,
|
|
35
|
+
summary: 1024,
|
|
36
|
+
body: 1048576,
|
|
37
|
+
inputs: 4096,
|
|
38
|
+
licence_spdx: 64,
|
|
39
|
+
licence_url: 512,
|
|
40
|
+
keys: 64,
|
|
41
|
+
};
|
|
42
|
+
exports.TYPES = ['blob', 'boolean', 'date', 'instant', 'number', 'string'];
|
|
43
|
+
exports.VALID_TIME_TYPES = ['date', 'instant', 'string'];
|
|
44
|
+
function isTypeName(t) {
|
|
45
|
+
return typeof t === 'string' && exports.TYPES.includes(t);
|
|
46
|
+
}
|
|
47
|
+
const NAME_RE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
|
|
48
|
+
const LABEL_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
49
|
+
/** SPEC 2.2. Grammar first (`code`), then the 64-character limit (E_CANON_LIMIT). */
|
|
50
|
+
function checkName(s, path, code = 'E_CANON_NAME') {
|
|
51
|
+
if (!NAME_RE.test(s))
|
|
52
|
+
(0, errors_js_1.fail)(code, `not a lowercase snake_case name: ${JSON.stringify(s)}`, path);
|
|
53
|
+
if (s.length > exports.LIMITS.name)
|
|
54
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', `name longer than ${exports.LIMITS.name} characters`, path);
|
|
55
|
+
}
|
|
56
|
+
/** SPEC 2.4: a name that is not `asof` and does not start with `dm_`. */
|
|
57
|
+
function checkArgName(s, path) {
|
|
58
|
+
checkName(s, path, 'E_CANON_ARG_NAME');
|
|
59
|
+
if (s === 'asof' || s.startsWith('dm_'))
|
|
60
|
+
(0, errors_js_1.fail)('E_CANON_ARG_NAME', `reserved argument name ${JSON.stringify(s)}`, path);
|
|
61
|
+
}
|
|
62
|
+
/** SPEC 2.1. Every failure is E_CANON_NAME. */
|
|
63
|
+
function checkDnsName(s, path) {
|
|
64
|
+
const labels = s.split('.');
|
|
65
|
+
if (labels.length < 2)
|
|
66
|
+
(0, errors_js_1.fail)('E_CANON_NAME', `DNS name needs at least two labels: ${JSON.stringify(s)}`, path);
|
|
67
|
+
for (const l of labels) {
|
|
68
|
+
if (l.length < 1 || l.length > 63 || !LABEL_RE.test(l))
|
|
69
|
+
(0, errors_js_1.fail)('E_CANON_NAME', `bad DNS label ${JSON.stringify(l)}`, path);
|
|
70
|
+
}
|
|
71
|
+
if (/^[0-9]+$/.test(labels[labels.length - 1]))
|
|
72
|
+
(0, errors_js_1.fail)('E_CANON_NAME', 'top-level label is numeric', path);
|
|
73
|
+
if (s.length > 253)
|
|
74
|
+
(0, errors_js_1.fail)('E_CANON_NAME', 'DNS name longer than 253 characters', path);
|
|
75
|
+
}
|
|
76
|
+
/** SPEC 2.3: dns-name *( "/" name ). */
|
|
77
|
+
function checkNamespace(s, path) {
|
|
78
|
+
const parts = s.split('/');
|
|
79
|
+
checkDnsName(parts[0], path);
|
|
80
|
+
for (const seg of parts.slice(1))
|
|
81
|
+
checkName(seg, path);
|
|
82
|
+
}
|
|
83
|
+
/** SPEC 2.3: namespace "/" name, at most 512 characters. */
|
|
84
|
+
function checkFunctionName(s, path) {
|
|
85
|
+
const i = s.lastIndexOf('/');
|
|
86
|
+
if (i < 0)
|
|
87
|
+
(0, errors_js_1.fail)('E_CANON_NAME', `function name has no namespace: ${JSON.stringify(s)}`, path);
|
|
88
|
+
checkNamespace(s.slice(0, i), path);
|
|
89
|
+
checkName(s.slice(i + 1), path);
|
|
90
|
+
if (s.length > exports.LIMITS.function_name)
|
|
91
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', 'function name too long', path);
|
|
92
|
+
}
|
|
93
|
+
function isHex(s, digits) {
|
|
94
|
+
return s.length === digits && /^[0-9a-f]*$/.test(s);
|
|
95
|
+
}
|
|
96
|
+
function checkHex(s, digits, path) {
|
|
97
|
+
if (!isHex(s, digits))
|
|
98
|
+
(0, errors_js_1.fail)('E_CANON_HEX', `expected ${digits} lowercase hex digits`, path);
|
|
99
|
+
}
|
|
100
|
+
/** SPEC 9.1: `<host>#<key id>`. Host failures are E_CANON_NAME, key id failures E_CANON_HEX. */
|
|
101
|
+
function checkKeyRef(s, path) {
|
|
102
|
+
const i = s.indexOf('#');
|
|
103
|
+
const host = i < 0 ? s : s.slice(0, i);
|
|
104
|
+
checkDnsName(host, path);
|
|
105
|
+
if (i < 0)
|
|
106
|
+
(0, errors_js_1.fail)('E_CANON_NAME', 'key reference is <host>#<key id>', path);
|
|
107
|
+
const id = s.slice(i + 1);
|
|
108
|
+
checkHex(id, 16, path);
|
|
109
|
+
return { host, id };
|
|
110
|
+
}
|
|
111
|
+
const NUMBER_RE = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*[1-9])?$/;
|
|
112
|
+
/** SPEC 3.1: a canonical decimal of at most 128 characters. */
|
|
113
|
+
function isNumber(s) {
|
|
114
|
+
return NUMBER_RE.test(s) && s !== '-0' && s.length <= exports.LIMITS.number;
|
|
115
|
+
}
|
|
116
|
+
/** SPEC 3.1. Grammar (E_CANON_NUMBER) before the 128-character limit (E_CANON_LIMIT). */
|
|
117
|
+
function checkNumber(s, path) {
|
|
118
|
+
if (!NUMBER_RE.test(s) || s === '-0')
|
|
119
|
+
(0, errors_js_1.fail)('E_CANON_NUMBER', `not a canonical decimal: ${JSON.stringify(s)}`, path);
|
|
120
|
+
if (s.length > exports.LIMITS.number)
|
|
121
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', `number longer than ${exports.LIMITS.number} characters`, path);
|
|
122
|
+
}
|
|
123
|
+
function isCalendarDay(y, m, d) {
|
|
124
|
+
if (y < 1 || y > 9999 || m < 1 || m > 12 || d < 1)
|
|
125
|
+
return false;
|
|
126
|
+
const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
|
|
127
|
+
const dim = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1];
|
|
128
|
+
return d <= dim;
|
|
129
|
+
}
|
|
130
|
+
const DATE_RE = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/;
|
|
131
|
+
const INSTANT_RE = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\.[0-9]{6}Z$/;
|
|
132
|
+
function isDate(s) {
|
|
133
|
+
const m = DATE_RE.exec(s);
|
|
134
|
+
return !!m && isCalendarDay(+m[1], +m[2], +m[3]);
|
|
135
|
+
}
|
|
136
|
+
function isInstant(s) {
|
|
137
|
+
const m = INSTANT_RE.exec(s);
|
|
138
|
+
return !!m && isCalendarDay(+m[1], +m[2], +m[3]) && +m[4] <= 23 && +m[5] <= 59 && +m[6] <= 59;
|
|
139
|
+
}
|
|
140
|
+
/** SPEC 3.2. */
|
|
141
|
+
function checkDate(s, path) {
|
|
142
|
+
if (!isDate(s))
|
|
143
|
+
(0, errors_js_1.fail)('E_CANON_DATE', `not a YYYY-MM-DD calendar date: ${JSON.stringify(s)}`, path);
|
|
144
|
+
}
|
|
145
|
+
/** SPEC 3.3. */
|
|
146
|
+
function checkInstant(s, path) {
|
|
147
|
+
if (!isInstant(s))
|
|
148
|
+
(0, errors_js_1.fail)('E_CANON_INSTANT', `not YYYY-MM-DDTHH:MM:SS.ffffffZ: ${JSON.stringify(s)}`, path);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* SPEC 3.5, in its order: characters, then NFC, then the UTF-8 length limit. With `body`
|
|
152
|
+
* set, TAB and LF are allowed and the limit is 1 MiB (SPEC 7, `code.body`).
|
|
153
|
+
*/
|
|
154
|
+
function checkString(s, path, opts = {}) {
|
|
155
|
+
const cps = (0, unicode_js_1.codePoints)(s);
|
|
156
|
+
for (const cp of cps) {
|
|
157
|
+
if (cp >= 0xd800 && cp <= 0xdfff)
|
|
158
|
+
(0, errors_js_1.fail)('E_CANON_STRING', `surrogate code point U+${hex4(cp)}`, path);
|
|
159
|
+
if (cp <= 0x1f || (cp >= 0x7f && cp <= 0x9f)) {
|
|
160
|
+
if (!(opts.body && (cp === 0x09 || cp === 0x0a)))
|
|
161
|
+
(0, errors_js_1.fail)('E_CANON_STRING', `control character U+${hex4(cp)}`, path);
|
|
162
|
+
}
|
|
163
|
+
if (!(0, unicode_js_1.isAssigned)(cp))
|
|
164
|
+
(0, errors_js_1.fail)('E_CANON_STRING', `code point U+${hex4(cp)} is not assigned in Unicode 15.1`, path);
|
|
165
|
+
}
|
|
166
|
+
if (!(0, unicode_js_1.isNFC)(s))
|
|
167
|
+
(0, errors_js_1.fail)('E_CANON_STRING', 'not in Normalization Form C', path);
|
|
168
|
+
const limit = opts.limit ?? (opts.body ? exports.LIMITS.body : exports.LIMITS.string);
|
|
169
|
+
if ((0, utf8_js_1.utf8Length)(s) > limit)
|
|
170
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', `longer than ${limit} UTF-8 bytes`, path);
|
|
171
|
+
}
|
|
172
|
+
function hex4(cp) {
|
|
173
|
+
return cp.toString(16).toUpperCase().padStart(4, '0');
|
|
174
|
+
}
|
|
175
|
+
// SPEC 3.7: <sha256>:<length>:<type>/<subtype>, with RFC 6838 restricted names in lowercase.
|
|
176
|
+
const BLOB_RE = /^[0-9a-f]{64}:(0|[1-9][0-9]*):([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)$/;
|
|
177
|
+
const MAX_SAFE = '9007199254740991';
|
|
178
|
+
exports.MEDIA_TYPE_MAX = 128;
|
|
179
|
+
/** SPEC 3.7. Grammar, then length <= 2^53 - 1, then media type <= 128 characters; all E_CANON_BLOB. */
|
|
180
|
+
function checkBlob(s, path) {
|
|
181
|
+
const m = BLOB_RE.exec(s);
|
|
182
|
+
if (!m)
|
|
183
|
+
(0, errors_js_1.fail)('E_CANON_BLOB', `not <sha256>:<length>:<type/subtype>: ${JSON.stringify(s)}`, path);
|
|
184
|
+
const len = m[1];
|
|
185
|
+
if (len.length > MAX_SAFE.length || (len.length === MAX_SAFE.length && len > MAX_SAFE))
|
|
186
|
+
(0, errors_js_1.fail)('E_CANON_BLOB', `length above ${MAX_SAFE}`, path);
|
|
187
|
+
if (m[2].length > exports.MEDIA_TYPE_MAX)
|
|
188
|
+
(0, errors_js_1.fail)('E_CANON_BLOB', `media type longer than ${exports.MEDIA_TYPE_MAX} characters`, path);
|
|
189
|
+
return { sha256: s.slice(0, 64), length: Number(len), mediaType: m[2] };
|
|
190
|
+
}
|
|
191
|
+
const SPDX_RE = /^[A-Za-z0-9][A-Za-z0-9.+-]*$/;
|
|
192
|
+
exports.URL_RE = /^https:\/\/[!-~]+$/;
|
|
193
|
+
/**
|
|
194
|
+
* SPEC 8.1 `licence`: an SPDX licence identifier or an https:// URL. SPEC.md gives no grammar;
|
|
195
|
+
* this follows schemas/receipt.schema.json (and the reference tool): an identifier matches
|
|
196
|
+
* [A-Za-z0-9][A-Za-z0-9.+-]* (at most 64 characters), a URL is "https://" followed by one or more
|
|
197
|
+
* printable ASCII characters other than space (at most 512). Grammar before length.
|
|
198
|
+
*/
|
|
199
|
+
function checkLicence(s, path) {
|
|
200
|
+
if (s.startsWith('https://')) {
|
|
201
|
+
if (!exports.URL_RE.test(s))
|
|
202
|
+
(0, errors_js_1.fail)('E_CANON_LICENCE', 'licence URL has characters outside printable ASCII', path);
|
|
203
|
+
if (s.length > exports.LIMITS.licence_url)
|
|
204
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', 'licence URL too long', path);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!SPDX_RE.test(s))
|
|
208
|
+
(0, errors_js_1.fail)('E_CANON_LICENCE', `not an SPDX identifier or https URL: ${JSON.stringify(s)}`, path);
|
|
209
|
+
if (s.length > exports.LIMITS.licence_spdx)
|
|
210
|
+
(0, errors_js_1.fail)('E_CANON_LIMIT', 'licence identifier too long', path);
|
|
211
|
+
}
|
|
212
|
+
/** Value of a typed scalar against its (known) type: JSON type first, then the lexical form. */
|
|
213
|
+
function checkTypedValue(type, value, path) {
|
|
214
|
+
if (type === 'boolean') {
|
|
215
|
+
if (typeof value !== 'boolean')
|
|
216
|
+
(0, errors_js_1.fail)('E_CANON_TYPE', 'boolean value must be JSON true or false', path);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (typeof value !== 'string')
|
|
220
|
+
(0, errors_js_1.fail)('E_CANON_TYPE', `${type} value must be a JSON string`, path);
|
|
221
|
+
if (type === 'number')
|
|
222
|
+
checkNumber(value, path);
|
|
223
|
+
else if (type === 'date')
|
|
224
|
+
checkDate(value, path);
|
|
225
|
+
else if (type === 'instant')
|
|
226
|
+
checkInstant(value, path);
|
|
227
|
+
else if (type === 'blob')
|
|
228
|
+
checkBlob(value, path);
|
|
229
|
+
else
|
|
230
|
+
checkString(value, path);
|
|
231
|
+
}
|