@atproto/jwk 0.5.0 → 0.7.0-next.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/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@atproto/jwk",
3
- "version": "0.5.0",
3
+ "version": "0.7.0-next.0",
4
+ "engines": {
5
+ "node": ">=22"
6
+ },
4
7
  "license": "MIT",
5
8
  "description": "A library for working with JSON Web Keys (JWKs) in TypeScript. This is meant to be extended by environment-specific libraries like @atproto/jwk-jose.",
6
9
  "keywords": [
@@ -16,9 +19,7 @@
16
19
  "url": "https://github.com/bluesky-social/atproto",
17
20
  "directory": "packages/oauth/jwk"
18
21
  },
19
- "type": "commonjs",
20
- "main": "dist/index.js",
21
- "types": "dist/index.d.ts",
22
+ "type": "module",
22
23
  "exports": {
23
24
  ".": {
24
25
  "types": "./dist/index.d.ts",
@@ -26,13 +27,13 @@
26
27
  }
27
28
  },
28
29
  "dependencies": {
29
- "multiformats": "^9.9.0",
30
+ "multiformats": "^13.0.0",
30
31
  "zod": "^3.23.8"
31
32
  },
32
33
  "devDependencies": {
33
- "typescript": "^5.6.3"
34
+ "typescript": "^6.0.3"
34
35
  },
35
36
  "scripts": {
36
- "build": "tsc --build tsconfig.json"
37
+ "build": "tsc --build tsconfig.build.json"
37
38
  }
38
39
  }
package/src/alg.ts CHANGED
@@ -1,29 +1,29 @@
1
1
  import { JwkError } from './errors.js'
2
- import { Jwk } from './jwk.js'
2
+ import { JwkBase, isEncKeyUsage, isSigKeyUsage } from './jwk.js'
3
3
 
4
4
  // Copy variable to prevent bundlers from automatically polyfilling "process" (e.g. parcel)
5
5
  const { process } = globalThis
6
6
  const IS_NODE_RUNTIME =
7
7
  typeof process !== 'undefined' && typeof process?.versions?.node === 'string'
8
8
 
