@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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type FullIdentity, type SigningIdentity } from '@kokuin/token';
|
|
2
|
+
import { type SignedEvent } from './events.js';
|
|
3
|
+
import type { FoldOptions } from './fold.js';
|
|
4
|
+
export type CreateControllerIdentityParams = {
|
|
5
|
+
seed: Uint8Array;
|
|
6
|
+
profile: number;
|
|
7
|
+
log: Array<SignedEvent>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A signing identity for a `did:kokuin:` profile, whose `iss` is the profile DID.
|
|
11
|
+
*
|
|
12
|
+
* Takes the log and folds it here, so the signing key is always the one that log's current state
|
|
13
|
+
* establishes, never one the caller named. The log is the caller's freshness contract: an identity
|
|
14
|
+
* built from a stale or truncated log signs with a retired key, minting tokens a current verifier
|
|
15
|
+
* rejects. Rebuild from a re-read log after a rotation rather than holding one across it.
|
|
16
|
+
*
|
|
17
|
+
* The key is derived at `keyGen`/`keySeq`, not `gen`/`seq`: a revoke advances the sequence without
|
|
18
|
+
* establishing a key (Amendment A), so deriving at `gen`/`seq` would produce a key never in `k` — an
|
|
19
|
+
* unverifiable token, silently. Every token carries `kid: #<the key that signed it>`, so there is no
|
|
20
|
+
* `kid` parameter — one naming any other key is a request it could not honour, and is rejected on
|
|
21
|
+
* mismatch rather than dropped.
|
|
22
|
+
*
|
|
23
|
+
* Synchronous, and stays so (kubun's apply path depends on it): a log whose revoke carries a
|
|
24
|
+
* capability cannot fold without awaiting a verifier, so it throws here — use
|
|
25
|
+
* {@link createControllerIdentityAsync}.
|
|
26
|
+
*
|
|
27
|
+
* @throws when the log does not fold, or when the derived key is not one of the profile's current
|
|
28
|
+
* authority keys (a wrong `seed` or `profile` for this log).
|
|
29
|
+
*/
|
|
30
|
+
export declare function createControllerIdentity({ seed, profile, log, }: CreateControllerIdentityParams): FullIdentity;
|
|
31
|
+
export type CreateControllerIdentityAsyncParams = CreateControllerIdentityParams & {
|
|
32
|
+
options?: FoldOptions;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Async sibling of {@link createControllerIdentity} for a log whose revoke carries a capability
|
|
36
|
+
* authorising a non-authority signer, which only `foldLogAsync` can check. Identical otherwise, so a
|
|
37
|
+
* log without such a revoke resolves to the same identity through either entry point.
|
|
38
|
+
*
|
|
39
|
+
* @param options forwarded to the async fold; without `verifyCapability` such a log still fails to
|
|
40
|
+
* fold rather than being trusted.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createControllerIdentityAsync({ seed, profile, log, options, }: CreateControllerIdentityAsyncParams): Promise<FullIdentity>;
|
|
43
|
+
export type CreateControllerIdentityWithKeyParams = {
|
|
44
|
+
privateKey: Uint8Array;
|
|
45
|
+
log: Array<SignedEvent>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A signing identity for a `did:kokuin:` profile, from the current authority private key rather than
|
|
49
|
+
* the profile seed.
|
|
50
|
+
*
|
|
51
|
+
* The seed form above needs the root seed to derive the key, but the custody tiers say the daily path
|
|
52
|
+
* must not touch it: the root seed lives on a Ledger or cold mnemonic, and a device never receives the
|
|
53
|
+
* profile sub-seed (a sub-seed holder can derive the *next* key and therefore rotate, voiding
|
|
54
|
+
* pre-rotation). This is the same split {@link createRevokeWithKey} makes, for the same reason.
|
|
55
|
+
*
|
|
56
|
+
* What the key in hand can do is exactly the intended granularity: it signs as the profile and cannot
|
|
57
|
+
* rotate, since a rotate must reveal the key the log committed in `n` and only the seed derives that.
|
|
58
|
+
* Losing it costs a rotate-then-deny, not the identity. Takes the raw private key (no identity type
|
|
59
|
+
* here signs raw bytes; a `KeyStore` entry hands back exactly this); the public half is derived from
|
|
60
|
+
* it, so a caller cannot present one key and sign with another.
|
|
61
|
+
*
|
|
62
|
+
* @throws when the log does not fold, or when the key's public half is not one of the profile's
|
|
63
|
+
* current authority keys — a stale log, or a key the profile has rotated away.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createControllerIdentityWithKey({ privateKey, log, }: CreateControllerIdentityWithKeyParams): SigningIdentity;
|
|
66
|
+
export type CreateControllerIdentityWithKeyAsyncParams = CreateControllerIdentityWithKeyParams & {
|
|
67
|
+
options?: FoldOptions;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Async sibling of {@link createControllerIdentityWithKey}, for a log whose revoke carries a
|
|
71
|
+
* capability authorising a non-authority signer. The pairing is not incidental: a profile that uses
|
|
72
|
+
* the management tier is the same profile whose daily signer should not hold the seed.
|
|
73
|
+
*/
|
|
74
|
+
export declare function createControllerIdentityWithKeyAsync({ privateKey, log, options, }: CreateControllerIdentityWithKeyAsyncParams): Promise<SigningIdentity>;
|
package/lib/identity.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createKeyAgreementIdentityForDID, createSigningIdentityForDID } from '@kokuin/token';
|
|
2
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
3
|
+
import { agreementPath, authorityPath, deriveKeyPair } from './derivation.js';
|
|
4
|
+
import { didFromInception } from './events.js';
|
|
5
|
+
import { encodeKey } from './keys.js';
|
|
6
|
+
import { currentState, currentStateAsync } from './state.js';
|
|
7
|
+
const CONTEXT = 'Controller identity';
|
|
8
|
+
/**
|
|
9
|
+
* The DID the log claims to be, read from its inception before any fold runs — the fold needs the DID
|
|
10
|
+
* to check the inception against, so it cannot be the thing that produces it.
|
|
11
|
+
*/ function didFromLog(log) {
|
|
12
|
+
if (log.length === 0) {
|
|
13
|
+
throw new Error(`${CONTEXT}: empty log`);
|
|
14
|
+
}
|
|
15
|
+
const first = log[0];
|
|
16
|
+
if (first.event.t !== 'icp') {
|
|
17
|
+
throw new Error(`${CONTEXT}: first event must be an inception`);
|
|
18
|
+
}
|
|
19
|
+
return didFromInception(first.event);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Stamp `kid` on every token this identity signs, naming the key that produced the signature. The
|
|
23
|
+
* identity derives exactly one key pair, so `kid` is a fact about the signature, not an input: a
|
|
24
|
+
* caller-supplied one is only ever checked, never honoured — dropping a mismatched one silently would
|
|
25
|
+
* mint a token whose header names a key that did not sign it. Both spellings are checked:
|
|
26
|
+
* `options.kid` and `options.header.kid`.
|
|
27
|
+
*/ function withKid(identity, kid) {
|
|
28
|
+
return {
|
|
29
|
+
...identity,
|
|
30
|
+
async signToken (payload, options = {}) {
|
|
31
|
+
for (const supplied of [
|
|
32
|
+
options.kid,
|
|
33
|
+
options.header?.kid
|
|
34
|
+
]){
|
|
35
|
+
if (supplied != null && supplied !== kid) {
|
|
36
|
+
throw new Error(`${CONTEXT}: cannot sign under kid ${String(supplied)}, this identity holds ${kid}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return identity.signToken(payload, {
|
|
40
|
+
...options,
|
|
41
|
+
header: {
|
|
42
|
+
...options.header,
|
|
43
|
+
kid
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Bind a private key the caller holds to the DID, having checked the folded state publishes its
|
|
51
|
+
* public half. Shared by all four entry points, so the membership rule and `kid` binding are written
|
|
52
|
+
* once. Membership, not `keys[0]`: a set may publish several keys and the one in hand need not be
|
|
53
|
+
* first (co-signers' keys belong to other holders); what must hold is that the resolver can answer
|
|
54
|
+
* with this key. A key outside the set signs unverifiable tokens, so fail loudly at construction.
|
|
55
|
+
*/ function identityForKey({ privateKey, publicKey, did, state, mismatch }) {
|
|
56
|
+
if (state.keys.length === 0) {
|
|
57
|
+
// Defensive, unreachable through either fold today (`verifySignatures` rejects an empty key set,
|
|
58
|
+
// `rev` carries `keys` forward). Kept so a future event type that can empty the set fails here
|
|
59
|
+
// rather than deriving a key for a controller that publishes none. `resolver.ts` holds it too.
|
|
60
|
+
throw new Error(`Controller ${did} has no signing key`);
|
|
61
|
+
}
|
|
62
|
+
const key = encodeKey(publicKey, 'EdDSA');
|
|
63
|
+
if (!state.keys.includes(key)) {
|
|
64
|
+
throw new Error(`${CONTEXT}: ${mismatch} of ${did}`);
|
|
65
|
+
}
|
|
66
|
+
// The resolver picks by `kid` and defaults to `keys[0]`, so a token whose key is not first is
|
|
67
|
+
// unverifiable unless the header names the key that signed it.
|
|
68
|
+
return withKid(createSigningIdentityForDID(did, privateKey), `#${key}`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Derive the signing key the folded state establishes and bind it to the DID. Shared by the sync and
|
|
72
|
+
* async seed entry points, which differ only in how they reach the state.
|
|
73
|
+
*/ function identityForState({ seed, profile, did, state }) {
|
|
74
|
+
const authority = deriveKeyPair(seed, authorityPath(profile, state.keyGen, state.keySeq), 'EdDSA');
|
|
75
|
+
const signing = identityForKey({
|
|
76
|
+
privateKey: authority.privateKey,
|
|
77
|
+
publicKey: authority.publicKey,
|
|
78
|
+
did,
|
|
79
|
+
state,
|
|
80
|
+
mismatch: 'derived key is not one of the current authority keys'
|
|
81
|
+
});
|
|
82
|
+
// The agreement key sits at the same (keyGen, keySeq) as the authority key — both icp and rot
|
|
83
|
+
// derive them at matching indices, and a rev carries the agreement set forward — so one position
|
|
84
|
+
// locates both. Verified against the folded set, failing closed like the authority membership check.
|
|
85
|
+
const agreement = deriveKeyPair(seed, agreementPath(profile, state.keyGen, state.keySeq), 'X25519');
|
|
86
|
+
const encoded = encodeKey(agreement.publicKey, 'X25519');
|
|
87
|
+
if (!state.agreement.includes(encoded)) {
|
|
88
|
+
throw new Error(`${CONTEXT}: derived agreement key is not one of the current agreement keys of ${did}`);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...signing,
|
|
92
|
+
...createKeyAgreementIdentityForDID(did, agreement.privateKey)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A signing identity for a `did:kokuin:` profile, whose `iss` is the profile DID.
|
|
97
|
+
*
|
|
98
|
+
* Takes the log and folds it here, so the signing key is always the one that log's current state
|
|
99
|
+
* establishes, never one the caller named. The log is the caller's freshness contract: an identity
|
|
100
|
+
* built from a stale or truncated log signs with a retired key, minting tokens a current verifier
|
|
101
|
+
* rejects. Rebuild from a re-read log after a rotation rather than holding one across it.
|
|
102
|
+
*
|
|
103
|
+
* The key is derived at `keyGen`/`keySeq`, not `gen`/`seq`: a revoke advances the sequence without
|
|
104
|
+
* establishing a key (Amendment A), so deriving at `gen`/`seq` would produce a key never in `k` — an
|
|
105
|
+
* unverifiable token, silently. Every token carries `kid: #<the key that signed it>`, so there is no
|
|
106
|
+
* `kid` parameter — one naming any other key is a request it could not honour, and is rejected on
|
|
107
|
+
* mismatch rather than dropped.
|
|
108
|
+
*
|
|
109
|
+
* Synchronous, and stays so (kubun's apply path depends on it): a log whose revoke carries a
|
|
110
|
+
* capability cannot fold without awaiting a verifier, so it throws here — use
|
|
111
|
+
* {@link createControllerIdentityAsync}.
|
|
112
|
+
*
|
|
113
|
+
* @throws when the log does not fold, or when the derived key is not one of the profile's current
|
|
114
|
+
* authority keys (a wrong `seed` or `profile` for this log).
|
|
115
|
+
*/ export function createControllerIdentity({ seed, profile, log }) {
|
|
116
|
+
const did = didFromLog(log);
|
|
117
|
+
return identityForState({
|
|
118
|
+
seed,
|
|
119
|
+
profile,
|
|
120
|
+
did,
|
|
121
|
+
state: currentState(did, log, CONTEXT)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Async sibling of {@link createControllerIdentity} for a log whose revoke carries a capability
|
|
126
|
+
* authorising a non-authority signer, which only `foldLogAsync` can check. Identical otherwise, so a
|
|
127
|
+
* log without such a revoke resolves to the same identity through either entry point.
|
|
128
|
+
*
|
|
129
|
+
* @param options forwarded to the async fold; without `verifyCapability` such a log still fails to
|
|
130
|
+
* fold rather than being trusted.
|
|
131
|
+
*/ export async function createControllerIdentityAsync({ seed, profile, log, options }) {
|
|
132
|
+
const did = didFromLog(log);
|
|
133
|
+
return identityForState({
|
|
134
|
+
seed,
|
|
135
|
+
profile,
|
|
136
|
+
did,
|
|
137
|
+
state: await currentStateAsync({
|
|
138
|
+
did,
|
|
139
|
+
events: log,
|
|
140
|
+
context: CONTEXT,
|
|
141
|
+
options: options
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* A signing identity for a `did:kokuin:` profile, from the current authority private key rather than
|
|
147
|
+
* the profile seed.
|
|
148
|
+
*
|
|
149
|
+
* The seed form above needs the root seed to derive the key, but the custody tiers say the daily path
|
|
150
|
+
* must not touch it: the root seed lives on a Ledger or cold mnemonic, and a device never receives the
|
|
151
|
+
* profile sub-seed (a sub-seed holder can derive the *next* key and therefore rotate, voiding
|
|
152
|
+
* pre-rotation). This is the same split {@link createRevokeWithKey} makes, for the same reason.
|
|
153
|
+
*
|
|
154
|
+
* What the key in hand can do is exactly the intended granularity: it signs as the profile and cannot
|
|
155
|
+
* rotate, since a rotate must reveal the key the log committed in `n` and only the seed derives that.
|
|
156
|
+
* Losing it costs a rotate-then-deny, not the identity. Takes the raw private key (no identity type
|
|
157
|
+
* here signs raw bytes; a `KeyStore` entry hands back exactly this); the public half is derived from
|
|
158
|
+
* it, so a caller cannot present one key and sign with another.
|
|
159
|
+
*
|
|
160
|
+
* @throws when the log does not fold, or when the key's public half is not one of the profile's
|
|
161
|
+
* current authority keys — a stale log, or a key the profile has rotated away.
|
|
162
|
+
*/ export function createControllerIdentityWithKey({ privateKey, log }) {
|
|
163
|
+
const did = didFromLog(log);
|
|
164
|
+
return identityForKey({
|
|
165
|
+
privateKey,
|
|
166
|
+
publicKey: ed25519.getPublicKey(privateKey),
|
|
167
|
+
did,
|
|
168
|
+
state: currentState(did, log, CONTEXT),
|
|
169
|
+
mismatch: 'the supplied key is not one of the current authority keys'
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Async sibling of {@link createControllerIdentityWithKey}, for a log whose revoke carries a
|
|
174
|
+
* capability authorising a non-authority signer. The pairing is not incidental: a profile that uses
|
|
175
|
+
* the management tier is the same profile whose daily signer should not hold the seed.
|
|
176
|
+
*/ export async function createControllerIdentityWithKeyAsync({ privateKey, log, options }) {
|
|
177
|
+
const did = didFromLog(log);
|
|
178
|
+
return identityForKey({
|
|
179
|
+
privateKey,
|
|
180
|
+
publicKey: ed25519.getPublicKey(privateKey),
|
|
181
|
+
did,
|
|
182
|
+
state: await currentStateAsync({
|
|
183
|
+
did,
|
|
184
|
+
events: log,
|
|
185
|
+
context: CONTEXT,
|
|
186
|
+
options: options
|
|
187
|
+
}),
|
|
188
|
+
mismatch: 'the supplied key is not one of the current authority keys'
|
|
189
|
+
});
|
|
190
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* did:kokuin controller key event log.
|
|
3
|
+
*
|
|
4
|
+
* ## Installation
|
|
5
|
+
*
|
|
6
|
+
* ```sh
|
|
7
|
+
* npm install @kokuin/controller
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @module controller
|
|
11
|
+
*/
|
|
12
|
+
export { canonicalBytes, digestOf, isCanonicalizable, MAX_CANONICAL_DEPTH, verifyDigest, withinCanonicalDepth, } from './canonical.js';
|
|
13
|
+
export { agreementPath, authorityPath, deriveKeyMaterial, deriveKeyPair, recoveryPath, } from './derivation.js';
|
|
14
|
+
export { type CreateRevokeOptions, type CreateRotateOptions, createInception, createReset, createRevoke, createRevokeWithKey, createRotate, DID_PREFIX, didFor, didFromInception, type EventCommon, type EventType, type InceptionEvent, KEY_TARGET_PREFIX, keyFromTarget, keyTarget, type RevokeEvent, type RotateEvent, type SignedEvent, verifyInception, verifyReset, verifyRevoke, verifyRotate, verifySignatures, } from './events.js';
|
|
15
|
+
export { CAPABILITY_REVOKE_NEEDS_ASYNC_FOLD, CAPABILITY_REVOKE_NEEDS_VERIFIER, CAPABILITY_VERIFIER_FAILED, CAPABILITY_VERIFIER_MALFORMED_ANSWER, type CapabilityAuthorisation, type FoldOptions, type FoldResult, foldLog, foldLogAsync, type KeyState, keyStateAt, MAX_SKIPPED_SLACK, pruneDenySet, REVOKE_NOT_SIGNED_BY_AUDIENCE, TOO_MANY_UNKNOWN_EVENTS, type VerifyCapabilityParams, } from './fold.js';
|
|
16
|
+
export { authoritativeStates, createMemoryLogStore, LOG_FORKED, LOG_NOT_AUTHORITATIVE, type LogStore, } from './history.js';
|
|
17
|
+
export { createControllerIdentity, createControllerIdentityAsync, createControllerIdentityWithKey, createControllerIdentityWithKeyAsync, } from './identity.js';
|
|
18
|
+
export { decodeKey, encodeKey, type KeyAlgorithm, type TaggedKey, tryDecodeKey, } from './keys.js';
|
|
19
|
+
export { type ProvideControllerIdentityParams, provideControllerIdentity, } from './keystore-identity.js';
|
|
20
|
+
export { enumerateProfiles, handleForDID, type ProfileEntry } from './profiles.js';
|
|
21
|
+
export { type ControllerResolverOptions, createControllerResolver } from './resolver.js';
|
|
22
|
+
export { type Duplicity, type ResolveFailure, type ResolveResult, resolveBranches, resolveBranchesAsync, } from './supersede.js';
|
|
23
|
+
export { VERSION_TAG } from './version.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* did:kokuin controller key event log.
|
|
3
|
+
*
|
|
4
|
+
* ## Installation
|
|
5
|
+
*
|
|
6
|
+
* ```sh
|
|
7
|
+
* npm install @kokuin/controller
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @module controller
|
|
11
|
+
*/ export { canonicalBytes, digestOf, isCanonicalizable, MAX_CANONICAL_DEPTH, verifyDigest, withinCanonicalDepth } from './canonical.js';
|
|
12
|
+
export { agreementPath, authorityPath, deriveKeyMaterial, deriveKeyPair, recoveryPath } from './derivation.js';
|
|
13
|
+
export { createInception, createReset, createRevoke, createRevokeWithKey, createRotate, DID_PREFIX, didFor, didFromInception, KEY_TARGET_PREFIX, keyFromTarget, keyTarget, verifyInception, verifyReset, verifyRevoke, verifyRotate, verifySignatures } from './events.js';
|
|
14
|
+
export { CAPABILITY_REVOKE_NEEDS_ASYNC_FOLD, CAPABILITY_REVOKE_NEEDS_VERIFIER, CAPABILITY_VERIFIER_FAILED, CAPABILITY_VERIFIER_MALFORMED_ANSWER, foldLog, foldLogAsync, keyStateAt, MAX_SKIPPED_SLACK, pruneDenySet, REVOKE_NOT_SIGNED_BY_AUDIENCE, TOO_MANY_UNKNOWN_EVENTS } from './fold.js';
|
|
15
|
+
export { authoritativeStates, createMemoryLogStore, LOG_FORKED, LOG_NOT_AUTHORITATIVE } from './history.js';
|
|
16
|
+
export { createControllerIdentity, createControllerIdentityAsync, createControllerIdentityWithKey, createControllerIdentityWithKeyAsync } from './identity.js';
|
|
17
|
+
export { decodeKey, encodeKey, tryDecodeKey } from './keys.js';
|
|
18
|
+
export { provideControllerIdentity } from './keystore-identity.js';
|
|
19
|
+
export { enumerateProfiles, handleForDID } from './profiles.js';
|
|
20
|
+
export { createControllerResolver } from './resolver.js';
|
|
21
|
+
export { resolveBranches, resolveBranchesAsync } from './supersede.js';
|
|
22
|
+
export { VERSION_TAG } from './version.js';
|
package/lib/keys.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type KeyAlgorithm = 'EdDSA' | 'X25519';
|
|
2
|
+
export type TaggedKey = {
|
|
3
|
+
alg: KeyAlgorithm;
|
|
4
|
+
publicKey: Uint8Array;
|
|
5
|
+
};
|
|
6
|
+
export declare function encodeKey(publicKey: Uint8Array, alg: KeyAlgorithm): string;
|
|
7
|
+
/** Total: any malformed, unknown-codec, or wrong-length value yields undefined rather than throwing. */
|
|
8
|
+
export declare function tryDecodeKey(value: string): TaggedKey | undefined;
|
|
9
|
+
export declare function decodeKey(value: string): TaggedKey;
|
package/lib/keys.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { decodeMultibase, encodeMultibase } from '@kokuin/token';
|
|
2
|
+
/**
|
|
3
|
+
* Multicodec prefixes, following the `did:key` convention. Tagging is what makes a new algorithm
|
|
4
|
+
* additive: an untagged key is opaque bytes, and telling X25519 from a future algorithm of the
|
|
5
|
+
* same length would mean sniffing the length.
|
|
6
|
+
*/ const CODECS = {
|
|
7
|
+
EdDSA: new Uint8Array([
|
|
8
|
+
0xed,
|
|
9
|
+
0x01
|
|
10
|
+
]),
|
|
11
|
+
X25519: new Uint8Array([
|
|
12
|
+
0xec,
|
|
13
|
+
0x01
|
|
14
|
+
])
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Expected payload length per algorithm, checked after the codec prefix is stripped. A per-algorithm
|
|
18
|
+
* table rather than a bare `=== 32`, so a future algorithm with a different length slots in without
|
|
19
|
+
* touching this check. Without this, a bare untagged key can collide with a codec prefix by chance
|
|
20
|
+
* (about 1 in 65536) and be silently accepted at the wrong length, and a truncated payload of the
|
|
21
|
+
* right prefix but wrong length would mint a key that throws downstream instead of failing here.
|
|
22
|
+
*/ const KEY_LENGTHS = {
|
|
23
|
+
EdDSA: 32,
|
|
24
|
+
X25519: 32
|
|
25
|
+
};
|
|
26
|
+
export function encodeKey(publicKey, alg) {
|
|
27
|
+
const codec = CODECS[alg];
|
|
28
|
+
const bytes = new Uint8Array(codec.length + publicKey.length);
|
|
29
|
+
bytes.set(codec, 0);
|
|
30
|
+
bytes.set(publicKey, codec.length);
|
|
31
|
+
return encodeMultibase(bytes);
|
|
32
|
+
}
|
|
33
|
+
/** Total: any malformed, unknown-codec, or wrong-length value yields undefined rather than throwing. */ export function tryDecodeKey(value) {
|
|
34
|
+
let bytes;
|
|
35
|
+
try {
|
|
36
|
+
bytes = decodeMultibase(value);
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
for (const alg of Object.keys(CODECS)){
|
|
41
|
+
const codec = CODECS[alg];
|
|
42
|
+
if (bytes.length > codec.length && bytes[0] === codec[0] && bytes[1] === codec[1]) {
|
|
43
|
+
const publicKey = bytes.slice(codec.length);
|
|
44
|
+
if (publicKey.length !== KEY_LENGTHS[alg]) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
alg,
|
|
49
|
+
publicKey
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export function decodeKey(value) {
|
|
56
|
+
const key = tryDecodeKey(value);
|
|
57
|
+
if (key == null) {
|
|
58
|
+
throw new Error(`Unrecognised key encoding: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
return key;
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FullIdentity, KeyEntry } from '@kokuin/token';
|
|
2
|
+
import type { FoldOptions } from './fold.js';
|
|
3
|
+
import type { LogStore } from './history.js';
|
|
4
|
+
export type ProvideControllerIdentityParams = {
|
|
5
|
+
/** The seed source: `provideAsync()`'s bytes are the controller root seed. Generates on first use. */
|
|
6
|
+
entry: KeyEntry<Uint8Array>;
|
|
7
|
+
/** Which profile under the seed to resolve. */
|
|
8
|
+
profile: number;
|
|
9
|
+
/** Where the last accepted log for the DID lives — loaded to restore, written to on first generate. */
|
|
10
|
+
logStore: LogStore;
|
|
11
|
+
/** Forwarded to the fold for a log whose revoke carries a capability only the async fold can verify. */
|
|
12
|
+
options?: FoldOptions;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a `did:kokuin:` {@link FullIdentity} from a keystore entry and a log store.
|
|
16
|
+
*
|
|
17
|
+
* Generate and restore in one call: the entry yields the seed (a fresh one on first use, the stored
|
|
18
|
+
* one after), and the log store yields the event log (bootstrapping the inception when the DID has no
|
|
19
|
+
* log yet). The DID is a pure function of `(seed, profile)`, so the same entry and profile always
|
|
20
|
+
* resolve the same identity. A `KeyStore` caller passes `keyStore.entry(keyID)` as `entry`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function provideControllerIdentity({ entry, profile, logStore, options, }: ProvideControllerIdentityParams): Promise<FullIdentity>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createInception, didFor } from './events.js';
|
|
2
|
+
import { createControllerIdentityAsync } from './identity.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a `did:kokuin:` {@link FullIdentity} from a keystore entry and a log store.
|
|
5
|
+
*
|
|
6
|
+
* Generate and restore in one call: the entry yields the seed (a fresh one on first use, the stored
|
|
7
|
+
* one after), and the log store yields the event log (bootstrapping the inception when the DID has no
|
|
8
|
+
* log yet). The DID is a pure function of `(seed, profile)`, so the same entry and profile always
|
|
9
|
+
* resolve the same identity. A `KeyStore` caller passes `keyStore.entry(keyID)` as `entry`.
|
|
10
|
+
*/ export async function provideControllerIdentity({ entry, profile, logStore, options }) {
|
|
11
|
+
const seed = await entry.provideAsync();
|
|
12
|
+
const did = didFor(seed, profile);
|
|
13
|
+
let log = await logStore.get(did);
|
|
14
|
+
if (log == null) {
|
|
15
|
+
log = [
|
|
16
|
+
createInception(seed, profile)
|
|
17
|
+
];
|
|
18
|
+
await logStore.set(did, log);
|
|
19
|
+
}
|
|
20
|
+
return createControllerIdentityAsync({
|
|
21
|
+
seed,
|
|
22
|
+
profile,
|
|
23
|
+
log,
|
|
24
|
+
options
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ProfileEntry = {
|
|
2
|
+
index: number;
|
|
3
|
+
did: string;
|
|
4
|
+
handle: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* A three-syllable handle derived from the DID itself — no stored data, no wordlist, so a cold picker
|
|
8
|
+
* can label profiles with no network or cache. Each syllable is consonant-vowel-consonant, so it is
|
|
9
|
+
* pronounceable; ~30 bits across three, ample to tell a handful of profiles apart and not a security
|
|
10
|
+
* boundary. The label is a function of the identifier, never the reverse (a user label must never
|
|
11
|
+
* feed the DID).
|
|
12
|
+
*/
|
|
13
|
+
export declare function handleForDID(did: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Enumerate the first `count` profiles for a seed.
|
|
16
|
+
*
|
|
17
|
+
* Every index yields a valid-looking DID whether or not the profile was ever used — which
|
|
18
|
+
* profiles were used is not seed-derived. A picker should present all of them and let a probe
|
|
19
|
+
* grey out the unused ones when a group, hub, or cache is reachable.
|
|
20
|
+
*/
|
|
21
|
+
export declare function enumerateProfiles(seed: Uint8Array, count: number): Array<ProfileEntry>;
|
package/lib/profiles.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
2
|
+
import { createInception, didFromInception } from './events.js';
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
// Frozen forever: a handle a user wrote down must keep resolving to the same profile. Letters
|
|
5
|
+
// that are easily confused when read aloud or handwritten are left out — no c/k or q, no i/l.
|
|
6
|
+
const CONSONANTS = 'bdfghjkmnprstvwz';
|
|
7
|
+
const VOWELS = 'aeou';
|
|
8
|
+
/**
|
|
9
|
+
* A three-syllable handle derived from the DID itself — no stored data, no wordlist, so a cold picker
|
|
10
|
+
* can label profiles with no network or cache. Each syllable is consonant-vowel-consonant, so it is
|
|
11
|
+
* pronounceable; ~30 bits across three, ample to tell a handful of profiles apart and not a security
|
|
12
|
+
* boundary. The label is a function of the identifier, never the reverse (a user label must never
|
|
13
|
+
* feed the DID).
|
|
14
|
+
*/ export function handleForDID(did) {
|
|
15
|
+
const digest = sha256(encoder.encode(did));
|
|
16
|
+
const syllables = [];
|
|
17
|
+
for(let i = 0; i < 3; i++){
|
|
18
|
+
syllables.push(CONSONANTS[digest[i * 3] % CONSONANTS.length] + VOWELS[digest[i * 3 + 1] % VOWELS.length] + CONSONANTS[digest[i * 3 + 2] % CONSONANTS.length]);
|
|
19
|
+
}
|
|
20
|
+
return syllables.join('-');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Enumerate the first `count` profiles for a seed.
|
|
24
|
+
*
|
|
25
|
+
* Every index yields a valid-looking DID whether or not the profile was ever used — which
|
|
26
|
+
* profiles were used is not seed-derived. A picker should present all of them and let a probe
|
|
27
|
+
* grey out the unused ones when a group, hub, or cache is reachable.
|
|
28
|
+
*/ export function enumerateProfiles(seed, count) {
|
|
29
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
30
|
+
throw new Error('enumerateProfiles: count must be at least 1');
|
|
31
|
+
}
|
|
32
|
+
const entries = [];
|
|
33
|
+
for(let index = 0; index < count; index++){
|
|
34
|
+
const did = didFromInception(createInception(seed, index).event);
|
|
35
|
+
entries.push({
|
|
36
|
+
index,
|
|
37
|
+
did,
|
|
38
|
+
handle: handleForDID(did)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DIDMethodResolver } from '@kokuin/token';
|
|
2
|
+
import { type SignedEvent } from './events.js';
|
|
3
|
+
import type { FoldOptions } from './fold.js';
|
|
4
|
+
import { type LogStore } from './history.js';
|
|
5
|
+
export type ControllerResolverOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* Load a controller's event log. Returns undefined when the DID is unknown.
|
|
8
|
+
*
|
|
9
|
+
* **Answer with the whole log**, even when a `verifyCapability` is configured. A capability
|
|
10
|
+
* authorising a revoke must be checked at the position the fold is at — a rotated-away key set must
|
|
11
|
+
* not verify a grant made under it, a device denied before that position must not author one —
|
|
12
|
+
* and neither question can be answered from a DID alone, all this callback receives. So neither is
|
|
13
|
+
* asked of it: the fold answers both by handing the verifier a resolver built from the prefix
|
|
14
|
+
* states. See `FoldOptions.verifyCapability`.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const resolver = createControllerResolver({
|
|
18
|
+
* loadLog: async (did) => await store.log(did),
|
|
19
|
+
* verifyCapability: createControllerCapabilityVerifier(),
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Reuse the instance rather than building one per resolution: concurrent resolutions of one DID
|
|
24
|
+
* share a single in-flight fold. Not a cache — the entry drops as soon as the fold settles.
|
|
25
|
+
*
|
|
26
|
+
* One re-entry hazard remains, not this callback's: a `verifyToken` hook or a `resolver` passed to
|
|
27
|
+
* the capability verifier that resolves *this* DID through *this* resolver joins the calling fold
|
|
28
|
+
* and awaits a promise that cannot settle. Give such a hook a resolver of its own.
|
|
29
|
+
*/
|
|
30
|
+
loadLog(did: string): Promise<Array<SignedEvent> | undefined>;
|
|
31
|
+
/**
|
|
32
|
+
* Verify a capability authorising a non-authority signer to revoke, forwarded to the fold. Optional
|
|
33
|
+
* because most logs carry none: without it a log containing one fails to fold rather than being
|
|
34
|
+
* trusted. `createControllerCapabilityVerifier()` from `@kokuin/capability` is the implementation;
|
|
35
|
+
* it needs a registry only for a chain delegate whose DID method is not resolvable from the
|
|
36
|
+
* identifier alone.
|
|
37
|
+
*/
|
|
38
|
+
verifyCapability?: FoldOptions['verifyCapability'];
|
|
39
|
+
/**
|
|
40
|
+
* Where to remember the last log accepted for each DID, so a later one that is *behind* it is
|
|
41
|
+
* refused rather than answered from. Without one, truncation is a silent revocation bypass: a peer
|
|
42
|
+
* serving a prefix that stops just before the `rev` denying their device produces a log that folds
|
|
43
|
+
* cleanly and yields a deny set missing exactly that entry.
|
|
44
|
+
*
|
|
45
|
+
* Optional, because the guarantee needs storage this package cannot provide and a first encounter
|
|
46
|
+
* has nothing to compare against. **Configure one if this process resolves the same DIDs more than
|
|
47
|
+
* once.** See {@link LogStore}, and `createMemoryLogStore` for the process-lifetime version.
|
|
48
|
+
*/
|
|
49
|
+
history?: LogStore;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A `did:kokuin:` resolver for `@kokuin/token`. Injected rather than imported by token: this package
|
|
53
|
+
* depends on token for signing, so the reverse import would cycle. Token resolves `iss` through the
|
|
54
|
+
* interface without knowing the fold exists.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createControllerResolver(options: ControllerResolverOptions): DIDMethodResolver;
|