@kokuin/token 0.4.0 → 0.5.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/did.d.ts +100 -5
- package/lib/did.js +168 -11
- package/lib/identity.d.ts +26 -8
- package/lib/identity.js +48 -33
- package/lib/index.d.ts +3 -5
- package/lib/index.js +3 -4
- package/lib/method.d.ts +70 -0
- package/lib/method.js +8 -0
- package/lib/schemas.d.ts +22 -4
- package/lib/schemas.js +16 -3
- package/lib/token.d.ts +23 -0
- package/lib/token.js +32 -8
- package/package.json +2 -3
- package/lib/jwe.d.ts +0 -106
- package/lib/jwe.js +0 -315
- package/lib/rotation.d.ts +0 -16
- package/lib/rotation.js +0 -13
package/lib/did.d.ts
CHANGED
|
@@ -1,15 +1,86 @@
|
|
|
1
1
|
import type { DIDResolver } from './cache.js';
|
|
2
|
+
import { type MethodRegistry } from './method.js';
|
|
2
3
|
import type { DIDDoc } from './peer4.js';
|
|
3
4
|
import type { SignatureAlgorithm } from './schemas.js';
|
|
4
5
|
import type { DIDString } from './types.js';
|
|
5
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Multicodec prefixes per signature algorithm, following the `did:key` convention. Supported, not
|
|
8
|
+
* internal: `@kokuin/capability` builds the `cnf.kid` confirmation claim from this and
|
|
9
|
+
* {@link getAlgorithmAndPublicKey}, and `cnf.kid` is a published wire format third parties produce
|
|
10
|
+
* and read.
|
|
11
|
+
*/
|
|
6
12
|
export declare const CODECS: Record<SignatureAlgorithm, Uint8Array>;
|
|
7
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Split multicodec-prefixed key bytes into their algorithm and the raw public key, or `null` when
|
|
15
|
+
* the prefix names no algorithm this package knows.
|
|
16
|
+
*
|
|
17
|
+
* The decoder half of the `cnf.kid` wire format — see {@link CODECS}.
|
|
18
|
+
*/
|
|
8
19
|
export declare function getAlgorithmAndPublicKey(bytes: Uint8Array): [SignatureAlgorithm, Uint8Array] | null;
|
|
9
20
|
/** @internal */
|
|
10
21
|
export declare function getDID(codec: Uint8Array, publicKey: Uint8Array): DIDString;
|
|
11
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The signature algorithm and raw public key a `did:key` identifier carries. Throws when the string
|
|
24
|
+
* is not a `did:key`, its encoded form is implausibly long, or the codec names no supported
|
|
25
|
+
* algorithm.
|
|
26
|
+
*
|
|
27
|
+
* Supported, not internal: `@kumiai/mls` checks an MLS credential's key against the key its DID
|
|
28
|
+
* names, a comparison only this function makes correctly — the alternative is a second decoder in
|
|
29
|
+
* another repo whose failure mode is accepting a credential for the wrong key. Any other method
|
|
30
|
+
* resolves through a `DIDMethodResolver`; this is the self-contained case.
|
|
31
|
+
*/
|
|
12
32
|
export declare function getSignatureInfo(did: string): [SignatureAlgorithm, Uint8Array];
|
|
33
|
+
/**
|
|
34
|
+
* The issuer could not be resolved to a usable signing key: no method, resolver, or registry entry
|
|
35
|
+
* turned `iss` into one — including a resolver that has no answer, throws, or answers with something
|
|
36
|
+
* unusable (an oversized document, or one not hashing to the DID asked for).
|
|
37
|
+
*
|
|
38
|
+
* Distinct from every other verification failure on purpose. An invalid signature or a bad `kid`
|
|
39
|
+
* means the issuer *was* resolved and the token is bad (positive evidence); this means nothing was
|
|
40
|
+
* learned either way, and a caller treating "could not check" as "checked and fine" fails open.
|
|
41
|
+
* `@kokuin/capability`'s revocation checker turns on this distinction, so it must be a type, not a
|
|
42
|
+
* message — text matching is how such a check regresses silently.
|
|
43
|
+
*/
|
|
44
|
+
export declare class UnresolvableIssuerError extends Error {
|
|
45
|
+
/**
|
|
46
|
+
* Identifies the error by value, not identity: within this package `instanceof` is exact, but a
|
|
47
|
+
* duplicated `@kokuin/token` in a consumer's tree would make a cross-copy `instanceof` false (which
|
|
48
|
+
* fails closed, so this is hardening). Consumers should use `isUnresolvableIssuerError`.
|
|
49
|
+
*/
|
|
50
|
+
static get brand(): string;
|
|
51
|
+
/** The brand, readable from an instance — what `isUnresolvableIssuerError` matches on. */
|
|
52
|
+
get brand(): string;
|
|
53
|
+
constructor(message: string, options?: ErrorOptions);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether a thrown value means the issuer could not be resolved.
|
|
57
|
+
*
|
|
58
|
+
* Prefer this to `instanceof UnresolvableIssuerError` across a package boundary: it matches on the
|
|
59
|
+
* brand, so it holds even if the thrower and the checker resolved different copies of this package.
|
|
60
|
+
*/
|
|
61
|
+
export declare function isUnresolvableIssuerError(value: unknown): value is UnresolvableIssuerError;
|
|
62
|
+
/**
|
|
63
|
+
* A DID method resolved the issuer, and the token then named a key that issuer does not have. The
|
|
64
|
+
* counterpart of {@link UnresolvableIssuerError}: a `DIDMethodResolver` needs a way to say "resolved,
|
|
65
|
+
* and the token is bad" too, else `resolveIssuerWithDoc` wraps everything a method throws as
|
|
66
|
+
* unresolvable and an unauthenticated `kid` naming a real DID and an invented key reads as "could not
|
|
67
|
+
* check" rather than "checked, and bad". Throw it only for that; not knowing the DID at all is
|
|
68
|
+
* {@link UnresolvableIssuerError}.
|
|
69
|
+
*/
|
|
70
|
+
export declare class IssuerKeyNotFoundError extends Error {
|
|
71
|
+
/** @see UnresolvableIssuerError.brand — same reasoning, across the same package boundary. */
|
|
72
|
+
static get brand(): string;
|
|
73
|
+
/** The brand, readable from an instance — what `isIssuerKeyNotFoundError` matches on. */
|
|
74
|
+
get brand(): string;
|
|
75
|
+
constructor(message: string, options?: ErrorOptions);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether a thrown value means the issuer resolved but does not have the key the token named.
|
|
79
|
+
*
|
|
80
|
+
* Prefer this to `instanceof IssuerKeyNotFoundError` across a package boundary, for the same
|
|
81
|
+
* reason as {@link isUnresolvableIssuerError}.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isIssuerKeyNotFoundError(value: unknown): value is IssuerKeyNotFoundError;
|
|
13
84
|
export type ResolveIssuerHeader = {
|
|
14
85
|
kid?: string;
|
|
15
86
|
};
|
|
@@ -22,16 +93,40 @@ export type ResolveIssuerWithDocResult = {
|
|
|
22
93
|
doc: DIDDoc;
|
|
23
94
|
};
|
|
24
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* Whether to resolve the issuer's *current* signing key or one it signed with in the past. `false`
|
|
98
|
+
* (default) asks {@link DIDMethodResolver.resolve} — the safe question, right for a live signer.
|
|
99
|
+
* `true` asks {@link DIDMethodResolver.resolveHistoric}, an explicit statement that the artefact was
|
|
100
|
+
* issued in the past and must survive key rotation. A resolver with no `resolveHistoric` **refuses**
|
|
101
|
+
* rather than falling back to `resolve`, which would be the permissive scan by another name.
|
|
102
|
+
*/
|
|
103
|
+
export type ResolveIssuerMode = {
|
|
104
|
+
historic?: boolean;
|
|
105
|
+
};
|
|
106
|
+
/** Params for {@link resolveIssuer} / {@link resolveIssuerWithDoc}. */
|
|
107
|
+
export type ResolveIssuerParams = {
|
|
108
|
+
iss: string;
|
|
109
|
+
header?: ResolveIssuerHeader;
|
|
110
|
+
resolver?: DIDResolver;
|
|
111
|
+
methods?: MethodRegistry;
|
|
112
|
+
/** See {@link ResolveIssuerMode.historic}. */
|
|
113
|
+
historic?: boolean;
|
|
114
|
+
};
|
|
25
115
|
/**
|
|
26
116
|
* Resolve a token issuer (did:key or did:peer:4) and return alg + public key,
|
|
27
117
|
* plus the decoded peer:4 doc when one was obtained inline or via the resolver.
|
|
28
118
|
* Callers writing to a DID cache should write `peer4Doc` only after signature verification.
|
|
119
|
+
*
|
|
120
|
+
* The `historic` param selects which question is asked of a `DIDMethodResolver` — see
|
|
121
|
+
* {@link ResolveIssuerMode}. It reaches only the method-registry branch: `did:key` carries its key
|
|
122
|
+
* in the identifier and a `did:peer:4` document is fixed by its own hash, so neither has a past key
|
|
123
|
+
* set distinct from its present one.
|
|
29
124
|
*/
|
|
30
|
-
export declare function resolveIssuerWithDoc(iss
|
|
125
|
+
export declare function resolveIssuerWithDoc({ iss, header, resolver, methods, historic, }: ResolveIssuerParams): Promise<ResolveIssuerWithDocResult>;
|
|
31
126
|
/**
|
|
32
127
|
* Resolve a token issuer to [alg, publicKey]. Backward-compatible wrapper around resolveIssuerWithDoc.
|
|
33
128
|
*/
|
|
34
|
-
export declare function resolveIssuer(iss
|
|
129
|
+
export declare function resolveIssuer({ iss, header, resolver, methods, historic, }: ResolveIssuerParams): Promise<[SignatureAlgorithm, Uint8Array]>;
|
|
35
130
|
/**
|
|
36
131
|
* Fold a DID to its canonical form for equality comparison.
|
|
37
132
|
* For did:peer:4, returns the short form regardless of whether input is long or short.
|
package/lib/did.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { base58 } from '@scure/base';
|
|
2
|
+
import { findMethodResolver } from './method.js';
|
|
2
3
|
import { decodeMultibase } from './multibase.js';
|
|
3
4
|
import { assertDocWithinMaxSize, decodePeer4, encodePeer4, getPeer4ShortForm, isPeer4 } from './peer4.js';
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Multicodec prefixes per signature algorithm, following the `did:key` convention. Supported, not
|
|
7
|
+
* internal: `@kokuin/capability` builds the `cnf.kid` confirmation claim from this and
|
|
8
|
+
* {@link getAlgorithmAndPublicKey}, and `cnf.kid` is a published wire format third parties produce
|
|
9
|
+
* and read.
|
|
10
|
+
*/ export const CODECS = {
|
|
5
11
|
ES256: new Uint8Array([
|
|
6
12
|
128,
|
|
7
13
|
36
|
|
@@ -28,7 +34,12 @@ function isCodecMatch(codec, bytes) {
|
|
|
28
34
|
}
|
|
29
35
|
return true;
|
|
30
36
|
}
|
|
31
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Split multicodec-prefixed key bytes into their algorithm and the raw public key, or `null` when
|
|
39
|
+
* the prefix names no algorithm this package knows.
|
|
40
|
+
*
|
|
41
|
+
* The decoder half of the `cnf.kid` wire format — see {@link CODECS}.
|
|
42
|
+
*/ export function getAlgorithmAndPublicKey(bytes) {
|
|
32
43
|
for (const [alg, codec] of Object.entries(CODECS)){
|
|
33
44
|
if (isCodecMatch(codec, bytes)) {
|
|
34
45
|
return [
|
|
@@ -47,7 +58,16 @@ function isCodecMatch(codec, bytes) {
|
|
|
47
58
|
bytes.set(publicKey, codec.length);
|
|
48
59
|
return `${PREFIX}${base58.encode(bytes)}`;
|
|
49
60
|
}
|
|
50
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* The signature algorithm and raw public key a `did:key` identifier carries. Throws when the string
|
|
63
|
+
* is not a `did:key`, its encoded form is implausibly long, or the codec names no supported
|
|
64
|
+
* algorithm.
|
|
65
|
+
*
|
|
66
|
+
* Supported, not internal: `@kumiai/mls` checks an MLS credential's key against the key its DID
|
|
67
|
+
* names, a comparison only this function makes correctly — the alternative is a second decoder in
|
|
68
|
+
* another repo whose failure mode is accepting a credential for the wrong key. Any other method
|
|
69
|
+
* resolves through a `DIDMethodResolver`; this is the self-contained case.
|
|
70
|
+
*/ export function getSignatureInfo(did) {
|
|
51
71
|
if (!did.startsWith(PREFIX)) {
|
|
52
72
|
throw new Error('Invalid DID format');
|
|
53
73
|
}
|
|
@@ -67,11 +87,114 @@ function isCodecMatch(codec, bytes) {
|
|
|
67
87
|
}
|
|
68
88
|
return info;
|
|
69
89
|
}
|
|
90
|
+
const UNRESOLVABLE_ISSUER_BRAND = '@kokuin/token/UnresolvableIssuerError';
|
|
91
|
+
/**
|
|
92
|
+
* The issuer could not be resolved to a usable signing key: no method, resolver, or registry entry
|
|
93
|
+
* turned `iss` into one — including a resolver that has no answer, throws, or answers with something
|
|
94
|
+
* unusable (an oversized document, or one not hashing to the DID asked for).
|
|
95
|
+
*
|
|
96
|
+
* Distinct from every other verification failure on purpose. An invalid signature or a bad `kid`
|
|
97
|
+
* means the issuer *was* resolved and the token is bad (positive evidence); this means nothing was
|
|
98
|
+
* learned either way, and a caller treating "could not check" as "checked and fine" fails open.
|
|
99
|
+
* `@kokuin/capability`'s revocation checker turns on this distinction, so it must be a type, not a
|
|
100
|
+
* message — text matching is how such a check regresses silently.
|
|
101
|
+
*/ export class UnresolvableIssuerError extends Error {
|
|
102
|
+
/**
|
|
103
|
+
* Identifies the error by value, not identity: within this package `instanceof` is exact, but a
|
|
104
|
+
* duplicated `@kokuin/token` in a consumer's tree would make a cross-copy `instanceof` false (which
|
|
105
|
+
* fails closed, so this is hardening). Consumers should use `isUnresolvableIssuerError`.
|
|
106
|
+
*/ static get brand() {
|
|
107
|
+
return UNRESOLVABLE_ISSUER_BRAND;
|
|
108
|
+
}
|
|
109
|
+
/** The brand, readable from an instance — what `isUnresolvableIssuerError` matches on. */ get brand() {
|
|
110
|
+
return UNRESOLVABLE_ISSUER_BRAND;
|
|
111
|
+
}
|
|
112
|
+
constructor(message, options){
|
|
113
|
+
super(message, options);
|
|
114
|
+
this.name = 'UnresolvableIssuerError';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Whether a thrown value means the issuer could not be resolved.
|
|
119
|
+
*
|
|
120
|
+
* Prefer this to `instanceof UnresolvableIssuerError` across a package boundary: it matches on the
|
|
121
|
+
* brand, so it holds even if the thrower and the checker resolved different copies of this package.
|
|
122
|
+
*/ export function isUnresolvableIssuerError(value) {
|
|
123
|
+
return value instanceof Error && value.brand === UNRESOLVABLE_ISSUER_BRAND;
|
|
124
|
+
}
|
|
125
|
+
const ISSUER_KEY_NOT_FOUND_BRAND = '@kokuin/token/IssuerKeyNotFoundError';
|
|
126
|
+
/**
|
|
127
|
+
* A DID method resolved the issuer, and the token then named a key that issuer does not have. The
|
|
128
|
+
* counterpart of {@link UnresolvableIssuerError}: a `DIDMethodResolver` needs a way to say "resolved,
|
|
129
|
+
* and the token is bad" too, else `resolveIssuerWithDoc` wraps everything a method throws as
|
|
130
|
+
* unresolvable and an unauthenticated `kid` naming a real DID and an invented key reads as "could not
|
|
131
|
+
* check" rather than "checked, and bad". Throw it only for that; not knowing the DID at all is
|
|
132
|
+
* {@link UnresolvableIssuerError}.
|
|
133
|
+
*/ export class IssuerKeyNotFoundError extends Error {
|
|
134
|
+
/** @see UnresolvableIssuerError.brand — same reasoning, across the same package boundary. */ static get brand() {
|
|
135
|
+
return ISSUER_KEY_NOT_FOUND_BRAND;
|
|
136
|
+
}
|
|
137
|
+
/** The brand, readable from an instance — what `isIssuerKeyNotFoundError` matches on. */ get brand() {
|
|
138
|
+
return ISSUER_KEY_NOT_FOUND_BRAND;
|
|
139
|
+
}
|
|
140
|
+
constructor(message, options){
|
|
141
|
+
super(message, options);
|
|
142
|
+
this.name = 'IssuerKeyNotFoundError';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Whether a thrown value means the issuer resolved but does not have the key the token named.
|
|
147
|
+
*
|
|
148
|
+
* Prefer this to `instanceof IssuerKeyNotFoundError` across a package boundary, for the same
|
|
149
|
+
* reason as {@link isUnresolvableIssuerError}.
|
|
150
|
+
*/ export function isIssuerKeyNotFoundError(value) {
|
|
151
|
+
return value instanceof Error && value.brand === ISSUER_KEY_NOT_FOUND_BRAND;
|
|
152
|
+
}
|
|
70
153
|
/**
|
|
71
154
|
* Resolve a token issuer (did:key or did:peer:4) and return alg + public key,
|
|
72
155
|
* plus the decoded peer:4 doc when one was obtained inline or via the resolver.
|
|
73
156
|
* Callers writing to a DID cache should write `peer4Doc` only after signature verification.
|
|
74
|
-
|
|
157
|
+
*
|
|
158
|
+
* The `historic` param selects which question is asked of a `DIDMethodResolver` — see
|
|
159
|
+
* {@link ResolveIssuerMode}. It reaches only the method-registry branch: `did:key` carries its key
|
|
160
|
+
* in the identifier and a `did:peer:4` document is fixed by its own hash, so neither has a past key
|
|
161
|
+
* set distinct from its present one.
|
|
162
|
+
*/ export async function resolveIssuerWithDoc({ iss, header = {}, resolver, methods, historic = false }) {
|
|
163
|
+
if (methods != null) {
|
|
164
|
+
const methodResolver = findMethodResolver(methods, iss);
|
|
165
|
+
if (methodResolver != null) {
|
|
166
|
+
// The historic question is answered only by `resolveHistoric`; a resolver without one is not
|
|
167
|
+
// asked `resolve` instead, which would substitute a different question. `UnresolvableIssuerError`
|
|
168
|
+
// is right — nothing was learned either way, which a fail-closed caller treats as a denial.
|
|
169
|
+
if (historic && methodResolver.resolveHistoric == null) {
|
|
170
|
+
throw new UnresolvableIssuerError(`DID method ${methodResolver.method} cannot resolve historic keys: ${iss}`);
|
|
171
|
+
}
|
|
172
|
+
// A method resolver throws its own error strings; re-type them so a method-backed resolution
|
|
173
|
+
// failure is indistinguishable from the built-in ones below. Message preserved, original kept
|
|
174
|
+
// as `cause`.
|
|
175
|
+
let resolved;
|
|
176
|
+
try {
|
|
177
|
+
// Called as a method of its own resolver, not detached: an implementation may be a class with
|
|
178
|
+
// private state.
|
|
179
|
+
resolved = historic && methodResolver.resolveHistoric != null ? await methodResolver.resolveHistoric(iss, header) : await methodResolver.resolve(iss, header);
|
|
180
|
+
} catch (cause) {
|
|
181
|
+
// Except the one failure that is not a failure to resolve: "I have this issuer, it has no such
|
|
182
|
+
// key" is what the `kid` branches below report for `did:peer:4` (ordinary errors — see
|
|
183
|
+
// IssuerKeyNotFoundError). Wrapping it would let an unauthenticated header make any issuer read
|
|
184
|
+
// as unresolvable, which callers fail closed on.
|
|
185
|
+
if (isIssuerKeyNotFoundError(cause)) {
|
|
186
|
+
throw cause;
|
|
187
|
+
}
|
|
188
|
+
throw new UnresolvableIssuerError(cause instanceof Error ? cause.message : `Unknown DID: ${iss}`, {
|
|
189
|
+
cause
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
alg: resolved.alg,
|
|
194
|
+
publicKey: resolved.publicKey
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
75
198
|
if (isPeer4(iss)) {
|
|
76
199
|
const shortForm = getPeer4ShortForm(iss);
|
|
77
200
|
if (iss !== shortForm) {
|
|
@@ -87,16 +210,36 @@ function isCodecMatch(codec, bytes) {
|
|
|
87
210
|
};
|
|
88
211
|
}
|
|
89
212
|
if (resolver == null) {
|
|
90
|
-
throw new
|
|
213
|
+
throw new UnresolvableIssuerError(`Unknown DID: ${shortForm}`);
|
|
214
|
+
}
|
|
215
|
+
// Throwing is the normal style for a network-backed resolver, so one that throws must be
|
|
216
|
+
// indistinguishable from one that returns nothing — else the fail-closed guarantee holds only for
|
|
217
|
+
// resolvers that signal failure by returning `undefined`.
|
|
218
|
+
let doc;
|
|
219
|
+
try {
|
|
220
|
+
doc = await resolver(shortForm);
|
|
221
|
+
} catch (cause) {
|
|
222
|
+
throw new UnresolvableIssuerError(`Unknown DID: ${shortForm}`, {
|
|
223
|
+
cause
|
|
224
|
+
});
|
|
91
225
|
}
|
|
92
|
-
const doc = await resolver(shortForm);
|
|
93
226
|
if (doc == null) {
|
|
94
|
-
throw new
|
|
227
|
+
throw new UnresolvableIssuerError(`Unknown DID: ${shortForm}`);
|
|
228
|
+
}
|
|
229
|
+
// An unusable answer — oversized, or not hashing to the DID asked for — counts as unresolvable,
|
|
230
|
+
// not an ordinary fault: else a caller failing closed solely on `UnresolvableIssuerError` takes a
|
|
231
|
+
// broken or lying resolver as "not revoked". No availability cost worth weighing — a resolver
|
|
232
|
+
// willing to lie about documents already controls resolution completely.
|
|
233
|
+
try {
|
|
234
|
+
assertDocWithinMaxSize(doc);
|
|
235
|
+
} catch (cause) {
|
|
236
|
+
throw new UnresolvableIssuerError(cause instanceof Error ? cause.message : `Unknown DID: ${shortForm}`, {
|
|
237
|
+
cause
|
|
238
|
+
});
|
|
95
239
|
}
|
|
96
|
-
assertDocWithinMaxSize(doc);
|
|
97
240
|
const expected = encodePeer4(doc).shortForm;
|
|
98
241
|
if (expected !== shortForm) {
|
|
99
|
-
throw new
|
|
242
|
+
throw new UnresolvableIssuerError('DIDResolver: short form/doc hash mismatch');
|
|
100
243
|
}
|
|
101
244
|
const [alg, publicKey] = resolveKidOrAuth(doc, header.kid);
|
|
102
245
|
return {
|
|
@@ -108,16 +251,30 @@ function isCodecMatch(codec, bytes) {
|
|
|
108
251
|
}
|
|
109
252
|
};
|
|
110
253
|
}
|
|
254
|
+
// `iss` narrows to `never` here (isPeer4's `value is string` collapses the false case), so route
|
|
255
|
+
// the prefix check through a helper taking an unnarrowed `string`.
|
|
256
|
+
if (!hasKeyPrefix(iss)) {
|
|
257
|
+
throw new UnresolvableIssuerError(`Unknown DID: ${iss}`);
|
|
258
|
+
}
|
|
111
259
|
const [alg, publicKey] = getSignatureInfo(iss);
|
|
112
260
|
return {
|
|
113
261
|
alg,
|
|
114
262
|
publicKey
|
|
115
263
|
};
|
|
116
264
|
}
|
|
265
|
+
function hasKeyPrefix(did) {
|
|
266
|
+
return did.startsWith(PREFIX);
|
|
267
|
+
}
|
|
117
268
|
/**
|
|
118
269
|
* Resolve a token issuer to [alg, publicKey]. Backward-compatible wrapper around resolveIssuerWithDoc.
|
|
119
|
-
*/ export async function resolveIssuer(iss, header = {}, resolver) {
|
|
120
|
-
const { alg, publicKey } = await resolveIssuerWithDoc(
|
|
270
|
+
*/ export async function resolveIssuer({ iss, header = {}, resolver, methods, historic = false }) {
|
|
271
|
+
const { alg, publicKey } = await resolveIssuerWithDoc({
|
|
272
|
+
iss,
|
|
273
|
+
header,
|
|
274
|
+
resolver,
|
|
275
|
+
methods,
|
|
276
|
+
historic
|
|
277
|
+
});
|
|
121
278
|
return [
|
|
122
279
|
alg,
|
|
123
280
|
publicKey
|
package/lib/identity.d.ts
CHANGED
|
@@ -23,11 +23,10 @@ export type SigningIdentity = Identity & {
|
|
|
23
23
|
publicKey: Uint8Array;
|
|
24
24
|
signToken: <Payload extends Record<string, unknown> = Record<string, unknown>>(payload: Payload, options?: SignTokenOptions) => Promise<SignedToken<Payload>>;
|
|
25
25
|
};
|
|
26
|
-
export type
|
|
27
|
-
decrypt(jwe: string): Promise<Uint8Array>;
|
|
26
|
+
export type KeyAgreementIdentity = Identity & {
|
|
28
27
|
agreeKey(ephemeralPublicKey: Uint8Array): Promise<Uint8Array>;
|
|
29
28
|
};
|
|
30
|
-
export type FullIdentity = SigningIdentity &
|
|
29
|
+
export type FullIdentity = SigningIdentity & KeyAgreementIdentity;
|
|
31
30
|
export type OwnIdentity = FullIdentity & {
|
|
32
31
|
privateKey: Uint8Array;
|
|
33
32
|
};
|
|
@@ -35,20 +34,40 @@ export type IdentityProvider<T extends SigningIdentity = SigningIdentity> = {
|
|
|
35
34
|
provideIdentity(keyID: string): Promise<T>;
|
|
36
35
|
};
|
|
37
36
|
export declare function isSigningIdentity(identity: Identity): identity is SigningIdentity;
|
|
38
|
-
export declare function
|
|
37
|
+
export declare function isKeyAgreementIdentity(identity: Identity): identity is KeyAgreementIdentity;
|
|
39
38
|
export declare function isFullIdentity(identity: Identity): identity is FullIdentity;
|
|
40
39
|
export declare function isOwnIdentity(identity: Identity): identity is OwnIdentity;
|
|
40
|
+
/**
|
|
41
|
+
* Create a signing identity for an Ed25519 private key under a caller-supplied DID.
|
|
42
|
+
*
|
|
43
|
+
* `createSigningIdentity` derives the DID *from* the key, which only works for `did:key`. A method
|
|
44
|
+
* whose identifier is not a function of the current key — `did:kokuin:`, where the key rotates under
|
|
45
|
+
* a fixed inception digest — must supply its own `id`, so `iss` is the method's DID, not the key's
|
|
46
|
+
* `did:key`. The caller owns the pairing: nothing here checks `id` resolves to `privateKey`'s public
|
|
47
|
+
* key, and a mismatch produces tokens that verify nowhere.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createSigningIdentityForDID(id: DIDString, privateKey: Uint8Array): SigningIdentity;
|
|
41
50
|
/**
|
|
42
51
|
* Create a signing identity from an Ed25519 private key.
|
|
43
52
|
*/
|
|
44
53
|
export declare function createSigningIdentity(privateKey: Uint8Array): SigningIdentity;
|
|
45
54
|
/**
|
|
46
|
-
* Create a
|
|
55
|
+
* Create a key-agreement identity from an Ed25519 private key.
|
|
47
56
|
* Uses X25519 key derivation for ECDH key agreement.
|
|
48
57
|
*/
|
|
49
|
-
export declare function
|
|
58
|
+
export declare function createKeyAgreementIdentity(privateKey: Uint8Array): KeyAgreementIdentity;
|
|
59
|
+
/**
|
|
60
|
+
* A key-agreement identity for a **raw X25519 private key** under a caller-supplied DID.
|
|
61
|
+
*
|
|
62
|
+
* The counterpart of {@link createSigningIdentityForDID} for the agreement half. Two differences from
|
|
63
|
+
* {@link createKeyAgreementIdentity}: the identifier is supplied (a `did:kokuin:` key rotates under a
|
|
64
|
+
* fixed inception digest, so `id` cannot be a function of the key), and the input is the X25519 scalar
|
|
65
|
+
* itself rather than an Ed25519 key to Montgomery-convert — a controller derives an independent
|
|
66
|
+
* agreement keypair, not the Montgomery form of its signing key.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createKeyAgreementIdentityForDID(id: DIDString, x25519PrivateKey: Uint8Array): KeyAgreementIdentity;
|
|
50
69
|
/**
|
|
51
|
-
* Create a full identity (signing +
|
|
70
|
+
* Create a full identity (signing + key agreement) from an Ed25519 private key.
|
|
52
71
|
*/
|
|
53
72
|
export declare function createFullIdentity(privateKey: Uint8Array): FullIdentity;
|
|
54
73
|
/**
|
|
@@ -93,7 +112,6 @@ export type MultiKeyIdentity = {
|
|
|
93
112
|
publicKey: Uint8Array;
|
|
94
113
|
privateKey: Uint8Array;
|
|
95
114
|
signToken<Payload extends Record<string, unknown> = Record<string, unknown>>(payload: Payload, options?: SignTokenOptions): Promise<SignedToken<Payload>>;
|
|
96
|
-
decrypt(jwe: string): Promise<Uint8Array>;
|
|
97
115
|
agreeKey(ephemeralPublicKey: Uint8Array, kid?: string): Promise<Uint8Array>;
|
|
98
116
|
};
|
|
99
117
|
/**
|
package/lib/identity.js
CHANGED
|
@@ -3,7 +3,6 @@ import { ed25519, x25519 } from '@noble/curves/ed25519.js';
|
|
|
3
3
|
import { b64uFromJSON, fromUTF, toB64U } from '@sozai/codec';
|
|
4
4
|
import { withSpan } from '@sozai/otel';
|
|
5
5
|
import { CODECS, getDID, normalizeDID } from './did.js';
|
|
6
|
-
import { decryptToken } from './jwe.js';
|
|
7
6
|
import { encodeMultibase } from './multibase.js';
|
|
8
7
|
import { encodePeer4, isPeer4 } from './peer4.js';
|
|
9
8
|
import { concatBytes } from './utils.js';
|
|
@@ -11,20 +10,29 @@ const tracer = createTracer('token');
|
|
|
11
10
|
export function isSigningIdentity(identity) {
|
|
12
11
|
return 'publicKey' in identity && 'signToken' in identity && typeof identity.signToken === 'function';
|
|
13
12
|
}
|
|
14
|
-
export function
|
|
15
|
-
return '
|
|
13
|
+
export function isKeyAgreementIdentity(identity) {
|
|
14
|
+
return 'agreeKey' in identity && typeof identity.agreeKey === 'function';
|
|
16
15
|
}
|
|
17
16
|
export function isFullIdentity(identity) {
|
|
18
|
-
return isSigningIdentity(identity) &&
|
|
17
|
+
return isSigningIdentity(identity) && isKeyAgreementIdentity(identity);
|
|
19
18
|
}
|
|
20
19
|
export function isOwnIdentity(identity) {
|
|
21
20
|
return isFullIdentity(identity) && 'privateKey' in identity;
|
|
22
21
|
}
|
|
23
22
|
/**
|
|
24
|
-
* Create a signing identity
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
* Create a signing identity for an Ed25519 private key under a caller-supplied DID.
|
|
24
|
+
*
|
|
25
|
+
* `createSigningIdentity` derives the DID *from* the key, which only works for `did:key`. A method
|
|
26
|
+
* whose identifier is not a function of the current key — `did:kokuin:`, where the key rotates under
|
|
27
|
+
* a fixed inception digest — must supply its own `id`, so `iss` is the method's DID, not the key's
|
|
28
|
+
* `did:key`. The caller owns the pairing: nothing here checks `id` resolves to `privateKey`'s public
|
|
29
|
+
* key, and a mismatch produces tokens that verify nowhere.
|
|
30
|
+
*/ export function createSigningIdentityForDID(id, privateKey) {
|
|
31
|
+
return buildSigningIdentity(id, privateKey, ed25519.getPublicKey(privateKey));
|
|
32
|
+
}
|
|
33
|
+
// Shared body. Takes `publicKey` rather than deriving it, so `createSigningIdentity` — which
|
|
34
|
+
// needs the public key anyway to build the `did:key` — derives it once instead of twice.
|
|
35
|
+
function buildSigningIdentity(id, privateKey, publicKey) {
|
|
28
36
|
async function signToken(payload, options = {}) {
|
|
29
37
|
return withSpan(tracer, KokuinSpanNames.TOKEN_SIGN, {
|
|
30
38
|
attributes: {
|
|
@@ -60,36 +68,51 @@ export function isOwnIdentity(identity) {
|
|
|
60
68
|
};
|
|
61
69
|
}
|
|
62
70
|
/**
|
|
63
|
-
* Create a
|
|
71
|
+
* Create a signing identity from an Ed25519 private key.
|
|
72
|
+
*/ export function createSigningIdentity(privateKey) {
|
|
73
|
+
const publicKey = ed25519.getPublicKey(privateKey);
|
|
74
|
+
return buildSigningIdentity(getDID(CODECS.EdDSA, publicKey), privateKey, publicKey);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create a key-agreement identity from an Ed25519 private key.
|
|
64
78
|
* Uses X25519 key derivation for ECDH key agreement.
|
|
65
|
-
*/ export function
|
|
79
|
+
*/ export function createKeyAgreementIdentity(privateKey) {
|
|
66
80
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
67
81
|
const id = getDID(CODECS.EdDSA, publicKey);
|
|
68
82
|
const x25519Private = ed25519.utils.toMontgomerySecret(privateKey);
|
|
69
|
-
async function decrypt(jwe) {
|
|
70
|
-
return decryptToken({
|
|
71
|
-
id,
|
|
72
|
-
decrypt,
|
|
73
|
-
agreeKey
|
|
74
|
-
}, jwe);
|
|
75
|
-
}
|
|
76
83
|
async function agreeKey(ephemeralPublicKey) {
|
|
77
84
|
return x25519.getSharedSecret(x25519Private, ephemeralPublicKey);
|
|
78
85
|
}
|
|
79
86
|
return {
|
|
80
87
|
id,
|
|
81
|
-
decrypt,
|
|
82
88
|
agreeKey
|
|
83
89
|
};
|
|
84
90
|
}
|
|
85
91
|
/**
|
|
86
|
-
*
|
|
92
|
+
* A key-agreement identity for a **raw X25519 private key** under a caller-supplied DID.
|
|
93
|
+
*
|
|
94
|
+
* The counterpart of {@link createSigningIdentityForDID} for the agreement half. Two differences from
|
|
95
|
+
* {@link createKeyAgreementIdentity}: the identifier is supplied (a `did:kokuin:` key rotates under a
|
|
96
|
+
* fixed inception digest, so `id` cannot be a function of the key), and the input is the X25519 scalar
|
|
97
|
+
* itself rather than an Ed25519 key to Montgomery-convert — a controller derives an independent
|
|
98
|
+
* agreement keypair, not the Montgomery form of its signing key.
|
|
99
|
+
*/ export function createKeyAgreementIdentityForDID(id, x25519PrivateKey) {
|
|
100
|
+
async function agreeKey(ephemeralPublicKey) {
|
|
101
|
+
return x25519.getSharedSecret(x25519PrivateKey, ephemeralPublicKey);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
agreeKey
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Create a full identity (signing + key agreement) from an Ed25519 private key.
|
|
87
110
|
*/ export function createFullIdentity(privateKey) {
|
|
88
111
|
const signing = createSigningIdentity(privateKey);
|
|
89
|
-
const
|
|
112
|
+
const keyAgreement = createKeyAgreementIdentity(privateKey);
|
|
90
113
|
return {
|
|
91
114
|
...signing,
|
|
92
|
-
...
|
|
115
|
+
...keyAgreement
|
|
93
116
|
};
|
|
94
117
|
}
|
|
95
118
|
/**
|
|
@@ -217,14 +240,15 @@ function signWith(key, data) {
|
|
|
217
240
|
*
|
|
218
241
|
* A peer:4 identity uses its published `keyAgreement` key. A `did:key` EdDSA identity has no
|
|
219
242
|
* published agreement key — a sender derives one from its signing key via the birational map —
|
|
220
|
-
* so it must derive the matching secret the same way, exactly as `
|
|
243
|
+
* so it must derive the matching secret the same way, exactly as `createKeyAgreementIdentity` does.
|
|
221
244
|
*/ function pickAgreementSecret(keys, isPeer, kid) {
|
|
222
245
|
if (kid != null) {
|
|
223
246
|
// No birational fallback here, unlike the no-kid branch below: a kid names one specific
|
|
224
247
|
// key, so falling back to a *different*, derived key would silently agree with the wrong
|
|
225
248
|
// secret. This means a did:key identity's agreeKey(epk, '#key-0') throws where
|
|
226
|
-
// agreeKey(epk) succeeds — a known asymmetry, currently unreachable: decryptToken
|
|
227
|
-
// never passes a kid, and
|
|
249
|
+
// agreeKey(epk) succeeds — a known asymmetry, currently unreachable: decryptToken
|
|
250
|
+
// (@kokuin/jwe) never passes a kid, and KeyAgreementIdentity#agreeKey has no kid parameter
|
|
251
|
+
// at all.
|
|
228
252
|
const found = keys.find((key)=>key.fragment === kid);
|
|
229
253
|
if (found == null) throw new Error(`KidNotFound: ${kid}`);
|
|
230
254
|
if (found.purpose !== 'kem' || found.alg !== 'X25519') {
|
|
@@ -291,14 +315,6 @@ function buildIdentity(id, longForm, doc, keys) {
|
|
|
291
315
|
async function agreeKey(ephemeralPublicKey, kid) {
|
|
292
316
|
return x25519.getSharedSecret(pickAgreementSecret(keys, isPeer, kid), ephemeralPublicKey);
|
|
293
317
|
}
|
|
294
|
-
async function decrypt(jwe) {
|
|
295
|
-
pickAgreementSecret(keys, isPeer); // fail fast when this identity cannot agree at all
|
|
296
|
-
return decryptToken({
|
|
297
|
-
id,
|
|
298
|
-
decrypt,
|
|
299
|
-
agreeKey
|
|
300
|
-
}, jwe);
|
|
301
|
-
}
|
|
302
318
|
return {
|
|
303
319
|
id,
|
|
304
320
|
longForm,
|
|
@@ -307,7 +323,6 @@ function buildIdentity(id, longForm, doc, keys) {
|
|
|
307
323
|
publicKey: primarySig.publicKey,
|
|
308
324
|
privateKey: primarySig.privateKey,
|
|
309
325
|
signToken,
|
|
310
|
-
decrypt,
|
|
311
326
|
agreeKey
|
|
312
327
|
};
|
|
313
328
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -10,14 +10,12 @@
|
|
|
10
10
|
* @module token
|
|
11
11
|
*/
|
|
12
12
|
export { type CreateInMemoryDIDCacheOptions, createInMemoryDIDCache, type DIDCache, type DIDResolver, } from './cache.js';
|
|
13
|
-
export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, normalizeDID, type ResolveIssuerHeader, type ResolveIssuerWithDocResult, resolveIssuer, resolveIssuerWithDoc, } from './did.js';
|
|
14
|
-
export { type CreateIdentityInput,
|
|
15
|
-
export type { ConcatKDFParams, EncryptOptions, EnvelopeMode, JWEHeader, SharedSecretResult, TokenEncrypter, UnwrapOptions, UnwrappedEnvelope, WrapOptions, } from './jwe.js';
|
|
16
|
-
export { concatKDF, createTokenEncrypter, decryptToken, deriveSharedSecret, encryptToken, unwrapEnvelope, wrapEnvelope, } from './jwe.js';
|
|
13
|
+
export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, IssuerKeyNotFoundError, isIssuerKeyNotFoundError, isUnresolvableIssuerError, normalizeDID, type ResolveIssuerHeader, type ResolveIssuerMode, type ResolveIssuerWithDocResult, resolveIssuer, resolveIssuerWithDoc, UnresolvableIssuerError, } from './did.js';
|
|
14
|
+
export { type CreateIdentityInput, createFullIdentity, createIdentity, createKeyAgreementIdentity, createKeyAgreementIdentityForDID, createSigningIdentity, createSigningIdentityForDID, type FullIdentity, type Identity, type IdentityKeySpec, type IdentityProvider, isFullIdentity, isKeyAgreementIdentity, isOwnIdentity, isSigningIdentity, type KeyAgreementIdentity, type KeyAlg, type KeyPurpose, type MultiKeyIdentity, type OwnIdentity, type ResolvedKey, randomIdentity, type SigningIdentity, type SignTokenOptions, } from './identity.js';
|
|
17
15
|
export type { KeyEntry, KeyStore, MutableKeyEntry } from './keystore.js';
|
|
16
|
+
export { type AgreementAlgorithm, type DIDMethodResolver, findMethodResolver, type MethodRegistry, type ResolvedAgreementKey, type ResolvedSigningKey, } from './method.js';
|
|
18
17
|
export { decodeMultibase, encodeMultibase, multihashSHA256, verifyMultihash, } from './multibase.js';
|
|
19
18
|
export { type DecodePeer4Options, type DIDDoc, decodePeer4, encodePeer4, getPeer4ShortForm, isPeer4, type VerificationMethod, validateDIDDoc, } from './peer4.js';
|
|
20
|
-
export { createRotationAssertion, type RotationPayload, } from './rotation.js';
|
|
21
19
|
export { capabilitySchema, type SignatureAlgorithm, type SignedHeader, type SignedPayload, type SupportedHeader, signedHeaderSchema, signedPayloadSchema, supportedHeaderSchema, type UnsignedHeader, unsignedHeaderSchema, validateAlgorithm, validateSignedHeader, validateSignedPayload, validateUnsignedHeader, } from './schemas.js';
|
|
22
20
|
export { decodePrivateKey, encodePrivateKey, randomPrivateKey, } from './signer.js';
|
|
23
21
|
export { assertTimeClaimsValid, now, type TimeClaimsPayload, type TimeValidationOptions, } from './time.js';
|
package/lib/index.js
CHANGED
|
@@ -9,12 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module token
|
|
11
11
|
*/ export { createInMemoryDIDCache } from './cache.js';
|
|
12
|
-
export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, normalizeDID, resolveIssuer, resolveIssuerWithDoc } from './did.js';
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
12
|
+
export { CODECS, getAgreementKey, getAlgorithmAndPublicKey, getDID, getSignatureInfo, IssuerKeyNotFoundError, isIssuerKeyNotFoundError, isUnresolvableIssuerError, normalizeDID, resolveIssuer, resolveIssuerWithDoc, UnresolvableIssuerError } from './did.js';
|
|
13
|
+
export { createFullIdentity, createIdentity, createKeyAgreementIdentity, createKeyAgreementIdentityForDID, createSigningIdentity, createSigningIdentityForDID, isFullIdentity, isKeyAgreementIdentity, isOwnIdentity, isSigningIdentity, randomIdentity } from './identity.js';
|
|
14
|
+
export { findMethodResolver } from './method.js';
|
|
15
15
|
export { decodeMultibase, encodeMultibase, multihashSHA256, verifyMultihash } from './multibase.js';
|
|
16
16
|
export { decodePeer4, encodePeer4, getPeer4ShortForm, isPeer4, validateDIDDoc } from './peer4.js';
|
|
17
|
-
export { createRotationAssertion } from './rotation.js';
|
|
18
17
|
export { capabilitySchema, signedHeaderSchema, signedPayloadSchema, supportedHeaderSchema, unsignedHeaderSchema, validateAlgorithm, validateSignedHeader, validateSignedPayload, validateUnsignedHeader } from './schemas.js';
|
|
19
18
|
export { decodePrivateKey, encodePrivateKey, randomPrivateKey } from './signer.js';
|
|
20
19
|
export { assertTimeClaimsValid, now } from './time.js';
|