@kokuin/controller 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/lib/epoch-events.d.ts +139 -0
- package/lib/epoch-events.js +177 -0
- package/lib/epoch.d.ts +90 -0
- package/lib/epoch.js +505 -0
- package/lib/events.d.ts +97 -143
- package/lib/events.js +94 -143
- package/lib/fold.d.ts +78 -100
- package/lib/fold.js +200 -225
- package/lib/history.d.ts +102 -51
- package/lib/history.js +160 -71
- package/lib/identity.d.ts +13 -36
- package/lib/identity.js +27 -63
- package/lib/index.d.ts +11 -6
- package/lib/index.js +10 -5
- package/lib/keystore-identity.d.ts +1 -4
- package/lib/keystore-identity.js +11 -8
- package/lib/limits.d.ts +29 -0
- package/lib/limits.js +27 -0
- package/lib/log.d.ts +124 -0
- package/lib/log.js +268 -0
- package/lib/members.d.ts +103 -0
- package/lib/members.js +152 -0
- package/lib/profiles.js +7 -1
- package/lib/resolver.d.ts +34 -29
- package/lib/resolver.js +64 -68
- package/lib/state-resolver.d.ts +63 -27
- package/lib/state-resolver.js +130 -53
- package/lib/state.d.ts +10 -26
- package/lib/state.js +23 -28
- package/lib/supersede.d.ts +22 -63
- package/lib/supersede.js +100 -186
- package/package.json +12 -12
package/lib/events.js
CHANGED
|
@@ -25,7 +25,7 @@ export function signEvent(event, privateKeys) {
|
|
|
25
25
|
* the folded state, so a bad shape must be rejected where it is published, not one event away where
|
|
26
26
|
* it is next read (a non-array `n` would throw in the *next* rotate; a non-array `k` is a `KeyState`
|
|
27
27
|
* whose type lies).
|
|
28
|
-
*/ function isKeyList(value) {
|
|
28
|
+
*/ export function isKeyList(value) {
|
|
29
29
|
return Array.isArray(value) && value.every((entry)=>typeof entry === 'string');
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
@@ -62,13 +62,17 @@ export function signEvent(event, privateKeys) {
|
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
const bytes = canonicalBytes(event);
|
|
65
|
-
for(
|
|
65
|
+
for (const [i, sig] of sigs.entries()){
|
|
66
66
|
try {
|
|
67
|
-
const
|
|
67
|
+
const encodedKey = keys[i];
|
|
68
|
+
if (encodedKey == null) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const key = tryDecodeKey(encodedKey);
|
|
68
72
|
if (key == null || key.alg !== 'EdDSA') {
|
|
69
73
|
return false;
|
|
70
74
|
}
|
|
71
|
-
if (!ed25519.verify(base64urlnopad.decode(
|
|
75
|
+
if (!ed25519.verify(base64urlnopad.decode(sig), bytes, key.publicKey)) {
|
|
72
76
|
return false;
|
|
73
77
|
}
|
|
74
78
|
} catch {
|
|
@@ -77,33 +81,6 @@ export function signEvent(event, privateKeys) {
|
|
|
77
81
|
}
|
|
78
82
|
return true;
|
|
79
83
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Whether one of an event's signatures was made by `key` — the counterpart of
|
|
82
|
-
* {@link verifySignatures} for a signer the *event* does not name (resolved from a DID elsewhere). A
|
|
83
|
-
* capability-authorised revoke needs exactly this: its author is the capability's `aud`, which the
|
|
84
|
-
* fold cannot resolve, so it is handed the key and checks the signature itself.
|
|
85
|
-
*
|
|
86
|
-
* Total, EdDSA only — all {@link signEvent} produces. An ES256 audience therefore cannot author a
|
|
87
|
-
* revoke, which fails closed until events grow a second signature algorithm.
|
|
88
|
-
*/ export function verifyEventSignedBy(signed, key) {
|
|
89
|
-
// Both arms are unkillable through the fold (`isSignedEventShape` already established `sigs` is a
|
|
90
|
-
// string array; a non-EdDSA key would fail `ed25519.verify`; `[].some()` is `false`) and both stay
|
|
91
|
-
// by the file's guard rule: a direct caller reaches this with a parsed body.
|
|
92
|
-
if (key.alg !== 'EdDSA' || signed.sigs.length === 0) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
if (!isCanonicalizable(signed.event)) {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
const bytes = canonicalBytes(signed.event);
|
|
99
|
-
return signed.sigs.some((sig)=>{
|
|
100
|
-
try {
|
|
101
|
-
return ed25519.verify(base64urlnopad.decode(sig), bytes, key.publicKey);
|
|
102
|
-
} catch {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
84
|
/**
|
|
108
85
|
* Deterministic inception. Only seed-derived, canonical material — no timestamp, nonce, or label —
|
|
109
86
|
* so the DID is a pure function of the seed and profile index. Never add a label: a mistyped one on
|
|
@@ -175,22 +152,33 @@ export function verifyInception(signed, did) {
|
|
|
175
152
|
}
|
|
176
153
|
return verifySignatures(signed.event, signed.sigs, signed.event.k);
|
|
177
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* The shape of one snapshot member, nothing more: whether the DID carries the key, whether the key
|
|
157
|
+
* is Ed25519, and whether it collides with a root key are rules over the whole snapshot and its chain,
|
|
158
|
+
* checked by the fold. `lease` must be a non-negative safe integer — unix seconds — because grants
|
|
159
|
+
* compare `exp` against it, and a fraction, a negative or an unsafe integer has no single reading.
|
|
160
|
+
*/ export function isMemberEntry(value) {
|
|
161
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const entry = value;
|
|
165
|
+
return typeof entry.did === 'string' && typeof entry.key === 'string' && typeof entry.add === 'string' && typeof entry.lease === 'number' && Number.isSafeInteger(entry.lease) && entry.lease >= 0;
|
|
166
|
+
}
|
|
178
167
|
/**
|
|
179
168
|
* A rotate reveals the keys the prior event pre-committed and commits the next set, signed by the
|
|
180
169
|
* newly revealed keys (per KERI — what makes a stolen current key unable to rotate). Reproducible
|
|
181
|
-
* from the seed
|
|
170
|
+
* from the seed and the snapshot unless it carries a seal.
|
|
171
|
+
*
|
|
172
|
+
* The root chain holds only key-establishing events, so a rotate's sequence and its derivation index
|
|
173
|
+
* are the same number: the key it reveals sits one past `prior`'s position.
|
|
182
174
|
*
|
|
183
175
|
* **Throws rather than emitting an event the fold will reject.** The revealed key must be the one
|
|
184
|
-
* `prior` committed in `n`,
|
|
185
|
-
|
|
186
|
-
* against each other here. When `prior` carries no commitment (a `rev`), `keyPosition` is required:
|
|
187
|
-
* the default would derive a key one past the *revoke*, which nothing committed, and this is the one
|
|
188
|
-
* case unverifiable from `prior` alone.
|
|
189
|
-
*/ export function createRotate({ seed, profile, did, prior, options = {} }) {
|
|
176
|
+
* `prior` committed in `n`, which a prior not built from this chain's own events need not be.
|
|
177
|
+
*/ export function createRotate({ seed, profile, did, prior, snapshot, seal }) {
|
|
190
178
|
const gen = prior.g;
|
|
191
179
|
const seq = prior.s + 1;
|
|
192
|
-
// The commitment the revealed key must match
|
|
193
|
-
//
|
|
180
|
+
// The commitment the revealed key must match. Every `icp`/`rot` carries one; a hand-built prior of
|
|
181
|
+
// another shape does not.
|
|
194
182
|
const commitment = prior.n;
|
|
195
183
|
const committed = isKeyList(commitment) && commitment.length > 0 ? commitment : undefined;
|
|
196
184
|
// The checks below only have an answer when this seed *is* the log's root — otherwise the
|
|
@@ -198,23 +186,20 @@ export function verifyInception(signed, did) {
|
|
|
198
186
|
// there would stop this generator building a foreign rotate at all (which the conformance suite
|
|
199
187
|
// legitimately does); the fold is the layer that rejects it.
|
|
200
188
|
const root = didFromInception(inceptionEvent(seed, profile)) === did;
|
|
201
|
-
if (root && committed == null
|
|
202
|
-
throw new Error('createRotate: prior event pre-commits no key, so
|
|
189
|
+
if (root && committed == null) {
|
|
190
|
+
throw new Error('createRotate: prior event pre-commits no key, so there is nothing to reveal');
|
|
203
191
|
}
|
|
204
|
-
// The key
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const current = deriveKeyPair(seed, authorityPath(profile, keyGen, keySeq), 'EdDSA');
|
|
210
|
-
const next = deriveKeyPair(seed, authorityPath(profile, keyGen, keySeq + 1), 'EdDSA');
|
|
211
|
-
const agreement = deriveKeyPair(seed, agreementPath(profile, keyGen, keySeq), 'X25519');
|
|
192
|
+
// The agreement key lands at the same index as the authority key, so `KeyState.keyGen`/`keySeq`
|
|
193
|
+
// names both for a recipient re-deriving from the seed.
|
|
194
|
+
const current = deriveKeyPair(seed, authorityPath(profile, gen, seq), 'EdDSA');
|
|
195
|
+
const next = deriveKeyPair(seed, authorityPath(profile, gen, seq + 1), 'EdDSA');
|
|
196
|
+
const agreement = deriveKeyPair(seed, agreementPath(profile, gen, seq), 'X25519');
|
|
212
197
|
if (root && committed != null) {
|
|
213
198
|
// Exactly what `verifyRotate` checks, checked here where the caller can still act. Arity first:
|
|
214
199
|
// this generator emits a single key, so a prior committing any other number has no rotate here.
|
|
215
200
|
const revealed = encodeKey(current.publicKey, 'EdDSA');
|
|
216
201
|
if (committed.length !== 1 || digestOf(revealed) !== committed[0]) {
|
|
217
|
-
throw new Error(`createRotate: the key at (gen ${
|
|
202
|
+
throw new Error(`createRotate: the key at (gen ${gen}, seq ${seq}) is not the one the prior event ` + 'pre-committed — the prior is not an event of this log');
|
|
218
203
|
}
|
|
219
204
|
}
|
|
220
205
|
const event = {
|
|
@@ -236,8 +221,9 @@ export function verifyInception(signed, did) {
|
|
|
236
221
|
],
|
|
237
222
|
kt: 1,
|
|
238
223
|
nt: 1,
|
|
239
|
-
a:
|
|
240
|
-
|
|
224
|
+
a: seal,
|
|
225
|
+
m: snapshot.members,
|
|
226
|
+
d: snapshot.deny
|
|
241
227
|
};
|
|
242
228
|
return {
|
|
243
229
|
event,
|
|
@@ -248,9 +234,14 @@ export function verifyInception(signed, did) {
|
|
|
248
234
|
}
|
|
249
235
|
/**
|
|
250
236
|
* Whether the members a rotate publishes into the folded state have the shape the fold will carry.
|
|
251
|
-
* Shared by {@link verifyRotate} and {@link verifyReset}: both hand `k`/`n
|
|
252
|
-
*
|
|
253
|
-
*
|
|
237
|
+
* Shared by {@link verifyRotate} and {@link verifyReset}: both hand `k`/`n` to the next `KeyState`,
|
|
238
|
+
* and a reset verifies against the recovery key not `k`, so without this it could publish a key set
|
|
239
|
+
* of any shape.
|
|
240
|
+
*
|
|
241
|
+
* The snapshot (`m`/`d`) is not judged here: whether it is well formed, disjoint from the root keys
|
|
242
|
+
* and keeps the generation's retired keys depends on the chain it lands on, so the fold judges it
|
|
243
|
+
* with a reason of its own (see `validateSnapshot`). These verifiers answer only whether the event
|
|
244
|
+
* is signed by, and chains to, what it claims.
|
|
254
245
|
*
|
|
255
246
|
* `r` is refused outright — the runtime half of removing it from {@link RotateEvent}. A member the
|
|
256
247
|
* fold silently ignored would be the field-that-lies this removal is about: a reader seeing `r` on a
|
|
@@ -258,9 +249,7 @@ export function verifyInception(signed, did) {
|
|
|
258
249
|
*/ function isPublishedRotate(event) {
|
|
259
250
|
return isKeyList(event.k) && isKeyList(event.n) && isEnforcedThreshold(event.kt, event.k) && isEnforcedThreshold(event.nt, event.n) && // A seal is opaque but still a digest string on the wire (see `RotateEvent.a`); a non-string one
|
|
260
251
|
// is a member a reader would have to guess at.
|
|
261
|
-
(event.a == null || typeof event.a === 'string') && event.r === undefined
|
|
262
|
-
// throws, a string `d` would silently become a set of characters.
|
|
263
|
-
(event.d == null || isKeyList(event.d));
|
|
252
|
+
(event.a == null || typeof event.a === 'string') && event.r === undefined;
|
|
264
253
|
}
|
|
265
254
|
/**
|
|
266
255
|
* A rotate is valid when it chains to the prior digest, its revealed keys match the prior event's
|
|
@@ -292,14 +281,24 @@ export function verifyInception(signed, did) {
|
|
|
292
281
|
*
|
|
293
282
|
* Anchored to the inception, not the log head: the recovery key lives on the root-retained
|
|
294
283
|
* derivation branch and its digest is committed in the deterministic inception, so a restored
|
|
295
|
-
* mnemonic can author one with no log knowledge or availability.
|
|
296
|
-
*
|
|
297
|
-
*
|
|
284
|
+
* mnemonic can author one with no log knowledge or availability.
|
|
285
|
+
*
|
|
286
|
+
* **Blind** (no `snapshot`): no seal, `d: []` and no `m`, so a blind reset is a pure function of
|
|
287
|
+
* `(seed, profile, gen)` and two blind resets at one generation produce identical bytes that resolve
|
|
288
|
+
* as idempotent re-derivation, not duplicity. The new epoch starts empty and root re-adds members.
|
|
289
|
+
* This is the path for a root that does not know the log's state.
|
|
290
|
+
*
|
|
291
|
+
* **With a snapshot**: the same reset carries the member and deny sets, so one recovery-key
|
|
292
|
+
* signature is the whole recovery. Two resets at one generation with different bytes are then
|
|
293
|
+
* duplicity — which only the recovery-key holder can cause, and a reset at a higher generation
|
|
294
|
+
* resolves. Members keep their `add` references, but every grant names its generation, so every
|
|
295
|
+
* grant minted under the prior one still dies. The snapshot is checked against the **new**
|
|
296
|
+
* generation's keys, and no retired-key denial of the old one need carry over.
|
|
298
297
|
*
|
|
299
298
|
* The root need not know the current generation, only eventually exceed it (no attacker can author a
|
|
300
299
|
* competing reset at any generation): a blind root starts at `gen = 1` and retries higher if it
|
|
301
300
|
* learns of one — the cost is a round trip, never loss of control.
|
|
302
|
-
*/ export function createReset(seed, profile, gen) {
|
|
301
|
+
*/ export function createReset(seed, profile, gen, snapshot) {
|
|
303
302
|
if (gen < 1) {
|
|
304
303
|
throw new Error(`Reset: gen must be >= 1, got ${gen}`);
|
|
305
304
|
}
|
|
@@ -327,10 +326,18 @@ export function verifyInception(signed, did) {
|
|
|
327
326
|
digestOf(encodeKey(next.publicKey, 'EdDSA'))
|
|
328
327
|
],
|
|
329
328
|
kt: 1,
|
|
330
|
-
nt: 1
|
|
331
|
-
// A reset clears the deny set: every capability under the prior generation is gone anyway.
|
|
332
|
-
d: []
|
|
329
|
+
nt: 1
|
|
333
330
|
};
|
|
331
|
+
if (snapshot == null || snapshot.members.length === 0 && snapshot.deny.length === 0) {
|
|
332
|
+
// A blind reset clears the deny set: every capability under the prior generation is gone anyway.
|
|
333
|
+
// Written as `d: []` rather than omitted so a blind reset keeps the bytes it always had. An empty
|
|
334
|
+
// snapshot is the same state and is written the same way, so a retry with one is byte-identical
|
|
335
|
+
// to a blind reset rather than a second encoding of it.
|
|
336
|
+
event.d = [];
|
|
337
|
+
} else {
|
|
338
|
+
event.m = snapshot.members;
|
|
339
|
+
event.d = snapshot.deny;
|
|
340
|
+
}
|
|
334
341
|
return {
|
|
335
342
|
event,
|
|
336
343
|
sigs: signEvent(event, [
|
|
@@ -355,11 +362,23 @@ export function verifyInception(signed, did) {
|
|
|
355
362
|
if (!isPublishedRotate(event)) {
|
|
356
363
|
return false;
|
|
357
364
|
}
|
|
358
|
-
// A reset carries
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
|
|
365
|
+
// A reset carries no seal, and either a whole snapshot (`m` and `d` together) or none. Without `m`
|
|
366
|
+
// it is blind, and a blind reset's `d` is exactly the `[]` {@link createReset} writes: one encoding,
|
|
367
|
+
// so "blind reset" means the same bytes for every holder of the seed — the idempotency the branch
|
|
368
|
+
// selector relies on. For the same reason an empty snapshot (`m: []` with `d: []`) is refused: it is
|
|
369
|
+
// the blind state spelled a second way. Only the recovery-key holder can sign one, so this is not an
|
|
370
|
+
// attacker's event; it is the rule that keeps one state from having two resets at a generation,
|
|
371
|
+
// and half a snapshot (`d` with no `m`, or `m` with no `d`) from folding as a reset that silently
|
|
372
|
+
// dismissed every member or cleared every denial. The snapshot's contents are the fold's to judge,
|
|
373
|
+
// against the new generation's keys.
|
|
374
|
+
if (event.a !== undefined) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const emptyDeny = isKeyList(event.d) && event.d.length === 0;
|
|
378
|
+
if (event.m == null ? !emptyDeny : event.d == null) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
if (Array.isArray(event.m) && event.m.length === 0 && emptyDeny) {
|
|
363
382
|
return false;
|
|
364
383
|
}
|
|
365
384
|
if (!Array.isArray(sigs) || sigs.length !== 1) {
|
|
@@ -379,87 +398,19 @@ export function verifyInception(signed, did) {
|
|
|
379
398
|
]);
|
|
380
399
|
}
|
|
381
400
|
/**
|
|
382
|
-
* How a
|
|
401
|
+
* How a deny entry names a **key** rather than a DID: `#<the multibase key exactly as it appears in
|
|
383
402
|
* `k`>`, the same spelling a token's `kid` uses. One spelling for one thing — unambiguous against
|
|
384
403
|
* `did:…` on sight, so a bare multibase string is not a second accepted encoding, any more than for
|
|
385
404
|
* `kid`.
|
|
386
405
|
*/ export const KEY_TARGET_PREFIX = '#';
|
|
387
|
-
/** The deny-set spelling of `key` — what
|
|
406
|
+
/** The deny-set spelling of `key` — what a snapshot's `d` carries to deny it. */ export function keyTarget(key) {
|
|
388
407
|
return `${KEY_TARGET_PREFIX}${key}`;
|
|
389
408
|
}
|
|
390
409
|
/**
|
|
391
|
-
* The key a
|
|
392
|
-
*
|
|
410
|
+
* The key a deny entry names, or `null` when it names a DID instead. Total on any string: an entry of
|
|
411
|
+
* neither form lands in the deny set as the opaque identifier it is. The body is not validated
|
|
393
412
|
* against multibase — a `#` naming nothing denies nothing, exactly as a `did:` naming nobody does,
|
|
394
413
|
* and rejecting it would only add a way for a well-formed log to stop folding.
|
|
395
414
|
*/ export function keyFromTarget(target) {
|
|
396
415
|
return target.startsWith(KEY_TARGET_PREFIX) ? target.slice(KEY_TARGET_PREFIX.length) : null;
|
|
397
416
|
}
|
|
398
|
-
/**
|
|
399
|
-
* Revoke a DID or a key — see {@link RevokeEvent.x} for the two spellings.
|
|
400
|
-
*
|
|
401
|
-
* Naming the device DID rather than a `jti` makes this one entry per device for its life — covering
|
|
402
|
-
* capabilities the verifier has never seen and future re-mints, where per-`jti` revocation would grow
|
|
403
|
-
* with every renewal. A key target retires a leaked key for material it *already* signed: `rotate`
|
|
404
|
-
* only retires it for new issuance (`resolve` is head-only), but already-issued material verifies
|
|
405
|
-
* through `resolveHistoric`, which survives a rotate by design, so a thief holding a rotated-away key
|
|
406
|
-
* could still mint one that verified. Retirement is therefore explicit, keeping the promise that a
|
|
407
|
-
* routine rotate does not invalidate already-issued material intact.
|
|
408
|
-
*
|
|
409
|
-
* **A key the profile currently publishes cannot be denied** — the fold rejects it (see the `rev`
|
|
410
|
-
* branch of `stepEvent`). Rotate first, then deny the key the rotate retired.
|
|
411
|
-
*
|
|
412
|
-
* `prior` answers "next sequence, what to chain to"; `keyPosition` separately answers "where the
|
|
413
|
-
* active authority key lives". They coincide only for an `icp`/`rot` prior — a `rev` establishes no
|
|
414
|
-
* key, so a revoke on a revoke still points at the last `icp`/`rot` position.
|
|
415
|
-
*/ export function createRevoke({ seed, profile, did, prior, target, keyPosition, cap }) {
|
|
416
|
-
const current = deriveKeyPair(seed, authorityPath(profile, keyPosition.gen, keyPosition.seq), 'EdDSA');
|
|
417
|
-
return createRevokeWithKey({
|
|
418
|
-
privateKey: current.privateKey,
|
|
419
|
-
did,
|
|
420
|
-
prior,
|
|
421
|
-
target,
|
|
422
|
-
cap
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Revoke a DID, signing with an Ed25519 key the caller already holds rather than deriving one from
|
|
427
|
-
* the profile seed.
|
|
428
|
-
*
|
|
429
|
-
* The builder for the actor a capability-authorised revoke exists for: a device holding a management
|
|
430
|
-
* capability, which by design never receives the profile sub-seed. `createRevoke` can only sign as
|
|
431
|
-
* the profile itself, so without this the feature had no API but re-implementing the signing
|
|
432
|
-
* convention per consumer — the duplication `createControllerCapabilityVerifier` prevents on the
|
|
433
|
-
* verifying side.
|
|
434
|
-
*
|
|
435
|
-
* The key is the audience key the capability pins in `cnf`, and the fold checks the event's signature
|
|
436
|
-
* against exactly that, so the two must match. Ed25519 only. Takes the private key rather than an
|
|
437
|
-
* identity because no identity type here signs raw bytes — a `KeyStore` entry hands back exactly this
|
|
438
|
-
* shape. Byte-identical to {@link createRevoke} given the same key; no `keyPosition`, which exists
|
|
439
|
-
* only to derive a key from a seed.
|
|
440
|
-
*/ export function createRevokeWithKey({ privateKey, did, prior, target, cap }) {
|
|
441
|
-
const event = {
|
|
442
|
-
v: 1,
|
|
443
|
-
t: 'rev',
|
|
444
|
-
i: did,
|
|
445
|
-
g: prior.g,
|
|
446
|
-
s: prior.s + 1,
|
|
447
|
-
p: digestOf(prior),
|
|
448
|
-
crit: true,
|
|
449
|
-
x: target,
|
|
450
|
-
cap
|
|
451
|
-
};
|
|
452
|
-
return {
|
|
453
|
-
event,
|
|
454
|
-
sigs: signEvent(event, [
|
|
455
|
-
privateKey
|
|
456
|
-
])
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
/** Authority-signed revoke. Capability-authorised revokes are checked in the fold. */ export function verifyRevoke(signed, prior) {
|
|
460
|
-
const { event, sigs } = signed;
|
|
461
|
-
if (event.t !== 'rev' || event.p !== prior.digest) {
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
return verifySignatures(event, sigs, prior.keys);
|
|
465
|
-
}
|
package/lib/fold.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type
|
|
1
|
+
import { type MemberEntry, type StateSnapshot } from './events.js';
|
|
2
|
+
import { type ControllerLog } from './log.js';
|
|
3
|
+
export { ROTATE_NEEDS_SNAPSHOT, SNAPSHOT_DROPS_RETIRED_KEY, SNAPSHOT_MALFORMED, SNAPSHOT_MEMBER_DID_MISMATCH, SNAPSHOT_MEMBER_KEY_IS_ROOT, } from './members.js';
|
|
4
|
+
/**
|
|
5
|
+
* One management member as a state holds it: the snapshot entry, plus whether a removal has ended
|
|
6
|
+
* it (`removed`) and whether it may act at all (`effective`: present, not removed, and not targeted
|
|
7
|
+
* by any entry of the same state's deny set — see `denialTargetsMember`). A chain state's members are
|
|
8
|
+
* its snapshot's, so none is removed; removal is an epoch effect.
|
|
9
|
+
*/
|
|
10
|
+
export type MemberState = MemberEntry & {
|
|
11
|
+
removed: boolean;
|
|
12
|
+
effective: boolean;
|
|
13
|
+
};
|
|
3
14
|
export type KeyState = {
|
|
4
15
|
did: string;
|
|
5
16
|
gen: number;
|
|
@@ -8,14 +19,13 @@ export type KeyState = {
|
|
|
8
19
|
keyGen: number;
|
|
9
20
|
/**
|
|
10
21
|
* Derivation index of the current `keys`: key-establishing events this generation, counting the
|
|
11
|
-
* `icp`/`rot` that opened it as 0.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `seq` after a revoke produces a key the log never committed.
|
|
22
|
+
* `icp`/`rot` that opened it as 0. The root chain holds only key-establishing events, so this is
|
|
23
|
+
* `seq`; it is named apart because it answers a different question — where to derive the key, not
|
|
24
|
+
* where the event sits — and an inception's `s` is wire data this package does not derive from.
|
|
15
25
|
*/
|
|
16
26
|
keySeq: number;
|
|
17
27
|
keys: Array<string>;
|
|
18
|
-
/** Key agreement keys — an OR set
|
|
28
|
+
/** Key agreement keys — an OR set, established by the same icp/rot as `keys`. */
|
|
19
29
|
agreement: Array<string>;
|
|
20
30
|
next: Array<string>;
|
|
21
31
|
/**
|
|
@@ -26,101 +36,59 @@ export type KeyState = {
|
|
|
26
36
|
*/
|
|
27
37
|
recovery: string;
|
|
28
38
|
/**
|
|
29
|
-
* What this position denies, in
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
39
|
+
* What this position denies, in two spellings. A `did:…` entry denies a *holder*
|
|
40
|
+
* (`@kokuin/capability` refuses a capability whose `aud` it names); a `#<multibase key>` entry
|
|
41
|
+
* ({@link keyTarget}) denies a *signer* (`signingKeyFrom` refuses that key, `agreementKeysFrom` drops
|
|
42
|
+
* it). A chain state's set is exactly its event's snapshot `d` (empty after an inception or a blind
|
|
43
|
+
* reset).
|
|
33
44
|
*
|
|
34
45
|
* One heterogeneous set rather than two fields: the spellings cannot collide, and every mechanism
|
|
35
|
-
* the set already has (position-dependence, the
|
|
36
|
-
*
|
|
46
|
+
* the set already has (position-dependence, the snapshot, a reset's clear, `resolveDenySet`, the
|
|
47
|
+
* `@kokuin/capability` wrapper that forwards it) then covers keys with nothing new to wire or
|
|
37
48
|
* forget. A second optional resolver member is a second thing a wrapper can drop, and dropping it
|
|
38
49
|
* fails open.
|
|
39
50
|
*
|
|
40
|
-
* **Invariant, enforced by
|
|
51
|
+
* **Invariant, enforced by the rotate branch of `stepEvent`: no key in this state's `keys` or
|
|
41
52
|
* `agreement` is denied here.** So every reader may take a folded head's key set at face value.
|
|
42
53
|
*/
|
|
43
54
|
deny: ReadonlySet<string>;
|
|
55
|
+
/**
|
|
56
|
+
* The management set, keyed by each member's `add` digest (its incarnation). A chain state's is its
|
|
57
|
+
* event's snapshot `m`, in snapshot order.
|
|
58
|
+
*/
|
|
59
|
+
members: ReadonlyMap<string, MemberState>;
|
|
60
|
+
/**
|
|
61
|
+
* Digest of the chain event that opened the epoch this state belongs to. Every chain event opens
|
|
62
|
+
* one, so for a chain state this is its own `digest`; epoch events name it as their `e`.
|
|
63
|
+
*/
|
|
64
|
+
epoch: string;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the epoch holds more authenticated events than a verifier will evaluate, failing it
|
|
67
|
+
* closed until root settles. A chain state has seen no epoch events, so it is never saturated.
|
|
68
|
+
*/
|
|
69
|
+
saturated: boolean;
|
|
44
70
|
/** Digest of the event that produced this state — the `p` any successor must carry. */
|
|
45
71
|
digest: string;
|
|
46
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* `states` is one state per chain event, which is what historic resolution scans. `head` is the last
|
|
75
|
+
* chain state merged with the current epoch: its `deny`, `members` and `saturated` are the live
|
|
76
|
+
* answers. `applied`, `pending` and `rejected` account for the epoch events presented -- the digests
|
|
77
|
+
* that took effect, the well-formed ones still waiting on a parent, and the ones refused (see
|
|
78
|
+
* `FoldEpochResult` for how refusals are counted).
|
|
79
|
+
*/
|
|
47
80
|
export type FoldResult = {
|
|
48
81
|
ok: true;
|
|
49
82
|
states: Array<KeyState>;
|
|
83
|
+
head: KeyState;
|
|
84
|
+
applied: Array<string>;
|
|
85
|
+
pending: number;
|
|
86
|
+
rejected: number;
|
|
50
87
|
} | {
|
|
51
88
|
ok: false;
|
|
52
89
|
reason: string;
|
|
53
90
|
index: number;
|
|
54
91
|
};
|
|
55
|
-
/**
|
|
56
|
-
* What a capability verifier answers a cap-bearing revoke with.
|
|
57
|
-
*
|
|
58
|
-
* `audienceKey` is the key the capability pins as its audience at mint time, never resolved at
|
|
59
|
-
* verification time; the fold checks the event's own signature against it, binding the grant to the
|
|
60
|
-
* party it was issued to.
|
|
61
|
-
*
|
|
62
|
-
* A key rather than a boolean because the fold, not the verifier, must tie the answer to the event
|
|
63
|
-
* in hand. The log is public, so the serialized capability inside a revoke is readable by everyone
|
|
64
|
-
* who can resolve the profile; with a boolean, any such reader could lift it out and chain a revoke
|
|
65
|
-
* of their own with any bytes in `sigs`, covering whatever the capability's `res` covers — a
|
|
66
|
-
* wildcard, for a management capability, so every device. Among divisions keeping a *single*
|
|
67
|
-
* callback, this is the only one where neither side can leave the binding out.
|
|
68
|
-
*/
|
|
69
|
-
export type CapabilityAuthorisation = {
|
|
70
|
-
authorised: true;
|
|
71
|
-
audienceKey: ResolvedSigningKey;
|
|
72
|
-
}
|
|
73
|
-
/** `reason` becomes the fold's failure reason verbatim, so it must describe this rejection. */
|
|
74
|
-
| {
|
|
75
|
-
authorised: false;
|
|
76
|
-
reason: string;
|
|
77
|
-
};
|
|
78
|
-
export type FoldOptions = {
|
|
79
|
-
/**
|
|
80
|
-
* Verify a capability authorising a non-authority signer to revoke. Injected rather than imported
|
|
81
|
-
* so the sync path stays free of a capability dependency.
|
|
82
|
-
*
|
|
83
|
-
* Receives the serialized capability, the controller DID (which must be the capability `sub`), the
|
|
84
|
-
* DID being denied, and a resolver for the controller **at the position being verified**. That
|
|
85
|
-
* fourth argument makes the position contract keepable: the capability is issued by the very
|
|
86
|
-
* profile whose log carries the revoke, and the state to resolve it against is the prefix before
|
|
87
|
-
* this event — not the head (which would let a device revoked at event 1 keep authoring at event 2,
|
|
88
|
-
* the deny set being position-dependent only inside the fold) and not the whole log (which would
|
|
89
|
-
* recurse into the asking fold). Nothing outside the fold knows which position is being verified,
|
|
90
|
-
* so the fold answers rather than asks: this resolver holds exactly `states[0..i-1]`, is complete
|
|
91
|
-
* for the profile, and answers `Unknown DID` for anything else so a verifier can merge it into a
|
|
92
|
-
* wider registry for a chain's delegates. See {@link CapabilityAuthorisation}.
|
|
93
|
-
*/
|
|
94
|
-
verifyCapability?: (params: VerifyCapabilityParams) => Promise<CapabilityAuthorisation>;
|
|
95
|
-
};
|
|
96
|
-
/** What the fold hands {@link FoldOptions.verifyCapability} — see that member for each field. */
|
|
97
|
-
export type VerifyCapabilityParams = {
|
|
98
|
-
cap: string;
|
|
99
|
-
subject: string;
|
|
100
|
-
target: string;
|
|
101
|
-
subjectAtPosition: DIDMethodResolver;
|
|
102
|
-
};
|
|
103
|
-
/** A verifier threw. The thrown message is appended after `: `, so match with `startsWith`. */
|
|
104
|
-
export declare const CAPABILITY_VERIFIER_FAILED = "capability verifier failed";
|
|
105
|
-
/** A verifier answered with something that is not a {@link CapabilityAuthorisation}. */
|
|
106
|
-
export declare const CAPABILITY_VERIFIER_MALFORMED_ANSWER = "capability verifier returned a malformed answer";
|
|
107
|
-
/**
|
|
108
|
-
* The capability authorised the revoke and pinned an audience key, and somebody else signed the
|
|
109
|
-
* event. Distinct from a rejected grant: the delegation is sound and the signature is not.
|
|
110
|
-
*/
|
|
111
|
-
export declare const REVOKE_NOT_SIGNED_BY_AUDIENCE = "revoke is not signed by the capability audience";
|
|
112
|
-
/**
|
|
113
|
-
* The sync fold met a capability-authorised revoke, which it cannot verify by construction — the
|
|
114
|
-
* management tier working as designed, answered by `foldLogAsync`. The capability follows after `: `,
|
|
115
|
-
* so match with `startsWith`. Exported so speculative folders (`resolveBranches`) can tell it from a
|
|
116
|
-
* genuinely broken log.
|
|
117
|
-
*/
|
|
118
|
-
export declare const CAPABILITY_REVOKE_NEEDS_ASYNC_FOLD = "capability-authorised revoke needs an async fold";
|
|
119
|
-
/**
|
|
120
|
-
* The async fold met a capability-authorised revoke with no `verifyCapability` configured. Same
|
|
121
|
-
* shape: the log is fine and the call was not equipped for it.
|
|
122
|
-
*/
|
|
123
|
-
export declare const CAPABILITY_REVOKE_NEEDS_VERIFIER = "capability-authorised revoke needs a verifier";
|
|
124
92
|
/**
|
|
125
93
|
* How many events a log may carry that this version cannot understand, beyond the number it can.
|
|
126
94
|
*
|
|
@@ -135,28 +103,38 @@ export declare const CAPABILITY_REVOKE_NEEDS_VERIFIER = "capability-authorised r
|
|
|
135
103
|
export declare const MAX_SKIPPED_SLACK = 8;
|
|
136
104
|
/** What a log padded past {@link MAX_SKIPPED_SLACK} fails with. */
|
|
137
105
|
export declare const TOO_MANY_UNKNOWN_EVENTS = "too many unknown events";
|
|
106
|
+
/** A fold-loop index landed off the event or state array — a broken invariant, not a bad log. */
|
|
107
|
+
export declare const INTERNAL_FOLD_INVARIANT = "internal fold invariant: missing event or prior state";
|
|
138
108
|
/**
|
|
139
|
-
* Fold a controller's log into per-position key state. `states[i]` is the state *after* `
|
|
109
|
+
* Fold a controller's log into per-position key state. `states[i]` is the state *after* `chain[i]`,
|
|
140
110
|
* which a verifier evaluating at position `i` must use — that is what makes the deny set
|
|
141
111
|
* position-dependent: clearing a DID later never retroactively validates its earlier actions.
|
|
142
112
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* closed.
|
|
113
|
+
* The epoch events then merge into the chain's last state to give `head` (see `foldEpoch`). Only request
|
|
114
|
+
* limits can fail that half: more than {@link MAX_LOG_EVENTS} entries is `LOG_TOO_LARGE`, at
|
|
115
|
+
* index -1 because it is no event's fault, and so is more than `MAX_FAILED_SIGNATURES` signatures
|
|
116
|
+
* failing to verify (`TOO_MANY_FAILED_SIGNATURES`). Short of those, nothing in `log.events` can fail
|
|
117
|
+
* the fold -- a bad entry is counted and dropped -- so a relay cannot make a log unfoldable by padding
|
|
118
|
+
* it, only refused by forging past the limit, which no honest relay does.
|
|
119
|
+
*
|
|
120
|
+
* Synchronous, so it stays usable on an offline verifier's apply path.
|
|
152
121
|
*/
|
|
153
|
-
export declare function
|
|
122
|
+
export declare function foldLog(did: string, log: ControllerLog): FoldResult;
|
|
154
123
|
export declare function keyStateAt(result: FoldResult, position: number): KeyState | undefined;
|
|
155
124
|
/**
|
|
156
|
-
* The
|
|
157
|
-
* few entries rather than
|
|
158
|
-
*
|
|
159
|
-
* un-revokes a device
|
|
160
|
-
*
|
|
125
|
+
* The state snapshot `state` holds, minus `drop` — what to pass as a rotate's `snapshot` to carry the
|
|
126
|
+
* state forward, pruning a few entries rather than replacing it. A rotate's snapshot replaces the
|
|
127
|
+
* state outright, so writing one by hand means writing out everything that stays; miss one and the
|
|
128
|
+
* profile silently un-revokes a device, un-retires a leaked key or dismisses a management member.
|
|
129
|
+
* This builds it from the fold's own answer instead: every member not removed (`drop.members` names
|
|
130
|
+
* members by `add` digest) and every deny entry (`drop.deny`). Entries in `drop` the state does not
|
|
131
|
+
* carry are ignored.
|
|
132
|
+
*
|
|
133
|
+
* Root's tooling owes its user the warnings the fold cannot give: dropping a holder DID denial
|
|
134
|
+
* re-authorises it, safe only once every authority that could have minted it a grant has ended; and
|
|
135
|
+
* a retired root key the chain shows but this deny set lacks is one to add, not to leave out.
|
|
161
136
|
*/
|
|
162
|
-
export declare function
|
|
137
|
+
export declare function snapshotOf(state: KeyState, drop?: {
|
|
138
|
+
deny?: Iterable<string>;
|
|
139
|
+
members?: Iterable<string>;
|
|
140
|
+
}): StateSnapshot;
|