@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.
@@ -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 in header pointing at the key to bind cnf to
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
- // Verify the JWS signature using the key embedded in the header.
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
- try { keyObj = nodeCrypto.createPublicKey({ key: holderKeyJwk, format: "jwk" }); }
194
- catch (e) {
195
- throw new AuthError("auth-oid4vci/bad-jwk",
196
- "credential issuance: proof JWT jwk is not parseable: " + ((e && e.message) || String(e)));
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.delete(eopts.preAuthCode);
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.delete(iopts.accessToken);
588
- await cNonceStore.delete(iopts.accessToken);
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