@atproto/jwk 0.4.0 → 0.6.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 +26 -0
- package/dist/alg.d.ts +2 -2
- package/dist/alg.d.ts.map +1 -1
- package/dist/alg.js +14 -8
- package/dist/alg.js.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/jwk.d.ts +3513 -1310
- package/dist/jwk.d.ts.map +1 -1
- package/dist/jwk.js +148 -48
- package/dist/jwk.js.map +1 -1
- package/dist/jwks.d.ts +209 -1512
- package/dist/jwks.d.ts.map +1 -1
- package/dist/jwks.js +27 -2
- package/dist/jwks.js.map +1 -1
- package/dist/jwt-decode.js.map +1 -1
- package/dist/jwt-verify.js.map +1 -1
- package/dist/jwt.d.ts +3668 -1487
- package/dist/jwt.d.ts.map +1 -1
- 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 +101 -18
- 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 +32 -46
- 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 +4 -0
- package/dist/util.js.map +1 -1
- package/package.json +2 -2
- package/src/alg.ts +22 -10
- package/src/jwk.ts +177 -60
- package/src/jwks.ts +29 -4
- package/src/key.ts +137 -24
- package/src/keyset.ts +60 -60
- package/src/util.ts +6 -18
package/dist/util.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.segmentedStringRefinementFactory = exports.jwtCharsRefinement = exports.cachedGetter = exports.preferredOrderCmp = exports.isDefined = void 0;
|
|
4
4
|
exports.matchesAny = matchesAny;
|
|
5
5
|
exports.parseB64uJson = parseB64uJson;
|
|
6
|
+
exports.isLastOccurrence = isLastOccurrence;
|
|
6
7
|
const base64_1 = require("multiformats/bases/base64");
|
|
7
8
|
const zod_1 = require("zod");
|
|
8
9
|
const isDefined = (i) => i !== undefined;
|
|
@@ -140,4 +141,7 @@ const segmentedStringRefinementFactory = (count, minPartLength = 2) => {
|
|
|
140
141
|
};
|
|
141
142
|
};
|
|
142
143
|
exports.segmentedStringRefinementFactory = segmentedStringRefinementFactory;
|
|
144
|
+
function isLastOccurrence(v, i, arr) {
|
|
145
|
+
return arr.indexOf(v, i + 1) === -1;
|
|
146
|
+
}
|
|
143
147
|
//# sourceMappingURL=util.js.map
|
package/dist/util.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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","sourcesContent":["import { base64url } from 'multiformats/bases/base64'\nimport { RefinementCtx, ZodIssueCode } from 'zod'\n\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport type Simplify<T> = { [K in keyof T]: T[K] } & {}\nexport type Override<T, V> = Simplify<V & Omit<T, keyof V>>\n\nexport type RequiredKey<T, K extends keyof T = never> = Simplify<\n T & {\n [L in K]-?: unknown extends T[L]\n ? NonNullable<unknown> | null\n : Exclude<T[L], undefined>\n }\n>\n\nexport const isDefined = <T>(i: T | undefined): i is T => i !== undefined\n\nexport const preferredOrderCmp =\n <T>(order: readonly T[]) =>\n (a: T, b: T) => {\n const aIdx = order.indexOf(a)\n const bIdx = order.indexOf(b)\n if (aIdx === bIdx) return 0\n if (aIdx === -1) return 1\n if (bIdx === -1) return -1\n return aIdx - bIdx\n }\n\nexport function matchesAny<T extends string | number | symbol | boolean>(\n value: null | undefined | T | readonly T[],\n): (v: unknown) => v is T {\n return value == null\n ? (v): v is T => true\n : Array.isArray(value)\n ? (v): v is T => value.includes(v)\n : (v): v is T => v === value\n}\n\n/**\n * Decorator to cache the result of a getter on a class instance.\n */\nexport const cachedGetter = <T extends object, V>(\n target: (this: T) => V,\n _context: ClassGetterDecoratorContext<T, V>,\n) => {\n return function (this: T) {\n const value = target.call(this)\n Object.defineProperty(this, target.name, {\n get: () => value,\n enumerable: true,\n configurable: true,\n })\n return value\n }\n}\n\nconst decoder = new TextDecoder()\nexport function parseB64uJson(input: string): unknown {\n const inputBytes = base64url.baseDecode(input)\n const json = decoder.decode(inputBytes)\n return JSON.parse(json)\n}\n\n/**\n * @example\n * ```ts\n * // jwtSchema will only allow base64url chars & \".\" (dot)\n * const jwtSchema = z.string().superRefine(jwtCharsRefinement)\n * ```\n */\nexport const jwtCharsRefinement = (data: string, ctx: RefinementCtx): void => {\n // Note: this is a hot path, let's avoid using a RegExp\n let char\n\n for (let i = 0; i < data.length; i++) {\n char = data.charCodeAt(i)\n\n if (\n // Base64 URL encoding (most frequent)\n (65 <= char && char <= 90) || // A-Z\n (97 <= char && char <= 122) || // a-z\n (48 <= char && char <= 57) || // 0-9\n char === 45 || // -\n char === 95 || // _\n // Boundary (least frequent, check last)\n char === 46 // .\n ) {\n // continue\n } else {\n // Invalid char might be a surrogate pair\n const invalidChar = String.fromCodePoint(data.codePointAt(i)!)\n return ctx.addIssue({\n code: ZodIssueCode.custom,\n message: `Invalid character \"${invalidChar}\" in JWT at position ${i}`,\n })\n }\n }\n}\n\n/**\n * @example\n * ```ts\n * type SegmentedString3 = SegmentedString<3> // `${string}.${string}.${string}`\n * type SegmentedString4 = SegmentedString<4> // `${string}.${string}.${string}.${string}`\n * ```\n *\n * @note\n * This utility only provides one way type safety (A SegmentedString<4> can be\n * assigned to SegmentedString<3> but not vice versa). The purpose of this\n * utility is to improve DX by avoiding as many potential errors as build time.\n * DO NOT rely on this to enforce security or data integrity.\n */\ntype SegmentedString<\n C extends number,\n Acc extends string[] = [string],\n> = Acc['length'] extends C\n ? `${Acc[0]}`\n : `${Acc[0]}.${SegmentedString<C, [string, ...Acc]>}`\n\n/**\n * @example\n * ```ts\n * const jwtSchema = z.string().superRefine(segmentedStringRefinementFactory(3))\n * type Jwt = z.infer<typeof jwtSchema> // `${string}.${string}.${string}`\n * ```\n */\nexport const segmentedStringRefinementFactory = <C extends number>(\n count: C,\n minPartLength = 2,\n) => {\n if (!Number.isFinite(count) || count < 1 || (count | 0) !== count) {\n throw new TypeError(`Count must be a natural number (got ${count})`)\n }\n\n const minTotalLength = count * minPartLength + (count - 1)\n const errorPrefix = `Invalid JWT format`\n\n return (data: string, ctx: RefinementCtx): data is SegmentedString<C> => {\n if (data.length < minTotalLength) {\n ctx.addIssue({\n code: ZodIssueCode.custom,\n message: `${errorPrefix}: too short`,\n })\n return false\n }\n let currentStart = 0\n for (let i = 0; i < count - 1; i++) {\n const nextDot = data.indexOf('.', currentStart)\n if (nextDot === -1) {\n ctx.addIssue({\n code: ZodIssueCode.custom,\n message: `${errorPrefix}: expected ${count} segments, got ${i + 1}`,\n })\n return false\n }\n if (nextDot - currentStart < minPartLength) {\n ctx.addIssue({\n code: ZodIssueCode.custom,\n message: `${errorPrefix}: segment ${i + 1} is too short`,\n })\n return false\n }\n currentStart = nextDot + 1\n }\n if (data.indexOf('.', currentStart) !== -1) {\n ctx.addIssue({\n code: ZodIssueCode.custom,\n message: `${errorPrefix}: too many segments`,\n })\n return false\n }\n if (data.length - currentStart < minPartLength) {\n ctx.addIssue({\n code: ZodIssueCode.custom,\n message: `${errorPrefix}: last segment is too short`,\n })\n return false\n }\n return true\n }\n}\n\nexport function isLastOccurrence<\n T extends number | boolean | string | null | undefined | symbol | bigint,\n>(v: T, i: number, arr: readonly T[]): boolean {\n return arr.indexOf(v, i + 1) === -1\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/jwk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"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
6
|
"keywords": [
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"typescript": "^5.6.3"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
|
-
"build": "tsc --build tsconfig.json"
|
|
36
|
+
"build": "tsc --build tsconfig.build.json"
|
|
37
37
|
}
|
|
38
38
|
}
|
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'
|
|
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,49 +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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Zod parser for known JWK types
|
|
160
|
+
*/
|
|
161
|
+
export const jwkSchema = z
|
|
162
|
+
.union([
|
|
163
|
+
jwkRsaKeySchema,
|
|
164
|
+
jwkEcKeySchema,
|
|
165
|
+
jwkEcSecp256k1KeySchema,
|
|
166
|
+
jwkOkpKeySchema,
|
|
167
|
+
jwkSymKeySchema,
|
|
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
|
+
)
|
|
189
|
+
.refine(
|
|
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)),
|
|
199
|
+
{
|
|
200
|
+
message: '"key_ops" must be consistent with "use"',
|
|
201
|
+
path: ['key_ops'],
|
|
202
|
+
},
|
|
203
|
+
)
|
|
122
204
|
|
|
123
|
-
export type Jwk = z.
|
|
205
|
+
export type Jwk = z.output<typeof jwkSchema>
|
|
124
206
|
|
|
207
|
+
/** @deprecated use {@link jwkSchema} instead */
|
|
125
208
|
export const jwkValidator = jwkSchema
|
|
126
|
-
.refine((k) => k.use != null || k.key_ops != null, 'use or key_ops required')
|
|
127
|
-
.refine(
|
|
128
|
-
(k) =>
|
|
129
|
-
!k.use ||
|
|
130
|
-
!k.key_ops ||
|
|
131
|
-
k.key_ops.every((o) =>
|
|
132
|
-
k.use === 'sig'
|
|
133
|
-
? o === 'sign' || o === 'verify'
|
|
134
|
-
: o === 'encrypt' || o === 'decrypt',
|
|
135
|
-
),
|
|
136
|
-
'use and key_ops must be consistent',
|
|
137
|
-
)
|
|
138
209
|
|
|
139
|
-
export const jwkPubSchema =
|
|
140
|
-
.refine(
|
|
141
|
-
|
|
210
|
+
export const jwkPubSchema = jwkSchema
|
|
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
|
+
}
|
|
142
255
|
|
|
143
|
-
export
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { jwkPubSchema, jwkSchema } from './jwk.js'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* JSON Web Key Set schema. The keys set, in this context, represents a
|
|
6
|
+
* collection of JSON Web Keys (JWKs), that can be both public and private.
|
|
7
|
+
*/
|
|
4
8
|
export const jwksSchema = z.object({
|
|
5
|
-
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
|
+
}),
|
|
6
19
|
})
|
|
7
20
|
|
|
8
|
-
export type Jwks = z.
|
|
21
|
+
export type Jwks = z.output<typeof jwksSchema>
|
|
9
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Public JSON Web Key Set schema.
|
|
25
|
+
*/
|
|
10
26
|
export const jwksPubSchema = z.object({
|
|
11
|
-
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
|
+
}),
|
|
12
37
|
})
|
|
13
38
|
|
|
14
|
-
export type JwksPub = z.
|
|
39
|
+
export type JwksPub = z.output<typeof jwksPubSchema>
|