@blamejs/core 0.8.59 → 0.8.64

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.
@@ -128,16 +128,32 @@ function create(opts) {
128
128
  throw new AuthError("auth-sd-jwt-vc/bad-token",
129
129
  "holder.store: sdJwt is required");
130
130
  }
131
+ if (spec.keyAttestation !== undefined && spec.keyAttestation !== null) {
132
+ // OpenID4VCI key-attestation extension — operator-supplied JWT
133
+ // signed by the holder-device attestation issuer (TEE / Apple
134
+ // App Attest / Play Integrity / FIDO MDS3 anchor). Stored
135
+ // verbatim; surfaced in present()'s key-binding header so the
136
+ // verifier can validate the holder-key provenance alongside
137
+ // the cnf-bound presentation. Refuse anything that doesn't
138
+ // look like a JWS (3 dot-separated segments).
139
+ if (typeof spec.keyAttestation !== "string" ||
140
+ spec.keyAttestation.split(".").length !== 3) {
141
+ throw new AuthError("auth-sd-jwt-vc/bad-key-attestation",
142
+ "holder.store: keyAttestation must be a JWS-compact-serialized JWT (3 dot-separated segments)");
143
+ }
144
+ }
131
145
  var record = {
132
- id: spec.id,
133
- sdJwt: spec.sdJwt,
134
- vct: spec.vct || null,
135
- issuer: spec.issuer || null,
136
- receivedAt: Date.now(),
146
+ id: spec.id,
147
+ sdJwt: spec.sdJwt,
148
+ vct: spec.vct || null,
149
+ issuer: spec.issuer || null,
150
+ keyAttestation: spec.keyAttestation || null,
151
+ receivedAt: Date.now(),
137
152
  };
138
153
  await opts.storage.put(spec.id, record);
139
154
  _emitAudit("auth.sdJwtVc.holder.stored", "success", {
140
155
  id: spec.id, vct: record.vct, issuer: record.issuer,
156
+ hasKeyAttestation: !!record.keyAttestation,
141
157
  });
142
158
  _emitMetric("stored");
143
159
  return record;
@@ -153,6 +169,10 @@ function create(opts) {
153
169
  throw new AuthError("auth-sd-jwt-vc/credential-not-found",
154
170
  "holder.present: credentialId \"" + spec.credentialId + "\" not found in storage");
155
171
  }
