@statewalker/webrun-biscuit 0.1.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/src/builder.ts ADDED
@@ -0,0 +1,360 @@
1
+ /**
2
+ * The write path: minting tokens, attenuating them with extra blocks, and
3
+ * sealing them. New blocks are signed with signature payload version 1, and
4
+ * declare the lowest Datalog version their content legally requires.
5
+ */
6
+
7
+ import { DEFAULT_SYMBOLS, parseAuthorizer } from "./authorizer.js";
8
+ import {
9
+ authorityPayloadV1,
10
+ blockPayloadV1,
11
+ externalPayloadV1,
12
+ generateKeypair,
13
+ publicKeyFromSecret,
14
+ sealPayloadV0,
15
+ sign,
16
+ } from "./crypto.js";
17
+ import type { Check, Op, Predicate, Rule, Scope, Term } from "./datalog.js";
18
+ import {
19
+ type BiscuitMsg,
20
+ type BlockMsg,
21
+ decodeBiscuit,
22
+ decodeBlock,
23
+ encodeBiscuit,
24
+ encodeBlock,
25
+ type MapKeyMsg,
26
+ type OpMsg,
27
+ type PredicateMsg,
28
+ type PublicKeyMsg,
29
+ type RuleMsg,
30
+ type ScopeMsg,
31
+ type TermMsg,
32
+ } from "./proto.js";
33
+ import { DATALOG_3_2, requiredVersion } from "./version.js";
34
+
35
+ const SIGNATURE_VERSION = 1;
36
+ const OFFSET = 1024;
37
+
38
+ export class BuilderError extends Error {}
39
+
40
+ export interface Keypair {
41
+ secretKey: Uint8Array;
42
+ publicKey: Uint8Array;
43
+ }
44
+ export { generateKeypair };
45
+
46
+ /* -------------------------------------------------- symbol table building */
47
+
48
+ class SymbolWriter {
49
+ /** symbols added by this block, in insertion order */
50
+ readonly added: string[] = [];
51
+ readonly addedKeys: string[] = [];
52
+ constructor(
53
+ private readonly known: string[] = [],
54
+ private readonly knownKeys: string[] = [],
55
+ ) {}
56
+
57
+ insert(s: string): number {
58
+ const d = DEFAULT_SYMBOLS.indexOf(s);
59
+ if (d >= 0) return d;
60
+ const k = this.known.indexOf(s);
61
+ if (k >= 0) return OFFSET + k;
62
+ let i = this.added.indexOf(s);
63
+ if (i < 0) i = this.added.push(s) - 1;
64
+ return OFFSET + this.known.length + i;
65
+ }
66
+
67
+ insertKey(key: string): number {
68
+ const k = this.knownKeys.indexOf(key);
69
+ if (k >= 0) return k;
70
+ let i = this.addedKeys.indexOf(key);
71
+ if (i < 0) i = this.addedKeys.push(key) - 1;
72
+ return this.knownKeys.length + i;
73
+ }
74
+ }
75
+
76
+ const parseKeyString = (key: string): PublicKeyMsg => {
77
+ const [alg, h] = key.split("/");
78
+ const bytes = new Uint8Array(h.length / 2);
79
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(h.substr(i * 2, 2), 16);
80
+ return { algorithm: alg === "secp256r1" ? 1 : 0, key: bytes };
81
+ };
82
+
83
+ /* -------------------------------------------------- runtime -> proto model */
84
+
85
+ function toTerm(t: Term, w: SymbolWriter): TermMsg {
86
+ switch (t.t) {
87
+ case "var":
88
+ return { kind: "variable", value: t.v };
89
+ case "int":
90
+ return { kind: "integer", value: t.v };
91
+ case "str":
92
+ return { kind: "string", value: w.insert(t.v) };
93
+ case "date":
94
+ return { kind: "date", value: t.v };
95
+ case "bytes":
96
+ return { kind: "bytes", value: t.v };
97
+ case "bool":
98
+ return { kind: "bool", value: t.v };
99
+ case "null":
100
+ return { kind: "null" };
101
+ case "set":
102
+ return { kind: "set", value: t.v.map((x) => toTerm(x, w)) };
103
+ case "array":
104
+ return { kind: "array", value: t.v.map((x) => toTerm(x, w)) };
105
+ case "map":
106
+ return {
107
+ kind: "map",
108
+ value: t.v.map((e) => ({
109
+ key: (e[0].t === "int"
110
+ ? { kind: "integer", value: e[0].v }
111
+ : { kind: "string", value: w.insert(e[0].v) }) as MapKeyMsg,
112
+ value: toTerm(e[1], w),
113
+ })),
114
+ };
115
+ }
116
+ }
117
+
118
+ const toPredicate = (p: Predicate, w: SymbolWriter): PredicateMsg => ({
119
+ name: w.insert(p.name),
120
+ terms: p.terms.map((t) => toTerm(t, w)),
121
+ });
122
+
123
+ const toOps = (ops: Op[], w: SymbolWriter): OpMsg[] =>
124
+ ops.map((op): OpMsg => {
125
+ switch (op.kind) {
126
+ case "value":
127
+ return { kind: "value", value: toTerm(op.value, w) };
128
+ case "unary":
129
+ return { kind: "unary", op: op.op, ffiName: op.ffi ? w.insert(op.ffi) : undefined };
130
+ case "binary":
131
+ return { kind: "binary", op: op.op, ffiName: op.ffi ? w.insert(op.ffi) : undefined };
132
+ case "closure":
133
+ return { kind: "closure", params: op.params, ops: toOps(op.ops, w) };
134
+ default:
135
+ // unreachable for a well-typed Op; loud rather than `undefined`
136
+ throw new Error(`unknown expression op kind ${(op as { kind: string }).kind}`);
137
+ }
138
+ });
139
+
140
+ const toScope = (s: Scope, w: SymbolWriter): ScopeMsg =>
141
+ s.kind === "authority"
142
+ ? { kind: "type", value: 0 }
143
+ : s.kind === "previous"
144
+ ? { kind: "type", value: 1 }
145
+ : { kind: "publicKey", value: w.insertKey(s.key) };
146
+
147
+ const toRule = (r: Rule, w: SymbolWriter): RuleMsg => ({
148
+ head: toPredicate(r.head, w),
149
+ body: r.body.map((p) => toPredicate(p, w)),
150
+ expressions: r.expressions.map((e) => toOps(e, w)),
151
+ scope: r.scopes.map((s) => toScope(s, w)),
152
+ });
153
+
154
+ /* -------------------------------------------------------------- block build */
155
+
156
+ export interface BlockContent {
157
+ facts: { predicate: Predicate }[];
158
+ rules: Rule[];
159
+ checks: Check[];
160
+ scopes: Scope[];
161
+ }
162
+
163
+ export function buildBlockMsg(
164
+ content: BlockContent,
165
+ knownSymbols: string[] = [],
166
+ knownKeys: string[] = [],
167
+ minVersion = 0,
168
+ ): BlockMsg {
169
+ const w = new SymbolWriter(knownSymbols, knownKeys);
170
+ const facts = content.facts.map((f) => toPredicate(f.predicate, w));
171
+ const rules = content.rules.map((r) => toRule(r, w));
172
+ const checks = content.checks.map((c) => ({
173
+ queries: c.queries.map((q) => toRule(q, w)),
174
+ kind: (c.kind === "all" ? 1 : c.kind === "reject" ? 2 : undefined) as 0 | 1 | 2 | undefined,
175
+ }));
176
+ const scope = content.scopes.map((s) => toScope(s, w));
177
+ return {
178
+ symbols: w.added,
179
+ version: Math.max(requiredVersion(content), minVersion),
180
+ facts,
181
+ rules,
182
+ checks,
183
+ scope,
184
+ publicKeys: w.addedKeys.map(parseKeyString),
185
+ };
186
+ }
187
+
188
+ const contentFromCode = (code: string): BlockContent => {
189
+ const parsed = parseAuthorizer(code);
190
+ if (parsed.policies.length)
191
+ throw new BuilderError("allow/deny policies belong to the authorizer, not to a block");
192
+ return { facts: parsed.facts, rules: parsed.rules, checks: parsed.checks, scopes: parsed.scopes };
193
+ };
194
+
195
+ /* ---------------------------------------------------- accumulated tables */
196
+
197
+ function knownTables(token: BiscuitMsg): { symbols: string[]; keys: string[] } {
198
+ const symbols: string[] = [];
199
+ const keys: string[] = [];
200
+ const signed = [token.authority, ...token.blocks];
201
+ for (const sb of signed) {
202
+ const b = decodeBlock(sb.block);
203
+ if (!sb.externalSignature) symbols.push(...b.symbols);
204
+ for (const k of b.publicKeys) {
205
+ const s = `${k.algorithm === 1 ? "secp256r1" : "ed25519"}/${Array.from(k.key, (x) => x.toString(16).padStart(2, "0")).join("")}`;
206
+ if (!keys.includes(s)) keys.push(s);
207
+ }
208
+ }
209
+ return { symbols, keys };
210
+ }
211
+
212
+ /* ------------------------------------------------------------- public API */
213
+
214
+ export interface BuildOptions {
215
+ rootKeyId?: number;
216
+ /** supply a next keypair instead of generating one (tests, determinism) */
217
+ nextKeypair?: Keypair;
218
+ algorithm?: 0 | 1;
219
+ }
220
+
221
+ /** Mint a new token whose authority block holds `code`. */
222
+ export function buildToken(
223
+ rootSecret: Uint8Array,
224
+ code: string | BlockContent,
225
+ options: BuildOptions = {},
226
+ ): Uint8Array {
227
+ const algorithm = options.algorithm ?? 0;
228
+ const content = typeof code === "string" ? contentFromCode(code) : code;
229
+ const blockBytes = encodeBlock(buildBlockMsg(content));
230
+ const next = options.nextKeypair ?? generateKeypair(algorithm);
231
+ const nextKey: PublicKeyMsg = { algorithm, key: next.publicKey };
232
+
233
+ const payload = authorityPayloadV1(blockBytes, nextKey, SIGNATURE_VERSION);
234
+ const signature = sign(payload, rootSecret, algorithm);
235
+
236
+ return encodeBiscuit({
237
+ rootKeyId: options.rootKeyId,
238
+ authority: { block: blockBytes, nextKey, signature, version: SIGNATURE_VERSION },
239
+ blocks: [],
240
+ proof: { kind: "nextSecret", value: next.secretKey },
241
+ });
242
+ }
243
+
244
+ /** Append an attenuation block. Uses the token's own proof secret to sign. */
245
+ export function attenuate(
246
+ tokenBytes: Uint8Array,
247
+ code: string | BlockContent,
248
+ options: BuildOptions = {},
249
+ ): Uint8Array {
250
+ const token = decodeBiscuit(tokenBytes);
251
+ if (token.proof.kind !== "nextSecret")
252
+ throw new BuilderError("the token is sealed and cannot be attenuated");
253
+
254
+ const algorithm = options.algorithm ?? 0;
255
+ const currentSecret = token.proof.value;
256
+ const previous = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
257
+ const currentAlgorithm = previous.nextKey.algorithm;
258
+
259
+ const tables = knownTables(token);
260
+ const content = typeof code === "string" ? contentFromCode(code) : code;
261
+ const blockBytes = encodeBlock(buildBlockMsg(content, tables.symbols, tables.keys));
262
+
263
+ const next = options.nextKeypair ?? generateKeypair(algorithm);
264
+ const nextKey: PublicKeyMsg = { algorithm, key: next.publicKey };
265
+ const payload = blockPayloadV1(
266
+ blockBytes,
267
+ nextKey,
268
+ undefined,
269
+ previous.signature,
270
+ SIGNATURE_VERSION,
271
+ );
272
+ const signature = sign(payload, currentSecret, currentAlgorithm);
273
+
274
+ token.blocks.push({ block: blockBytes, nextKey, signature, version: SIGNATURE_VERSION });
275
+ token.proof = { kind: "nextSecret", value: next.secretKey };
276
+ return encodeBiscuit(token);
277
+ }
278
+
279
+ /** Seal a token so no further block can be appended. */
280
+ export function sealToken(tokenBytes: Uint8Array): Uint8Array {
281
+ const token = decodeBiscuit(tokenBytes);
282
+ if (token.proof.kind !== "nextSecret") return tokenBytes;
283
+ const last = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
284
+ const signature = sign(sealPayloadV0(last), token.proof.value, last.nextKey.algorithm);
285
+ token.proof = { kind: "finalSignature", value: signature };
286
+ return encodeBiscuit(token);
287
+ }
288
+
289
+ /* -------------------------------------------------- third-party blocks */
290
+
291
+ export interface ThirdPartyRequest {
292
+ /** signature of the block this attenuation will follow */
293
+ previousSignature: Uint8Array;
294
+ }
295
+
296
+ /** What a token holder sends to a third party that will sign a block. */
297
+ export function thirdPartyRequest(tokenBytes: Uint8Array): ThirdPartyRequest {
298
+ const token = decodeBiscuit(tokenBytes);
299
+ const last = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
300
+ return { previousSignature: last.signature };
301
+ }
302
+
303
+ export interface ThirdPartyResponse {
304
+ block: Uint8Array;
305
+ signature: Uint8Array;
306
+ publicKey: PublicKeyMsg;
307
+ }
308
+
309
+ /** The third party builds and signs a block without holding the token. */
310
+ export function thirdPartyBlock(
311
+ request: ThirdPartyRequest,
312
+ externalSecret: Uint8Array,
313
+ code: string | BlockContent,
314
+ algorithm: 0 | 1 = 0,
315
+ ): ThirdPartyResponse {
316
+ const content = typeof code === "string" ? contentFromCode(code) : code;
317
+ // a third-party block carries its own symbol table, so it starts from empty,
318
+ // and third-party blocks themselves require datalog v3.2+
319
+ const blockBytes = encodeBlock(buildBlockMsg(content, [], [], DATALOG_3_2));
320
+ const payload = externalPayloadV1(blockBytes, request.previousSignature, SIGNATURE_VERSION);
321
+ return {
322
+ block: blockBytes,
323
+ signature: sign(payload, externalSecret, algorithm),
324
+ publicKey: { algorithm, key: publicKeyFromSecret(externalSecret, algorithm) },
325
+ };
326
+ }
327
+
328
+ /** The token holder appends a block signed by a third party. */
329
+ export function appendThirdParty(
330
+ tokenBytes: Uint8Array,
331
+ response: ThirdPartyResponse,
332
+ options: BuildOptions = {},
333
+ ): Uint8Array {
334
+ const token = decodeBiscuit(tokenBytes);
335
+ if (token.proof.kind !== "nextSecret")
336
+ throw new BuilderError("the token is sealed and cannot be attenuated");
337
+ const previous = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
338
+
339
+ const algorithm = options.algorithm ?? 0;
340
+ const next = options.nextKeypair ?? generateKeypair(algorithm);
341
+ const nextKey: PublicKeyMsg = { algorithm, key: next.publicKey };
342
+ const payload = blockPayloadV1(
343
+ response.block,
344
+ nextKey,
345
+ response.signature,
346
+ previous.signature,
347
+ SIGNATURE_VERSION,
348
+ );
349
+ const signature = sign(payload, token.proof.value, previous.nextKey.algorithm);
350
+
351
+ token.blocks.push({
352
+ block: response.block,
353
+ nextKey,
354
+ signature,
355
+ externalSignature: { signature: response.signature, publicKey: response.publicKey },
356
+ version: SIGNATURE_VERSION,
357
+ });
358
+ token.proof = { kind: "nextSecret", value: next.secretKey };
359
+ return encodeBiscuit(token);
360
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Biscuit cryptography: chained block signatures, sealing, third-party
3
+ * (external) signatures. Ed25519 and ECDSA/secp256r1, per SPECIFICATIONS.md.
4
+ */
5
+ import { ed25519 } from "@noble/curves/ed25519.js";
6
+ import { p256 } from "@noble/curves/nist.js";
7
+ import type { BiscuitMsg, PublicKeyMsg, SignedBlock } from "./proto.js";
8
+
9
+ export class SignatureError extends Error {}
10
+
11
+ const ALG_ED25519 = 0;
12
+ const ALG_SECP256R1 = 1;
13
+
14
+ function concat(...parts: Uint8Array[]): Uint8Array {
15
+ const len = parts.reduce((a, p) => a + p.length, 0);
16
+ const out = new Uint8Array(len);
17
+ let o = 0;
18
+ for (const p of parts) {
19
+ out.set(p, o);
20
+ o += p.length;
21
+ }
22
+ return out;
23
+ }
24
+
25
+ const ascii = (s: string): Uint8Array => Uint8Array.from(s, (c) => c.charCodeAt(0));
26
+
27
+ const bytesEqual = (a: Uint8Array, b: Uint8Array): boolean =>
28
+ a.length === b.length && a.every((x, i) => x === b[i]);
29
+
30
+ /** little-endian i32, as used for algorithm ids and payload versions */
31
+ function le32(n: number): Uint8Array {
32
+ const b = new Uint8Array(4);
33
+ new DataView(b.buffer).setInt32(0, n, true);
34
+ return b;
35
+ }
36
+
37
+ function verifySignature(key: PublicKeyMsg, payload: Uint8Array, sig: Uint8Array): boolean {
38
+ try {
39
+ if (key.algorithm === ALG_ED25519) return ed25519.verify(sig, payload, key.key);
40
+ if (key.algorithm === ALG_SECP256R1) {
41
+ // SEC1 DER (r, s) over SHA-256
42
+ // noble hashes the payload itself with `prehash`; biscuit/RustCrypto do
43
+ // not enforce low-S on verification, so malleable signatures are accepted
44
+ return p256.verify(sig, payload, key.key, { format: "der", prehash: true, lowS: false });
45
+ }
46
+ return false;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ export function publicKeyFromSecret(secret: Uint8Array, algorithm: number): Uint8Array {
53
+ if (algorithm === ALG_ED25519) return ed25519.getPublicKey(secret);
54
+ if (algorithm === ALG_SECP256R1) return p256.getPublicKey(secret, true);
55
+ throw new SignatureError(`unknown algorithm ${algorithm}`);
56
+ }
57
+
58
+ /* ------------------------------------------------------- signature payloads */
59
+
60
+ export function blockPayloadV0(
61
+ data: Uint8Array,
62
+ nextKey: PublicKeyMsg,
63
+ externalSig?: Uint8Array,
64
+ ): Uint8Array {
65
+ return concat(data, externalSig ?? new Uint8Array(0), le32(nextKey.algorithm), nextKey.key);
66
+ }
67
+
68
+ export function authorityPayloadV1(
69
+ data: Uint8Array,
70
+ nextKey: PublicKeyMsg,
71
+ version: number,
72
+ ): Uint8Array {
73
+ return concat(
74
+ ascii("\0BLOCK\0\0VERSION\0"),
75
+ le32(version),
76
+ ascii("\0PAYLOAD\0"),
77
+ data,
78
+ ascii("\0ALGORITHM\0"),
79
+ le32(nextKey.algorithm),
80
+ ascii("\0NEXTKEY\0"),
81
+ nextKey.key,
82
+ );
83
+ }
84
+
85
+ export function blockPayloadV1(
86
+ data: Uint8Array,
87
+ nextKey: PublicKeyMsg,
88
+ externalSig: Uint8Array | undefined,
89
+ previousSignature: Uint8Array,
90
+ version: number,
91
+ ): Uint8Array {
92
+ return concat(
93
+ ascii("\0BLOCK\0\0VERSION\0"),
94
+ le32(version),
95
+ ascii("\0PAYLOAD\0"),
96
+ data,
97
+ ascii("\0ALGORITHM\0"),
98
+ le32(nextKey.algorithm),
99
+ ascii("\0NEXTKEY\0"),
100
+ nextKey.key,
101
+ ascii("\0PREVSIG\0"),
102
+ previousSignature,
103
+ ...(externalSig ? [ascii("\0EXTERNALSIG\0"), externalSig] : []),
104
+ );
105
+ }
106
+
107
+ export function externalPayloadV1(
108
+ data: Uint8Array,
109
+ previousSignature: Uint8Array,
110
+ version: number,
111
+ ): Uint8Array {
112
+ return concat(
113
+ ascii("\0EXTERNAL\0\0VERSION\0"),
114
+ le32(version),
115
+ ascii("\0PAYLOAD\0"),
116
+ data,
117
+ ascii("\0PREVSIG\0"),
118
+ previousSignature,
119
+ );
120
+ }
121
+
122
+ export function sealPayloadV0(block: SignedBlock): Uint8Array {
123
+ return concat(block.block, le32(block.nextKey.algorithm), block.nextKey.key, block.signature);
124
+ }
125
+
126
+ export function sign(payload: Uint8Array, secret: Uint8Array, algorithm: number): Uint8Array {
127
+ if (algorithm === ALG_ED25519) return ed25519.sign(payload, secret);
128
+ if (algorithm === ALG_SECP256R1)
129
+ return p256.sign(payload, secret, { format: "der", prehash: true });
130
+ throw new SignatureError(`unknown algorithm ${algorithm}`);
131
+ }
132
+
133
+ /** a fresh keypair for the given algorithm */
134
+ export function generateKeypair(algorithm: 0 | 1 = 0): {
135
+ secretKey: Uint8Array;
136
+ publicKey: Uint8Array;
137
+ } {
138
+ const secretKey =
139
+ algorithm === ALG_ED25519 ? ed25519.utils.randomSecretKey() : p256.utils.randomSecretKey();
140
+ return { secretKey, publicKey: publicKeyFromSecret(secretKey, algorithm) };
141
+ }
142
+
143
+ export { bytesEqual };
144
+
145
+ /* --------------------------------------------------------------- verifying */
146
+
147
+ /** Verifies the full signature chain and the proof. Throws on failure. */
148
+ export function verifyToken(token: BiscuitMsg, rootPublicKey: Uint8Array, rootAlgorithm = 0): void {
149
+ const root: PublicKeyMsg = { algorithm: rootAlgorithm as 0 | 1, key: rootPublicKey };
150
+
151
+ if (token.authority.externalSignature)
152
+ throw new SignatureError("the authority block must not carry an external signature");
153
+
154
+ const authVersion = token.authority.version ?? 0;
155
+ const authPayload =
156
+ authVersion === 0
157
+ ? blockPayloadV0(token.authority.block, token.authority.nextKey)
158
+ : authVersion === 1
159
+ ? authorityPayloadV1(token.authority.block, token.authority.nextKey, authVersion)
160
+ : (() => {
161
+ throw new SignatureError(`unsupported block version ${authVersion}`);
162
+ })();
163
+ if (!verifySignature(root, authPayload, token.authority.signature))
164
+ throw new SignatureError("invalid authority block signature");
165
+
166
+ let currentKey = token.authority.nextKey;
167
+ let previousSignature = token.authority.signature;
168
+
169
+ for (const block of token.blocks) {
170
+ const version = block.version ?? 0;
171
+ const externalSig = block.externalSignature?.signature;
172
+ let payload: Uint8Array;
173
+ if (version === 0) payload = blockPayloadV0(block.block, block.nextKey, externalSig);
174
+ else if (version === 1)
175
+ payload = blockPayloadV1(block.block, block.nextKey, externalSig, previousSignature, version);
176
+ else throw new SignatureError(`unsupported block version ${version}`);
177
+
178
+ if (!verifySignature(currentKey, payload, block.signature))
179
+ throw new SignatureError("invalid block signature");
180
+
181
+ if (block.externalSignature) {
182
+ if (version !== 1) throw new SignatureError("unsupported third party block version");
183
+ const ext = externalPayloadV1(block.block, previousSignature, version);
184
+ if (
185
+ !verifySignature(block.externalSignature.publicKey, ext, block.externalSignature.signature)
186
+ )
187
+ throw new SignatureError("invalid external signature");
188
+ }
189
+
190
+ currentKey = block.nextKey;
191
+ previousSignature = block.signature;
192
+ }
193
+
194
+ if (token.proof.kind === "nextSecret") {
195
+ const derived = publicKeyFromSecret(token.proof.value, currentKey.algorithm);
196
+ if (!bytesEqual(derived, currentKey.key))
197
+ throw new SignatureError("the last public key does not match the private key");
198
+ } else {
199
+ const last = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
200
+ if (!verifySignature(currentKey, sealPayloadV0(last), token.proof.value))
201
+ throw new SignatureError("invalid seal signature");
202
+ }
203
+ }
204
+
205
+ /** Revocation identifier of each block: its signature bytes. */
206
+ export function revocationIds(token: BiscuitMsg): Uint8Array[] {
207
+ return [token.authority, ...token.blocks].map((b) => b.signature);
208
+ }