@kokuin/token 0.3.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/cache.js +8 -1
- package/lib/did.d.ts +102 -6
- package/lib/did.js +169 -12
- package/lib/identity.d.ts +35 -14
- package/lib/identity.js +51 -34
- package/lib/index.d.ts +4 -6
- package/lib/index.js +4 -5
- package/lib/method.d.ts +70 -0
- package/lib/method.js +8 -0
- package/lib/peer4.d.ts +7 -2
- package/lib/peer4.js +12 -0
- package/lib/schemas.d.ts +22 -4
- package/lib/schemas.js +16 -3
- package/lib/token.d.ts +25 -2
- package/lib/token.js +76 -24
- package/lib/types.d.ts +1 -0
- package/package.json +18 -16
- package/lib/jwe.d.ts +0 -67
- package/lib/jwe.js +0 -281
- package/lib/rotation.d.ts +0 -15
- package/lib/rotation.js +0 -12
package/lib/index.d.ts
CHANGED
|
@@ -10,18 +10,16 @@
|
|
|
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, TokenEncrypter, UnwrapOptions, UnwrappedEnvelope, WrapOptions, } from './jwe.js';
|
|
16
|
-
export { concatKDF, createTokenEncrypter, decryptToken, 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';
|
|
24
22
|
export { createUnsignedToken, isSignedToken, isUnsignedToken, isVerifiedToken, signToken, type VerifyTokenOptions, verifyToken, } from './token.js';
|
|
25
23
|
export type * from './types.js';
|
|
26
|
-
export { stringifyToken } from './utils.js';
|
|
24
|
+
export { concatBytes, stringifyToken } from './utils.js';
|
|
27
25
|
export { getVerifier, type Verifier, type Verifiers, } from './verifier.js';
|
package/lib/index.js
CHANGED
|
@@ -9,15 +9,14 @@
|
|
|
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';
|
|
21
20
|
export { createUnsignedToken, isSignedToken, isUnsignedToken, isVerifiedToken, signToken, verifyToken } from './token.js';
|
|
22
|
-
export { stringifyToken } from './utils.js';
|
|
21
|
+
export { concatBytes, stringifyToken } from './utils.js';
|
|
23
22
|
export { getVerifier } from './verifier.js';
|
package/lib/method.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ResolveIssuerHeader } from './did.js';
|
|
2
|
+
import type { SignatureAlgorithm } from './schemas.js';
|
|
3
|
+
/** The signing key a DID method resolved for a given issuer and header. */
|
|
4
|
+
export type ResolvedSigningKey = {
|
|
5
|
+
alg: SignatureAlgorithm;
|
|
6
|
+
publicKey: Uint8Array;
|
|
7
|
+
};
|
|
8
|
+
export type AgreementAlgorithm = 'X25519';
|
|
9
|
+
/** A key agreement key a DID method resolved for a recipient. */
|
|
10
|
+
export type ResolvedAgreementKey = {
|
|
11
|
+
alg: AgreementAlgorithm;
|
|
12
|
+
publicKey: Uint8Array;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A DID method this package can resolve `iss` through without importing its implementation.
|
|
16
|
+
*
|
|
17
|
+
* Methods whose document is a projection of an event log — `did:kokuin:` — cannot be resolved
|
|
18
|
+
* from the identifier alone and cannot be linked from here without a dependency cycle:
|
|
19
|
+
* `@kokuin/controller` depends on this package for signing. Injecting the resolver keeps the
|
|
20
|
+
* dependency one-way.
|
|
21
|
+
*/
|
|
22
|
+
export type DIDMethodResolver = {
|
|
23
|
+
/** The method segment, without `did:` and without the trailing colon. E.g. `kokuin`. */
|
|
24
|
+
method: string;
|
|
25
|
+
/**
|
|
26
|
+
* The key this subject signs with **now** — its head keys, nothing rotated away. The safe default:
|
|
27
|
+
* authenticating a live signer asks whether *this* party can sign today, so a retired key must not
|
|
28
|
+
* answer yes. {@link resolveHistoric} is the other question.
|
|
29
|
+
*/
|
|
30
|
+
resolve(did: string, header: ResolveIssuerHeader): Promise<ResolvedSigningKey>;
|
|
31
|
+
/**
|
|
32
|
+
* The key this subject signed with **at some point in the past**, for verifying an artefact it has
|
|
33
|
+
* already issued — the explicit opt-in half of the split. It accepts a key the subject may have
|
|
34
|
+
* rotated away *because it was compromised*, so the answer is "did once hold", never "holds". Use it
|
|
35
|
+
* only for past-minted material that must survive routine key hygiene (a capability, a revocation
|
|
36
|
+
* record, an archived grant), never to authenticate a live signer.
|
|
37
|
+
*
|
|
38
|
+
* A method whose key set never changes may alias it to {@link resolve}. Optional, so a resolver
|
|
39
|
+
* written before this member typechecks, but its **absence fails closed**: `resolveIssuerWithDoc`
|
|
40
|
+
* refuses rather than falling back to {@link resolve}, which would answer a different question. A
|
|
41
|
+
* method that can retire keys must implement it — including a *wrapper* around one that does.
|
|
42
|
+
*/
|
|
43
|
+
resolveHistoric?(did: string, header: ResolveIssuerHeader): Promise<ResolvedSigningKey>;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the recipient's key agreement key set, in the method's order. Every entry with its
|
|
46
|
+
* algorithm rather than one chosen key: the set is an OR set, and selection belongs to the
|
|
47
|
+
* encrypting package. Optional: a method with no key agreement omits it.
|
|
48
|
+
*/
|
|
49
|
+
resolveAgreementKey?(did: string): Promise<Array<ResolvedAgreementKey>>;
|
|
50
|
+
/**
|
|
51
|
+
* What this subject has revoked: no capability whose `aud` is in the set is valid. Entries are DIDs,
|
|
52
|
+
* but a wider vocabulary may add others — `did:kokuin:` also denies its own signing keys, spelled
|
|
53
|
+
* `#<key>` — so a reader must **match, never enumerate**. A DID and a key fragment cannot collide.
|
|
54
|
+
*
|
|
55
|
+
* Answer for **now**, not any position a token names: a capability's claims are backdatable, so an
|
|
56
|
+
* anchored check would let the holder pick a moment before it was revoked. A subject that cannot be
|
|
57
|
+
* resolved must reject, never answer the empty set (which reads as "nobody revoked").
|
|
58
|
+
*
|
|
59
|
+
* Optional, but unlike `resolveAgreementKey` its absence **disables enforcement** for every subject
|
|
60
|
+
* of this method, silently and in the passing direction. So a method with no revocation concept
|
|
61
|
+
* omits it, and **a method that can revoke must implement it — including a *wrapper*** (one
|
|
62
|
+
* forwarding only `resolve` type-checks and turns every denial into a pass). It rides this registry
|
|
63
|
+
* so a caller who wired resolution cannot separately forget enforcement; requiring it here would
|
|
64
|
+
* break every implementation for a member most methods have no answer for.
|
|
65
|
+
*/
|
|
66
|
+
resolveDenySet?(did: string): Promise<ReadonlySet<string>>;
|
|
67
|
+
};
|
|
68
|
+
export type MethodRegistry = ReadonlyArray<DIDMethodResolver>;
|
|
69
|
+
/** Total: a malformed DID yields `undefined` rather than throwing. */
|
|
70
|
+
export declare function findMethodResolver(registry: MethodRegistry, did: string): DIDMethodResolver | undefined;
|
package/lib/method.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Total: a malformed DID yields `undefined` rather than throwing. */ export function findMethodResolver(registry, did) {
|
|
2
|
+
const parts = did.split(':');
|
|
3
|
+
if (parts.length < 3 || parts[0] !== 'did') {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const method = parts[1];
|
|
7
|
+
return registry.find((entry)=>entry.method === method);
|
|
8
|
+
}
|
package/lib/peer4.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FromSchema } from '@sozai/schema';
|
|
2
|
+
import type { DIDString } from './types.js';
|
|
2
3
|
/** @internal */
|
|
3
4
|
export declare const verificationMethodSchema: {
|
|
4
5
|
readonly type: "object";
|
|
@@ -87,6 +88,10 @@ export type DecodePeer4Options = {
|
|
|
87
88
|
/**
|
|
88
89
|
* Throw if a DID document's canonical serialization exceeds `maxSize` bytes.
|
|
89
90
|
* Used to bound a resolver-returned doc before it reaches the O(n^2) base58 encode.
|
|
91
|
+
*
|
|
92
|
+
* The entry-count check runs first and is O(1): `canonicalStringify` below is linear in the
|
|
93
|
+
* document, so an attacker-supplied `verificationMethod` array with millions of entries would
|
|
94
|
+
* otherwise cost linear time before it could be measured and rejected.
|
|
90
95
|
*/
|
|
91
96
|
export declare function assertDocWithinMaxSize(doc: DIDDoc, maxSize?: number): void;
|
|
92
97
|
/**
|
|
@@ -111,7 +116,7 @@ export declare function decodePeer4(longForm: string, options?: DecodePeer4Optio
|
|
|
111
116
|
* Returns both the long form (self-contained, doc embedded) and short form (hash only).
|
|
112
117
|
*/
|
|
113
118
|
export declare function encodePeer4(doc: DIDDoc): {
|
|
114
|
-
longForm:
|
|
115
|
-
shortForm:
|
|
119
|
+
longForm: DIDString;
|
|
120
|
+
shortForm: DIDString;
|
|
116
121
|
doc: DIDDoc;
|
|
117
122
|
};
|
package/lib/peer4.js
CHANGED
|
@@ -79,6 +79,10 @@ const JSON_MULTICODEC = new Uint8Array([
|
|
|
79
79
|
0x04
|
|
80
80
|
]);
|
|
81
81
|
const DEFAULT_MAX_DOC_SIZE = 4 * 1024;
|
|
82
|
+
// The smallest legal verificationMethod entry — {"id":"","type":"","publicKeyMultibase":""} at 43
|
|
83
|
+
// bytes plus an array separator — serializes to 44. Undercounting here only widens the cap, so the
|
|
84
|
+
// O(1) guard below can never reject a doc the full byte measure would have accepted.
|
|
85
|
+
const MIN_VERIFICATION_METHOD_BYTES = 40;
|
|
82
86
|
// base58 expands bytes to characters by log(256) / log(58).
|
|
83
87
|
const BASE58_EXPANSION = 1.3658;
|
|
84
88
|
// A SHA-256 multihash is 34 bytes: 47 base58 characters plus the multibase 'z'.
|
|
@@ -86,7 +90,15 @@ const MAX_HASH_ENCODED = 64;
|
|
|
86
90
|
/**
|
|
87
91
|
* Throw if a DID document's canonical serialization exceeds `maxSize` bytes.
|
|
88
92
|
* Used to bound a resolver-returned doc before it reaches the O(n^2) base58 encode.
|
|
93
|
+
*
|
|
94
|
+
* The entry-count check runs first and is O(1): `canonicalStringify` below is linear in the
|
|
95
|
+
* document, so an attacker-supplied `verificationMethod` array with millions of entries would
|
|
96
|
+
* otherwise cost linear time before it could be measured and rejected.
|
|
89
97
|
*/ export function assertDocWithinMaxSize(doc, maxSize = DEFAULT_MAX_DOC_SIZE) {
|
|
98
|
+
const maxEntries = Math.ceil(maxSize / MIN_VERIFICATION_METHOD_BYTES);
|
|
99
|
+
if (Array.isArray(doc.verificationMethod) && doc.verificationMethod.length > maxEntries) {
|
|
100
|
+
throw new Error(`did:peer:4 resolver doc has too many verification methods: ${doc.verificationMethod.length} > ${maxEntries}`);
|
|
101
|
+
}
|
|
90
102
|
const size = fromUTF(canonicalStringify(doc)).length;
|
|
91
103
|
if (size > maxSize) {
|
|
92
104
|
throw new Error(`did:peer:4 resolver doc too large: ${size} > ${maxSize}`);
|
package/lib/schemas.d.ts
CHANGED
|
@@ -10,7 +10,13 @@ export declare const signatureAlgorithmSchema: {
|
|
|
10
10
|
export type SignatureAlgorithm = FromSchema<typeof signatureAlgorithmSchema>;
|
|
11
11
|
/** @internal */
|
|
12
12
|
export declare const validateAlgorithm: import("@sozai/schema").Validator<"EdDSA" | "ES256">;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The header of a signed token.
|
|
15
|
+
*
|
|
16
|
+
* Supported rather than internal: `@enkaku/protocol` composes its own message schemas out of this,
|
|
17
|
+
* `unsignedHeaderSchema` and `signedPayloadSchema` — a protocol built on kokuin tokens has to
|
|
18
|
+
* describe them in its own wire schema, and it cannot do that from a type alone.
|
|
19
|
+
*/
|
|
14
20
|
export declare const signedHeaderSchema: {
|
|
15
21
|
readonly type: "object";
|
|
16
22
|
readonly properties: {
|
|
@@ -38,7 +44,9 @@ export declare const validateSignedHeader: import("@sozai/schema").Validator<{
|
|
|
38
44
|
alg: "EdDSA" | "ES256";
|
|
39
45
|
typ: "JWT";
|
|
40
46
|
}>;
|
|
41
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The header of an unsigned token. See {@link signedHeaderSchema} for why this is supported.
|
|
49
|
+
*/
|
|
42
50
|
export declare const unsignedHeaderSchema: {
|
|
43
51
|
readonly type: "object";
|
|
44
52
|
readonly properties: {
|
|
@@ -110,7 +118,12 @@ export declare const capabilitySchema: {
|
|
|
110
118
|
};
|
|
111
119
|
}];
|
|
112
120
|
};
|
|
113
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* The registered claims a signed token carries. `additionalProperties` is open, so a consumer
|
|
123
|
+
* spreads `properties` into its own object schema and concatenates `required`.
|
|
124
|
+
*
|
|
125
|
+
* Supported rather than internal — see {@link signedHeaderSchema}.
|
|
126
|
+
*/
|
|
114
127
|
export declare const signedPayloadSchema: {
|
|
115
128
|
readonly type: "object";
|
|
116
129
|
readonly properties: {
|
|
@@ -146,7 +159,12 @@ export declare const signedPayloadSchema: {
|
|
|
146
159
|
readonly required: readonly ["iss"];
|
|
147
160
|
readonly additionalProperties: true;
|
|
148
161
|
};
|
|
149
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* The registered claims of a signed token, inferred from {@link signedPayloadSchema}.
|
|
164
|
+
*
|
|
165
|
+
* Supported rather than internal, and the most widely used of these: it is what a consumer
|
|
166
|
+
* intersects its own payload type with to describe a token it issues or verifies.
|
|
167
|
+
*/
|
|
150
168
|
export type SignedPayload = FromSchema<typeof signedPayloadSchema>;
|
|
151
169
|
/** @internal */
|
|
152
170
|
export declare const validateSignedPayload: import("@sozai/schema").Validator<{
|
package/lib/schemas.js
CHANGED
|
@@ -8,7 +8,13 @@ import { createValidator } from '@sozai/schema';
|
|
|
8
8
|
enum: SUPPORTED_ALGORITHMS
|
|
9
9
|
};
|
|
10
10
|
/** @internal */ export const validateAlgorithm = createValidator(signatureAlgorithmSchema);
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The header of a signed token.
|
|
13
|
+
*
|
|
14
|
+
* Supported rather than internal: `@enkaku/protocol` composes its own message schemas out of this,
|
|
15
|
+
* `unsignedHeaderSchema` and `signedPayloadSchema` — a protocol built on kokuin tokens has to
|
|
16
|
+
* describe them in its own wire schema, and it cannot do that from a type alone.
|
|
17
|
+
*/ export const signedHeaderSchema = {
|
|
12
18
|
type: 'object',
|
|
13
19
|
properties: {
|
|
14
20
|
typ: {
|
|
@@ -27,7 +33,9 @@ import { createValidator } from '@sozai/schema';
|
|
|
27
33
|
additionalProperties: true
|
|
28
34
|
};
|
|
29
35
|
/** @internal */ export const validateSignedHeader = createValidator(signedHeaderSchema);
|
|
30
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The header of an unsigned token. See {@link signedHeaderSchema} for why this is supported.
|
|
38
|
+
*/ export const unsignedHeaderSchema = {
|
|
31
39
|
type: 'object',
|
|
32
40
|
properties: {
|
|
33
41
|
typ: {
|
|
@@ -65,7 +73,12 @@ import { createValidator } from '@sozai/schema';
|
|
|
65
73
|
}
|
|
66
74
|
]
|
|
67
75
|
};
|
|
68
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* The registered claims a signed token carries. `additionalProperties` is open, so a consumer
|
|
78
|
+
* spreads `properties` into its own object schema and concatenates `required`.
|
|
79
|
+
*
|
|
80
|
+
* Supported rather than internal — see {@link signedHeaderSchema}.
|
|
81
|
+
*/ export const signedPayloadSchema = {
|
|
69
82
|
type: 'object',
|
|
70
83
|
properties: {
|
|
71
84
|
iss: {
|
package/lib/token.d.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import type { DIDCache, DIDResolver } from './cache.js';
|
|
2
2
|
import type { SigningIdentity } from './identity.js';
|
|
3
|
+
import type { MethodRegistry } from './method.js';
|
|
3
4
|
import { type SignedPayload } from './schemas.js';
|
|
4
5
|
import { type TimeValidationOptions } from './time.js';
|
|
5
|
-
import type { SignedToken, Token, UnsignedToken, VerifiedToken } from './types.js';
|
|
6
|
+
import type { DIDString, SignedToken, Token, UnsignedToken, VerifiedToken } from './types.js';
|
|
6
7
|
import { type Verifiers } from './verifier.js';
|
|
7
8
|
export type VerifyTokenOptions = TimeValidationOptions & {
|
|
8
9
|
verifiers?: Verifiers;
|
|
9
10
|
resolver?: DIDResolver;
|
|
10
11
|
cache?: DIDCache;
|
|
12
|
+
/**
|
|
13
|
+
* DID methods that need external resolution to verify an issuer. `did:key` and `did:peer:4` need no
|
|
14
|
+
* entry (they carry their key in the identifier or document); a method whose key set is a projection
|
|
15
|
+
* of state held elsewhere — `did:kokuin:` — cannot be verified without one, and fails with `Unknown
|
|
16
|
+
* DID` when no registry is passed.
|
|
17
|
+
*/
|
|
18
|
+
methods?: MethodRegistry;
|
|
11
19
|
/**
|
|
12
20
|
* Expected audience(s) for the token. When set, verification rejects a token whose `aud`
|
|
13
21
|
* claim is not among the given value(s), and rejects unsigned / `alg:none` tokens outright
|
|
@@ -16,7 +24,19 @@ export type VerifyTokenOptions = TimeValidationOptions & {
|
|
|
16
24
|
* capability's `aud` is the delegation next-hop rather than a service audience. An empty
|
|
17
25
|
* array accepts no audience (every token is rejected).
|
|
18
26
|
*/
|
|
19
|
-
audience?:
|
|
27
|
+
audience?: DIDString | Array<DIDString>;
|
|
28
|
+
/**
|
|
29
|
+
* Verify against a key the issuer has already rotated away, as well as its current ones. Defaults
|
|
30
|
+
* to `false` — the safe question: the signature must be from a key the issuer holds **now**, so a
|
|
31
|
+
* rotated-away (leaked) key mints nothing. Opting in asks whether the issuer held the key **at some
|
|
32
|
+
* point**, which a past-issued capability or revocation record needs to survive routine key hygiene.
|
|
33
|
+
*
|
|
34
|
+
* Set it only for a past-issued token that is not itself proof a live party holds a key. Not a
|
|
35
|
+
* compatibility switch: it is the difference between "a compromised key mints nothing" and "mints
|
|
36
|
+
* anything until the profile resets". A resolver with no `resolveHistoric` rejects with
|
|
37
|
+
* `UnresolvableIssuerError` rather than answering the current-key question.
|
|
38
|
+
*/
|
|
39
|
+
historic?: boolean;
|
|
20
40
|
/**
|
|
21
41
|
* Accept unsigned (`alg:none`) tokens. Defaults to `false`.
|
|
22
42
|
*
|
|
@@ -38,6 +58,9 @@ export type VerifySignedPayloadInput<Payload extends Record<string, unknown> = R
|
|
|
38
58
|
verifiers?: Verifiers;
|
|
39
59
|
resolver?: DIDResolver;
|
|
40
60
|
cache?: DIDCache;
|
|
61
|
+
methods?: MethodRegistry;
|
|
62
|
+
/** See `VerifyTokenOptions.historic`. Defaults to `false` — the issuer's current keys only. */
|
|
63
|
+
historic?: boolean;
|
|
41
64
|
};
|
|
42
65
|
/**
|
|
43
66
|
* Verify the signature of a signed payload and return the public key of the issuer.
|
package/lib/token.js
CHANGED
|
@@ -11,6 +11,17 @@ const tokenTracer = createTracer('token');
|
|
|
11
11
|
// `verifiedPublicKey` property but can never be a member of this set, so verification
|
|
12
12
|
// is only ever skipped for objects produced by `verifyToken` itself.
|
|
13
13
|
const verifiedTokens = new WeakSet();
|
|
14
|
+
// The `data` string each verified token's signature was checked against, keyed off the object
|
|
15
|
+
// rather than read back from it. `token.data` is a mutable property of that same object, so
|
|
16
|
+
// comparing a recomputed header/payload against it is vacuous: code that mutates the payload can
|
|
17
|
+
// re-derive `data` from the mutation and stay self-consistent. This map is not reachable from the
|
|
18
|
+
// token, so the value captured at verification time is the one the re-bind compares against.
|
|
19
|
+
const verifiedTokenData = new WeakMap();
|
|
20
|
+
// Tokens verified with `historic: true` — against a key the issuer has since rotated away.
|
|
21
|
+
// Membership blocks the fast path: a later `verifyToken` without `historic` asks the stricter
|
|
22
|
+
// question, and answering it from a weaker check is fail-open, so it re-verifies (which may pass).
|
|
23
|
+
// The reverse needs nothing — a strict answer already implies the loose one.
|
|
24
|
+
const historicallyVerifiedTokens = new WeakSet();
|
|
14
25
|
function assertAudienceValid(payload, audience) {
|
|
15
26
|
if (audience == null) {
|
|
16
27
|
return;
|
|
@@ -38,16 +49,22 @@ function assertUnsignedAllowed(allowUnsigned) {
|
|
|
38
49
|
/**
|
|
39
50
|
* Verify the signature of a signed payload and return the public key of the issuer.
|
|
40
51
|
*/ export async function verifySignedPayload(input) {
|
|
41
|
-
const { signature, payload, header, data, verifiers, resolver, cache } = input;
|
|
52
|
+
const { signature, payload, header, data, verifiers, resolver, cache, methods, historic } = input;
|
|
42
53
|
assertType(validateSignedPayload, payload);
|
|
43
54
|
const effectiveResolver = cache == null ? resolver : async (did)=>{
|
|
44
55
|
const cached = await cache.get(did);
|
|
45
56
|
if (cached != null) return cached;
|
|
46
57
|
return resolver != null ? resolver(did) : undefined;
|
|
47
58
|
};
|
|
48
|
-
const { alg, publicKey, peer4Doc } = await resolveIssuerWithDoc(
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
const { alg, publicKey, peer4Doc } = await resolveIssuerWithDoc({
|
|
60
|
+
iss: payload.iss,
|
|
61
|
+
header: {
|
|
62
|
+
kid: header.kid
|
|
63
|
+
},
|
|
64
|
+
resolver: effectiveResolver,
|
|
65
|
+
methods,
|
|
66
|
+
historic
|
|
67
|
+
});
|
|
51
68
|
const verify = getVerifier(alg, verifiers);
|
|
52
69
|
const message = typeof data === 'string' ? fromUTF(data) : data;
|
|
53
70
|
const verified = await verify(signature, message, publicKey);
|
|
@@ -101,29 +118,45 @@ function assertUnsignedAllowed(allowUnsigned) {
|
|
|
101
118
|
header: token.header
|
|
102
119
|
});
|
|
103
120
|
}
|
|
121
|
+
// Whether `data` is a serialization of the token's current header and payload. It may use a
|
|
122
|
+
// different JSON serialization of the same values; accept it only if it decodes to exactly those,
|
|
123
|
+
// so the signed bytes can never be decoupled from the payload used for authorization.
|
|
124
|
+
function matchesVerifiableData(token, data) {
|
|
125
|
+
if (data === `${b64uFromJSON(token.header)}.${b64uFromJSON(token.payload)}`) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
const parts = data.split('.');
|
|
129
|
+
if (parts.length !== 2) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return canonicalStringify(b64uToJSON(parts[0])) === canonicalStringify(token.header) && canonicalStringify(b64uToJSON(parts[1])) === canonicalStringify(token.payload);
|
|
134
|
+
} catch {
|
|
135
|
+
// invalid base64url or JSON in data
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
104
139
|
function getVerifiableData(token) {
|
|
105
|
-
const recomputed = `${b64uFromJSON(token.header)}.${b64uFromJSON(token.payload)}`;
|
|
106
140
|
const data = token.data;
|
|
107
|
-
if (data == null
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// can never be decoupled from the payload used for authorization.
|
|
113
|
-
const parts = typeof data === 'string' ? data.split('.') : [];
|
|
114
|
-
if (parts.length === 2) {
|
|
115
|
-
try {
|
|
116
|
-
if (canonicalStringify(b64uToJSON(parts[0])) === canonicalStringify(token.header) && canonicalStringify(b64uToJSON(parts[1])) === canonicalStringify(token.payload)) {
|
|
117
|
-
return data;
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
// invalid base64url or JSON in data: fall through to the error below
|
|
121
|
-
}
|
|
141
|
+
if (data == null) {
|
|
142
|
+
return `${b64uFromJSON(token.header)}.${b64uFromJSON(token.payload)}`;
|
|
143
|
+
}
|
|
144
|
+
if (typeof data === 'string' && matchesVerifiableData(token, data)) {
|
|
145
|
+
return data;
|
|
122
146
|
}
|
|
123
147
|
throw new Error('Invalid token: data does not match header and payload');
|
|
124
148
|
}
|
|
149
|
+
// Re-bind an already-verified token to the bytes its signature was checked against. The `data`
|
|
150
|
+
// comes from `verifiedTokenData`, never from the token, so a caller that mutates the payload
|
|
151
|
+
// cannot make the check pass by re-deriving `token.data` alongside it.
|
|
152
|
+
function assertVerifiedDataUnchanged(token) {
|
|
153
|
+
const data = verifiedTokenData.get(token);
|
|
154
|
+
if (data == null || !matchesVerifiableData(token, data)) {
|
|
155
|
+
throw new Error('Invalid token: data does not match header and payload');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
125
158
|
async function verifyTokenInner(token, options = {}) {
|
|
126
|
-
const { verifiers, resolver, cache, audience, allowUnsigned = false, ...timeOptions } = options;
|
|
159
|
+
const { verifiers, resolver, cache, methods, audience, allowUnsigned = false, historic, ...timeOptions } = options;
|
|
127
160
|
if (typeof token !== 'string') {
|
|
128
161
|
if (isUnsignedToken(token)) {
|
|
129
162
|
assertSignedForAudience(audience);
|
|
@@ -131,7 +164,14 @@ async function verifyTokenInner(token, options = {}) {
|
|
|
131
164
|
assertTimeClaimsValid(token.payload, timeOptions);
|
|
132
165
|
return token;
|
|
133
166
|
}
|
|
134
|
-
|
|
167
|
+
// The `historic` arm is what keeps the fast path from weakening the check: a token verified
|
|
168
|
+
// against a rotated-away key must not satisfy a later caller who asked for the issuer's current
|
|
169
|
+
// keys. It falls through to the full verification below rather than being rejected.
|
|
170
|
+
if (isVerifiedToken(token) && !(historic !== true && historicallyVerifiedTokens.has(token))) {
|
|
171
|
+
// The signature was checked when this object entered `verifiedTokens`, but its payload may
|
|
172
|
+
// have been mutated in place since. Re-bind it to the signed bytes — cheap next to a
|
|
173
|
+
// signature verification, and enough to reject tampering.
|
|
174
|
+
assertVerifiedDataUnchanged(token);
|
|
135
175
|
assertTimeClaimsValid(token.payload, timeOptions);
|
|
136
176
|
assertAudienceValid(token.payload, audience);
|
|
137
177
|
return token;
|
|
@@ -145,7 +185,9 @@ async function verifyTokenInner(token, options = {}) {
|
|
|
145
185
|
data,
|
|
146
186
|
verifiers,
|
|
147
187
|
resolver,
|
|
148
|
-
cache
|
|
188
|
+
cache,
|
|
189
|
+
methods,
|
|
190
|
+
historic
|
|
149
191
|
});
|
|
150
192
|
assertTimeClaimsValid(token.payload, timeOptions);
|
|
151
193
|
assertAudienceValid(token.payload, audience);
|
|
@@ -155,6 +197,10 @@ async function verifyTokenInner(token, options = {}) {
|
|
|
155
197
|
verifiedPublicKey
|
|
156
198
|
};
|
|
157
199
|
verifiedTokens.add(result);
|
|
200
|
+
verifiedTokenData.set(result, data);
|
|
201
|
+
if (historic === true) {
|
|
202
|
+
historicallyVerifiedTokens.add(result);
|
|
203
|
+
}
|
|
158
204
|
return result;
|
|
159
205
|
}
|
|
160
206
|
throw new Error('Unsupported token');
|
|
@@ -191,7 +237,9 @@ async function verifyTokenInner(token, options = {}) {
|
|
|
191
237
|
data,
|
|
192
238
|
verifiers,
|
|
193
239
|
resolver,
|
|
194
|
-
cache
|
|
240
|
+
cache,
|
|
241
|
+
methods,
|
|
242
|
+
historic
|
|
195
243
|
});
|
|
196
244
|
assertTimeClaimsValid(payload, timeOptions);
|
|
197
245
|
assertAudienceValid(payload, audience);
|
|
@@ -203,6 +251,10 @@ async function verifyTokenInner(token, options = {}) {
|
|
|
203
251
|
verifiedPublicKey
|
|
204
252
|
};
|
|
205
253
|
verifiedTokens.add(result);
|
|
254
|
+
verifiedTokenData.set(result, data);
|
|
255
|
+
if (historic === true) {
|
|
256
|
+
historicallyVerifiedTokens.add(result);
|
|
257
|
+
}
|
|
206
258
|
return result;
|
|
207
259
|
}
|
|
208
260
|
throw new Error('Unsupported signature algorithm');
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SignedHeader, SignedPayload, UnsignedHeader } from './schemas.js';
|
|
2
|
+
export type DIDString = `did:${string}:${string}`;
|
|
2
3
|
export type SignedToken<Payload extends Record<string, unknown> = Record<string, unknown>, Header extends Record<string, unknown> = Record<string, unknown>> = {
|
|
3
4
|
data: string;
|
|
4
5
|
header: SignedHeader & Header;
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kokuin/token",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.5.0",
|
|
5
4
|
"description": "JWT signing and verification",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"json",
|
|
@@ -15,38 +14,41 @@
|
|
|
15
14
|
"url": "https://github.com/TairuFramework/kokuin",
|
|
16
15
|
"directory": "packages/token"
|
|
17
16
|
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"sideEffects": false,
|
|
18
19
|
"type": "module",
|
|
19
|
-
"main": "lib/index.js",
|
|
20
|
-
"types": "lib/index.d.ts",
|
|
21
20
|
"exports": {
|
|
22
21
|
".": "./lib/index.js"
|
|
23
22
|
},
|
|
23
|
+
"main": "lib/index.js",
|
|
24
|
+
"types": "lib/index.d.ts",
|
|
24
25
|
"files": [
|
|
25
26
|
"lib/*"
|
|
26
27
|
],
|
|
27
|
-
"sideEffects": false,
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@noble/
|
|
30
|
-
"@noble/
|
|
31
|
-
"@
|
|
32
|
-
"@
|
|
33
|
-
"@sozai/
|
|
34
|
-
"@sozai/otel": "^0.3.0",
|
|
29
|
+
"@noble/curves": "^2.3.0",
|
|
30
|
+
"@noble/hashes": "^2.3.0",
|
|
31
|
+
"@scure/base": "^2.3.0",
|
|
32
|
+
"@sozai/codec": "^0.4.0",
|
|
33
|
+
"@sozai/otel": "^0.3.2",
|
|
35
34
|
"@sozai/schema": "^0.1.1",
|
|
36
35
|
"@kokuin/otel": "^0.3.0"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
39
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
40
|
-
"@opentelemetry/sdk-trace-node": "^2.
|
|
38
|
+
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
39
|
+
"@opentelemetry/sdk-trace-node": "^2.10.0",
|
|
41
40
|
"uint8arrays": "^6.1.1"
|
|
42
41
|
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
43
45
|
"scripts": {
|
|
46
|
+
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
44
47
|
"build:clean": "del lib",
|
|
45
48
|
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
46
49
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
47
|
-
"
|
|
50
|
+
"test": "pnpm run test:types && pnpm run test:unit",
|
|
48
51
|
"test:types": "tsc --noEmit --skipLibCheck -p tsconfig.test.json",
|
|
49
|
-
"test:unit": "vitest run"
|
|
50
|
-
"test": "pnpm run test:types && pnpm run test:unit"
|
|
52
|
+
"test:unit": "vitest run"
|
|
51
53
|
}
|
|
52
54
|
}
|
package/lib/jwe.d.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { DecryptingIdentity, SigningIdentity } from './identity.js';
|
|
2
|
-
import type { Verifiers } from './verifier.js';
|
|
3
|
-
export type ConcatKDFParams = {
|
|
4
|
-
sharedSecret: Uint8Array;
|
|
5
|
-
keyLength: number;
|
|
6
|
-
algorithmID: string;
|
|
7
|
-
partyUInfo: Uint8Array;
|
|
8
|
-
partyVInfo: Uint8Array;
|
|
9
|
-
};
|
|
10
|
-
export type JWEHeader = {
|
|
11
|
-
alg: string;
|
|
12
|
-
enc: string;
|
|
13
|
-
epk: {
|
|
14
|
-
kty: string;
|
|
15
|
-
crv: string;
|
|
16
|
-
x: string;
|
|
17
|
-
};
|
|
18
|
-
apu?: string;
|
|
19
|
-
apv?: string;
|
|
20
|
-
};
|
|
21
|
-
export type TokenEncrypter = {
|
|
22
|
-
recipientID?: string;
|
|
23
|
-
encrypt(plaintext: Uint8Array): Promise<string>;
|
|
24
|
-
};
|
|
25
|
-
export type EncryptOptions = {
|
|
26
|
-
algorithm: 'X25519';
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Concat KDF per RFC 7518 Section 4.6.2.
|
|
30
|
-
* Single SHA-256 iteration (sufficient for 256-bit keys).
|
|
31
|
-
*/
|
|
32
|
-
export declare function concatKDF(params: ConcatKDFParams): Uint8Array;
|
|
33
|
-
/**
|
|
34
|
-
* Create a token encrypter for a recipient identified by X25519 public key or DID string.
|
|
35
|
-
*/
|
|
36
|
-
export declare function createTokenEncrypter(recipient: Uint8Array, options: EncryptOptions): TokenEncrypter;
|
|
37
|
-
export declare function createTokenEncrypter(recipient: string): TokenEncrypter;
|
|
38
|
-
/**
|
|
39
|
-
* Encrypt plaintext to JWE compact serialization using the given encrypter.
|
|
40
|
-
*/
|
|
41
|
-
export declare function encryptToken(encrypter: TokenEncrypter, plaintext: Uint8Array): Promise<string>;
|
|
42
|
-
/**
|
|
43
|
-
* Decrypt a JWE compact serialization string.
|
|
44
|
-
*/
|
|
45
|
-
export declare function decryptToken(decrypter: DecryptingIdentity, jwe: string): Promise<Uint8Array>;
|
|
46
|
-
export type EnvelopeMode = 'plain' | 'jws' | 'jws-in-jwe' | 'jwe-in-jws';
|
|
47
|
-
export type UnwrappedEnvelope = {
|
|
48
|
-
payload: Record<string, unknown>;
|
|
49
|
-
mode: EnvelopeMode;
|
|
50
|
-
};
|
|
51
|
-
export type WrapOptions = {
|
|
52
|
-
signer?: SigningIdentity;
|
|
53
|
-
encrypter?: TokenEncrypter;
|
|
54
|
-
header?: Record<string, unknown>;
|
|
55
|
-
};
|
|
56
|
-
export type UnwrapOptions = {
|
|
57
|
-
decrypter?: DecryptingIdentity;
|
|
58
|
-
verifiers?: Verifiers;
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* Wrap a payload into a token string according to the specified envelope mode.
|
|
62
|
-
*/
|
|
63
|
-
export declare function wrapEnvelope(mode: EnvelopeMode, payload: Record<string, unknown>, options: WrapOptions): Promise<string>;
|
|
64
|
-
/**
|
|
65
|
-
* Unwrap a token string, auto-detecting the envelope mode from its structure.
|
|
66
|
-
*/
|
|
67
|
-
export declare function unwrapEnvelope(message: string, options: UnwrapOptions): Promise<UnwrappedEnvelope>;
|