@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/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, createDecryptingIdentity, createFullIdentity, createIdentity, createSigningIdentity, type DecryptingIdentity, type FullIdentity, type Identity, type IdentityKeySpec, type IdentityProvider, isDecryptingIdentity, isFullIdentity, isOwnIdentity, isSigningIdentity, type KeyAlg, type KeyPurpose, type MultiKeyIdentity, type OwnIdentity, type ResolvedKey, randomIdentity, type SigningIdentity, type SignTokenOptions, } from './identity.js';
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 { createDecryptingIdentity, createFullIdentity, createIdentity, createSigningIdentity, isDecryptingIdentity, isFullIdentity, isOwnIdentity, isSigningIdentity, randomIdentity } from './identity.js';
14
- export { concatKDF, createTokenEncrypter, decryptToken, encryptToken, unwrapEnvelope, wrapEnvelope } from './jwe.js';
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';
@@ -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: string;
115
- shortForm: string;
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */ export const signedHeaderSchema = {
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
- /** @internal */ export const unsignedHeaderSchema = {
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
- /** @internal */ export const signedPayloadSchema = {
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?: string | Array<string>;
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(payload.iss, {
49
- kid: header.kid
50
- }, effectiveResolver);
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 || data === recomputed) {
108
- return recomputed;
109
- }
110
- // `data` may use a different JSON serialization of the same header and payload.
111
- // Accept it only if it decodes to exactly the same values, so the signed bytes
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
- if (isVerifiedToken(token)) {
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.3.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/ciphers": "^2.1.1",
30
- "@noble/curves": "^2.0.1",
31
- "@noble/hashes": "^2.0.0",
32
- "@scure/base": "^2.2.0",
33
- "@sozai/codec": "^0.2.0",
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.9.0",
40
- "@opentelemetry/sdk-trace-node": "^2.9.0",
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
- "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
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>;