@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/CHANGELOG.md +30 -0
- package/LICENSE.txt +1 -1
- package/dist/alg.d.ts +2 -2
- package/dist/alg.d.ts.map +1 -1
- package/dist/alg.js +19 -16
- package/dist/alg.js.map +1 -1
- package/dist/errors.js +15 -36
- package/dist/errors.js.map +1 -1
- package/dist/index.js +10 -28
- package/dist/index.js.map +1 -1
- package/dist/jwk.d.ts +3725 -1143
- package/dist/jwk.d.ts.map +1 -1
- package/dist/jwk.js +178 -96
- package/dist/jwk.js.map +1 -1
- package/dist/jwks.d.ts +212 -1523
- package/dist/jwks.d.ts.map +1 -1
- package/dist/jwks.js +25 -11
- package/dist/jwks.js.map +1 -1
- package/dist/jwt-decode.js +8 -11
- package/dist/jwt-decode.js.map +1 -1
- package/dist/jwt-verify.js +1 -2
- package/dist/jwt-verify.js.map +1 -1
- package/dist/jwt.d.ts +3937 -1186
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +97 -102
- package/dist/jwt.js.map +1 -1
- package/dist/key.d.ts +22 -9
- package/dist/key.d.ts.map +1 -1
- package/dist/key.js +159 -88
- package/dist/key.js.map +1 -1
- package/dist/keyset.d.ts +382 -15
- package/dist/keyset.d.ts.map +1 -1
- package/dist/keyset.js +153 -183
- package/dist/keyset.js.map +1 -1
- package/dist/util.d.ts +1 -6
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +21 -26
- package/dist/util.js.map +1 -1
- package/package.json +8 -7
- package/src/alg.ts +22 -10
- package/src/jwk.ts +163 -51
- package/src/jwks.ts +23 -6
- package/src/key.ts +137 -27
- package/src/keyset.ts +60 -60
- package/src/util.ts +8 -19
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/jwk",
|
|
3
|
-
"version": "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": "
|
|
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": "^
|
|
30
|
+
"multiformats": "^13.0.0",
|
|
30
31
|
"zod": "^3.23.8"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"typescript": "^
|
|
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 {
|
|
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:
|
|
9
|
+
export function* jwkAlgorithms(jwk: JwkBase): Generator<string, void, unknown> {
|
|
10
10
|
// Ed25519, Ed448, and secp256k1 always have "alg"
|
|
11
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
.
|
|
66
|
-
|
|
116
|
+
.min(1)
|
|
67
117
|
.optional(), // Other Primes Info
|
|
68
118
|
})
|
|
69
119
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
k.key_ops
|
|
131
|
-
|
|
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: '
|
|
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.
|
|
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(
|
|
146
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
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(
|
|
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.
|
|
21
|
+
export type Jwks = z.output<typeof jwksSchema>
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
|
-
* Public JSON Web Key Set schema.
|
|
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(
|
|
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.
|
|
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 {
|
|
3
|
-
|
|
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
|
-
|
|
21
|
+
export type KeyMatchOptions = {
|
|
22
|
+
usage?: KeyUsage
|
|
23
|
+
kid?: string | string[]
|
|
24
|
+
alg?: string | string[]
|
|
25
|
+
}
|
|
9
26
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
export type ActivityCheckOptions = {
|
|
28
|
+
allowRevoked?: boolean
|
|
29
|
+
clockTolerance?: number
|
|
30
|
+
currentDate?: Date
|
|
31
|
+
}
|
|
14
32
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
33
|
-
|
|
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
|
-
|
|
57
|
+
const validated = jwkPubSchema.safeParse({
|
|
43
58
|
...this.jwk,
|
|
44
59
|
d: undefined,
|
|
45
60
|
k: undefined,
|
|
46
|
-
|
|
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
|
|
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
|
|
57
|
-
return this.jwk.
|
|
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
|
+
}
|