@statewalker/webrun-biscuit 0.1.0 → 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/README.md +69 -10
- package/dist/authorizer.d.ts +41 -4
- package/dist/authorizer.d.ts.map +1 -1
- package/dist/builder.d.ts +6 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/crypto.d.ts +7 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/datalog.d.ts +2 -0
- package/dist/datalog.d.ts.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +377 -118
- package/dist/parser.d.ts +13 -2
- package/dist/parser.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/authorizer.ts +123 -27
- package/src/builder.ts +31 -9
- package/src/crypto.ts +164 -18
- package/src/datalog.ts +10 -0
- package/src/index.ts +26 -2
- package/src/parser.ts +69 -2
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
2
2
|
import { p256 } from "@noble/curves/nist.js";
|
|
3
|
+
//#region src/base64.ts
|
|
4
|
+
/** URL-safe base64 without padding, the wire form of a Biscuit token.
|
|
5
|
+
* Implemented directly so the library stays runtime-agnostic (no Buffer,
|
|
6
|
+
* no atob/btoa). */
|
|
7
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
8
|
+
const REVERSE = new Map([...ALPHABET].map((c, i) => [c, i]));
|
|
9
|
+
REVERSE.set("+", 62);
|
|
10
|
+
REVERSE.set("/", 63);
|
|
11
|
+
function toBase64(bytes) {
|
|
12
|
+
let out = "";
|
|
13
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
14
|
+
const b0 = bytes[i];
|
|
15
|
+
const b1 = bytes[i + 1];
|
|
16
|
+
const b2 = bytes[i + 2];
|
|
17
|
+
out += ALPHABET[b0 >> 2];
|
|
18
|
+
out += ALPHABET[(b0 & 3) << 4 | (b1 ?? 0) >> 4];
|
|
19
|
+
if (b1 === void 0) break;
|
|
20
|
+
out += ALPHABET[(b1 & 15) << 2 | (b2 ?? 0) >> 6];
|
|
21
|
+
if (b2 === void 0) break;
|
|
22
|
+
out += ALPHABET[b2 & 63];
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function fromBase64(text) {
|
|
27
|
+
const s = text.trim().replace(/=+$/, "");
|
|
28
|
+
const out = new Uint8Array(Math.floor(s.length * 3 / 4));
|
|
29
|
+
let o = 0;
|
|
30
|
+
let acc = 0;
|
|
31
|
+
let bits = 0;
|
|
32
|
+
for (const c of s) {
|
|
33
|
+
const v = REVERSE.get(c);
|
|
34
|
+
if (v === void 0) throw new Error(`invalid base64 character ${JSON.stringify(c)}`);
|
|
35
|
+
acc = acc << 6 | v;
|
|
36
|
+
bits += 6;
|
|
37
|
+
if (bits >= 8) {
|
|
38
|
+
bits -= 8;
|
|
39
|
+
out[o++] = acc >> bits & 255;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out.subarray(0, o);
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
3
45
|
//#region src/crypto.ts
|
|
4
46
|
/**
|
|
5
47
|
* Biscuit cryptography: chained block signatures, sealing, third-party
|
|
@@ -75,17 +117,30 @@ function generateKeypair(algorithm = 0) {
|
|
|
75
117
|
publicKey: publicKeyFromSecret(secretKey, algorithm)
|
|
76
118
|
};
|
|
77
119
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Everything verifying a token requires, as data: the signatures to check, in
|
|
122
|
+
* chain order, and — for an unsealed token — the proof secret whose public key
|
|
123
|
+
* must equal the last block's next key. Structural problems throw here, before
|
|
124
|
+
* any signature is looked at. `verifyToken` and `verifyTokenAsync` both consume
|
|
125
|
+
* this, so neither can skip a check the other makes.
|
|
126
|
+
*/
|
|
127
|
+
function chainChecks(token, rootPublicKey, rootAlgorithm) {
|
|
80
128
|
const root = {
|
|
81
129
|
algorithm: rootAlgorithm,
|
|
82
130
|
key: rootPublicKey
|
|
83
131
|
};
|
|
132
|
+
const checks = [];
|
|
84
133
|
if (token.authority.externalSignature) throw new SignatureError("the authority block must not carry an external signature");
|
|
85
134
|
const authVersion = token.authority.version ?? 0;
|
|
86
|
-
|
|
135
|
+
const authPayload = authVersion === 0 ? blockPayloadV0(token.authority.block, token.authority.nextKey) : authVersion === 1 ? authorityPayloadV1(token.authority.block, token.authority.nextKey, authVersion) : (() => {
|
|
87
136
|
throw new SignatureError(`unsupported block version ${authVersion}`);
|
|
88
|
-
})()
|
|
137
|
+
})();
|
|
138
|
+
checks.push({
|
|
139
|
+
key: root,
|
|
140
|
+
payload: authPayload,
|
|
141
|
+
signature: token.authority.signature,
|
|
142
|
+
error: "invalid authority block signature"
|
|
143
|
+
});
|
|
89
144
|
let currentKey = token.authority.nextKey;
|
|
90
145
|
let previousSignature = token.authority.signature;
|
|
91
146
|
for (const block of token.blocks) {
|
|
@@ -95,21 +150,121 @@ function verifyToken(token, rootPublicKey, rootAlgorithm = 0) {
|
|
|
95
150
|
if (version === 0) payload = blockPayloadV0(block.block, block.nextKey, externalSig);
|
|
96
151
|
else if (version === 1) payload = blockPayloadV1(block.block, block.nextKey, externalSig, previousSignature, version);
|
|
97
152
|
else throw new SignatureError(`unsupported block version ${version}`);
|
|
98
|
-
|
|
153
|
+
checks.push({
|
|
154
|
+
key: currentKey,
|
|
155
|
+
payload,
|
|
156
|
+
signature: block.signature,
|
|
157
|
+
error: "invalid block signature"
|
|
158
|
+
});
|
|
99
159
|
if (block.externalSignature) {
|
|
100
160
|
if (version !== 1) throw new SignatureError("unsupported third party block version");
|
|
101
|
-
|
|
102
|
-
|
|
161
|
+
checks.push({
|
|
162
|
+
key: block.externalSignature.publicKey,
|
|
163
|
+
payload: externalPayloadV1(block.block, previousSignature, version),
|
|
164
|
+
signature: block.externalSignature.signature,
|
|
165
|
+
error: "invalid external signature"
|
|
166
|
+
});
|
|
103
167
|
}
|
|
104
168
|
currentKey = block.nextKey;
|
|
105
169
|
previousSignature = block.signature;
|
|
106
170
|
}
|
|
107
|
-
if (token.proof.kind === "nextSecret") {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
171
|
+
if (token.proof.kind === "nextSecret") return {
|
|
172
|
+
checks,
|
|
173
|
+
proof: {
|
|
174
|
+
secret: token.proof.value,
|
|
175
|
+
key: currentKey
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const last = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
|
|
179
|
+
checks.push({
|
|
180
|
+
key: currentKey,
|
|
181
|
+
payload: sealPayloadV0(last),
|
|
182
|
+
signature: token.proof.value,
|
|
183
|
+
error: "invalid seal signature"
|
|
184
|
+
});
|
|
185
|
+
return { checks };
|
|
186
|
+
}
|
|
187
|
+
/** Verifies the full signature chain and the proof. Throws on failure. */
|
|
188
|
+
function verifyToken(token, rootPublicKey, rootAlgorithm = 0) {
|
|
189
|
+
const { checks, proof } = chainChecks(token, rootPublicKey, rootAlgorithm);
|
|
190
|
+
for (const check of checks) if (!verifySignature(check.key, check.payload, check.signature)) throw new SignatureError(check.error);
|
|
191
|
+
if (proof && !bytesEqual(publicKeyFromSecret(proof.secret, proof.key.algorithm), proof.key.key)) throw new SignatureError("the last public key does not match the private key");
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* `verifyToken`, using the platform's WebCrypto for Ed25519 where it offers it
|
|
195
|
+
* — about ten times faster than the pure-JS path in Node and current browsers.
|
|
196
|
+
* secp256r1, and any runtime without WebCrypto Ed25519, fall back to
|
|
197
|
+
* `@noble/curves`, so the result never depends on where it runs.
|
|
198
|
+
*/
|
|
199
|
+
async function verifyTokenAsync(token, rootPublicKey, rootAlgorithm = 0) {
|
|
200
|
+
const { checks, proof } = chainChecks(token, rootPublicKey, rootAlgorithm);
|
|
201
|
+
const [verdicts, derived] = await Promise.all([Promise.all(checks.map((c) => verifySignatureAsync(c.key, c.payload, c.signature))), proof ? publicKeyFromSecretAsync(proof.secret, proof.key.algorithm) : void 0]);
|
|
202
|
+
verdicts.forEach((ok, i) => {
|
|
203
|
+
if (!ok) throw new SignatureError(checks[i].error);
|
|
204
|
+
});
|
|
205
|
+
if (proof && !bytesEqual(derived, proof.key.key)) throw new SignatureError("the last public key does not match the private key");
|
|
206
|
+
}
|
|
207
|
+
const subtle = globalThis.crypto?.subtle;
|
|
208
|
+
/** PKCS#8 wrapping of a raw 32-byte Ed25519 seed (RFC 8410) */
|
|
209
|
+
const PKCS8_ED25519 = Uint8Array.from([
|
|
210
|
+
48,
|
|
211
|
+
46,
|
|
212
|
+
2,
|
|
213
|
+
1,
|
|
214
|
+
0,
|
|
215
|
+
48,
|
|
216
|
+
5,
|
|
217
|
+
6,
|
|
218
|
+
3,
|
|
219
|
+
43,
|
|
220
|
+
101,
|
|
221
|
+
112,
|
|
222
|
+
4,
|
|
223
|
+
34,
|
|
224
|
+
4,
|
|
225
|
+
32
|
|
226
|
+
]);
|
|
227
|
+
let ed25519Native;
|
|
228
|
+
/** Whether this runtime's WebCrypto verifies Ed25519, and does so correctly. Probed once. */
|
|
229
|
+
function nativeEd25519() {
|
|
230
|
+
ed25519Native ??= (async () => {
|
|
231
|
+
if (!subtle) return false;
|
|
232
|
+
try {
|
|
233
|
+
const secret = (/* @__PURE__ */ new Uint8Array(32)).fill(7);
|
|
234
|
+
const message = ascii("webrun-biscuit");
|
|
235
|
+
const signature = ed25519.sign(message, secret);
|
|
236
|
+
const key = await subtle.importKey("raw", ed25519.getPublicKey(secret), { name: "Ed25519" }, false, ["verify"]);
|
|
237
|
+
const forged = signature.slice();
|
|
238
|
+
forged[0] ^= 1;
|
|
239
|
+
return await subtle.verify("Ed25519", key, signature, message) && !await subtle.verify("Ed25519", key, forged, message);
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
return ed25519Native;
|
|
245
|
+
}
|
|
246
|
+
async function verifySignatureAsync(key, payload, sig) {
|
|
247
|
+
if (key.algorithm !== ALG_ED25519 || !subtle || !await nativeEd25519()) return verifySignature(key, payload, sig);
|
|
248
|
+
if (key.key.length !== 32 || sig.length !== 64) return false;
|
|
249
|
+
try {
|
|
250
|
+
const imported = await subtle.importKey("raw", key.key, { name: "Ed25519" }, false, ["verify"]);
|
|
251
|
+
return await subtle.verify("Ed25519", imported, sig, payload);
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function publicKeyFromSecretAsync(secret, algorithm) {
|
|
257
|
+
if (algorithm !== ALG_ED25519 || secret.length !== 32 || !subtle || !await nativeEd25519()) return publicKeyFromSecret(secret, algorithm);
|
|
258
|
+
try {
|
|
259
|
+
const pkcs8 = new Uint8Array(PKCS8_ED25519.length + 32);
|
|
260
|
+
pkcs8.set(PKCS8_ED25519);
|
|
261
|
+
pkcs8.set(secret, PKCS8_ED25519.length);
|
|
262
|
+
const key = await subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, true, ["sign"]);
|
|
263
|
+
const { x } = await subtle.exportKey("jwk", key);
|
|
264
|
+
if (x === void 0) return publicKeyFromSecret(secret, algorithm);
|
|
265
|
+
return fromBase64(x);
|
|
266
|
+
} catch {
|
|
267
|
+
return publicKeyFromSecret(secret, algorithm);
|
|
113
268
|
}
|
|
114
269
|
}
|
|
115
270
|
/** Revocation identifier of each block: its signature bytes. */
|
|
@@ -250,10 +405,10 @@ const B = {
|
|
|
250
405
|
};
|
|
251
406
|
const BinaryOp = B;
|
|
252
407
|
const UnaryOp = U;
|
|
253
|
-
const I64_MIN = -(2n ** 63n);
|
|
254
|
-
const I64_MAX = 2n ** 63n - 1n;
|
|
408
|
+
const I64_MIN$1 = -(2n ** 63n);
|
|
409
|
+
const I64_MAX$1 = 2n ** 63n - 1n;
|
|
255
410
|
function checkedI64(v) {
|
|
256
|
-
if (v < I64_MIN || v > I64_MAX) throw new ExecutionError("Overflow");
|
|
411
|
+
if (v < I64_MIN$1 || v > I64_MAX$1) throw new ExecutionError("Overflow");
|
|
257
412
|
return {
|
|
258
413
|
t: "int",
|
|
259
414
|
v
|
|
@@ -700,6 +855,15 @@ var World = class {
|
|
|
700
855
|
} }];
|
|
701
856
|
}
|
|
702
857
|
}
|
|
858
|
+
/** every distinct fact `rule` derives from the facts `trusted` can see */
|
|
859
|
+
query(rule, trusted) {
|
|
860
|
+
const seen = /* @__PURE__ */ new Map();
|
|
861
|
+
for (const [, fact] of this.apply(rule, this.visible(trusted), AUTHORIZER)) {
|
|
862
|
+
const key = factKey(fact);
|
|
863
|
+
if (!seen.has(key)) seen.set(key, fact.predicate);
|
|
864
|
+
}
|
|
865
|
+
return [...seen.values()];
|
|
866
|
+
}
|
|
703
867
|
/** `check if` / policies: does at least one combination match? */
|
|
704
868
|
queryMatch(rule, origin, trusted) {
|
|
705
869
|
for (const _ of this.apply(rule, this.visible(trusted), origin)) return true;
|
|
@@ -728,16 +892,73 @@ var World = class {
|
|
|
728
892
|
* compiled straight to the opcode form the VM executes.
|
|
729
893
|
*/
|
|
730
894
|
var ParseError = class extends Error {};
|
|
895
|
+
const I64_MIN = -(2n ** 63n);
|
|
896
|
+
const I64_MAX = 2n ** 63n - 1n;
|
|
897
|
+
function paramTerm(name, value) {
|
|
898
|
+
if (typeof value === "string") return {
|
|
899
|
+
t: "str",
|
|
900
|
+
v: value
|
|
901
|
+
};
|
|
902
|
+
if (typeof value === "boolean") return {
|
|
903
|
+
t: "bool",
|
|
904
|
+
v: value
|
|
905
|
+
};
|
|
906
|
+
if (value === null) return { t: "null" };
|
|
907
|
+
if (typeof value === "number") {
|
|
908
|
+
if (!Number.isSafeInteger(value)) throw new ParseError(`parameter {${name}}: ${value} is not a safe integer`);
|
|
909
|
+
return {
|
|
910
|
+
t: "int",
|
|
911
|
+
v: BigInt(value)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (typeof value === "bigint") {
|
|
915
|
+
if (value < I64_MIN || value > I64_MAX) throw new ParseError(`parameter {${name}}: ${value} does not fit in i64`);
|
|
916
|
+
return {
|
|
917
|
+
t: "int",
|
|
918
|
+
v: value
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (value instanceof Date) {
|
|
922
|
+
const ms = value.getTime();
|
|
923
|
+
if (Number.isNaN(ms)) throw new ParseError(`parameter {${name}}: invalid date`);
|
|
924
|
+
return {
|
|
925
|
+
t: "date",
|
|
926
|
+
v: BigInt(Math.floor(ms / 1e3))
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
if (value instanceof Uint8Array) return {
|
|
930
|
+
t: "bytes",
|
|
931
|
+
v: value
|
|
932
|
+
};
|
|
933
|
+
if (Array.isArray(value)) return {
|
|
934
|
+
t: "array",
|
|
935
|
+
v: value.map((x) => paramTerm(name, x))
|
|
936
|
+
};
|
|
937
|
+
if (value instanceof Set) return {
|
|
938
|
+
t: "set",
|
|
939
|
+
v: normalizeSet([...value].map((x) => paramTerm(name, x)))
|
|
940
|
+
};
|
|
941
|
+
throw new ParseError(`parameter {${name}}: no Datalog term for this value`);
|
|
942
|
+
}
|
|
943
|
+
/** `{name}` in term position; `{true}`, `{false}` and `{null}` stay one-element sets */
|
|
944
|
+
const PARAMETER = /^\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}/;
|
|
731
945
|
const NAME_START = /[\p{L}]/u;
|
|
732
946
|
const NAME_CHAR = /[\p{L}\p{N}_:]/u;
|
|
733
947
|
var Parser = class {
|
|
734
948
|
src;
|
|
949
|
+
params;
|
|
735
950
|
i = 0;
|
|
736
951
|
vars;
|
|
737
|
-
|
|
952
|
+
usedParams = /* @__PURE__ */ new Set();
|
|
953
|
+
constructor(src, vars, params = {}) {
|
|
738
954
|
this.src = src;
|
|
955
|
+
this.params = params;
|
|
739
956
|
this.vars = vars ?? /* @__PURE__ */ new Map();
|
|
740
957
|
}
|
|
958
|
+
/** names in `params` that the source never referred to */
|
|
959
|
+
unusedParameters() {
|
|
960
|
+
return Object.keys(this.params).filter((name) => !this.usedParams.has(name));
|
|
961
|
+
}
|
|
741
962
|
/** id -> name, for printing rules back out */
|
|
742
963
|
variableNames() {
|
|
743
964
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -890,7 +1111,7 @@ var Parser = class {
|
|
|
890
1111
|
};
|
|
891
1112
|
}
|
|
892
1113
|
if (this.peek("[")) return this.array(allowVariables);
|
|
893
|
-
if (this.peek("{")) return this.setOrMap(allowVariables);
|
|
1114
|
+
if (this.peek("{")) return this.parameter() ?? this.setOrMap(allowVariables);
|
|
894
1115
|
const m = /^-?\d+/.exec(this.src.slice(this.i));
|
|
895
1116
|
if (m) {
|
|
896
1117
|
this.i += m[0].length;
|
|
@@ -901,6 +1122,15 @@ var Parser = class {
|
|
|
901
1122
|
}
|
|
902
1123
|
throw new ParseError(`unexpected term at offset ${this.i}: ${this.src.slice(this.i, this.i + 20)}`);
|
|
903
1124
|
}
|
|
1125
|
+
parameter() {
|
|
1126
|
+
const m = PARAMETER.exec(this.src.slice(this.i));
|
|
1127
|
+
if (!m || m[1] === "true" || m[1] === "false" || m[1] === "null") return null;
|
|
1128
|
+
const name = m[1];
|
|
1129
|
+
if (!Object.hasOwn(this.params, name)) throw new ParseError(`unbound parameter {${name}}`);
|
|
1130
|
+
this.i += m[0].length;
|
|
1131
|
+
this.usedParams.add(name);
|
|
1132
|
+
return paramTerm(name, this.params[name]);
|
|
1133
|
+
}
|
|
904
1134
|
array(allowVariables) {
|
|
905
1135
|
this.expect("[");
|
|
906
1136
|
const items = [];
|
|
@@ -2474,6 +2704,18 @@ function convBlock(b, s, externalKey) {
|
|
|
2474
2704
|
function loadToken(bytes, rootPublicKey, rootAlgorithm = 0) {
|
|
2475
2705
|
const token = decodeBiscuit(bytes);
|
|
2476
2706
|
verifyToken(token, rootPublicKey, rootAlgorithm);
|
|
2707
|
+
return toLoadedToken(token);
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* `loadToken`, verifying signatures with the platform's WebCrypto Ed25519 where
|
|
2711
|
+
* available — see `verifyTokenAsync`. Same result, same errors.
|
|
2712
|
+
*/
|
|
2713
|
+
async function loadTokenAsync(bytes, rootPublicKey, rootAlgorithm = 0) {
|
|
2714
|
+
const token = decodeBiscuit(bytes);
|
|
2715
|
+
await verifyTokenAsync(token, rootPublicKey, rootAlgorithm);
|
|
2716
|
+
return toLoadedToken(token);
|
|
2717
|
+
}
|
|
2718
|
+
function toLoadedToken(token) {
|
|
2477
2719
|
const signed = [token.authority, ...token.blocks];
|
|
2478
2720
|
const decoded = signed.map((sb) => decodeBlock(sb.block));
|
|
2479
2721
|
const globalSymbols = [];
|
|
@@ -2528,7 +2770,12 @@ function headVariablesAreBound(rule) {
|
|
|
2528
2770
|
for (const p of rule.body) for (const t of p.terms) if (t.t === "var") bound.add(t.v);
|
|
2529
2771
|
return rule.head.terms.every((t) => t.t !== "var" || bound.has(t.v));
|
|
2530
2772
|
}
|
|
2531
|
-
|
|
2773
|
+
/**
|
|
2774
|
+
* Parse authorizer (or block) source. `{name}` parameters are bound from
|
|
2775
|
+
* `params` as terms; an unbound or an unused parameter is a `ParseError`, as it
|
|
2776
|
+
* is in the reference.
|
|
2777
|
+
*/
|
|
2778
|
+
function parseAuthorizer(src, params) {
|
|
2532
2779
|
const out = {
|
|
2533
2780
|
facts: [],
|
|
2534
2781
|
rules: [],
|
|
@@ -2537,8 +2784,10 @@ function parseAuthorizer(src) {
|
|
|
2537
2784
|
scopes: [],
|
|
2538
2785
|
varNames: /* @__PURE__ */ new Map()
|
|
2539
2786
|
};
|
|
2540
|
-
const parser = new Parser(src);
|
|
2787
|
+
const parser = new Parser(src, void 0, params);
|
|
2541
2788
|
const statements = parser.parse();
|
|
2789
|
+
const unused = parser.unusedParameters();
|
|
2790
|
+
if (unused.length > 0) throw new ParseError(`unused parameter${unused.length > 1 ? "s" : ""} {${unused.join("}, {")}}`);
|
|
2542
2791
|
out.varNames = parser.variableNames();
|
|
2543
2792
|
for (const st of statements) if (st.k === "fact") out.facts.push(st.fact);
|
|
2544
2793
|
else if (st.k === "rule") out.rules.push(st.rule);
|
|
@@ -2551,7 +2800,7 @@ function parseAuthorizer(src) {
|
|
|
2551
2800
|
return out;
|
|
2552
2801
|
}
|
|
2553
2802
|
function authorize(token, authorizerSrc, options = {}) {
|
|
2554
|
-
return
|
|
2803
|
+
return evaluate(token, authorizerSrc, options).result;
|
|
2555
2804
|
}
|
|
2556
2805
|
const EMPTY_WORLD = {
|
|
2557
2806
|
facts: [],
|
|
@@ -2562,18 +2811,32 @@ const EMPTY_WORLD = {
|
|
|
2562
2811
|
/** Same as `authorize`, and also returns the post-run world, in the shape the
|
|
2563
2812
|
* official sample corpus records it. */
|
|
2564
2813
|
function authorizeDetailed(token, authorizerSrc, options = {}) {
|
|
2814
|
+
const evaluation = evaluate(token, authorizerSrc, options);
|
|
2815
|
+
return {
|
|
2816
|
+
result: evaluation.result,
|
|
2817
|
+
world: evaluation.snapshot()
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
const NO_TOKEN = {
|
|
2821
|
+
blocks: [],
|
|
2822
|
+
publicKeyToBlockIds: /* @__PURE__ */ new Map(),
|
|
2823
|
+
revocationIds: []
|
|
2824
|
+
};
|
|
2825
|
+
/**
|
|
2826
|
+
* Run the authorizer against a verified token — or against no token at all,
|
|
2827
|
+
* for a decision made from the authorizer's own facts and rules.
|
|
2828
|
+
*/
|
|
2829
|
+
function evaluate(loadedToken, authorizerSrc, options = {}) {
|
|
2830
|
+
const token = loadedToken ?? NO_TOKEN;
|
|
2565
2831
|
const limits = options.limits ?? DEFAULT_LIMITS;
|
|
2566
2832
|
let code;
|
|
2567
2833
|
try {
|
|
2568
|
-
code = parseAuthorizer(authorizerSrc);
|
|
2834
|
+
code = parseAuthorizer(authorizerSrc, options.params);
|
|
2569
2835
|
} catch (e) {
|
|
2570
|
-
return {
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
},
|
|
2575
|
-
world: EMPTY_WORLD
|
|
2576
|
-
};
|
|
2836
|
+
return unusable({
|
|
2837
|
+
kind: "format",
|
|
2838
|
+
error: e.message
|
|
2839
|
+
});
|
|
2577
2840
|
}
|
|
2578
2841
|
const world = new World();
|
|
2579
2842
|
if (options.externs) world.externs = options.externs;
|
|
@@ -2586,14 +2849,11 @@ function authorizeDetailed(token, authorizerSrc, options = {}) {
|
|
|
2586
2849
|
const origin = Origin.of(id);
|
|
2587
2850
|
for (const f of block.facts) world.addFact(origin, f);
|
|
2588
2851
|
for (const r of block.rules) {
|
|
2589
|
-
if (!headVariablesAreBound(r)) return {
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
},
|
|
2595
|
-
world: EMPTY_WORLD
|
|
2596
|
-
};
|
|
2852
|
+
if (!headVariablesAreBound(r)) return unusable({
|
|
2853
|
+
kind: "invalidBlockRule",
|
|
2854
|
+
blockId: 0,
|
|
2855
|
+
rule: printRule(r, (v) => block.varNames.get(v) ?? String(v))
|
|
2856
|
+
});
|
|
2597
2857
|
world.addRule(id, trustedOriginsFromScopes(r.scopes, trusted, id, keys), r);
|
|
2598
2858
|
}
|
|
2599
2859
|
}
|
|
@@ -2643,16 +2903,29 @@ function authorizeDetailed(token, authorizerSrc, options = {}) {
|
|
|
2643
2903
|
policies: code.policies.map((p) => printPolicy(p.kind, p.queries, name(AUTHORIZER)))
|
|
2644
2904
|
};
|
|
2645
2905
|
};
|
|
2906
|
+
let failure;
|
|
2907
|
+
const settled = (result) => ({
|
|
2908
|
+
result,
|
|
2909
|
+
snapshot,
|
|
2910
|
+
query(src, queryOptions = {}) {
|
|
2911
|
+
if (failure) throw failure;
|
|
2912
|
+
const parsed = parseAuthorizer(`${src};`, queryOptions.params);
|
|
2913
|
+
if (parsed.facts.length + parsed.rules.length + parsed.checks.length + parsed.policies.length !== 1 || parsed.rules.length !== 1) throw new ParseError("a query must be exactly one rule");
|
|
2914
|
+
const rule = parsed.rules[0];
|
|
2915
|
+
const trusted = trustedOriginsFromScopes(rule.scopes, authorizerTrusted, AUTHORIZER, keys);
|
|
2916
|
+
return world.query(rule, trusted);
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2646
2919
|
try {
|
|
2647
2920
|
world.run(limits);
|
|
2648
2921
|
} catch (e) {
|
|
2649
|
-
if (e instanceof ExecutionError)
|
|
2650
|
-
|
|
2922
|
+
if (e instanceof ExecutionError) {
|
|
2923
|
+
failure = e;
|
|
2924
|
+
return settled({
|
|
2651
2925
|
kind: "execution",
|
|
2652
2926
|
error: e.kind
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
};
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2656
2929
|
throw e;
|
|
2657
2930
|
}
|
|
2658
2931
|
const errors = [];
|
|
@@ -2667,14 +2940,16 @@ function authorizeDetailed(token, authorizerSrc, options = {}) {
|
|
|
2667
2940
|
code.checks.forEach((check, i) => {
|
|
2668
2941
|
if (!runCheck(check, 4294967295, authorizerTrusted)) errors.push({
|
|
2669
2942
|
source: "authorizer",
|
|
2670
|
-
checkId: i
|
|
2943
|
+
checkId: i,
|
|
2944
|
+
rule: printCheck(check, name(AUTHORIZER))
|
|
2671
2945
|
});
|
|
2672
2946
|
});
|
|
2673
2947
|
token.blocks[0]?.checks.forEach((check, j) => {
|
|
2674
2948
|
if (!runCheck(check, 0, blockTrusted[0])) errors.push({
|
|
2675
2949
|
source: "block",
|
|
2676
2950
|
blockId: 0,
|
|
2677
|
-
checkId: j
|
|
2951
|
+
checkId: j,
|
|
2952
|
+
rule: printCheck(check, name(0))
|
|
2678
2953
|
});
|
|
2679
2954
|
});
|
|
2680
2955
|
let policyResult = null;
|
|
@@ -2689,34 +2964,42 @@ function authorizeDetailed(token, authorizerSrc, options = {}) {
|
|
|
2689
2964
|
if (!runCheck(check, id, blockTrusted[id])) errors.push({
|
|
2690
2965
|
source: "block",
|
|
2691
2966
|
blockId: id,
|
|
2692
|
-
checkId: j
|
|
2967
|
+
checkId: j,
|
|
2968
|
+
rule: printCheck(check, name(id))
|
|
2693
2969
|
});
|
|
2694
2970
|
});
|
|
2695
|
-
return {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
},
|
|
2707
|
-
world: snapshot()
|
|
2708
|
-
};
|
|
2971
|
+
return settled(policyResult === null ? {
|
|
2972
|
+
kind: "noMatchingPolicy",
|
|
2973
|
+
checks: errors
|
|
2974
|
+
} : "allow" in policyResult && errors.length === 0 ? {
|
|
2975
|
+
kind: "ok",
|
|
2976
|
+
policy: policyResult.allow
|
|
2977
|
+
} : {
|
|
2978
|
+
kind: "unauthorized",
|
|
2979
|
+
policy: policyResult,
|
|
2980
|
+
checks: errors
|
|
2981
|
+
});
|
|
2709
2982
|
} catch (e) {
|
|
2710
|
-
if (e instanceof ExecutionError)
|
|
2711
|
-
|
|
2983
|
+
if (e instanceof ExecutionError) {
|
|
2984
|
+
failure = e;
|
|
2985
|
+
return settled({
|
|
2712
2986
|
kind: "execution",
|
|
2713
2987
|
error: e.kind
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
};
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2717
2990
|
throw e;
|
|
2718
2991
|
}
|
|
2719
2992
|
}
|
|
2993
|
+
/** An evaluation that never produced a world: the result says why, and queries refuse. */
|
|
2994
|
+
function unusable(result) {
|
|
2995
|
+
return {
|
|
2996
|
+
result,
|
|
2997
|
+
snapshot: () => EMPTY_WORLD,
|
|
2998
|
+
query() {
|
|
2999
|
+
throw new Error(`cannot query an evaluation that ended in ${result.kind}`);
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
2720
3003
|
function compareOrigins(a, b) {
|
|
2721
3004
|
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
2722
3005
|
const x = a[i] === void 0 ? -Infinity : a[i] ?? -1;
|
|
@@ -2726,48 +3009,6 @@ function compareOrigins(a, b) {
|
|
|
2726
3009
|
return 0;
|
|
2727
3010
|
}
|
|
2728
3011
|
//#endregion
|
|
2729
|
-
//#region src/base64.ts
|
|
2730
|
-
/** URL-safe base64 without padding, the wire form of a Biscuit token.
|
|
2731
|
-
* Implemented directly so the library stays runtime-agnostic (no Buffer,
|
|
2732
|
-
* no atob/btoa). */
|
|
2733
|
-
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
2734
|
-
const REVERSE = new Map([...ALPHABET].map((c, i) => [c, i]));
|
|
2735
|
-
REVERSE.set("+", 62);
|
|
2736
|
-
REVERSE.set("/", 63);
|
|
2737
|
-
function toBase64(bytes) {
|
|
2738
|
-
let out = "";
|
|
2739
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
2740
|
-
const b0 = bytes[i];
|
|
2741
|
-
const b1 = bytes[i + 1];
|
|
2742
|
-
const b2 = bytes[i + 2];
|
|
2743
|
-
out += ALPHABET[b0 >> 2];
|
|
2744
|
-
out += ALPHABET[(b0 & 3) << 4 | (b1 ?? 0) >> 4];
|
|
2745
|
-
if (b1 === void 0) break;
|
|
2746
|
-
out += ALPHABET[(b1 & 15) << 2 | (b2 ?? 0) >> 6];
|
|
2747
|
-
if (b2 === void 0) break;
|
|
2748
|
-
out += ALPHABET[b2 & 63];
|
|
2749
|
-
}
|
|
2750
|
-
return out;
|
|
2751
|
-
}
|
|
2752
|
-
function fromBase64(text) {
|
|
2753
|
-
const s = text.trim().replace(/=+$/, "");
|
|
2754
|
-
const out = new Uint8Array(Math.floor(s.length * 3 / 4));
|
|
2755
|
-
let o = 0;
|
|
2756
|
-
let acc = 0;
|
|
2757
|
-
let bits = 0;
|
|
2758
|
-
for (const c of s) {
|
|
2759
|
-
const v = REVERSE.get(c);
|
|
2760
|
-
if (v === void 0) throw new Error(`invalid base64 character ${JSON.stringify(c)}`);
|
|
2761
|
-
acc = acc << 6 | v;
|
|
2762
|
-
bits += 6;
|
|
2763
|
-
if (bits >= 8) {
|
|
2764
|
-
bits -= 8;
|
|
2765
|
-
out[o++] = acc >> bits & 255;
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
return out.subarray(0, o);
|
|
2769
|
-
}
|
|
2770
|
-
//#endregion
|
|
2771
3012
|
//#region src/builder.ts
|
|
2772
3013
|
/**
|
|
2773
3014
|
* The write path: minting tokens, attenuating them with extra blocks, and
|
|
@@ -2780,12 +3021,18 @@ var BuilderError = class extends Error {};
|
|
|
2780
3021
|
var SymbolWriter = class {
|
|
2781
3022
|
known;
|
|
2782
3023
|
knownKeys;
|
|
3024
|
+
varNames;
|
|
2783
3025
|
/** symbols added by this block, in insertion order */
|
|
2784
3026
|
added = [];
|
|
2785
3027
|
addedKeys = [];
|
|
2786
|
-
constructor(known = [], knownKeys = []) {
|
|
3028
|
+
constructor(known = [], knownKeys = [], varNames = /* @__PURE__ */ new Map()) {
|
|
2787
3029
|
this.known = known;
|
|
2788
3030
|
this.knownKeys = knownKeys;
|
|
3031
|
+
this.varNames = varNames;
|
|
3032
|
+
}
|
|
3033
|
+
/** A variable's wire id is the symbol index of its NAME, not the parser's local id. */
|
|
3034
|
+
variable(id) {
|
|
3035
|
+
return this.insert(this.varNames.get(id) ?? String(id));
|
|
2789
3036
|
}
|
|
2790
3037
|
insert(s) {
|
|
2791
3038
|
const d = DEFAULT_SYMBOLS.indexOf(s);
|
|
@@ -2817,7 +3064,7 @@ function toTerm(t, w) {
|
|
|
2817
3064
|
switch (t.t) {
|
|
2818
3065
|
case "var": return {
|
|
2819
3066
|
kind: "variable",
|
|
2820
|
-
value: t.v
|
|
3067
|
+
value: w.variable(t.v)
|
|
2821
3068
|
};
|
|
2822
3069
|
case "int": return {
|
|
2823
3070
|
kind: "integer",
|
|
@@ -2885,7 +3132,7 @@ const toOps = (ops, w) => ops.map((op) => {
|
|
|
2885
3132
|
};
|
|
2886
3133
|
case "closure": return {
|
|
2887
3134
|
kind: "closure",
|
|
2888
|
-
params: op.params,
|
|
3135
|
+
params: op.params.map((p) => w.variable(p)),
|
|
2889
3136
|
ops: toOps(op.ops, w)
|
|
2890
3137
|
};
|
|
2891
3138
|
default: throw new Error(`unknown expression op kind ${op.kind}`);
|
|
@@ -2908,7 +3155,7 @@ const toRule = (r, w) => ({
|
|
|
2908
3155
|
scope: r.scopes.map((s) => toScope(s, w))
|
|
2909
3156
|
});
|
|
2910
3157
|
function buildBlockMsg(content, knownSymbols = [], knownKeys = [], minVersion = 0) {
|
|
2911
|
-
const w = new SymbolWriter(knownSymbols, knownKeys);
|
|
3158
|
+
const w = new SymbolWriter(knownSymbols, knownKeys, content.varNames);
|
|
2912
3159
|
const facts = content.facts.map((f) => toPredicate(f.predicate, w));
|
|
2913
3160
|
const rules = content.rules.map((r) => toRule(r, w));
|
|
2914
3161
|
const checks = content.checks.map((c) => ({
|
|
@@ -2926,14 +3173,15 @@ function buildBlockMsg(content, knownSymbols = [], knownKeys = [], minVersion =
|
|
|
2926
3173
|
publicKeys: w.addedKeys.map(parseKeyString)
|
|
2927
3174
|
};
|
|
2928
3175
|
}
|
|
2929
|
-
const contentFromCode = (code) => {
|
|
2930
|
-
const parsed = parseAuthorizer(code);
|
|
3176
|
+
const contentFromCode = (code, params) => {
|
|
3177
|
+
const parsed = parseAuthorizer(code, params);
|
|
2931
3178
|
if (parsed.policies.length) throw new BuilderError("allow/deny policies belong to the authorizer, not to a block");
|
|
2932
3179
|
return {
|
|
2933
3180
|
facts: parsed.facts,
|
|
2934
3181
|
rules: parsed.rules,
|
|
2935
3182
|
checks: parsed.checks,
|
|
2936
|
-
scopes: parsed.scopes
|
|
3183
|
+
scopes: parsed.scopes,
|
|
3184
|
+
varNames: parsed.varNames
|
|
2937
3185
|
};
|
|
2938
3186
|
};
|
|
2939
3187
|
function knownTables(token) {
|
|
@@ -2956,7 +3204,7 @@ function knownTables(token) {
|
|
|
2956
3204
|
/** Mint a new token whose authority block holds `code`. */
|
|
2957
3205
|
function buildToken(rootSecret, code, options = {}) {
|
|
2958
3206
|
const algorithm = options.algorithm ?? 0;
|
|
2959
|
-
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code) : code));
|
|
3207
|
+
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code, options.params) : code));
|
|
2960
3208
|
const next = options.nextKeypair ?? generateKeypair(algorithm);
|
|
2961
3209
|
const nextKey = {
|
|
2962
3210
|
algorithm,
|
|
@@ -2987,7 +3235,7 @@ function attenuate(tokenBytes, code, options = {}) {
|
|
|
2987
3235
|
const previous = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
|
|
2988
3236
|
const currentAlgorithm = previous.nextKey.algorithm;
|
|
2989
3237
|
const tables = knownTables(token);
|
|
2990
|
-
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code) : code, tables.symbols, tables.keys));
|
|
3238
|
+
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code, options.params) : code, tables.symbols, tables.keys));
|
|
2991
3239
|
const next = options.nextKeypair ?? generateKeypair(algorithm);
|
|
2992
3240
|
const nextKey = {
|
|
2993
3241
|
algorithm,
|
|
@@ -3023,8 +3271,8 @@ function thirdPartyRequest(tokenBytes) {
|
|
|
3023
3271
|
return { previousSignature: (token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority).signature };
|
|
3024
3272
|
}
|
|
3025
3273
|
/** The third party builds and signs a block without holding the token. */
|
|
3026
|
-
function thirdPartyBlock(request, externalSecret, code, algorithm = 0) {
|
|
3027
|
-
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code) : code, [], [], 5));
|
|
3274
|
+
function thirdPartyBlock(request, externalSecret, code, algorithm = 0, params) {
|
|
3275
|
+
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code, params) : code, [], [], 5));
|
|
3028
3276
|
return {
|
|
3029
3277
|
block: blockBytes,
|
|
3030
3278
|
signature: sign(externalPayloadV1(blockBytes, request.previousSignature, SIGNATURE_VERSION), externalSecret, algorithm),
|
|
@@ -3106,6 +3354,13 @@ var Biscuit = class Biscuit {
|
|
|
3106
3354
|
verify(rootPublicKey, rootAlgorithm = 0) {
|
|
3107
3355
|
return new VerifiedBiscuit(loadToken(this.bytes, rootPublicKey, rootAlgorithm), this.bytes);
|
|
3108
3356
|
}
|
|
3357
|
+
/**
|
|
3358
|
+
* `verify`, using WebCrypto Ed25519 where the platform has it — roughly ten
|
|
3359
|
+
* times faster than the pure-JS path. Rejects exactly where `verify` throws.
|
|
3360
|
+
*/
|
|
3361
|
+
async verifyAsync(rootPublicKey, rootAlgorithm = 0) {
|
|
3362
|
+
return new VerifiedBiscuit(await loadTokenAsync(this.bytes, rootPublicKey, rootAlgorithm), this.bytes);
|
|
3363
|
+
}
|
|
3109
3364
|
};
|
|
3110
3365
|
/** A token whose signature chain has been checked against a root key. */
|
|
3111
3366
|
var VerifiedBiscuit = class {
|
|
@@ -3125,6 +3380,10 @@ var VerifiedBiscuit = class {
|
|
|
3125
3380
|
authorize(authorizerCode, options) {
|
|
3126
3381
|
return authorize(this.token, authorizerCode, options);
|
|
3127
3382
|
}
|
|
3383
|
+
/** Authorize, keeping the evaluated world available to `query`. */
|
|
3384
|
+
evaluate(authorizerCode, options) {
|
|
3385
|
+
return evaluate(this.token, authorizerCode, options);
|
|
3386
|
+
}
|
|
3128
3387
|
};
|
|
3129
3388
|
//#endregion
|
|
3130
|
-
export { Biscuit, BuilderError, DATALOG_3_1, DATALOG_3_2, DATALOG_3_3, DEFAULT_SYMBOLS, ExecutionError, MAX_SCHEMA_VERSION, MIN_SCHEMA_VERSION, ParseError, ProtoError, SignatureError, TokenError, VerifiedBiscuit, VersionError, appendThirdParty, attenuate, authorize, authorizeDetailed, blockFeatures, buildBlockMsg, buildToken, fromBase64, generateKeypair, loadToken, parseAuthorizer, peekRootKeyId, requiredVersion, sealToken, thirdPartyBlock, thirdPartyRequest, toBase64, validateBlockVersion };
|
|
3389
|
+
export { Biscuit, BuilderError, DATALOG_3_1, DATALOG_3_2, DATALOG_3_3, DEFAULT_SYMBOLS, ExecutionError, MAX_SCHEMA_VERSION, MIN_SCHEMA_VERSION, ParseError, ProtoError, SignatureError, TokenError, VerifiedBiscuit, VersionError, appendThirdParty, attenuate, authorize, authorizeDetailed, blockFeatures, buildBlockMsg, buildToken, evaluate, fromBase64, generateKeypair, loadToken, loadTokenAsync, parseAuthorizer, peekRootKeyId, requiredVersion, sealToken, thirdPartyBlock, thirdPartyRequest, toBase64, validateBlockVersion };
|