@blamejs/core 0.14.18 → 0.14.19
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 +2 -0
- package/README.md +1 -1
- package/lib/archive.js +206 -52
- package/lib/auth/oauth.js +58 -0
- package/lib/auth/oid4vci.js +84 -27
- package/lib/mail-auth.js +554 -55
- package/lib/middleware/scim-server.js +294 -10
- package/lib/openapi-paths-builder.js +105 -29
- package/lib/openapi.js +225 -100
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/auth/oid4vci.js
CHANGED
|
@@ -79,14 +79,15 @@ function _b64uDecodeStr(s) {
|
|
|
79
79
|
return Buffer.from(s, "base64url").toString("utf8");
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId, supportedAlgs, proofMaxAgeMs) {
|
|
82
|
+
async function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId, supportedAlgs, proofMaxAgeMs, resolveKid) {
|
|
83
83
|
// OID4VCI §7.2.1.1: the proof JWT MUST:
|
|
84
84
|
// - typ = "openid4vci-proof+jwt"
|
|
85
85
|
// - alg in supported list (issuer publishes these)
|
|
86
86
|
// - aud = credential issuer URL (this issuer's `credential_issuer`)
|
|
87
87
|
// - iat = recent
|
|
88
88
|
// - nonce = c_nonce previously issued to the wallet
|
|
89
|
-
// - jwk OR kid
|
|
89
|
+
// - jwk (inline) OR kid (resolved via resolveKid) in the header
|
|
90
|
+
// pointing at the holder key to bind cnf to (RFC 7515 §4.1.3/§4.1.4)
|
|
90
91
|
if (typeof proofJwt !== "string" || proofJwt.length === 0 || proofJwt.length > MAX_PROOF_BYTES) {
|
|
91
92
|
throw new AuthError("auth-oid4vci/bad-proof",
|
|
92
93
|
"credential issuance: proof JWT is empty or exceeds " + MAX_PROOF_BYTES + " bytes");
|
|
@@ -171,29 +172,78 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
|
|
|
171
172
|
"credential issuance: proof JWT iss does not match the access-token client_id");
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
//
|
|
175
|
+
// Resolve the holder key the proof is signed with. Two paths:
|
|
176
|
+
// - inline `jwk` (RFC 7515 §4.1.3) — the wallet ships the public
|
|
177
|
+
// key in the header; bind `cnf` to it directly.
|
|
178
|
+
// - `kid` (RFC 7515 §4.1.4) without inline `jwk` — the wallet
|
|
179
|
+
// references a key by identifier (EUDI-Wallet attested-key flow,
|
|
180
|
+
// OID4VCI §8.2.1.1 `key_attestation` proof). The operator
|
|
181
|
+
// supplies `resolveKid(kid, header)` to map the kid → public key.
|
|
182
|
+
// With no resolver configured the issuer keeps the clear refusal
|
|
183
|
+
// (back-compat): a kid-only proof can't be verified without one.
|
|
175
184
|
var holderKeyJwk = header.jwk || null;
|
|
176
|
-
if (!holderKeyJwk && header.kid) {
|
|
177
|
-
// Operators with kid-only proofs supply a resolver; until then,
|
|
178
|
-
// require jwk inline. Refuse rather than silently downgrade.
|
|
179
|
-
throw new AuthError("auth-oid4vci/kid-resolver-not-supported",
|
|
180
|
-
"credential issuance: proof JWT used `kid` without inline `jwk` — supply { jwk } in the header for inline binding (kid-resolver path is operator-side)");
|
|
181
|
-
}
|
|
182
|
-
if (!holderKeyJwk) {
|
|
183
|
-
throw new AuthError("auth-oid4vci/no-jwk-in-header",
|
|
184
|
-
"credential issuance: proof JWT must carry `jwk` for inline holder-key binding");
|
|
185
|
-
}
|
|
186
|
-
// CVE-2026-22817 — cross-check alg/kty before importing the holder
|
|
187
|
-
// JWK. Without this an attacker-controlled `alg: "HS256"` against an
|
|
188
|
-
// RSA holder JWK would have node:crypto.verify treat the RSA public
|
|
189
|
-
// key as an HMAC secret. Routed through the shared helper so every
|
|
190
|
-
// JWT verifier in the framework enforces the same check.
|
|
191
|
-
jwtExternal._assertAlgKtyMatch(header.alg, holderKeyJwk);
|
|
192
185
|
var keyObj;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
186
|
+
if (!holderKeyJwk && header.kid) {
|
|
187
|
+
if (typeof resolveKid !== "function") {
|
|
188
|
+
throw new AuthError("auth-oid4vci/kid-resolver-not-supported",
|
|
189
|
+
"credential issuance: proof JWT used `kid` without inline `jwk` — supply { jwk } in the header for inline binding, or configure issuer.create({ resolveKid }) to resolve kid-referenced holder keys");
|
|
190
|
+
}
|
|
191
|
+
var resolved;
|
|
192
|
+
try {
|
|
193
|
+
resolved = await resolveKid(header.kid, header);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// Wrap a resolver exception in a stable AuthError code so the
|
|
196
|
+
// /credential handler returns a typed refusal instead of an
|
|
197
|
+
// unhandled rejection. resolveKid is operator code, so its own
|
|
198
|
+
// message is allowed through for operator-side debugging.
|
|
199
|
+
throw new AuthError("auth-oid4vci/kid-resolver-failed",
|
|
200
|
+
"credential issuance: resolveKid threw while resolving the proof JWT kid: " + ((e && e.message) || String(e)));
|
|
201
|
+
}
|
|
202
|
+
if (!resolved) {
|
|
203
|
+
throw new AuthError("auth-oid4vci/kid-unresolved",
|
|
204
|
+
"credential issuance: resolveKid returned no key for the proof JWT kid — refused");
|
|
205
|
+
}
|
|
206
|
+
// Normalize to (verify KeyObject) + (cnf JWK). A KeyObject verifies
|
|
207
|
+
// the signature directly; the cnf binding sdJwtIssuer.issue expects
|
|
208
|
+
// a JWK, so a resolved KeyObject is exported to one. A resolved JWK
|
|
209
|
+
// is used for both.
|
|
210
|
+
if (resolved instanceof nodeCrypto.KeyObject) {
|
|
211
|
+
try { holderKeyJwk = resolved.export({ format: "jwk" }); }
|
|
212
|
+
catch (e) {
|
|
213
|
+
throw new AuthError("auth-oid4vci/bad-resolved-key",
|
|
214
|
+
"credential issuance: resolveKid returned a KeyObject that does not export to JWK: " + ((e && e.message) || String(e)));
|
|
215
|
+
}
|
|
216
|
+
} else if (typeof resolved === "object" && typeof resolved.kty === "string") {
|
|
217
|
+
holderKeyJwk = resolved;
|
|
218
|
+
} else {
|
|
219
|
+
throw new AuthError("auth-oid4vci/bad-resolved-key",
|
|
220
|
+
"credential issuance: resolveKid must return a JWK object (with kty) or a node:crypto KeyObject");
|
|
221
|
+
}
|
|
222
|
+
// CVE-2026-22817 — same alg/kty cross-check the inline path applies.
|
|
223
|
+
// A resolver that returns an RSA key for a proof declaring an HMAC
|
|
224
|
+
// alg would otherwise be verified as an HMAC secret.
|
|
225
|
+
jwtExternal._assertAlgKtyMatch(header.alg, holderKeyJwk);
|
|
226
|
+
try { keyObj = nodeCrypto.createPublicKey({ key: holderKeyJwk, format: "jwk" }); }
|
|
227
|
+
catch (e) {
|
|
228
|
+
throw new AuthError("auth-oid4vci/bad-resolved-key",
|
|
229
|
+
"credential issuance: resolveKid-returned key is not importable as a public key: " + ((e && e.message) || String(e)));
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
if (!holderKeyJwk) {
|
|
233
|
+
throw new AuthError("auth-oid4vci/no-jwk-in-header",
|
|
234
|
+
"credential issuance: proof JWT must carry `jwk` for inline holder-key binding");
|
|
235
|
+
}
|
|
236
|
+
// CVE-2026-22817 — cross-check alg/kty before importing the holder
|
|
237
|
+
// JWK. Without this an attacker-controlled `alg: "HS256"` against an
|
|
238
|
+
// RSA holder JWK would have node:crypto.verify treat the RSA public
|
|
239
|
+
// key as an HMAC secret. Routed through the shared helper so every
|
|
240
|
+
// JWT verifier in the framework enforces the same check.
|
|
241
|
+
jwtExternal._assertAlgKtyMatch(header.alg, holderKeyJwk);
|
|
242
|
+
try { keyObj = nodeCrypto.createPublicKey({ key: holderKeyJwk, format: "jwk" }); }
|
|
243
|
+
catch (e) {
|
|
244
|
+
throw new AuthError("auth-oid4vci/bad-jwk",
|
|
245
|
+
"credential issuance: proof JWT jwk is not parseable: " + ((e && e.message) || String(e)));
|
|
246
|
+
}
|
|
197
247
|
}
|
|
198
248
|
|
|
199
249
|
var signingInput = parts[0] + "." + parts[1];
|
|
@@ -241,6 +291,7 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
|
|
|
241
291
|
* sdJwtIssuer: <b.auth.sdJwtVc.issuer instance>, // mints the SD-JWT VC
|
|
242
292
|
* supportedCredentials: { [id]: { format, vct, claims, ... } },
|
|
243
293
|
* proofAlgorithms: string[], // default ["ES256", "ES384", "EdDSA"]
|
|
294
|
+
* resolveKid?: function(kid, header), // resolve a kid-only proof's holder key (JWK | KeyObject); without it, kid-only proofs are refused
|
|
244
295
|
* preAuthCodeTtlMs?: number, // default 5m
|
|
245
296
|
* accessTokenTtlMs?: number, // default 15m
|
|
246
297
|
* cNonceTtlMs?: number, // default 5m
|
|
@@ -301,6 +352,12 @@ function create(opts) {
|
|
|
301
352
|
var proofAlgs = Array.isArray(opts.proofAlgorithms) && opts.proofAlgorithms.length > 0
|
|
302
353
|
? opts.proofAlgorithms : ["ES256", "ES384", "EdDSA"];
|
|
303
354
|
|
|
355
|
+
// Optional kid-resolver for kid-only proofs (EUDI-Wallet attested-key
|
|
356
|
+
// flow). Config-time throw if supplied but not a function. Absent →
|
|
357
|
+
// kid-only proofs keep the clear refusal (back-compat).
|
|
358
|
+
var resolveKid = validateOpts.optionalFunction(opts.resolveKid,
|
|
359
|
+
"issuer.create: resolveKid", AuthError, "auth-oid4vci/bad-resolve-kid");
|
|
360
|
+
|
|
304
361
|
var preAuthTtl = opts.preAuthCodeTtlMs || DEFAULT_PRE_AUTH_TTL_MS;
|
|
305
362
|
var accessTokenTtl = opts.accessTokenTtlMs || DEFAULT_ACCESS_TOKEN_TTL;
|
|
306
363
|
var cNonceTtl = opts.cNonceTtlMs || DEFAULT_C_NONCE_TTL_MS;
|
|
@@ -466,7 +523,7 @@ function create(opts) {
|
|
|
466
523
|
"exchangePreAuthorizedCode: tx_code does not match");
|
|
467
524
|
}
|
|
468
525
|
}
|
|
469
|
-
await codeStore.
|
|
526
|
+
await codeStore.del(eopts.preAuthCode);
|
|
470
527
|
var accessToken = generateToken(32); // 256-bit access token
|
|
471
528
|
var cNonce = generateToken(16); // 128-bit c_nonce
|
|
472
529
|
var record = {
|
|
@@ -555,7 +612,7 @@ function create(opts) {
|
|
|
555
612
|
}
|
|
556
613
|
|
|
557
614
|
var expectedCNonce = await cNonceStore.get(iopts.accessToken);
|
|
558
|
-
var verified = _verifyProofJwt(iopts.proof, opts.credentialIssuerUrl, expectedCNonce, null, proofAlgs, proofMaxAgeMs);
|
|
615
|
+
var verified = await _verifyProofJwt(iopts.proof, opts.credentialIssuerUrl, expectedCNonce, null, proofAlgs, proofMaxAgeMs, resolveKid);
|
|
559
616
|
|
|
560
617
|
if (!iopts.claims || typeof iopts.claims !== "object") {
|
|
561
618
|
throw new AuthError("auth-oid4vci/no-claims",
|
|
@@ -584,8 +641,8 @@ function create(opts) {
|
|
|
584
641
|
// explicitly tightens cleanup.
|
|
585
642
|
if (accessTokenSingleUse) {
|
|
586
643
|
try {
|
|
587
|
-
await atStore.
|
|
588
|
-
await cNonceStore.
|
|
644
|
+
await atStore.del(iopts.accessToken);
|
|
645
|
+
await cNonceStore.del(iopts.accessToken);
|
|
589
646
|
} catch (_e) { /* drop-silent — cleanup is best-effort */ }
|
|
590
647
|
}
|
|
591
648
|
|