9
- export function* jwkAlgorithms(jwk: Jwk): Generator<string> {
9
+ export function* jwkAlgorithms(jwk: JwkBase): Generator<string, void, unknown> {
10
10
  // Ed25519, Ed448, and secp256k1 always have "alg"
11
- // OKP always has "use"
12
- if (jwk.alg) {
11
+
12
+ if (typeof jwk.alg === 'string') {
13
13
  yield jwk.alg
14
14
  return
15
15
  }
16
16
 
17
17
  switch (jwk.kty) {
18
18
  case 'EC': {
19
- if (jwk.use === 'enc' || jwk.use === undefined) {
19
+ if (jwkSupportsEnc(jwk)) {
20
20
  yield 'ECDH-ES'
21
21
  yield 'ECDH-ES+A128KW'
22
22
  yield 'ECDH-ES+A192KW'
23
23
  yield 'ECDH-ES+A256KW'
24
24
  }
25
25
 
26
- if (jwk.use === 'sig' || jwk.use === undefined) {
26
+ if (jwkSupportsSig(jwk)) {
27
27
  const crv = 'crv' in jwk ? jwk.crv : undefined
28
28
  switch (crv) {
29
29
  case 'P-256':
@@ -54,7 +54,7 @@ export function* jwkAlgorithms(jwk: Jwk): Generator<string> {
54
54
  }
55
55
 
56
56
  case 'RSA': {
57
- if (jwk.use === 'enc' || jwk.use === undefined) {
57
+ if (jwkSupportsEnc(jwk)) {
58
58
  yield 'RSA-OAEP'
59
59
  yield 'RSA-OAEP-256'
60
60
  yield 'RSA-OAEP-384'
@@ -62,7 +62,7 @@ export function* jwkAlgorithms(jwk: Jwk): Generator<string> {
62
62
  if (IS_NODE_RUNTIME) yield 'RSA1_5'
63
63
  }
64
64
 
65
- if (jwk.use === 'sig' || jwk.use === undefined) {
65
+ if (jwkSupportsSig(jwk)) {
66
66
  yield 'PS256'
67
67
  yield 'PS384'
68
68
  yield 'PS512'
@@ -75,7 +75,7 @@ export function* jwkAlgorithms(jwk: Jwk): Generator<string> {
75
75
  }
76
76
 
77
77
  case 'oct': {
78
- if (jwk.use === 'enc' || jwk.use === undefined) {
78
+ if (jwkSupportsEnc(jwk)) {
79
79
  yield 'A128GCMKW'
80
80
  yield 'A192GCMKW'
81
81
  yield 'A256GCMKW'
@@ -84,7 +84,7 @@ export function* jwkAlgorithms(jwk: Jwk): Generator<string> {
84
84
  yield 'A256KW'
85
85
  }
86
86
 
87
- if (jwk.use === 'sig' || jwk.use === undefined) {
87
+ if (jwkSupportsSig(jwk)) {
88
88
  yield 'HS256'
89
89
  yield 'HS384'
90
90
  yield 'HS512'
@@ -97,3 +97,15 @@ export function* jwkAlgorithms(jwk: Jwk): Generator<string> {
97
97
  throw new JwkError(`Unsupported kty "${jwk.kty}"`)
98
98
  }
99
99
  }
100
+
101
+ function jwkSupportsEnc(jwk: JwkBase): boolean {
102
+ return (
103
+ jwk.key_ops?.some(isEncKeyUsage) ?? (jwk.use == null || jwk.use === 'enc')
104
+ )
105
+ }
106
+
107
+ function jwkSupportsSig(jwk: JwkBase): boolean {
108
+ return (
109
+ jwk.key_ops?.some(isSigKeyUsage) ?? (jwk.use == null || jwk.use === 'sig')
110
+ )
111
+ }
package/src/jwk.ts CHANGED
@@ -1,45 +1,96 @@
1
1
  import { z } from 'zod'
2
+ import { isLastOccurrence } from './util.js'
2
3
 
3
- export const keyUsageSchema = z.enum([
4
+ export const PUBLIC_KEY_USAGE = ['verify', 'encrypt', 'wrapKey'] as const
5
+ export const publicKeyUsageSchema = z.enum(PUBLIC_KEY_USAGE)
6
+ export type PublicKeyUsage = (typeof PUBLIC_KEY_USAGE)[number]
7
+ export function isPublicKeyUsage(usage: unknown): usage is PublicKeyUsage {
8
+ return (PUBLIC_KEY_USAGE as readonly unknown[]).includes(usage)
9
+ }
10
+
11
+ /**
12
+ * Determines if the given key usage is consistent for "sig" (signature) public
13
+ * key use.
14
+ */
15
+ export function isSigKeyUsage(v: KeyUsage) {
16
+ return v === 'verify'
17
+ }
18
+
19
+ /**
20
+ * Determines if the given key usage is consistent for "enc" (encryption) public
21
+ * key use.
22
+ *
23
+ * > When a key is used to wrap another key and a public key use
24
+ * > designation for the first key is desired, the "enc" (encryption)
25
+ * > key use value is used, since key wrapping is a kind of encryption.
26
+ * > The "enc" value is also to be used for public keys used for key
27
+ * > agreement operations.
28
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4.2}
29
+ */
30
+ export function isEncKeyUsage(v: KeyUsage) {
31
+ return v === 'encrypt' || v === 'wrapKey'
32
+ }
33
+
34
+ export const PRIVATE_KEY_USAGE = [
4
35
  'sign',
5
- 'verify',
6
- 'encrypt',
7
36
  'decrypt',
8
- 'wrapKey',
9
37
  'unwrapKey',
10
38
  'deriveKey',
11
39
  'deriveBits',
12
- ])
40
+ ] as const
41
+ export const privateKeyUsageSchema = z.enum(PRIVATE_KEY_USAGE)
42
+ export type PrivateKeyUsage = (typeof PRIVATE_KEY_USAGE)[number]
43
+ export function isPrivateKeyUsage(usage: unknown): usage is PrivateKeyUsage {
44
+ return (PRIVATE_KEY_USAGE as readonly unknown[]).includes(usage)
45
+ }
13
46
 
14
- export type KeyUsage = z.infer<typeof keyUsageSchema>
47
+ export const KEY_USAGE = [...PRIVATE_KEY_USAGE, ...PUBLIC_KEY_USAGE] as const
48
+ export const keyUsageSchema = z.enum(KEY_USAGE)
49
+ export type KeyUsage = (typeof KEY_USAGE)[number]
15
50
 
16
51
  /**
17
- * The "use" and "key_ops" JWK members SHOULD NOT be used together;
18
- * however, if both are used, the information they convey MUST be
19
- * consistent. Applications should specify which of these members they
20
- * use, if either is to be used by the application.
21
- *
22
- * @todo Actually check that "use" and "key_ops" are consistent when both are present.
23
- * @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4.3}
52
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4 JSON Web Key (JWK) Format}
53
+ * @see {@link https://www.iana.org/assignments/jose/jose.xhtml#web-key-parameters IANA "JSON Web Key Parameters" registry}
24
54
  */
25
- export const jwkBaseSchema = z.object({
55
+ const jwkBaseSchema = z.object({
26
56
  kty: z.string().min(1),
27
57
  alg: z.string().min(1).optional(),
28
58
  kid: z.string().min(1).optional(),
29
- ext: z.boolean().optional(),
30
59
  use: z.enum(['sig', 'enc']).optional(),
31
- key_ops: z.array(keyUsageSchema).optional(),
60
+ key_ops: z
61
+ .array(keyUsageSchema)
62
+ .min(1, { message: 'At least one key usage must be specified' })
63
+ // https://datatracker.ietf.org/doc/html/rfc7517#section-4.3
64
+ // > Duplicate key operation values MUST NOT be present in the array.
65
+ .refine((ops) => ops.every(isLastOccurrence), {
66
+ message: 'key_ops must not contain duplicates',
67
+ })
68
+ .optional(),
32
69
 
33
70
  x5c: z.array(z.string()).optional(), // X.509 Certificate Chain
34
71
  x5t: z.string().min(1).optional(), // X.509 Certificate SHA-1 Thumbprint
35
72
  'x5t#S256': z.string().min(1).optional(), // X.509 Certificate SHA-256 Thumbprint
36
73
  x5u: z.string().url().optional(), // X.509 URL
74
+
75
+ // https://www.w3.org/TR/webcrypto/
76
+ ext: z.boolean().optional(), // Extractable
77
+
78
+ // Federation Historical Keys Response
79
+ // https://openid.net/specs/openid-federation-1_0.html#name-federation-historical-keys-res
80
+ iat: z.number().int().optional(), // Issued At (timestamp)
81
+ exp: z.number().int().optional(), // Expiration Time (timestamp)
82
+ nbf: z.number().int().optional(), // Not Before (timestamp)
83
+ revoked: z // properties of the revocation
84
+ .object({
85
+ revoked_at: z.number().int(),
86
+ reason: z.string().optional(),
87
+ })
88
+ .optional(),
37
89
  })
38
90
 
39
- /**
40
- * @todo: properly implement this
41
- */
42
- export const jwkRsaKeySchema = jwkBaseSchema.extend({
91
+ export type JwkBase = z.infer<typeof jwkBaseSchema>
92
+
93
+ const jwkRsaKeySchema = jwkBaseSchema.extend({
43
94
  kty: z.literal('RSA'),
44
95
  alg: z
45
96
  .enum(['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])
@@ -62,12 +113,11 @@ export const jwkRsaKeySchema = jwkBaseSchema.extend({
62
113
  t: z.string().optional(),
63
114
  }),
64
115
  )
65
- .nonempty()
66
-
116
+ .min(1)
67
117
  .optional(), // Other Primes Info
68
118
  })
69
119
 
70
- export const jwkEcKeySchema = jwkBaseSchema.extend({
120
+ const jwkEcKeySchema = jwkBaseSchema.extend({
71
121
  kty: z.literal('EC'),
72
122
  alg: z.enum(['ES256', 'ES384', 'ES512']).optional(),
73
123
  crv: z.enum(['P-256', 'P-384', 'P-521']),
@@ -78,7 +128,7 @@ export const jwkEcKeySchema = jwkBaseSchema.extend({
78
128
  d: z.string().min(1).optional(), // ECC Private Key
79
129
  })
80
130
 
81
- export const jwkEcSecp256k1KeySchema = jwkBaseSchema.extend({
131
+ const jwkEcSecp256k1KeySchema = jwkBaseSchema.extend({
82
132
  kty: z.literal('EC'),
83
133
  alg: z.enum(['ES256K']).optional(),
84
134
  crv: z.enum(['secp256k1']),
@@ -89,7 +139,7 @@ export const jwkEcSecp256k1KeySchema = jwkBaseSchema.extend({
89
139
  d: z.string().min(1).optional(), // ECC Private Key
90
140
  })
91
141
 
92
- export const jwkOkpKeySchema = jwkBaseSchema.extend({
142
+ const jwkOkpKeySchema = jwkBaseSchema.extend({
93
143
  kty: z.literal('OKP'),
94
144
  alg: z.enum(['EdDSA']).optional(),
95
145
  crv: z.enum(['Ed25519', 'Ed448']),
@@ -98,54 +148,116 @@ export const jwkOkpKeySchema = jwkBaseSchema.extend({
98
148
  d: z.string().min(1).optional(), // ECC Private Key
99
149
  })
100
150
 
101
- export const jwkSymKeySchema = jwkBaseSchema.extend({
151
+ const jwkSymKeySchema = jwkBaseSchema.extend({
102
152
  kty: z.literal('oct'), // Octet Sequence (used to represent symmetric keys)
103
153
  alg: z.enum(['HS256', 'HS384', 'HS512']).optional(),
104
154
 
105
155
  k: z.string(), // Key Value (base64url encoded)
106
156
  })
107
157
 
108
- export const jwkUnknownKeySchema = jwkBaseSchema.extend({
109
- kty: z
110
- .string()
111
- .refine((v) => v !== 'RSA' && v !== 'EC' && v !== 'OKP' && v !== 'oct'),
112
- })
113
-
158
+ /**
159
+ * Zod parser for known JWK types
160
+ */
114
161
  export const jwkSchema = z
115
162
  .union([
116
- jwkUnknownKeySchema,
117
163
  jwkRsaKeySchema,
118
164
  jwkEcKeySchema,
119
165
  jwkEcSecp256k1KeySchema,
120
166
  jwkOkpKeySchema,
121
167
  jwkSymKeySchema,
122
168
  ])
169
+ // @TODO These rules should be applied to jwkBaseSchema, but Zod 3 doesn't
170
+ // support extending refined schemas. Move these to the base schema when we
171
+ // upgrade to Zod 4.
172
+ .refine(
173
+ // https://datatracker.ietf.org/doc/html/rfc7517#section-4.2
174
+ // > The "use" (public key use) parameter identifies the intended use of the
175
+ // > public key
176
+ (k): boolean => k.use == null || isPublicJwk(k),
177
+ {
178
+ message: '"use" can only be used with public keys',
179
+ path: ['use'],
180
+ },
181
+ )
182
+ .refine(
183
+ (k): boolean => !k.key_ops?.some(isPrivateKeyUsage) || isPrivateJwk(k),
184
+ {
185
+ message: 'private key usage not allowed for public keys',
186
+ path: ['key_ops'],
187
+ },
188
+ )
123
189
  .refine(
124
- (k) =>
125
- // https://datatracker.ietf.org/doc/html/rfc7517#section-4.3
126
- // > The "use" parameter is employed to indicate whether a public key is
127
- // > used for encrypting data or verifying the signature on data.
128
- !k.use ||
129
- !k.key_ops ||
130
- k.key_ops.every(
131
- k.use === 'sig' ? (o) => o === 'verify' : (o) => o === 'decrypt',
132
- ),
190
+ // https://datatracker.ietf.org/doc/html/rfc7517#section-4.3
191
+ // > The "use" and "key_ops" JWK members SHOULD NOT be used together;
192
+ // > however, if both are used, the information they convey MUST be
193
+ // > consistent.
194
+ (k): boolean =>
195
+ k.use == null ||
196
+ k.key_ops == null ||
197
+ (k.use === 'sig' && k.key_ops.every(isSigKeyUsage)) ||
198
+ (k.use === 'enc' && k.key_ops.every(isEncKeyUsage)),
133
199
  {
134
- message: 'use and key_ops must be consistent',
200
+ message: '"key_ops" must be consistent with "use"',
135
201
  path: ['key_ops'],
136
202
  },
137
203
  )
138
204
 
139
- export type Jwk = z.infer<typeof jwkSchema>
205
+ export type Jwk = z.output<typeof jwkSchema>
140
206
 
141
- /** @deprecated use {@link jwkSchema} */
207
+ /** @deprecated use {@link jwkSchema} instead */
142
208
  export const jwkValidator = jwkSchema
143
209
 
144
210
  export const jwkPubSchema = jwkSchema
145
- .refine((k) => k.kid != null, 'kid is required')
146
- .refine((k) => !('k' in k) && !('d' in k), 'private key not allowed')
211
+ .refine(hasKid, {
212
+ message: '"kid" is required',
213
+ path: ['kid'],
214
+ })
215
+ // @NOTE for legacy reasons, we don't impose the presence of either "use" or "key_ops"
216
+ .refine(isPublicJwk, {
217
+ message: 'private key not allowed',
218
+ })
219
+ .refine((k): boolean => !k.key_ops || k.key_ops.every(isPublicKeyUsage), {
220
+ message: '"key_ops" must not contain private key usage for public keys',
221
+ path: ['key_ops'],
222
+ })
223
+
224
+ export type PublicJwk = z.output<typeof jwkPubSchema>
225
+
226
+ export const jwkPrivateSchema = jwkSchema
227
+ // @NOTE we don't impose the presence of "kid"
228
+ .refine(isPrivateJwk, {
229
+ message: 'private key required',
230
+ })
231
+
232
+ export type PrivateJwk = z.output<typeof jwkPrivateSchema>
233
+
234
+ export function hasKid<J extends object>(
235
+ jwk: J,
236
+ ): jwk is J & { kid: NonNullable<unknown> } {
237
+ return 'kid' in jwk && jwk.kid != null
238
+ }
239
+
240
+ export function hasSharedSecretJwk<J extends object>(
241
+ jwk: J,
242
+ ): jwk is J & { k: NonNullable<unknown> } {
243
+ return 'k' in jwk && jwk.k != null
244
+ }
245
+
246
+ export function hasPrivateSecretJwk<J extends object>(
247
+ jwk: J,
248
+ ): jwk is J & { d: NonNullable<unknown> } {
249
+ return 'd' in jwk && jwk.d != null
250
+ }
251
+
252
+ export function isPrivateJwk<J extends object>(jwk: J) {
253
+ return hasPrivateSecretJwk(jwk) || hasSharedSecretJwk(jwk)
254
+ }
147
255
 
148
- export const jwkPrivateSchema = jwkSchema.refine(
149
- (k) => ('k' in k && k.k != null) || ('d' in k && k.d != null),
150
- 'private key required',
151
- )
256
+ export function isPublicJwk<J extends object>(
257
+ jwk: J,
258
+ ): jwk is Extract<
259
+ Exclude<J, { k: NonNullable<unknown> }>,
260
+ { d?: NonNullable<unknown> }
261
+ > & { d?: never } {
262
+ return !hasPrivateSecretJwk(jwk) && !hasSharedSecretJwk(jwk)
263
+ }
package/src/jwks.ts CHANGED
@@ -6,17 +6,34 @@ import { jwkPubSchema, jwkSchema } from './jwk.js'
6
6
  * collection of JSON Web Keys (JWKs), that can be both public and private.
7
7
  */
8
8
  export const jwksSchema = z.object({
9
- keys: z.array(jwkSchema),
9
+ keys: z.array(z.unknown()).transform((input) => {
10
+ // > Implementations SHOULD ignore JWKs within a JWK Set that use "kty"
11
+ // > (key type) values that are not understood by them, that are missing
12
+ // > required members, or for which values are out of the supported
13
+ // > ranges.
14
+ return input
15
+ .map((item) => jwkSchema.safeParse(item))
16
+ .filter((res) => res.success)
17
+ .map((res) => res.data)
18
+ }),
10
19
  })
11
20
 
12
- export type Jwks = z.infer<typeof jwksSchema>
21
+ export type Jwks = z.output<typeof jwksSchema>
13
22
 
14
23
  /**
15
- * Public JSON Web Key Set schema. All keys must be public keys, have a `kid`,
16
- * and `use` or `key_ops` defined.
24
+ * Public JSON Web Key Set schema.
17
25
  */
18
26
  export const jwksPubSchema = z.object({
19
- keys: z.array(jwkPubSchema),
27
+ keys: z.array(z.unknown()).transform((input) => {
28
+ // > Implementations SHOULD ignore JWKs within a JWK Set that use "kty"
29
+ // > (key type) values that are not understood by them, that are missing
30
+ // > required members, or for which values are out of the supported
31
+ // > ranges.
32
+ return input
33
+ .map((item) => jwkPubSchema.safeParse(item))
34
+ .filter((res) => res.success)
35
+ .map((res) => res.data)
36
+ }),
20
37
  })
21
38
 
22
- export type JwksPub = z.infer<typeof jwksPubSchema>
39
+ export type JwksPub = z.output<typeof jwksPubSchema>
package/src/key.ts CHANGED
@@ -1,60 +1,92 @@
1
1
  import { jwkAlgorithms } from './alg.js'
2
- import { JwkError } from './errors.js'
3
- import { Jwk, jwkSchema } from './jwk.js'
2
+ import {
3
+ Jwk,
4
+ KeyUsage,
5
+ PUBLIC_KEY_USAGE,
6
+ PrivateJwk,
7
+ PublicJwk,
8
+ PublicKeyUsage,
9
+ hasSharedSecretJwk,
10
+ isEncKeyUsage,
11
+ isPrivateJwk,
12
+ isPublicKeyUsage,
13
+ isSigKeyUsage,
14
+ jwkPubSchema,
15
+ jwkSchema,
16
+ } from './jwk.js'
4
17
  import { VerifyOptions, VerifyResult } from './jwt-verify.js'
5
18
  import { JwtHeader, JwtPayload, SignedJwt } from './jwt.js'
6
19
  import { cachedGetter } from './util.js'
7
20
 
8
- const jwkSchemaReadonly = jwkSchema.readonly()
21
+ export type KeyMatchOptions = {
22
+ usage?: KeyUsage
23
+ kid?: string | string[]
24
+ alg?: string | string[]
25
+ }
9
26
 
10
- export abstract class Key<J extends Jwk = Jwk> {
11
- constructor(protected readonly jwk: Readonly<J>) {
12
- // @TODO "use" is actually only for public keys. We should allow missing
13
- // "use" here and automatically add it to the exposed `publicJwk`
27
+ export type ActivityCheckOptions = {
28
+ allowRevoked?: boolean
29
+ clockTolerance?: number
30
+ currentDate?: Date
31
+ }
14
32
 
15
- // A key should always be used either for signing or encryption.
16
- if (!jwk.use) throw new JwkError('Missing "use" Parameter value')
17
- }
33
+ export abstract class Key<J extends Jwk = Jwk> {
34
+ constructor(readonly jwk: Readonly<J>) {}
18
35
 
36
+ @cachedGetter
19
37
  get isPrivate(): boolean {
20
- const { jwk } = this
21
- if ('d' in jwk && jwk.d !== undefined) return true
22
- if ('k' in jwk && jwk.k !== undefined) return true
23
- return false
38
+ return isPrivateJwk(this.jwk)
24
39
  }
25
40
 
41
+ @cachedGetter
26
42
  get isSymetric(): boolean {
27
- const { jwk } = this
28
- if ('k' in jwk && jwk.k !== undefined) return true
29
- return false
43
+ return hasSharedSecretJwk(this.jwk)
30
44
  }
31
45
 
32
- get privateJwk(): Readonly<J> | undefined {
33
- return this.isPrivate ? this.jwk : undefined
46
+ get privateJwk(): Readonly<PrivateJwk> | undefined {
47
+ if (!this.isPrivate) return undefined
48
+
49
+ return this.jwk as Readonly<PrivateJwk>
34
50
  }
35
51
 
36
52
  @cachedGetter
37
- get publicJwk():
38
- | Readonly<Exclude<J, { kty: 'oct' }> & { d?: never }>
39
- | undefined {
53
+ get publicJwk(): Readonly<PublicJwk> | undefined {
40
54
  if (this.isSymetric) return undefined
55
+ if (!this.isPrivate) return this.jwk as Readonly<PublicJwk>
41
56
 
42
- return jwkSchemaReadonly.parse({
57
+ const validated = jwkPubSchema.safeParse({
43
58
  ...this.jwk,
44
59
  d: undefined,
45
60
  k: undefined,
46
- }) as Exclude<J, { kty: 'oct' }> & { d?: never }
61
+ use: undefined,
62
+ key_ops: buildPublicKeyOps(this.keyOps) ?? PUBLIC_KEY_USAGE,
63
+ })
64
+
65
+ // One reason why the parsing might fail is if key_ops is empty. This check
66
+ // also allows to future proof the code (e.g if another type of private key
67
+ // is added that uses a different property than "d" or "k" to store its
68
+ // private value).
69
+ if (!validated.success) return undefined
70
+
71
+ return Object.freeze(validated.data)
47
72
  }
48
73
 
49
74
  @cachedGetter
50
75
  get bareJwk(): Readonly<Jwk> | undefined {
51
76
  if (this.isSymetric) return undefined
52
77
  const { kty, crv, e, n, x, y } = this.jwk as any
53
- return jwkSchemaReadonly.parse({ crv, e, kty, n, x, y })
78
+ return Object.freeze(jwkSchema.parse({ crv, e, kty, n, x, y }))
79
+ }
80
+
81
+ /**
82
+ * @note Only defined on public keys
83
+ */
84
+ get use(): 'sig' | 'enc' | undefined {
85
+ return this.jwk.use
54
86
  }
55
87
 
56
- get use() {
57
- return this.jwk.use!
88
+ get keyOps(): readonly KeyUsage[] | undefined {
89
+ return this.jwk.key_ops
58
90
  }
59
91
 
60
92
  /**
@@ -84,6 +116,67 @@ export abstract class Key<J extends Jwk = Jwk> {
84
116
  return Object.freeze(Array.from(jwkAlgorithms(this.jwk)))
85
117
  }
86
118
 
119
+ get isRevoked() {
120
+ return this.jwk.revoked != null
121
+ }
122
+
123
+ isActive(options?: ActivityCheckOptions) {
124
+ if (!options?.allowRevoked && this.isRevoked) return false
125
+
126
+ const tolerance = options?.clockTolerance ?? 0
127
+ if (tolerance !== Infinity) {
128
+ const now = options?.currentDate?.getTime() ?? Date.now()
129
+ const { exp, nbf } = this.jwk
130
+
131
+ if (nbf != null && !(now >= nbf * 1e3 - tolerance)) return false
132
+ if (exp != null && !(now < exp * 1e3 + tolerance)) return false
133
+ }
134
+
135
+ return true
136
+ }
137
+
138
+ matches(opts: KeyMatchOptions): boolean {
139
+ if (opts.kid != null) {
140
+ const matchesKid = Array.isArray(opts.kid)
141
+ ? this.kid != null && opts.kid.includes(this.kid)
142
+ : this.kid === opts.kid
143
+ if (!matchesKid) return false
144
+ }
145
+
146
+ if (opts.alg != null) {
147
+ const matchesAlg = Array.isArray(opts.alg)
148
+ ? opts.alg.some((a) => this.algorithms.includes(a))
149
+ : this.algorithms.includes(opts.alg)
150
+ if (!matchesAlg) return false
151
+ }
152
+
153
+ if (opts.usage != null) {
154
+ const matchesOps =
155
+ this.keyOps == null ||
156
+ this.keyOps.includes(opts.usage) ||
157
+ // @NOTE Because this.jwk represents the private key (typically used for
158
+ // private operations), the public counterpart operations are allowed.
159
+ (opts.usage === 'verify' && this.keyOps.includes('sign')) ||
160
+ (opts.usage === 'encrypt' && this.keyOps.includes('decrypt')) ||
161
+ (opts.usage === 'wrapKey' && this.keyOps.includes('unwrapKey'))
162
+ if (!matchesOps) return false
163
+
164
+ const matchesUse =
165
+ this.use == null ||
166
+ (this.use === 'sig' && isSigKeyUsage(opts.usage)) ||
167
+ (this.use === 'enc' && isEncKeyUsage(opts.usage))
168
+ if (!matchesUse) return false
169
+
170
+ // @NOTE This is only relevant when "key_ops" and "use" are undefined.
171
+ // This line also ensures that when "opts.usage" is a private key usage
172
+ // (e.g. "sign"), the key is indeed a private key.
173
+ const matchesKeyType = this.isPrivate || isPublicKeyUsage(opts.usage)
174
+ if (!matchesKeyType) return false
175
+ }
176
+
177
+ return true
178
+ }
179
+
87
180
  /**
88
181
  * Create a signed JWT
89
182
  */
@@ -99,3 +192,20 @@ export abstract class Key<J extends Jwk = Jwk> {
99
192
  options?: VerifyOptions<C>,
100
193
  ): Promise<VerifyResult<C>>
101
194
  }
195
+
196
+ function buildPublicKeyOps(
197
+ keyUsages?: readonly KeyUsage[],
198
+ ): PublicKeyUsage[] | undefined {
199
+ if (keyUsages == null) return undefined
200
+
201
+ // https://datatracker.ietf.org/doc/html/rfc7517#section-4.3
202
+ // > Duplicate key operation values MUST NOT be present in the array.
203
+ const publicOps = new Set(keyUsages.filter(isPublicKeyUsage))
204
+
205
+ // @NOTE Translating private key usage into public key usage
206
+ if (keyUsages.includes('sign')) publicOps.add('verify')
207
+ if (keyUsages.includes('decrypt')) publicOps.add('encrypt')
208
+ if (keyUsages.includes('unwrapKey')) publicOps.add('wrapKey')
209
+
210
+ return Array.from(publicOps)
211
+ }