@kokuin/controller 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/README.md +7 -0
- package/lib/canonical.d.ts +61 -0
- package/lib/canonical.js +114 -0
- package/lib/derivation.d.ts +23 -0
- package/lib/derivation.js +75 -0
- package/lib/events.d.ts +286 -0
- package/lib/events.js +465 -0
- package/lib/fold.d.ts +162 -0
- package/lib/fold.js +499 -0
- package/lib/history.d.ts +63 -0
- package/lib/history.js +97 -0
- package/lib/identity.d.ts +74 -0
- package/lib/identity.js +190 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +22 -0
- package/lib/keys.d.ts +9 -0
- package/lib/keys.js +61 -0
- package/lib/keystore-identity.d.ts +22 -0
- package/lib/keystore-identity.js +26 -0
- package/lib/profiles.d.ts +21 -0
- package/lib/profiles.js +42 -0
- package/lib/resolver.d.ts +56 -0
- package/lib/resolver.js +121 -0
- package/lib/state-resolver.d.ts +60 -0
- package/lib/state-resolver.js +186 -0
- package/lib/state.d.ts +28 -0
- package/lib/state.js +34 -0
- package/lib/supersede.d.ts +97 -0
- package/lib/supersede.js +312 -0
- package/lib/version.d.ts +6 -0
- package/lib/version.js +5 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deepest nesting {@link canonicalBytes} will encode (top-level value is depth 1). The RFC 8785
|
|
3
|
+
* encoder recurses once per level and, like `JSON.stringify`, only fails at native stack exhaustion —
|
|
4
|
+
* a few kilobytes of `[[[[…]]]]` on the wire would be a `RangeError` thrown from a function every
|
|
5
|
+
* event passes through, and from inside `resolveBranches` where one hostile branch would take
|
|
6
|
+
* duplicity detection down for every honest one. So `canonicalBytes` rejects on this bound *before*
|
|
7
|
+
* encoding rather than catching a `RangeError` (which still drives the stack to exhaustion once per
|
|
8
|
+
* event). 64 is far above anything this stack canonicalizes (an event body reaches depth 3) and far
|
|
9
|
+
* below the ~10⁴ frames a JS stack holds. Raising it is safe; lowering below 3 is not.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAX_CANONICAL_DEPTH = 64;
|
|
12
|
+
/**
|
|
13
|
+
* Whether {@link canonicalBytes} could encode this value without exceeding
|
|
14
|
+
* {@link MAX_CANONICAL_DEPTH} — the total counterpart of the bound above, turning "too deep" into a
|
|
15
|
+
* rejection. Bounded by the same constant, so it is safe even on a cyclic object (which parsed JSON
|
|
16
|
+
* cannot produce but a caller can). Answers only the depth question; prefer {@link isCanonicalizable}
|
|
17
|
+
* for untrusted input, since a non-finite number is reachable from `JSON.parse` and this ignores it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withinCanonicalDepth(value: unknown, depth?: number): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether {@link canonicalBytes} could encode this value at all — every reason it throws, answered
|
|
22
|
+
* instead of raised. The total counterpart of the whole canonicalizer. {@link withinCanonicalDepth}
|
|
23
|
+
* answers only depth, which left a hole: `1e400` on the wire parses to `Infinity`, is a `number`,
|
|
24
|
+
* nests no deeper than anything, and reached `canonicalize` where it *threw* — breaking the total
|
|
25
|
+
* contract of `foldLog`/`verifySignatures` and taking `resolveBranches` down for every honest branch.
|
|
26
|
+
*
|
|
27
|
+
* Answers `false` for exactly what `canonicalize` refuses: a non-finite number (`Infinity`/`NaN`,
|
|
28
|
+
* the first reachable from `JSON.parse` via `1e400`); a type with no encoding (`bigint`, `symbol`,
|
|
29
|
+
* `function`, top-level `undefined` — all caller-only); a non-plain object (caller-only); a value
|
|
30
|
+
* nesting past {@link MAX_CANONICAL_DEPTH}, kept here so one call covers the whole contract.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately **not** rejected: `-0` (encodes as `0`, same value under every comparison this package
|
|
33
|
+
* makes), and integers past `Number.MAX_SAFE_INTEGER` (`JSON.parse` collapses the two wire spellings
|
|
34
|
+
* to one double before anything here sees them, so the shared digest names one value). Bounded by the
|
|
35
|
+
* same constant, so it is safe even on a cyclic object.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isCanonicalizable(value: unknown, depth?: number): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* RFC 8785 (JCS) canonical bytes via `@sozai/json`: keys sorted, no insignificant whitespace,
|
|
40
|
+
* `undefined` properties dropped so an absent optional field never encodes as `null`. The DID is the
|
|
41
|
+
* hash of these bytes, so any change here moves every identifier the stack has issued — frozen.
|
|
42
|
+
*
|
|
43
|
+
* Validated by {@link isCanonicalizable} before encoding, not left to the encoder: `@sozai/json`
|
|
44
|
+
* fails a deep value only at native stack exhaustion (see {@link MAX_CANONICAL_DEPTH}) and *encodes*
|
|
45
|
+
* a `Date`/`Map`/typed array rather than refusing it. The guard rejects every value the old encoder
|
|
46
|
+
* did — non-finite, non-plain, unrepresentable, or past the depth bound — so a wrong digest fails
|
|
47
|
+
* loud here instead of silently naming a value the wire form can never carry. It also makes the
|
|
48
|
+
* throw set exactly {@link isCanonicalizable}'s `false` set, the totality `verifyDigest` relies on.
|
|
49
|
+
*/
|
|
50
|
+
export declare function canonicalBytes(value: unknown): Uint8Array;
|
|
51
|
+
/** Self-addressing digest: multibase(multihash(canonical bytes)). */
|
|
52
|
+
export declare function digestOf(value: unknown): string;
|
|
53
|
+
/**
|
|
54
|
+
* Total: a malformed digest *or* a value this file cannot canonicalize returns false rather than
|
|
55
|
+
* throwing. The value side is the untrusted one in real use ("is this the body that digest names" is
|
|
56
|
+
* asked about wire bytes), and `false` is correct, not convenient: a digest this package produced can
|
|
57
|
+
* only name a value it could canonicalize. Uses {@link isCanonicalizable}, not
|
|
58
|
+
* {@link withinCanonicalDepth}, since a non-finite number is another way `canonicalize` throws.
|
|
59
|
+
* `canonicalBytes` keeps its throw — its contract is "these exact bytes or nothing".
|
|
60
|
+
*/
|
|
61
|
+
export declare function verifyDigest(digest: string, value: unknown): boolean;
|
package/lib/canonical.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { decodeMultibase, encodeMultibase, multihashSHA256, verifyMultihash } from '@kokuin/token';
|
|
2
|
+
import { canonicalize } from '@sozai/json';
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
/**
|
|
5
|
+
* Deepest nesting {@link canonicalBytes} will encode (top-level value is depth 1). The RFC 8785
|
|
6
|
+
* encoder recurses once per level and, like `JSON.stringify`, only fails at native stack exhaustion —
|
|
7
|
+
* a few kilobytes of `[[[[…]]]]` on the wire would be a `RangeError` thrown from a function every
|
|
8
|
+
* event passes through, and from inside `resolveBranches` where one hostile branch would take
|
|
9
|
+
* duplicity detection down for every honest one. So `canonicalBytes` rejects on this bound *before*
|
|
10
|
+
* encoding rather than catching a `RangeError` (which still drives the stack to exhaustion once per
|
|
11
|
+
* event). 64 is far above anything this stack canonicalizes (an event body reaches depth 3) and far
|
|
12
|
+
* below the ~10⁴ frames a JS stack holds. Raising it is safe; lowering below 3 is not.
|
|
13
|
+
*/ export const MAX_CANONICAL_DEPTH = 64;
|
|
14
|
+
/**
|
|
15
|
+
* Whether {@link canonicalBytes} could encode this value without exceeding
|
|
16
|
+
* {@link MAX_CANONICAL_DEPTH} — the total counterpart of the bound above, turning "too deep" into a
|
|
17
|
+
* rejection. Bounded by the same constant, so it is safe even on a cyclic object (which parsed JSON
|
|
18
|
+
* cannot produce but a caller can). Answers only the depth question; prefer {@link isCanonicalizable}
|
|
19
|
+
* for untrusted input, since a non-finite number is reachable from `JSON.parse` and this ignores it.
|
|
20
|
+
*/ export function withinCanonicalDepth(value, depth = 1) {
|
|
21
|
+
// Same comparison at the same point as `canonicalize`'s, so the two agree exactly on the boundary.
|
|
22
|
+
if (depth > MAX_CANONICAL_DEPTH) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (value == null || typeof value !== 'object') {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const entries = Array.isArray(value) ? value : Object.values(value);
|
|
29
|
+
return entries.every((entry)=>withinCanonicalDepth(entry, depth + 1));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether {@link canonicalBytes} could encode this value at all — every reason it throws, answered
|
|
33
|
+
* instead of raised. The total counterpart of the whole canonicalizer. {@link withinCanonicalDepth}
|
|
34
|
+
* answers only depth, which left a hole: `1e400` on the wire parses to `Infinity`, is a `number`,
|
|
35
|
+
* nests no deeper than anything, and reached `canonicalize` where it *threw* — breaking the total
|
|
36
|
+
* contract of `foldLog`/`verifySignatures` and taking `resolveBranches` down for every honest branch.
|
|
37
|
+
*
|
|
38
|
+
* Answers `false` for exactly what `canonicalize` refuses: a non-finite number (`Infinity`/`NaN`,
|
|
39
|
+
* the first reachable from `JSON.parse` via `1e400`); a type with no encoding (`bigint`, `symbol`,
|
|
40
|
+
* `function`, top-level `undefined` — all caller-only); a non-plain object (caller-only); a value
|
|
41
|
+
* nesting past {@link MAX_CANONICAL_DEPTH}, kept here so one call covers the whole contract.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately **not** rejected: `-0` (encodes as `0`, same value under every comparison this package
|
|
44
|
+
* makes), and integers past `Number.MAX_SAFE_INTEGER` (`JSON.parse` collapses the two wire spellings
|
|
45
|
+
* to one double before anything here sees them, so the shared digest names one value). Bounded by the
|
|
46
|
+
* same constant, so it is safe even on a cyclic object.
|
|
47
|
+
*/ export function isCanonicalizable(value, depth = 1) {
|
|
48
|
+
// Same comparison at the same point as `canonicalize`'s, so the two agree exactly on the boundary.
|
|
49
|
+
if (depth > MAX_CANONICAL_DEPTH) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (value === null) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'number') {
|
|
56
|
+
return Number.isFinite(value);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return value.every((entry)=>isCanonicalizable(entry, depth + 1));
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'object') {
|
|
65
|
+
const proto = Object.getPrototypeOf(value);
|
|
66
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// `undefined` members are dropped by `canonicalize` rather than encoded, so they are not a
|
|
70
|
+
// reason to refuse — mirroring the `filter` there.
|
|
71
|
+
return Object.values(value).every((entry)=>entry === undefined || isCanonicalizable(entry, depth + 1));
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* RFC 8785 (JCS) canonical bytes via `@sozai/json`: keys sorted, no insignificant whitespace,
|
|
77
|
+
* `undefined` properties dropped so an absent optional field never encodes as `null`. The DID is the
|
|
78
|
+
* hash of these bytes, so any change here moves every identifier the stack has issued — frozen.
|
|
79
|
+
*
|
|
80
|
+
* Validated by {@link isCanonicalizable} before encoding, not left to the encoder: `@sozai/json`
|
|
81
|
+
* fails a deep value only at native stack exhaustion (see {@link MAX_CANONICAL_DEPTH}) and *encodes*
|
|
82
|
+
* a `Date`/`Map`/typed array rather than refusing it. The guard rejects every value the old encoder
|
|
83
|
+
* did — non-finite, non-plain, unrepresentable, or past the depth bound — so a wrong digest fails
|
|
84
|
+
* loud here instead of silently naming a value the wire form can never carry. It also makes the
|
|
85
|
+
* throw set exactly {@link isCanonicalizable}'s `false` set, the totality `verifyDigest` relies on.
|
|
86
|
+
*/ export function canonicalBytes(value) {
|
|
87
|
+
if (!isCanonicalizable(value)) {
|
|
88
|
+
throw new Error('Canonicalization: value is not canonicalizable (non-finite, non-plain, unrepresentable, or ' + `nests deeper than ${MAX_CANONICAL_DEPTH})`);
|
|
89
|
+
}
|
|
90
|
+
// Never `undefined`: `isCanonicalizable` has already rejected the values with no JSON form.
|
|
91
|
+
return encoder.encode(canonicalize(value));
|
|
92
|
+
}
|
|
93
|
+
/** Self-addressing digest: multibase(multihash(canonical bytes)). */ export function digestOf(value) {
|
|
94
|
+
return encodeMultibase(multihashSHA256(canonicalBytes(value)));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Total: a malformed digest *or* a value this file cannot canonicalize returns false rather than
|
|
98
|
+
* throwing. The value side is the untrusted one in real use ("is this the body that digest names" is
|
|
99
|
+
* asked about wire bytes), and `false` is correct, not convenient: a digest this package produced can
|
|
100
|
+
* only name a value it could canonicalize. Uses {@link isCanonicalizable}, not
|
|
101
|
+
* {@link withinCanonicalDepth}, since a non-finite number is another way `canonicalize` throws.
|
|
102
|
+
* `canonicalBytes` keeps its throw — its contract is "these exact bytes or nothing".
|
|
103
|
+
*/ export function verifyDigest(digest, value) {
|
|
104
|
+
let expected;
|
|
105
|
+
try {
|
|
106
|
+
expected = decodeMultibase(digest);
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (!isCanonicalizable(value)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return verifyMultihash(expected, canonicalBytes(value));
|
|
114
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authority signing key at a position. Lives under the delegable branch, so handing out the
|
|
3
|
+
* profile sub-seed delegates it.
|
|
4
|
+
*/
|
|
5
|
+
export declare function authorityPath(profile: number, gen: number, seq: number): string;
|
|
6
|
+
/** Profile-level key agreement key. Durable data encrypts to these, never to device keys. */
|
|
7
|
+
export declare function agreementPath(profile: number, gen: number, seq: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Recovery key, on the root-retained branch — a sibling of the delegable subtree, never a
|
|
10
|
+
* descendant. Hardened derivation is one-way, so a sub-seed holder cannot reach it and the root
|
|
11
|
+
* always keeps the one key that can supersede.
|
|
12
|
+
*/
|
|
13
|
+
export declare function recoveryPath(profile: number): string;
|
|
14
|
+
/**
|
|
15
|
+
* Key material for an algorithm at a path. SLIP-0010 fixes the tree position; HKDF supplies
|
|
16
|
+
* algorithm separation and arbitrary lengths, so adding an algorithm needs a new `info` string
|
|
17
|
+
* and no path change.
|
|
18
|
+
*/
|
|
19
|
+
export declare function deriveKeyMaterial(seed: Uint8Array, path: string, alg: string, length?: number): Uint8Array;
|
|
20
|
+
export declare function deriveKeyPair(seed: Uint8Array, path: string, alg: string): {
|
|
21
|
+
privateKey: Uint8Array;
|
|
22
|
+
publicKey: Uint8Array;
|
|
23
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ed25519, x25519 } from '@noble/curves/ed25519.js';
|
|
2
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
4
|
+
import HDKey from 'micro-key-producer/slip10.js';
|
|
5
|
+
import { VERSION_TAG } from './version.js';
|
|
6
|
+
const BASE_PATH = "m/44'/876'";
|
|
7
|
+
const DELEGABLE = "0'";
|
|
8
|
+
const ROOT_ONLY = "1'";
|
|
9
|
+
const ROLE_AUTHORITY = "0'";
|
|
10
|
+
const ROLE_AGREEMENT = "1'";
|
|
11
|
+
function assertIndex(name, value) {
|
|
12
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
13
|
+
throw new Error(`Derivation: ${name} must be a non-negative integer, got ${value}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function profilePath(profile) {
|
|
17
|
+
assertIndex('profile', profile);
|
|
18
|
+
return `${BASE_PATH}/${DELEGABLE}/${profile}'`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Authority signing key at a position. Lives under the delegable branch, so handing out the
|
|
22
|
+
* profile sub-seed delegates it.
|
|
23
|
+
*/ export function authorityPath(profile, gen, seq) {
|
|
24
|
+
assertIndex('gen', gen);
|
|
25
|
+
assertIndex('seq', seq);
|
|
26
|
+
return `${profilePath(profile)}/${ROLE_AUTHORITY}/${gen}'/${seq}'`;
|
|
27
|
+
}
|
|
28
|
+
/** Profile-level key agreement key. Durable data encrypts to these, never to device keys. */ export function agreementPath(profile, gen, seq) {
|
|
29
|
+
assertIndex('gen', gen);
|
|
30
|
+
assertIndex('seq', seq);
|
|
31
|
+
return `${profilePath(profile)}/${ROLE_AGREEMENT}/${gen}'/${seq}'`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Recovery key, on the root-retained branch — a sibling of the delegable subtree, never a
|
|
35
|
+
* descendant. Hardened derivation is one-way, so a sub-seed holder cannot reach it and the root
|
|
36
|
+
* always keeps the one key that can supersede.
|
|
37
|
+
*/ export function recoveryPath(profile) {
|
|
38
|
+
assertIndex('profile', profile);
|
|
39
|
+
return `${BASE_PATH}/${ROOT_ONLY}/${profile}'`;
|
|
40
|
+
}
|
|
41
|
+
const KEY_LENGTHS = {
|
|
42
|
+
EdDSA: 32,
|
|
43
|
+
X25519: 32,
|
|
44
|
+
'ML-DSA-65': 32,
|
|
45
|
+
'ML-KEM-768': 64
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Key material for an algorithm at a path. SLIP-0010 fixes the tree position; HKDF supplies
|
|
49
|
+
* algorithm separation and arbitrary lengths, so adding an algorithm needs a new `info` string
|
|
50
|
+
* and no path change.
|
|
51
|
+
*/ export function deriveKeyMaterial(seed, path, alg, length = KEY_LENGTHS[alg] ?? 32) {
|
|
52
|
+
const node = HDKey.fromMasterSeed(seed).derive(path);
|
|
53
|
+
const ikm = node.privateKey;
|
|
54
|
+
if (ikm == null) {
|
|
55
|
+
throw new Error(`Derivation: no private key at path ${path}`);
|
|
56
|
+
}
|
|
57
|
+
const info = new TextEncoder().encode(`${VERSION_TAG}|${alg}`);
|
|
58
|
+
return hkdf(sha256, ikm, undefined, info, length);
|
|
59
|
+
}
|
|
60
|
+
export function deriveKeyPair(seed, path, alg) {
|
|
61
|
+
const privateKey = deriveKeyMaterial(seed, path, alg);
|
|
62
|
+
if (alg === 'EdDSA') {
|
|
63
|
+
return {
|
|
64
|
+
privateKey,
|
|
65
|
+
publicKey: ed25519.getPublicKey(privateKey)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (alg === 'X25519') {
|
|
69
|
+
return {
|
|
70
|
+
privateKey,
|
|
71
|
+
publicKey: x25519.getPublicKey(privateKey)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Derivation: Unsupported algorithm for key pair derivation: ${alg}`);
|
|
75
|
+
}
|
package/lib/events.d.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import type { DIDString, ResolvedSigningKey } from '@kokuin/token';
|
|
2
|
+
export declare const DID_PREFIX = "did:kokuin:";
|
|
3
|
+
export type EventType = 'icp' | 'rot' | 'rev';
|
|
4
|
+
export type EventCommon = {
|
|
5
|
+
/** Inception format version. Absent must never be inferred; always written. */
|
|
6
|
+
v: 1;
|
|
7
|
+
t: EventType;
|
|
8
|
+
/** Profile DID. Omitted from an inception, whose hash *is* the DID. */
|
|
9
|
+
i?: string;
|
|
10
|
+
/** Generation. Incremented only by a reset. */
|
|
11
|
+
g: number;
|
|
12
|
+
/** Sequence within the generation. */
|
|
13
|
+
s: number;
|
|
14
|
+
/** Digest of the previous event. Absent on inception. */
|
|
15
|
+
p?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Criticality. In the common envelope so a verifier reads it without understanding `t`. An unknown
|
|
18
|
+
* event fails the fold closed when true, and is skipped when false.
|
|
19
|
+
*/
|
|
20
|
+
crit: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type InceptionEvent = EventCommon & {
|
|
23
|
+
t: 'icp';
|
|
24
|
+
/** Current authority public keys, multicodec-tagged and multibase-encoded. */
|
|
25
|
+
k: Array<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Key agreement public keys -- an OR set, never combined. Carries no pre-rotation commitment: an
|
|
28
|
+
* exposed agreement key discloses past ciphertexts but confers no authority.
|
|
29
|
+
*/
|
|
30
|
+
ka: Array<string>;
|
|
31
|
+
/** Digests of the next public keys — pre-rotation. */
|
|
32
|
+
n: Array<string>;
|
|
33
|
+
/** Signing threshold: how many of `k` must sign. **Must equal `k.length`** — see {@link isEnforcedThreshold}. */
|
|
34
|
+
kt: number;
|
|
35
|
+
/** Rotation threshold: how many of `n` must sign the next rotate. **Must equal `n.length`.** */
|
|
36
|
+
nt: number;
|
|
37
|
+
/**
|
|
38
|
+
* Digest of the recovery key. Root-retained, **immutable for the life of the DID** — the only
|
|
39
|
+
* event that can carry one is this one, and this one is the DID. A rotate carrying `r` is refused
|
|
40
|
+
* rather than ignored — see {@link RotateEvent}.
|
|
41
|
+
*/
|
|
42
|
+
r: string;
|
|
43
|
+
};
|
|
44
|
+
export type SignedEvent<E extends EventCommon = EventCommon> = {
|
|
45
|
+
event: E;
|
|
46
|
+
/** base64url ed25519 signatures over the canonical event bytes, positional against `event.k`. */
|
|
47
|
+
sigs: Array<string>;
|
|
48
|
+
/**
|
|
49
|
+
* The revealed recovery public key, present only on a reset. The recovery key is committed as a
|
|
50
|
+
* digest and unpublished until used, so a reset must reveal it for the commitment to be checkable.
|
|
51
|
+
*/
|
|
52
|
+
recoveryKey?: string;
|
|
53
|
+
};
|
|
54
|
+
export declare function signEvent(event: EventCommon, privateKeys: Array<Uint8Array>): Array<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Total: a malformed signature or key yields false rather than throwing, including a body the
|
|
57
|
+
* canonicalizer refuses. `canonicalBytes` throws on a non-finite number or nesting past
|
|
58
|
+
* `MAX_CANONICAL_DEPTH` (both from `JSON.parse`); the fold's envelope guard rejects such a body one
|
|
59
|
+
* layer out, so this is unreachable through `foldLog` but stays because the export is documented
|
|
60
|
+
* total and a direct caller reaches it with a parsed body. `false` is honest: a body with no
|
|
61
|
+
* canonical encoding has no bytes for a signature to be over.
|
|
62
|
+
*/
|
|
63
|
+
export declare function verifySignatures(event: EventCommon, sigs: Array<string>, keys: Array<string>): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Whether one of an event's signatures was made by `key` — the counterpart of
|
|
66
|
+
* {@link verifySignatures} for a signer the *event* does not name (resolved from a DID elsewhere). A
|
|
67
|
+
* capability-authorised revoke needs exactly this: its author is the capability's `aud`, which the
|
|
68
|
+
* fold cannot resolve, so it is handed the key and checks the signature itself.
|
|
69
|
+
*
|
|
70
|
+
* Total, EdDSA only — all {@link signEvent} produces. An ES256 audience therefore cannot author a
|
|
71
|
+
* revoke, which fails closed until events grow a second signature algorithm.
|
|
72
|
+
*/
|
|
73
|
+
export declare function verifyEventSignedBy(signed: SignedEvent, key: ResolvedSigningKey): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Deterministic inception. Only seed-derived, canonical material — no timestamp, nonce, or label —
|
|
76
|
+
* so the DID is a pure function of the seed and profile index. Never add a label: a mistyped one on
|
|
77
|
+
* recovery would reproduce a different DID.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createInception(seed: Uint8Array, profile: number): SignedEvent<InceptionEvent>;
|
|
80
|
+
export declare function didFromInception(event: InceptionEvent): DIDString;
|
|
81
|
+
/** The did:kokuin: identifier a seed and profile produce, without signing an inception. */
|
|
82
|
+
export declare function didFor(seed: Uint8Array, profile: number): DIDString;
|
|
83
|
+
export declare function verifyInception(signed: SignedEvent<InceptionEvent>, did: string): boolean;
|
|
84
|
+
export type RotateEvent = EventCommon & {
|
|
85
|
+
t: 'rot';
|
|
86
|
+
/** Keys the prior event pre-committed, now revealed, multicodec-tagged and multibase-encoded. */
|
|
87
|
+
k: Array<string>;
|
|
88
|
+
/** Key agreement public keys — an OR set, never combined. Carries no pre-rotation commitment. */
|
|
89
|
+
ka: Array<string>;
|
|
90
|
+
/** Digests of the next public keys — pre-rotation. */
|
|
91
|
+
n: Array<string>;
|
|
92
|
+
/** Signing threshold: how many of `k` must sign. **Must equal `k.length`.** */
|
|
93
|
+
kt: number;
|
|
94
|
+
/** Rotation threshold: how many of `n` must sign the next rotate. **Must equal `n.length`.** */
|
|
95
|
+
nt: number;
|
|
96
|
+
/**
|
|
97
|
+
* Seal: an anchored external digest pinning a high-value grant to this position. Opaque to the fold
|
|
98
|
+
* by design — its meaning belongs to whatever produced it, and nothing in the key state can
|
|
99
|
+
* contradict it. The fold owes it only that it cannot ride in as a non-string (checked in
|
|
100
|
+
* {@link isPublishedRotate}). Not surfaced in `KeyState`: it belongs to a position, not the keys.
|
|
101
|
+
*/
|
|
102
|
+
a?: string;
|
|
103
|
+
/** Deny-set snapshot. Replaces the accumulated set, pruning it. */
|
|
104
|
+
d?: Array<string>;
|
|
105
|
+
};
|
|
106
|
+
export type CreateRotateOptions = {
|
|
107
|
+
seal?: string;
|
|
108
|
+
/**
|
|
109
|
+
* The complete deny set this rotate leaves behind — **a replacement, not an addition**. `d` present
|
|
110
|
+
* replaces the accumulated set, `d` absent carries it forward. A caller writing one entry meaning
|
|
111
|
+
* "also deny this" silently drops every accumulated entry — un-revoking devices and un-retiring
|
|
112
|
+
* leaked keys — with no error and nothing in the log saying anything was lost. Build it from the
|
|
113
|
+
* folded state with {@link pruneDenySet}. `[]` is the deliberate "clear everything" a cold rotate
|
|
114
|
+
* does to recover from a bad management tier. A rotate cannot establish a key its own snapshot
|
|
115
|
+
* denies; the fold refuses such an event.
|
|
116
|
+
*/
|
|
117
|
+
denySnapshot?: Array<string>;
|
|
118
|
+
/**
|
|
119
|
+
* Where the currently-active authority key lives — the position of the last `icp`/`rot`, i.e. the
|
|
120
|
+
* fold's `keyGen`/`keySeq`.
|
|
121
|
+
*
|
|
122
|
+
* Defaults to `prior`'s position, right only while sequence and derivation index still coincide: a
|
|
123
|
+
* `rev` advances `s` and establishes no key, so after the first revoke of a generation the two part
|
|
124
|
+
* company **and never rejoin**. Every rotate after a revoke needs this, not just the one directly
|
|
125
|
+
* on it. Optional rather than required because {@link createRotate} verifies the key it reveals
|
|
126
|
+
* against `prior`'s commitment and throws on disagreement — provably right where omitted, an error
|
|
127
|
+
* (not an unfoldable event) where not. Take it from `KeyState.keyGen`/`keySeq`.
|
|
128
|
+
*/
|
|
129
|
+
keyPosition?: {
|
|
130
|
+
gen: number;
|
|
131
|
+
seq: number;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
export type CreateRotateParams = {
|
|
135
|
+
seed: Uint8Array;
|
|
136
|
+
profile: number;
|
|
137
|
+
did: string;
|
|
138
|
+
prior: EventCommon;
|
|
139
|
+
options?: CreateRotateOptions;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* A rotate reveals the keys the prior event pre-committed and commits the next set, signed by the
|
|
143
|
+
* newly revealed keys (per KERI — what makes a stolen current key unable to rotate). Reproducible
|
|
144
|
+
* from the seed alone unless it carries a seal or deny snapshot.
|
|
145
|
+
*
|
|
146
|
+
* **Throws rather than emitting an event the fold will reject.** The revealed key must be the one
|
|
147
|
+
* `prior` committed in `n`, and `options.keyPosition` decides which key gets derived — so a wrong
|
|
148
|
+
* position used to produce a well-formed, signed event no fold would accept. The two are checked
|
|
149
|
+
* against each other here. When `prior` carries no commitment (a `rev`), `keyPosition` is required:
|
|
150
|
+
* the default would derive a key one past the *revoke*, which nothing committed, and this is the one
|
|
151
|
+
* case unverifiable from `prior` alone.
|
|
152
|
+
*/
|
|
153
|
+
export declare function createRotate({ seed, profile, did, prior, options, }: CreateRotateParams): SignedEvent<RotateEvent>;
|
|
154
|
+
/**
|
|
155
|
+
* A rotate is valid when it chains to the prior digest, its revealed keys match the prior event's
|
|
156
|
+
* pre-rotation commitment, and its signatures verify against those keys.
|
|
157
|
+
*/
|
|
158
|
+
export declare function verifyRotate(signed: SignedEvent<RotateEvent>, prior: {
|
|
159
|
+
digest: string;
|
|
160
|
+
n: Array<string>;
|
|
161
|
+
}): boolean;
|
|
162
|
+
/**
|
|
163
|
+
* A reset: a rotate signed by the recovery key that increments the generation and discards
|
|
164
|
+
* everything under the prior one, including every capability minted there.
|
|
165
|
+
*
|
|
166
|
+
* Anchored to the inception, not the log head: the recovery key lives on the root-retained
|
|
167
|
+
* derivation branch and its digest is committed in the deterministic inception, so a restored
|
|
168
|
+
* mnemonic can author one with no log knowledge or availability. Carries no options — no seal, `d`
|
|
169
|
+
* always `[]` — so a reset is a pure function of `(seed, profile, gen)`, and two blind resets at one
|
|
170
|
+
* generation produce identical bytes that resolve as idempotent re-derivation, not duplicity.
|
|
171
|
+
*
|
|
172
|
+
* The root need not know the current generation, only eventually exceed it (no attacker can author a
|
|
173
|
+
* competing reset at any generation): a blind root starts at `gen = 1` and retries higher if it
|
|
174
|
+
* learns of one — the cost is a round trip, never loss of control.
|
|
175
|
+
*/
|
|
176
|
+
export declare function createReset(seed: Uint8Array, profile: number, gen: number): SignedEvent<RotateEvent>;
|
|
177
|
+
/**
|
|
178
|
+
* A reset verifies against the committed recovery digest, not the pre-rotation set. Both values it
|
|
179
|
+
* needs — the anchor digest and the recovery commitment — come from the inception, the *only* place
|
|
180
|
+
* either can: that is what lets a root holding nothing but its seed author one with no log
|
|
181
|
+
* availability. If the commitment could move, the root would have to read the log to know which key
|
|
182
|
+
* to sign with, and the one recovery path that survives losing the log would be gone.
|
|
183
|
+
* `recoveryPath(profile)` carries no index for the same reason — one recovery key per profile, for
|
|
184
|
+
* its lifetime. See {@link RotateEvent} for what was removed.
|
|
185
|
+
*/
|
|
186
|
+
export declare function verifyReset(signed: SignedEvent<RotateEvent>, inception: InceptionEvent): boolean;
|
|
187
|
+
/**
|
|
188
|
+
* How a `rev` target names a **key** rather than a DID: `#<the multibase key exactly as it appears in
|
|
189
|
+
* `k`>`, the same spelling a token's `kid` uses. One spelling for one thing — unambiguous against
|
|
190
|
+
* `did:…` on sight, so a bare multibase string is not a second accepted encoding, any more than for
|
|
191
|
+
* `kid`.
|
|
192
|
+
*/
|
|
193
|
+
export declare const KEY_TARGET_PREFIX = "#";
|
|
194
|
+
/** The deny-set spelling of `key` — what {@link createRevoke} takes as its target to deny it. */
|
|
195
|
+
export declare function keyTarget(key: string): string;
|
|
196
|
+
/**
|
|
197
|
+
* The key a `rev` target names, or `null` when it names a DID instead. Total on any string: a target
|
|
198
|
+
* of neither form lands in the deny set as the opaque identifier it is. The body is not validated
|
|
199
|
+
* against multibase — a `#` naming nothing denies nothing, exactly as a `did:` naming nobody does,
|
|
200
|
+
* and rejecting it would only add a way for a well-formed log to stop folding.
|
|
201
|
+
*/
|
|
202
|
+
export declare function keyFromTarget(target: string): string | null;
|
|
203
|
+
export type RevokeEvent = EventCommon & {
|
|
204
|
+
t: 'rev';
|
|
205
|
+
/**
|
|
206
|
+
* What to deny, in one of two spellings, never a capability `jti`.
|
|
207
|
+
*
|
|
208
|
+
* A **DID** (`did:…`) denies a holder: no capability whose `aud` is that DID is valid from this
|
|
209
|
+
* position onward — one entry covers a device's life. A **key** ({@link keyTarget}) denies a
|
|
210
|
+
* signer: nothing this profile signed with that key verifies from this position onward, and it
|
|
211
|
+
* names a key of *this* profile (the deny set is the subject's own).
|
|
212
|
+
*
|
|
213
|
+
* One field carries both because they are enforced at opposite ends of the same set: the DID form
|
|
214
|
+
* by `@kokuin/capability` against a capability's `aud`, the key form by the resolver against the
|
|
215
|
+
* key a `kid` selects. Neither spelling can be mistaken for the other.
|
|
216
|
+
*/
|
|
217
|
+
x: string;
|
|
218
|
+
/** A serialized capability authorising a non-authority signer. Verified in the fold. */
|
|
219
|
+
cap?: string;
|
|
220
|
+
};
|
|
221
|
+
export type CreateRevokeParams = {
|
|
222
|
+
seed: Uint8Array;
|
|
223
|
+
profile: number;
|
|
224
|
+
did: string;
|
|
225
|
+
prior: EventCommon;
|
|
226
|
+
target: string;
|
|
227
|
+
keyPosition: {
|
|
228
|
+
gen: number;
|
|
229
|
+
seq: number;
|
|
230
|
+
};
|
|
231
|
+
/** A serialized capability authorising this signer, when the signer is not an authority key. */
|
|
232
|
+
cap?: string;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Revoke a DID or a key — see {@link RevokeEvent.x} for the two spellings.
|
|
236
|
+
*
|
|
237
|
+
* Naming the device DID rather than a `jti` makes this one entry per device for its life — covering
|
|
238
|
+
* capabilities the verifier has never seen and future re-mints, where per-`jti` revocation would grow
|
|
239
|
+
* with every renewal. A key target retires a leaked key for material it *already* signed: `rotate`
|
|
240
|
+
* only retires it for new issuance (`resolve` is head-only), but already-issued material verifies
|
|
241
|
+
* through `resolveHistoric`, which survives a rotate by design, so a thief holding a rotated-away key
|
|
242
|
+
* could still mint one that verified. Retirement is therefore explicit, keeping the promise that a
|
|
243
|
+
* routine rotate does not invalidate already-issued material intact.
|
|
244
|
+
*
|
|
245
|
+
* **A key the profile currently publishes cannot be denied** — the fold rejects it (see the `rev`
|
|
246
|
+
* branch of `stepEvent`). Rotate first, then deny the key the rotate retired.
|
|
247
|
+
*
|
|
248
|
+
* `prior` answers "next sequence, what to chain to"; `keyPosition` separately answers "where the
|
|
249
|
+
* active authority key lives". They coincide only for an `icp`/`rot` prior — a `rev` establishes no
|
|
250
|
+
* key, so a revoke on a revoke still points at the last `icp`/`rot` position.
|
|
251
|
+
*/
|
|
252
|
+
export declare function createRevoke({ seed, profile, did, prior, target, keyPosition, cap, }: CreateRevokeParams): SignedEvent<RevokeEvent>;
|
|
253
|
+
export type CreateRevokeOptions = {
|
|
254
|
+
/** A serialized capability authorising this signer, when the signer is not an authority key. */
|
|
255
|
+
cap?: string;
|
|
256
|
+
};
|
|
257
|
+
export type CreateRevokeWithKeyParams = {
|
|
258
|
+
privateKey: Uint8Array;
|
|
259
|
+
did: string;
|
|
260
|
+
prior: EventCommon;
|
|
261
|
+
target: string;
|
|
262
|
+
/** A serialized capability authorising this signer, when the signer is not an authority key. */
|
|
263
|
+
cap?: string;
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Revoke a DID, signing with an Ed25519 key the caller already holds rather than deriving one from
|
|
267
|
+
* the profile seed.
|
|
268
|
+
*
|
|
269
|
+
* The builder for the actor a capability-authorised revoke exists for: a device holding a management
|
|
270
|
+
* capability, which by design never receives the profile sub-seed. `createRevoke` can only sign as
|
|
271
|
+
* the profile itself, so without this the feature had no API but re-implementing the signing
|
|
272
|
+
* convention per consumer — the duplication `createControllerCapabilityVerifier` prevents on the
|
|
273
|
+
* verifying side.
|
|
274
|
+
*
|
|
275
|
+
* The key is the audience key the capability pins in `cnf`, and the fold checks the event's signature
|
|
276
|
+
* against exactly that, so the two must match. Ed25519 only. Takes the private key rather than an
|
|
277
|
+
* identity because no identity type here signs raw bytes — a `KeyStore` entry hands back exactly this
|
|
278
|
+
* shape. Byte-identical to {@link createRevoke} given the same key; no `keyPosition`, which exists
|
|
279
|
+
* only to derive a key from a seed.
|
|
280
|
+
*/
|
|
281
|
+
export declare function createRevokeWithKey({ privateKey, did, prior, target, cap, }: CreateRevokeWithKeyParams): SignedEvent<RevokeEvent>;
|
|
282
|
+
/** Authority-signed revoke. Capability-authorised revokes are checked in the fold. */
|
|
283
|
+
export declare function verifyRevoke(signed: SignedEvent<RevokeEvent>, prior: {
|
|
284
|
+
digest: string;
|
|
285
|
+
keys: Array<string>;
|
|
286
|
+
}): boolean;
|