unjwt 0.7.3 → 0.8.1
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/README.md +4 -4
- package/dist/THIRD-PARTY-LICENSES.md.gz +0 -0
- package/dist/_chunks/_codec-jwe.d.mts +170 -0
- package/dist/_chunks/_codec-jws.mjs +878 -0
- package/dist/_chunks/_internal.d.mts +7 -0
- package/dist/_chunks/_jwt-internal.mjs +388 -0
- package/dist/_chunks/algorithms.mjs +186 -0
- package/dist/_chunks/index.d.mts +500 -808
- package/dist/_chunks/index2.d.mts +11 -0
- package/dist/_chunks/index3.d.mts +108 -0
- package/dist/_chunks/jwe.d.mts +43 -66
- package/dist/_chunks/jwe.mjs +384 -252
- package/dist/_chunks/jwk.mjs +1371 -607
- package/dist/_chunks/jws.d.mts +48 -103
- package/dist/_chunks/jws.mjs +372 -184
- package/dist/_chunks/jwt.d.mts +56 -0
- package/dist/_chunks/libs/unsecure.d.mts +164 -85
- package/dist/_chunks/libs/unsecure.mjs +390 -123
- package/dist/adapters/elysia/index.d.mts +33 -171
- package/dist/adapters/elysia/index.mjs +106 -575
- package/dist/adapters/h3v1/index.d.mts +59 -344
- package/dist/adapters/h3v1/index.mjs +114 -619
- package/dist/adapters/h3v2/index.d.mts +58 -326
- package/dist/adapters/h3v2/index.mjs +108 -611
- package/dist/core/internal.d.mts +2 -0
- package/dist/core/internal.mjs +1 -0
- package/dist/core/jwe.d.mts +4 -2
- package/dist/core/jwe.mjs +4 -2
- package/dist/core/jwk.d.mts +183 -371
- package/dist/core/jwk.mjs +4 -3
- package/dist/core/jws.d.mts +4 -2
- package/dist/core/jws.mjs +4 -2
- package/dist/core/utils/index.d.mts +5 -158
- package/dist/core/utils/index.mjs +12 -3
- package/dist/index.d.mts +8 -4
- package/dist/index.mjs +6 -3
- package/package.json +30 -30
- package/dist/_chunks/_internal.mjs +0 -88
- package/dist/_chunks/utils.mjs +0 -392
package/dist/_chunks/jws.mjs
CHANGED
|
@@ -1,28 +1,103 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { JWS_ALG_CTX, buildJWKSetFilter, checkAlgAllowed, decodeJWSPayload, decodeProtectedHeader, resolveSigningKey,
|
|
1
|
+
import { sanitizeObjectCopy, textEncoder } from "./libs/unsecure.mjs";
|
|
2
|
+
import { JWTError, applyTypDefault, base64UrlStringify, computeJwtTimeClaims, getPlaintextBytes, isJWK, isJWKSet, parsePayloadText, validateCriticalHeadersJWS } from "./_jwt-internal.mjs";
|
|
3
|
+
import { inferJWSAllowedAlgorithms } from "./algorithms.mjs";
|
|
4
|
+
import { JWS_ALG_CTX, applyB64Header, asJWTError, asKeyLookupError, asKeyUsageError, assertB64Option, assertCompactToken, assertConsumerOptions, assertEntryLimit, assertJWKSetCandidates, assertNotStringKey, assertOptionEntry, assertProducerCrit, assertProtectedOnlyOption, assertSerializationObject, buildJWKSetFilter, checkAlgAllowed, checkJWSAlgSupported, checkJWSB64, checkProtectedTyp, checkSerializationEntry, decodeJWSPayload, decodeProtectedHeader, decodeSegment, getJWKsFromSet, importKey, keyMatchesHeader, resolveKeyInput, resolveLimit, resolvePayloadMode, resolveSigningKey, serializeProtectedHeader, sign as sign$1, stringKeyError, validateClaimsIfJson, verify as verify$1 } from "./jwk.mjs";
|
|
5
|
+
function buildJWSProtectedHeader(alg, key, userHeader, options) {
|
|
6
|
+
const safeHeader = sanitizeObjectCopy(userHeader);
|
|
7
|
+
const header = {};
|
|
8
|
+
if (isJWK(key) && key.kid) header.kid = key.kid;
|
|
9
|
+
if (safeHeader) Object.assign(header, safeHeader);
|
|
10
|
+
header.alg = alg;
|
|
11
|
+
applyTypDefault(header, options.payload);
|
|
12
|
+
if (options.b64 === false) header.b64 = false;
|
|
13
|
+
applyB64Header(header);
|
|
14
|
+
assertProducerCrit(header, "JWS");
|
|
15
|
+
return header;
|
|
16
|
+
}
|
|
17
|
+
async function verifyWithKey(alg, rawKeyMaterial, options) {
|
|
18
|
+
try {
|
|
19
|
+
return await verifyWithKeyMaterial(alg, rawKeyMaterial, options);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
throw asKeyUsageError(err, alg);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function verifyWithKeyMaterial(alg, rawKeyMaterial, options) {
|
|
25
|
+
const { signature, signingInput, kid, allowSymmetric, keyCandidates } = options;
|
|
26
|
+
if (isJWKSet(rawKeyMaterial)) {
|
|
27
|
+
const candidates = getJWKsFromSet(rawKeyMaterial, buildJWKSetFilter({
|
|
28
|
+
kid,
|
|
29
|
+
alg
|
|
30
|
+
}, {
|
|
31
|
+
op: "sig",
|
|
32
|
+
allowSymmetric
|
|
33
|
+
}));
|
|
34
|
+
assertJWKSetCandidates(candidates, {
|
|
35
|
+
limit: keyCandidates,
|
|
36
|
+
kid
|
|
37
|
+
});
|
|
38
|
+
const attempts = [];
|
|
39
|
+
for (const candidate of candidates) try {
|
|
40
|
+
const verificationKey = await resolveSigningKey(alg, await importKey(candidate, {
|
|
41
|
+
alg,
|
|
42
|
+
expect: "public"
|
|
43
|
+
}), "verify");
|
|
44
|
+
if (await verify$1(alg, verificationKey, signature, signingInput)) return true;
|
|
45
|
+
attempts.push(new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID"));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
attempts.push(asKeyUsageError(error, alg));
|
|
48
|
+
}
|
|
49
|
+
throw new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID", attempts);
|
|
50
|
+
}
|
|
51
|
+
const verificationKey = await resolveSigningKey(alg, await importKey(rawKeyMaterial, {
|
|
52
|
+
alg,
|
|
53
|
+
expect: "public"
|
|
54
|
+
}), "verify");
|
|
55
|
+
return verify$1(alg, verificationKey, signature, signingInput);
|
|
56
|
+
}
|
|
5
57
|
async function signMulti(payload, signers, options = {}) {
|
|
6
58
|
if (!Array.isArray(signers) || signers.length === 0) throw new JWTError("signMulti requires at least one signer.", "ERR_JWS_INVALID");
|
|
7
|
-
const
|
|
59
|
+
for (const [index, signer] of signers.entries()) {
|
|
60
|
+
assertOptionEntry(signer, {
|
|
61
|
+
label: `signers[${index}]`,
|
|
62
|
+
objects: ["protectedHeader", "unprotectedHeader"]
|
|
63
|
+
});
|
|
64
|
+
assertNotStringKey(signer.key);
|
|
65
|
+
assertProtectedOnlyOption(signer.unprotectedHeader, {
|
|
66
|
+
tier: "unprotected",
|
|
67
|
+
family: "JWS"
|
|
68
|
+
});
|
|
69
|
+
assertB64Option(signer.protectedHeader);
|
|
70
|
+
}
|
|
71
|
+
const resolvedAlgs = signers.map((s, i) => _resolveSignerAlg(s, i));
|
|
8
72
|
const b64 = _resolveB64(signers);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
73
|
+
const computedPayload = computeJwtTimeClaims(payload, options);
|
|
74
|
+
const payloadBytes = getPlaintextBytes(computedPayload || payload);
|
|
75
|
+
const payloadEncoded = b64 ? base64UrlStringify(payloadBytes) : parsePayloadText(payloadBytes);
|
|
11
76
|
const wireSignatures = [];
|
|
12
77
|
for (let i = 0; i < signers.length; i++) {
|
|
13
78
|
const signer = signers[i];
|
|
14
79
|
const alg = resolvedAlgs[i];
|
|
15
|
-
const protectedHeader =
|
|
80
|
+
const protectedHeader = buildJWSProtectedHeader(alg, signer.key, signer.protectedHeader, {
|
|
81
|
+
payload,
|
|
82
|
+
b64
|
|
83
|
+
});
|
|
16
84
|
const unprotectedHeader = sanitizeObjectCopy(signer.unprotectedHeader);
|
|
17
85
|
_assertDisjointHeaders(protectedHeader, unprotectedHeader);
|
|
18
|
-
const protectedHeaderEncoded =
|
|
86
|
+
const protectedHeaderEncoded = base64UrlStringify(serializeProtectedHeader(protectedHeader));
|
|
19
87
|
const signingInputBytes = textEncoder.encode(`${protectedHeaderEncoded}.${payloadEncoded}`);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
88
|
+
let signatureBytes;
|
|
89
|
+
try {
|
|
90
|
+
const signingKey = await resolveSigningKey(alg, await importKey(signer.key, {
|
|
23
91
|
alg,
|
|
24
92
|
expect: "private"
|
|
25
|
-
}), "sign")
|
|
93
|
+
}), "sign");
|
|
94
|
+
signatureBytes = await sign$1(alg, signingKey, signingInputBytes);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw asKeyUsageError(error, alg);
|
|
97
|
+
}
|
|
98
|
+
const wireSig = {
|
|
99
|
+
protected: protectedHeaderEncoded,
|
|
100
|
+
signature: base64UrlStringify(signatureBytes)
|
|
26
101
|
};
|
|
27
102
|
if (unprotectedHeader && Object.keys(unprotectedHeader).length > 0) wireSig.header = unprotectedHeader;
|
|
28
103
|
wireSignatures.push(wireSig);
|
|
@@ -33,54 +108,83 @@ async function signMulti(payload, signers, options = {}) {
|
|
|
33
108
|
};
|
|
34
109
|
}
|
|
35
110
|
async function verifyMulti(jws, keyOrLookup, options = {}) {
|
|
36
|
-
|
|
37
|
-
|
|
111
|
+
assertConsumerOptions(options);
|
|
112
|
+
const payloadMode = resolvePayloadMode(options);
|
|
113
|
+
const signatureLimit = resolveLimit(options.limits, "signatures");
|
|
114
|
+
const keyCandidates = resolveLimit(options.limits, "keyCandidates");
|
|
115
|
+
const general = _readGeneralSerialization(jws, signatureLimit);
|
|
38
116
|
let lastError;
|
|
39
117
|
let cryptoAttempted = false;
|
|
40
118
|
for (let i = 0; i < general.signatures.length; i++) {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
lastError =
|
|
119
|
+
const read = _readSignatureHeaders(general.signatures[i], i);
|
|
120
|
+
if (read.error) {
|
|
121
|
+
lastError = read.error;
|
|
44
122
|
continue;
|
|
45
123
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
124
|
+
const { wireSig, protectedHeader, unprotectedHeader, effective, alg } = read;
|
|
125
|
+
const typError = checkProtectedTyp(options.typ, protectedHeader, {
|
|
126
|
+
family: "JWS",
|
|
127
|
+
label: `signature[${i}]`
|
|
128
|
+
});
|
|
129
|
+
if (typError) {
|
|
130
|
+
lastError = typError;
|
|
51
131
|
continue;
|
|
52
132
|
}
|
|
53
|
-
|
|
54
|
-
|
|
133
|
+
let rawKeyMaterial;
|
|
134
|
+
try {
|
|
135
|
+
rawKeyMaterial = await resolveKeyInput(keyOrLookup, {
|
|
136
|
+
header: effective,
|
|
137
|
+
token: wireSig.protected ?? "",
|
|
138
|
+
what: `signature[${i}]`
|
|
139
|
+
});
|
|
140
|
+
} catch (err) {
|
|
141
|
+
lastError = asKeyLookupError(err, `signature[${i}]`);
|
|
55
142
|
continue;
|
|
56
143
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const effective = _mergeHeaders(protectedHeader, unprotectedHeader);
|
|
60
|
-
const alg = protectedHeader.alg;
|
|
61
|
-
if (options.typ && protectedHeader.typ !== options.typ) {
|
|
62
|
-
lastError = new JWTError(`Invalid JWS: "typ" Header Parameter mismatch on signature[${i}]. Expected "${options.typ}", got "${protectedHeader.typ}".`, "ERR_JWS_INVALID");
|
|
144
|
+
if (typeof rawKeyMaterial === "string") {
|
|
145
|
+
lastError = stringKeyError();
|
|
63
146
|
continue;
|
|
64
147
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
148
|
+
const algError = checkAlgAllowed(alg, rawKeyMaterial, {
|
|
149
|
+
ctx: JWS_ALG_CTX,
|
|
150
|
+
algorithms: options.algorithms,
|
|
151
|
+
allowSymmetric: options.allowSymmetricJWKSet
|
|
152
|
+
});
|
|
67
153
|
if (algError) {
|
|
68
154
|
lastError = algError;
|
|
69
155
|
continue;
|
|
70
156
|
}
|
|
71
|
-
if (options.strictSignerMatch && !
|
|
72
|
-
|
|
157
|
+
if (options.strictSignerMatch && !keyMatchesHeader(effective, rawKeyMaterial, {
|
|
158
|
+
ctx: JWS_ALG_CTX,
|
|
159
|
+
allowSymmetric: options.allowSymmetricJWKSet
|
|
160
|
+
})) continue;
|
|
161
|
+
let signatureBytes;
|
|
162
|
+
try {
|
|
163
|
+
signatureBytes = decodeSegment(wireSig.signature, {
|
|
164
|
+
family: "JWS",
|
|
165
|
+
what: "Signature"
|
|
166
|
+
});
|
|
167
|
+
} catch (err) {
|
|
168
|
+
lastError = asJWTError(err, INVALID_JWS);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
73
171
|
const signingInputBytes = textEncoder.encode(`${wireSig.protected ?? ""}.${general.payload}`);
|
|
74
172
|
cryptoAttempted = true;
|
|
75
173
|
try {
|
|
76
|
-
if (!await
|
|
174
|
+
if (!await verifyWithKey(alg, rawKeyMaterial, {
|
|
175
|
+
signature: signatureBytes,
|
|
176
|
+
signingInput: signingInputBytes,
|
|
177
|
+
kid: effective.kid,
|
|
178
|
+
allowSymmetric: options.allowSymmetricJWKSet,
|
|
179
|
+
keyCandidates
|
|
180
|
+
})) {
|
|
77
181
|
lastError = new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
78
182
|
continue;
|
|
79
183
|
}
|
|
80
|
-
const useB64 = protectedHeader.b64 !== false;
|
|
81
|
-
const payload = decodeJWSPayload(general.payload, useB64, options.forceUint8Array);
|
|
82
184
|
validateCriticalHeadersJWS(protectedHeader, options.recognizedHeaders);
|
|
83
|
-
|
|
185
|
+
const useB64 = protectedHeader.b64 !== false;
|
|
186
|
+
const payload = decodeJWSPayload(general.payload, useB64, payloadMode);
|
|
187
|
+
validateClaimsIfJson(payload, payloadMode, options);
|
|
84
188
|
const result = {
|
|
85
189
|
payload,
|
|
86
190
|
protectedHeader,
|
|
@@ -89,21 +193,31 @@ async function verifyMulti(jws, keyOrLookup, options = {}) {
|
|
|
89
193
|
if (unprotectedHeader) result.signerHeader = unprotectedHeader;
|
|
90
194
|
return result;
|
|
91
195
|
} catch (err) {
|
|
92
|
-
|
|
93
|
-
|
|
196
|
+
const error = asJWTError(err, INVALID_SIGNATURE);
|
|
197
|
+
if (!PER_SIGNATURE_ERROR_CODES.has(error.code)) throw error;
|
|
198
|
+
lastError = error;
|
|
94
199
|
}
|
|
95
200
|
}
|
|
96
201
|
if (options.strictSignerMatch && !cryptoAttempted) throw new JWTError("No signature matched the provided key under strictSignerMatch.", "ERR_JWS_NO_MATCHING_SIGNER");
|
|
97
202
|
throw lastError ?? new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
98
203
|
}
|
|
99
204
|
async function verifyMultiAll(jws, keyResolver, options = {}) {
|
|
100
|
-
|
|
101
|
-
|
|
205
|
+
assertConsumerOptions(options);
|
|
206
|
+
const payloadMode = resolvePayloadMode(options);
|
|
207
|
+
const signatureLimit = resolveLimit(options.limits, "signatures");
|
|
208
|
+
const keyCandidates = resolveLimit(options.limits, "keyCandidates");
|
|
209
|
+
const general = _readGeneralSerialization(jws, signatureLimit);
|
|
102
210
|
const outcomes = [];
|
|
103
|
-
for (let i = 0; i < general.signatures.length; i++) outcomes.push(await _verifyOneSignature(general.payload, general.signatures[i], i, keyResolver,
|
|
211
|
+
for (let i = 0; i < general.signatures.length; i++) outcomes.push(await _verifyOneSignature(general.payload, general.signatures[i], i, keyResolver, {
|
|
212
|
+
options,
|
|
213
|
+
payloadMode,
|
|
214
|
+
keyCandidates
|
|
215
|
+
}));
|
|
104
216
|
return outcomes;
|
|
105
217
|
}
|
|
106
218
|
function generalToFlattenedJWS(jws) {
|
|
219
|
+
assertSerializationObject(jws, "JWS");
|
|
220
|
+
if (!Array.isArray(jws.signatures)) throw new JWTError("Invalid JWS JSON Serialization: missing \"signatures\".", "ERR_JWS_INVALID_SERIALIZATION");
|
|
107
221
|
if (jws.signatures.length !== 1) throw new JWTError(`Flattened JWS Serialization requires exactly one signature, got ${jws.signatures.length}.`, "ERR_JWS_INVALID_SERIALIZATION");
|
|
108
222
|
const [signature] = jws.signatures;
|
|
109
223
|
const flattened = {
|
|
@@ -114,13 +228,86 @@ function generalToFlattenedJWS(jws) {
|
|
|
114
228
|
if (signature.header !== void 0) flattened.header = signature.header;
|
|
115
229
|
return flattened;
|
|
116
230
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
231
|
+
const INVALID_JWS = {
|
|
232
|
+
code: "ERR_JWS_INVALID",
|
|
233
|
+
message: "Invalid JWS."
|
|
234
|
+
};
|
|
235
|
+
const INVALID_SIGNATURE = {
|
|
236
|
+
code: "ERR_JWS_SIGNATURE_INVALID",
|
|
237
|
+
message: "JWS signature verification failed."
|
|
238
|
+
};
|
|
239
|
+
const INVALID_CLAIMS = {
|
|
240
|
+
code: "ERR_JWT_CLAIM_INVALID",
|
|
241
|
+
message: "JWT claim validation failed."
|
|
242
|
+
};
|
|
243
|
+
const PER_SIGNATURE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
244
|
+
"ERR_JWS_SIGNATURE_INVALID",
|
|
245
|
+
"ERR_JWK_KEY_NOT_FOUND",
|
|
246
|
+
"ERR_JWK_INVALID"
|
|
247
|
+
]);
|
|
248
|
+
function _checkUnprotectedHeader(header) {
|
|
249
|
+
if (!header) return void 0;
|
|
250
|
+
if (Object.hasOwn(header, "crit")) return new JWTError("Invalid JWS: \"crit\" must be integrity protected (RFC 7515 §4.1.11).", "ERR_JWS_INVALID");
|
|
251
|
+
if (Object.hasOwn(header, "b64")) return new JWTError("Invalid JWS: \"b64\" must occur only within the protected header (RFC 7797 §3).", "ERR_JWS_INVALID");
|
|
252
|
+
}
|
|
253
|
+
function _readSignatureHeaders(entry, index) {
|
|
254
|
+
const shapeError = checkSerializationEntry(entry, {
|
|
255
|
+
family: "JWS",
|
|
256
|
+
label: `Signature[${index}]`,
|
|
257
|
+
objects: ["header"],
|
|
258
|
+
strings: ["protected"]
|
|
259
|
+
});
|
|
260
|
+
if (shapeError) return { error: shapeError };
|
|
261
|
+
const wireSig = entry;
|
|
262
|
+
if (typeof wireSig.signature !== "string") return { error: new JWTError(`Signature[${index}] is missing the "signature" field.`, "ERR_JWS_INVALID") };
|
|
263
|
+
let protectedHeader;
|
|
264
|
+
try {
|
|
265
|
+
protectedHeader = decodeProtectedHeader(wireSig.protected, "JWS");
|
|
266
|
+
} catch (err) {
|
|
267
|
+
return { error: asJWTError(err, INVALID_JWS) };
|
|
268
|
+
}
|
|
269
|
+
const unprotectedHeader = sanitizeObjectCopy(wireSig.header);
|
|
270
|
+
const failed = (error) => ({
|
|
271
|
+
error,
|
|
272
|
+
protectedHeader,
|
|
273
|
+
...unprotectedHeader ? { unprotectedHeader } : {}
|
|
274
|
+
});
|
|
275
|
+
const unprotectedError = _checkUnprotectedHeader(unprotectedHeader);
|
|
276
|
+
if (unprotectedError) return failed(unprotectedError);
|
|
277
|
+
try {
|
|
278
|
+
_assertDisjointHeaders(protectedHeader, unprotectedHeader);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return failed(asJWTError(err, INVALID_JWS));
|
|
281
|
+
}
|
|
282
|
+
const effective = _mergeHeaders(protectedHeader, unprotectedHeader);
|
|
283
|
+
const alg = effective.alg;
|
|
284
|
+
if (!alg) return failed(new JWTError(`Invalid JWS: Signature[${index}] has no "alg" in its protected or unprotected header.`, "ERR_JWS_INVALID"));
|
|
285
|
+
const algError = checkJWSAlgSupported(alg) ?? checkJWSB64(protectedHeader);
|
|
286
|
+
if (algError) return failed(algError);
|
|
287
|
+
return {
|
|
288
|
+
wireSig,
|
|
289
|
+
protectedHeader,
|
|
290
|
+
unprotectedHeader,
|
|
291
|
+
effective,
|
|
292
|
+
alg
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function _resolveSignerAlg(signer, index) {
|
|
296
|
+
const alg = signer.alg ?? _inferSignerAlg(signer.key, index);
|
|
297
|
+
const unsupported = checkJWSAlgSupported(alg);
|
|
298
|
+
if (unsupported) throw unsupported;
|
|
122
299
|
return alg;
|
|
123
300
|
}
|
|
301
|
+
function _inferSignerAlg(key, index) {
|
|
302
|
+
if (isJWK(key)) {
|
|
303
|
+
const alg = key.alg;
|
|
304
|
+
if (!alg) throw new JWTError(`Cannot infer "alg" for signer[${index}]: the JWK has no "alg" property.`, "ERR_JWS_SIGNER_ALG_INFERENCE");
|
|
305
|
+
return alg;
|
|
306
|
+
}
|
|
307
|
+
const inferred = inferJWSAllowedAlgorithms(key);
|
|
308
|
+
if (!inferred?.length) throw new JWTError(`Cannot infer "alg" for signer[${index}]: pass it explicitly on the signer.`, "ERR_JWS_SIGNER_ALG_INFERENCE");
|
|
309
|
+
return inferred[0];
|
|
310
|
+
}
|
|
124
311
|
function _resolveB64(signers) {
|
|
125
312
|
let decided;
|
|
126
313
|
for (let i = 0; i < signers.length; i++) {
|
|
@@ -131,17 +318,6 @@ function _resolveB64(signers) {
|
|
|
131
318
|
}
|
|
132
319
|
return decided ?? true;
|
|
133
320
|
}
|
|
134
|
-
function _buildSignerProtectedHeader(signer, alg, payload, b64) {
|
|
135
|
-
const userHeader = sanitizeObjectCopy(signer.protectedHeader);
|
|
136
|
-
const header = {};
|
|
137
|
-
if (signer.key.kid) header.kid = signer.key.kid;
|
|
138
|
-
if (userHeader) Object.assign(header, userHeader);
|
|
139
|
-
header.alg = alg;
|
|
140
|
-
applyTypCtyDefaults(header, payload);
|
|
141
|
-
if (!b64) header.b64 = false;
|
|
142
|
-
else delete header.b64;
|
|
143
|
-
return header;
|
|
144
|
-
}
|
|
145
321
|
function _assertDisjointHeaders(protectedHeader, unprotectedHeader) {
|
|
146
322
|
if (!protectedHeader || !unprotectedHeader) return;
|
|
147
323
|
for (const k of Object.keys(unprotectedHeader)) if (k in protectedHeader) throw new JWTError(`JWS header parameter "${k}" appears in both protected and unprotected headers of a signature; they must be disjoint (RFC 7515 §7.2.1).`, "ERR_JWS_HEADER_PARAMS_NOT_DISJOINT");
|
|
@@ -153,12 +329,23 @@ function _mergeHeaders(protectedHeader, unprotectedHeader) {
|
|
|
153
329
|
...unprotectedHeader
|
|
154
330
|
};
|
|
155
331
|
}
|
|
332
|
+
function _readGeneralSerialization(input, signatureLimit) {
|
|
333
|
+
const general = _normalizeSerialization(input);
|
|
334
|
+
if (typeof general.payload !== "string" || !Array.isArray(general.signatures) || general.signatures.length === 0) throw new JWTError("Invalid JWS JSON Serialization: missing payload or signatures.", "ERR_JWS_INVALID_SERIALIZATION");
|
|
335
|
+
assertEntryLimit(general.signatures.length, signatureLimit, {
|
|
336
|
+
family: "JWS",
|
|
337
|
+
what: "signatures"
|
|
338
|
+
});
|
|
339
|
+
return general;
|
|
340
|
+
}
|
|
156
341
|
function _normalizeSerialization(input) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
342
|
+
assertSerializationObject(input, "JWS");
|
|
343
|
+
if ("signatures" in input && Array.isArray(input.signatures)) {
|
|
344
|
+
if ("signature" in input || "protected" in input) throw new JWTError("Invalid JWS JSON Serialization: \"signatures\" must not be combined with a top-level \"signature\" or \"protected\".", "ERR_JWS_INVALID_SERIALIZATION");
|
|
345
|
+
return input;
|
|
346
|
+
}
|
|
347
|
+
if ("signature" in input) {
|
|
348
|
+
const flattened = input;
|
|
162
349
|
const signature = { signature: flattened.signature };
|
|
163
350
|
if (flattened.protected !== void 0) signature.protected = flattened.protected;
|
|
164
351
|
if (flattened.header !== void 0) signature.header = flattened.header;
|
|
@@ -169,34 +356,8 @@ function _normalizeSerialization(input) {
|
|
|
169
356
|
}
|
|
170
357
|
throw new JWTError("Invalid JWS JSON Serialization: missing both signatures[] and flattened signature.", "ERR_JWS_INVALID_SERIALIZATION");
|
|
171
358
|
}
|
|
172
|
-
function
|
|
173
|
-
const
|
|
174
|
-
if (isJWKSet(keyMaterial)) return keyMaterial.keys.some((k) => !headerKid || k.kid === headerKid);
|
|
175
|
-
if (isJWK(keyMaterial)) {
|
|
176
|
-
if (headerKid && keyMaterial.kid && headerKid !== keyMaterial.kid) return false;
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
return !headerKid;
|
|
180
|
-
}
|
|
181
|
-
async function _verifyWithKey(alg, rawKeyMaterial, signatureBytes, signingInputBytes, headerKid) {
|
|
182
|
-
if (isJWKSet(rawKeyMaterial)) {
|
|
183
|
-
const candidates = getJWKsFromSet(rawKeyMaterial, buildJWKSetFilter({
|
|
184
|
-
kid: headerKid,
|
|
185
|
-
alg
|
|
186
|
-
}));
|
|
187
|
-
if (candidates.length === 0) throw new JWTError("No key found in JWK Set.", "ERR_JWK_KEY_NOT_FOUND");
|
|
188
|
-
for (const candidate of candidates) if (await verify$1(alg, await resolveSigningKey(alg, await importKey(candidate, {
|
|
189
|
-
alg,
|
|
190
|
-
expect: "public"
|
|
191
|
-
}), "verify"), signatureBytes, signingInputBytes)) return true;
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
return verify$1(alg, await resolveSigningKey(alg, await importKey(rawKeyMaterial, {
|
|
195
|
-
alg,
|
|
196
|
-
expect: "public"
|
|
197
|
-
}), "verify"), signatureBytes, signingInputBytes);
|
|
198
|
-
}
|
|
199
|
-
async function _verifyOneSignature(payload, wireSig, index, keyResolver, options) {
|
|
359
|
+
async function _verifyOneSignature(payload, entry, index, keyResolver, context) {
|
|
360
|
+
const { options, payloadMode, keyCandidates } = context;
|
|
200
361
|
const fail = (error, protectedHeader, signerHeader) => {
|
|
201
362
|
const outcome = {
|
|
202
363
|
signerIndex: index,
|
|
@@ -207,51 +368,70 @@ async function _verifyOneSignature(payload, wireSig, index, keyResolver, options
|
|
|
207
368
|
if (signerHeader) outcome.signerHeader = signerHeader;
|
|
208
369
|
return outcome;
|
|
209
370
|
};
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
if (
|
|
218
|
-
|
|
371
|
+
const read = _readSignatureHeaders(entry, index);
|
|
372
|
+
if (read.error) return fail(read.error, read.protectedHeader, read.unprotectedHeader);
|
|
373
|
+
const { wireSig, protectedHeader, unprotectedHeader, effective, alg } = read;
|
|
374
|
+
const typError = checkProtectedTyp(options.typ, protectedHeader, {
|
|
375
|
+
family: "JWS",
|
|
376
|
+
label: `signature[${index}]`
|
|
377
|
+
});
|
|
378
|
+
if (typError) return fail(typError, protectedHeader, unprotectedHeader);
|
|
379
|
+
let rawKeyMaterial;
|
|
219
380
|
try {
|
|
220
|
-
|
|
381
|
+
rawKeyMaterial = await resolveKeyInput(keyResolver, {
|
|
382
|
+
header: effective,
|
|
383
|
+
token: wireSig.protected ?? "",
|
|
384
|
+
what: `signature[${index}]`
|
|
385
|
+
});
|
|
221
386
|
} catch (err) {
|
|
222
|
-
return fail(
|
|
387
|
+
return fail(asKeyLookupError(err, `signature[${index}]`), protectedHeader, unprotectedHeader);
|
|
223
388
|
}
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
389
|
+
if (typeof rawKeyMaterial === "string") return fail(stringKeyError(), protectedHeader, unprotectedHeader);
|
|
390
|
+
const algError = checkAlgAllowed(alg, rawKeyMaterial, {
|
|
391
|
+
ctx: JWS_ALG_CTX,
|
|
392
|
+
algorithms: options.algorithms,
|
|
393
|
+
allowSymmetric: options.allowSymmetricJWKSet
|
|
394
|
+
});
|
|
395
|
+
if (algError) return fail(algError, protectedHeader, unprotectedHeader);
|
|
396
|
+
let signatureBytes;
|
|
228
397
|
try {
|
|
229
|
-
|
|
398
|
+
signatureBytes = decodeSegment(wireSig.signature, {
|
|
399
|
+
family: "JWS",
|
|
400
|
+
what: "Signature"
|
|
401
|
+
});
|
|
230
402
|
} catch (err) {
|
|
231
|
-
return fail(
|
|
403
|
+
return fail(asJWTError(err, INVALID_JWS), protectedHeader, unprotectedHeader);
|
|
232
404
|
}
|
|
233
|
-
const algError = checkAlgAllowed(alg, rawKeyMaterial, options.algorithms, JWS_ALG_CTX);
|
|
234
|
-
if (algError) return fail(algError, protectedHeader, unprotectedHeader);
|
|
235
|
-
const signatureBytes = base64UrlDecode(wireSig.signature, { returnAs: "uint8array" });
|
|
236
405
|
const signingInputBytes = textEncoder.encode(`${wireSig.protected ?? ""}.${payload}`);
|
|
237
406
|
let verified;
|
|
238
407
|
try {
|
|
239
|
-
verified = await
|
|
408
|
+
verified = await verifyWithKey(alg, rawKeyMaterial, {
|
|
409
|
+
signature: signatureBytes,
|
|
410
|
+
signingInput: signingInputBytes,
|
|
411
|
+
kid: effective.kid,
|
|
412
|
+
allowSymmetric: options.allowSymmetricJWKSet,
|
|
413
|
+
keyCandidates
|
|
414
|
+
});
|
|
240
415
|
} catch (err) {
|
|
241
|
-
return fail(
|
|
416
|
+
return fail(asJWTError(err, INVALID_SIGNATURE), protectedHeader, unprotectedHeader);
|
|
242
417
|
}
|
|
243
418
|
if (!verified) return fail(new JWTError(`JWS signature[${index}] verification failed.`, "ERR_JWS_SIGNATURE_INVALID"), protectedHeader, unprotectedHeader);
|
|
419
|
+
try {
|
|
420
|
+
validateCriticalHeadersJWS(protectedHeader, options.recognizedHeaders);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
return fail(asJWTError(err, INVALID_JWS), protectedHeader, unprotectedHeader);
|
|
423
|
+
}
|
|
244
424
|
let decodedPayload;
|
|
245
425
|
try {
|
|
246
|
-
|
|
426
|
+
const useB64 = protectedHeader.b64 !== false;
|
|
427
|
+
decodedPayload = decodeJWSPayload(payload, useB64, payloadMode);
|
|
247
428
|
} catch (err) {
|
|
248
|
-
return fail(
|
|
429
|
+
return fail(asJWTError(err, INVALID_JWS), protectedHeader, unprotectedHeader);
|
|
249
430
|
}
|
|
250
431
|
try {
|
|
251
|
-
|
|
252
|
-
validateJwtClaimsIfJsonPayload(decodedPayload, options);
|
|
432
|
+
validateClaimsIfJson(decodedPayload, payloadMode, options);
|
|
253
433
|
} catch (err) {
|
|
254
|
-
return fail(
|
|
434
|
+
return fail(asJWTError(err, INVALID_CLAIMS), protectedHeader, unprotectedHeader);
|
|
255
435
|
}
|
|
256
436
|
const outcome = {
|
|
257
437
|
signerIndex: index,
|
|
@@ -262,88 +442,96 @@ async function _verifyOneSignature(payload, wireSig, index, keyResolver, options
|
|
|
262
442
|
if (unprotectedHeader) outcome.signerHeader = unprotectedHeader;
|
|
263
443
|
return outcome;
|
|
264
444
|
}
|
|
265
|
-
function _asJWTError(err, fallbackCode, fallbackMessage) {
|
|
266
|
-
if (err instanceof JWTError) return err;
|
|
267
|
-
return new JWTError(fallbackMessage ?? (err instanceof Error ? err.message : typeof err === "string" ? err : "Unknown error"), fallbackCode, err);
|
|
268
|
-
}
|
|
269
445
|
async function sign(payload, key, options = {}) {
|
|
446
|
+
assertOptionEntry(options, {
|
|
447
|
+
label: "options",
|
|
448
|
+
objects: ["protectedHeader"]
|
|
449
|
+
});
|
|
450
|
+
assertNotStringKey(key);
|
|
270
451
|
const { protectedHeader: userHeader } = options;
|
|
271
452
|
let { alg } = options;
|
|
272
|
-
if (!alg)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
453
|
+
if (!alg) {
|
|
454
|
+
if (isJWK(key) && key.alg) alg = key.alg;
|
|
455
|
+
else throw new JWTError("JWS \"alg\" (Algorithm) must be provided in options", "ERR_JWS_ALG_MISSING");
|
|
456
|
+
}
|
|
457
|
+
const algSupportError = checkJWSAlgSupported(alg);
|
|
458
|
+
if (algSupportError) throw algSupportError;
|
|
459
|
+
let signingKey;
|
|
460
|
+
try {
|
|
461
|
+
signingKey = await resolveSigningKey(alg, await importKey(key, {
|
|
462
|
+
alg,
|
|
463
|
+
expect: "private"
|
|
464
|
+
}), "sign");
|
|
465
|
+
} catch (error) {
|
|
466
|
+
throw asKeyUsageError(error, alg);
|
|
467
|
+
}
|
|
468
|
+
const protectedHeader = buildJWSProtectedHeader(alg, key, userHeader, { payload });
|
|
280
469
|
const computedPayload = computeJwtTimeClaims(payload, options);
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
470
|
+
const protectedHeaderString = serializeProtectedHeader(protectedHeader);
|
|
471
|
+
const protectedHeaderEncoded = base64UrlStringify(protectedHeaderString);
|
|
472
|
+
const payloadBytes = getPlaintextBytes(computedPayload || payload);
|
|
473
|
+
const useB64 = protectedHeader.b64 !== false;
|
|
474
|
+
let payloadEncoded;
|
|
475
|
+
if (useB64) payloadEncoded = base64UrlStringify(payloadBytes);
|
|
476
|
+
else {
|
|
477
|
+
payloadEncoded = parsePayloadText(payloadBytes);
|
|
478
|
+
if (payloadEncoded.includes(".")) throw new JWTError("Invalid JWS: unencoded compact payload must not contain \".\" (RFC 7797 §5.2).", "ERR_JWS_INVALID");
|
|
479
|
+
}
|
|
480
|
+
const signingInputString = `${protectedHeaderEncoded}.${payloadEncoded}`;
|
|
288
481
|
const signingInputBytes = textEncoder.encode(signingInputString);
|
|
289
|
-
|
|
482
|
+
let signatureBytes;
|
|
483
|
+
try {
|
|
484
|
+
signatureBytes = await sign$1(alg, signingKey, signingInputBytes);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
throw asKeyUsageError(error, alg);
|
|
487
|
+
}
|
|
488
|
+
return `${signingInputString}.${base64UrlStringify(signatureBytes)}`;
|
|
290
489
|
}
|
|
291
490
|
async function verify(jws, key, options = {}) {
|
|
491
|
+
assertCompactToken(jws, "JWS");
|
|
492
|
+
assertConsumerOptions(options);
|
|
493
|
+
const payloadMode = resolvePayloadMode(options);
|
|
494
|
+
const keyCandidateLimit = resolveLimit(options.limits, "keyCandidates");
|
|
292
495
|
const parts = jws.split(".");
|
|
293
496
|
if (parts.length !== 3) throw new JWTError("Invalid JWS: Must contain three parts separated by dots.", "ERR_JWS_INVALID");
|
|
294
497
|
const [protectedHeaderEncoded, payloadEncoded, signatureEncoded] = parts;
|
|
295
498
|
const protectedHeader = decodeProtectedHeader(protectedHeaderEncoded, "JWS");
|
|
296
499
|
if (!protectedHeader.alg) throw new JWTError("Invalid JWS: Protected header must be an object with an \"alg\" property.", "ERR_JWS_INVALID");
|
|
297
500
|
const alg = protectedHeader.alg;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
501
|
+
const algSupportError = checkJWSAlgSupported(alg);
|
|
502
|
+
if (algSupportError) throw algSupportError;
|
|
503
|
+
const headerError = checkJWSB64(protectedHeader) ?? checkProtectedTyp(options.typ, protectedHeader, { family: "JWS" });
|
|
504
|
+
if (headerError) throw headerError;
|
|
505
|
+
const signatureBytes = decodeSegment(signatureEncoded, {
|
|
506
|
+
family: "JWS",
|
|
507
|
+
what: "Signature"
|
|
508
|
+
});
|
|
509
|
+
const keyInput = await resolveKeyInput(key, {
|
|
510
|
+
header: protectedHeader,
|
|
511
|
+
token: jws,
|
|
512
|
+
what: "the JWS"
|
|
513
|
+
});
|
|
514
|
+
assertNotStringKey(keyInput);
|
|
515
|
+
const algError = checkAlgAllowed(alg, keyInput, {
|
|
516
|
+
ctx: JWS_ALG_CTX,
|
|
517
|
+
algorithms: options.algorithms,
|
|
518
|
+
allowSymmetric: options.allowSymmetricJWKSet
|
|
519
|
+
});
|
|
307
520
|
if (algError) throw algError;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
expect: "public"
|
|
316
|
-
}), "verify"), signatureBytes, signingInputBytes)) {
|
|
317
|
-
verified = true;
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
if (!verified) throw new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
321
|
-
} else if (!await verify$1(alg, await resolveSigningKey(alg, await importKey(keyInput, {
|
|
322
|
-
alg,
|
|
323
|
-
expect: "public"
|
|
324
|
-
}), "verify"), signatureBytes, signingInputBytes)) throw new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
325
|
-
const useB64 = protectedHeader.b64 !== false;
|
|
326
|
-
let payload;
|
|
327
|
-
try {
|
|
328
|
-
payload = decodeJWSPayload(payloadEncoded, useB64, options.forceUint8Array);
|
|
329
|
-
} catch (error_) {
|
|
330
|
-
throw new JWTError(`Invalid JWS: Payload decoding failed (${error_ instanceof Error ? error_.message : String(error_)})`, "ERR_JWS_INVALID");
|
|
331
|
-
}
|
|
521
|
+
if (!await verifyWithKey(alg, keyInput, {
|
|
522
|
+
signature: signatureBytes,
|
|
523
|
+
signingInput: textEncoder.encode(`${protectedHeaderEncoded}.${payloadEncoded}`),
|
|
524
|
+
kid: protectedHeader.kid,
|
|
525
|
+
allowSymmetric: options.allowSymmetricJWKSet,
|
|
526
|
+
keyCandidates: keyCandidateLimit
|
|
527
|
+
})) throw new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
332
528
|
validateCriticalHeadersJWS(protectedHeader, options.recognizedHeaders);
|
|
333
|
-
|
|
529
|
+
const useB64 = protectedHeader.b64 !== false;
|
|
530
|
+
const payload = decodeJWSPayload(payloadEncoded, useB64, payloadMode);
|
|
531
|
+
validateClaimsIfJson(payload, payloadMode, options);
|
|
334
532
|
return {
|
|
335
533
|
payload,
|
|
336
534
|
protectedHeader
|
|
337
535
|
};
|
|
338
536
|
}
|
|
339
|
-
function _buildJWSHeader(alg, key, userHeader, payload) {
|
|
340
|
-
const safeHeader = sanitizeObjectCopy(userHeader);
|
|
341
|
-
const header = {};
|
|
342
|
-
if (isJWK(key) && key.kid) header.kid = key.kid;
|
|
343
|
-
if (safeHeader) Object.assign(header, safeHeader);
|
|
344
|
-
header.alg = alg;
|
|
345
|
-
applyTypCtyDefaults(header, payload);
|
|
346
|
-
if (header.b64 === true) delete header.b64;
|
|
347
|
-
return header;
|
|
348
|
-
}
|
|
349
537
|
export { generalToFlattenedJWS, sign, signMulti, verify, verifyMulti, verifyMultiAll };
|