172
+ // Operator may override the stored key_attestation per
173
+ // presentation (e.g., a fresh attestation token bound to the
174
+ // verifier nonce + audience for higher-AAL flows).
175
+ var keyAttestation = spec.keyAttestation || record.keyAttestation || null;
156
176
  var presentation = sdJwtVcCore().present({
157
177
  sdJwt: record.sdJwt,
158
178
  disclosedClaimNames: spec.disclosedClaimNames || [],
@@ -160,11 +180,13 @@ function create(opts) {
160
180
  nonce: spec.nonce || null,
161
181
  holderKey: opts.holderKey,
162
182
  algorithm: algorithm,
183
+ keyAttestation: keyAttestation,
163
184
  });
164
185
  _emitAudit("auth.sdJwtVc.holder.presented", "success", {
165
- credentialId: spec.credentialId,
166
- audience: spec.audience || null,
167
- disclosed: (spec.disclosedClaimNames || []).length,
186
+ credentialId: spec.credentialId,
187
+ audience: spec.audience || null,
188
+ disclosed: (spec.disclosedClaimNames || []).length,
189
+ hasKeyAttestation: !!keyAttestation,
168
190
  });
169
191
  _emitMetric("presented");
170
192
  return presentation;
@@ -271,6 +271,7 @@ function present(opts) {
271
271
  validateOpts(opts, [
272
272
  "sdJwt", "disclosedClaimNames", "audience",
273
273
  "nonce", "holderKey", "algorithm", "issuedAt",
274
+ "keyAttestation",
274
275
  ], "auth.sdJwtVc.present");
275
276
 
276
277
  validateOpts.requireNonEmptyString(opts.sdJwt,
@@ -324,6 +325,16 @@ function present(opts) {
324
325
  sd_hash: sdHash,
325
326
  };
326
327
  var kbHeader = { alg: algorithm, typ: "kb+jwt" };
328
+ if (typeof opts.keyAttestation === "string" && opts.keyAttestation.length > 0) {
329
+ // OpenID4VCI key-attestation extension. The attestation JWT
330
+ // travels in the KB-JWT header so a verifier with no extra
331
+ // round-trip can validate the holder-key provenance (TEE /
332
+ // hardware-backed key) alongside the cnf-bound key-binding
333
+ // signature. Operators wanting per-presentation freshness
334
+ // mint a new attestation per audience+nonce on the holder
335
+ // device and pass it via opts.keyAttestation.
336
+ kbHeader.key_attestation = opts.keyAttestation;
337
+ }
327
338
  var kbJwt = _signJwt(kbHeader, kbPayload, opts.holderKey, algorithm);
328
339
  presentation += kbJwt;
329
340
  }
@@ -343,6 +354,7 @@ async function verify(presentation, opts) {
343
354
  "issuerKeyResolver", "audience", "nonce",
344
355
  "now", "expectedVct", "maxClockSkewSec",
345
356
  "requireKeyBinding",
357
+ "keyAttestationVerifier", "requireKeyAttestation",
346
358
  ], "auth.sdJwtVc.verify");
347
359
 
348
360
  if (typeof presentation !== "string" || presentation.length === 0) {
@@ -438,6 +450,7 @@ async function verify(presentation, opts) {
438
450
  // 4. Optionally verify Key Binding JWT
439
451
  var kbValidated = false;
440
452
  var holderKey = null;
453
+ var keyAttestationClaims = null;
441
454
  if (jwtParsed.payload.cnf && jwtParsed.payload.cnf.jwk) {
442
455
  holderKey = jwtParsed.payload.cnf.jwk;
443
456
  }
@@ -488,6 +501,46 @@ async function verify(presentation, opts) {
488
501
  "verify: KB-JWT iat is in the future");
489
502
  }
490
503
  kbValidated = true;
504
+
505
+ // OpenID4VCI key-attestation extension: the holder may include
506
+ // a key_attestation JWT in the KB-JWT header. The framework
507
+ // surfaces it for the operator-supplied verifier callback so
508
+ // policy decisions about TEE provenance / hardware-backed-key
509
+ // requirement / app-attest origin / FIDO MDS3 anchor stay in
510
+ // the operator's hands. The framework does NOT trust-anchor
511
+ // resolve attestation issuers itself — the operator's
512
+ // verifier picks the right anchor for the use case.
513
+ if (typeof kbHeaderObj.key_attestation === "string" &&
514
+ kbHeaderObj.key_attestation.length > 0) {
515
+ if (typeof opts.keyAttestationVerifier !== "function") {
516
+ if (opts.requireKeyAttestation === true) {
517
+ throw new AuthError("auth-sd-jwt-vc/no-attestation-verifier",
518
+ "verify: requireKeyAttestation=true but no keyAttestationVerifier supplied");
519
+ }
520
+ // No verifier — surface the raw token and let the caller
521
+ // skip; we don't trust an attestation we can't verify.
522
+ } else {
523
+ try {
524
+ keyAttestationClaims = await opts.keyAttestationVerifier({
525
+ jwt: kbHeaderObj.key_attestation,
526
+ holderKey: holderKey,
527
+ audience: opts.audience || null,
528
+ nonce: opts.nonce || null,
529
+ });
530
+ } catch (e) {
531
+ throw new AuthError("auth-sd-jwt-vc/attestation-verify-failed",
532
+ "verify: keyAttestationVerifier rejected the attestation: " +
533
+ ((e && e.message) || String(e)));
534
+ }
535
+ if (!keyAttestationClaims || typeof keyAttestationClaims !== "object") {
536
+ throw new AuthError("auth-sd-jwt-vc/attestation-empty",
537
+ "verify: keyAttestationVerifier returned no claims (must return the verified attestation payload)");
538
+ }
539
+ }
540
+ } else if (opts.requireKeyAttestation === true) {
541
+ throw new AuthError("auth-sd-jwt-vc/missing-key-attestation",
542
+ "verify: requireKeyAttestation=true but KB-JWT carries no key_attestation header");
543
+ }
491
544
  } else if (opts.requireKeyBinding) {
492
545
  throw new AuthError("auth-sd-jwt-vc/missing-kb",
493
546
  "verify: KB-JWT required (requireKeyBinding=true) but not present");
@@ -502,13 +555,14 @@ async function verify(presentation, opts) {
502
555
  });
503
556
 
504
557
  return {
505
- valid: true,
506
- claims: resolved,
507
- disclosedClaims: disclosedClaims,
508
- issuerHeader: jwtParsed.header,
509
- issuerPayload: jwtParsed.payload,
510
- holderKey: holderKey,
511
- kbValidated: kbValidated,
558
+ valid: true,
559
+ claims: resolved,
560
+ disclosedClaims: disclosedClaims,
561
+ issuerHeader: jwtParsed.header,
562
+ issuerPayload: jwtParsed.payload,
563
+ holderKey: holderKey,
564
+ kbValidated: kbValidated,
565
+ keyAttestationClaims: keyAttestationClaims,
512
566
  };
513
567
  }
514
568