@openeudi/openid4vp 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { splitSdJwt, decodeSdJwt, getClaims } from '@sd-jwt/decode';
2
2
  import { decodeProtectedHeader, importX509, jwtVerify, importJWK } from 'jose';
3
- import { decode } from 'cbor-x';
3
+ import { Encoder, Tag } from 'cbor-x';
4
4
  import { v4 } from 'uuid';
5
5
 
6
6
  // src/parsers/sd-jwt.parser.ts
@@ -45,8 +45,8 @@ var NonceValidationError = class extends Error {
45
45
  // src/parsers/sd-jwt.parser.ts
46
46
  var DEFAULT_ALLOWED_ALGORITHMS = ["ES256", "ES384", "ES512"];
47
47
  var sha256Hasher = async (data, _alg) => {
48
- const encoder = new TextEncoder();
49
- const input = typeof data === "string" ? encoder.encode(data) : data;
48
+ const encoder2 = new TextEncoder();
49
+ const input = typeof data === "string" ? encoder2.encode(data) : data;
50
50
  const hashBuffer = await crypto.subtle.digest("SHA-256", input);
51
51
  return new Uint8Array(hashBuffer);
52
52
  };
@@ -181,7 +181,13 @@ var SdJwtParser = class {
181
181
  certificate: issuerCertBytes,
182
182
  country: extractCountryHint(issuerString)
183
183
  };
184
- if (options.trustedCertificates.length > 0) {
184
+ if (options.trustedCertificates.length === 0) {
185
+ if (options.skipTrustCheck !== true) {
186
+ throw new MalformedCredentialError(
187
+ "trustedCertificates must not be empty unless skipTrustCheck is true"
188
+ );
189
+ }
190
+ } else {
185
191
  const isTrusted = options.trustedCertificates.some((trusted) => bytesEqual(trusted, issuerCertBytes));
186
192
  if (!isTrusted) {
187
193
  return invalidResult("Issuer certificate is not trusted");
@@ -257,8 +263,111 @@ var SdJwtParser = class {
257
263
  };
258
264
  }
259
265
  };
260
- function isBinaryData(value) {
261
- return value instanceof Uint8Array;
266
+ var decoder = new Encoder({ mapsAsObjects: false, useRecords: false });
267
+ var HEADER_ALG = 1;
268
+ var HEADER_X5CHAIN = 33;
269
+ var COSE_ALG_TO_NAME = {
270
+ [-7]: "ES256",
271
+ [-35]: "ES384",
272
+ [-36]: "ES512"
273
+ };
274
+ function decodeCoseSign1(bytes) {
275
+ let outer = decoder.decode(bytes);
276
+ if (outer instanceof Tag && outer.tag === 18) {
277
+ const tagValue = outer.value;
278
+ outer = tagValue instanceof Uint8Array ? decoder.decode(tagValue) : tagValue;
279
+ }
280
+ if (!Array.isArray(outer) || outer.length !== 4) {
281
+ throw new MalformedCredentialError("COSE_Sign1 must be a CBOR array of length 4");
282
+ }
283
+ const [protectedHeaderBytes, unprotectedHeader, payloadRaw, signature] = outer;
284
+ if (!(protectedHeaderBytes instanceof Uint8Array)) {
285
+ throw new MalformedCredentialError("COSE_Sign1 protected header must be a bstr");
286
+ }
287
+ if (!(unprotectedHeader instanceof Map)) {
288
+ throw new MalformedCredentialError("COSE_Sign1 unprotected header must be a Map");
289
+ }
290
+ if (!(signature instanceof Uint8Array)) {
291
+ throw new MalformedCredentialError("COSE_Sign1 signature must be a bstr");
292
+ }
293
+ let protectedHeader;
294
+ try {
295
+ const decoded = decoder.decode(protectedHeaderBytes);
296
+ if (!(decoded instanceof Map)) {
297
+ throw new Error("protected header not a Map");
298
+ }
299
+ protectedHeader = decoded;
300
+ } catch {
301
+ throw new MalformedCredentialError("COSE_Sign1 protected header is not decodable CBOR");
302
+ }
303
+ const algLabel = protectedHeader.get(HEADER_ALG);
304
+ const alg = typeof algLabel === "number" ? COSE_ALG_TO_NAME[algLabel] : void 0;
305
+ if (!alg) {
306
+ throw new MalformedCredentialError(`COSE_Sign1 uses unknown alg label: ${String(algLabel)}`);
307
+ }
308
+ let payload;
309
+ if (payloadRaw instanceof Uint8Array) {
310
+ payload = payloadRaw;
311
+ } else if (payloadRaw instanceof Tag && payloadRaw.tag === 24 && payloadRaw.value instanceof Uint8Array) {
312
+ payload = payloadRaw.value;
313
+ } else {
314
+ throw new MalformedCredentialError("COSE_Sign1 payload must be a byte string");
315
+ }
316
+ const x5raw = unprotectedHeader.get(HEADER_X5CHAIN);
317
+ let x5chain;
318
+ if (x5raw === void 0) {
319
+ throw new MalformedCredentialError("COSE_Sign1 missing x5chain in unprotected header (label 33)");
320
+ }
321
+ if (x5raw instanceof Uint8Array) {
322
+ x5chain = [x5raw];
323
+ } else if (Array.isArray(x5raw) && x5raw.every((v) => v instanceof Uint8Array)) {
324
+ x5chain = x5raw;
325
+ } else {
326
+ throw new MalformedCredentialError("COSE_Sign1 x5chain must be a bstr or array of bstr");
327
+ }
328
+ return {
329
+ protectedHeaderBytes,
330
+ protectedHeader,
331
+ unprotectedHeader,
332
+ payload,
333
+ signature,
334
+ x5chain,
335
+ alg
336
+ };
337
+ }
338
+ var ALG_TO_HASH = {
339
+ ES256: "SHA-256",
340
+ ES384: "SHA-384",
341
+ ES512: "SHA-512"
342
+ };
343
+ async function verifyCoseSign1(coseSign1, publicKey, allowedAlgorithms) {
344
+ if (!allowedAlgorithms.includes(coseSign1.alg)) {
345
+ throw new MalformedCredentialError(`COSE_Sign1 algorithm ${coseSign1.alg} not in allowlist`);
346
+ }
347
+ const hashAlg = ALG_TO_HASH[coseSign1.alg];
348
+ if (!hashAlg) {
349
+ throw new MalformedCredentialError(`no hash mapping for alg ${coseSign1.alg}`);
350
+ }
351
+ const sigStructure = ["Signature1", coseSign1.protectedHeaderBytes, new Uint8Array(0), coseSign1.payload];
352
+ const sigInput = decoder.encode(sigStructure);
353
+ const ok = await crypto.subtle.verify(
354
+ { name: "ECDSA", hash: hashAlg },
355
+ publicKey,
356
+ new Uint8Array(coseSign1.signature),
357
+ sigInput
358
+ );
359
+ if (!ok) {
360
+ throw new MalformedCredentialError("COSE_Sign1 signature verification failed");
361
+ }
362
+ }
363
+ var decoder2 = new Encoder({ mapsAsObjects: false, useRecords: false });
364
+ var ALLOWED_HASH_ALGS = /* @__PURE__ */ new Set(["SHA-256", "SHA-384", "SHA-512"]);
365
+ async function computeItemDigest(itemBytes, digestAlgorithm) {
366
+ if (!ALLOWED_HASH_ALGS.has(digestAlgorithm)) {
367
+ throw new MalformedCredentialError(`Unknown digest algorithm: ${digestAlgorithm}`);
368
+ }
369
+ const buf = await crypto.subtle.digest(digestAlgorithm, new Uint8Array(itemBytes));
370
+ return new Uint8Array(buf);
262
371
  }
263
372
  function bytesEqual2(a, b) {
264
373
  if (a.length !== b.length) return false;
@@ -267,96 +376,231 @@ function bytesEqual2(a, b) {
267
376
  }
268
377
  return true;
269
378
  }
270
- function extractCertificateFromIssuerAuth(issuerAuth) {
271
- const unprotected = issuerAuth[1];
272
- if (unprotected && typeof unprotected === "object") {
273
- const certFromUnprotected = extractX5Chain(unprotected);
274
- if (certFromUnprotected) return certFromUnprotected;
275
- }
276
- const protectedHeader = issuerAuth[0];
277
- if (isBinaryData(protectedHeader)) {
278
- try {
279
- const decoded = decode(protectedHeader);
280
- if (decoded && typeof decoded === "object") {
281
- const certFromProtected = extractX5Chain(decoded);
282
- if (certFromProtected) return certFromProtected;
379
+ function unwrapItemMap(itemBytes) {
380
+ let decoded;
381
+ try {
382
+ decoded = decoder2.decode(itemBytes);
383
+ } catch {
384
+ throw new MalformedCredentialError("IssuerSignedItem is not decodable CBOR");
385
+ }
386
+ let inner;
387
+ if (decoded instanceof Tag && decoded.tag === 24 && decoded.value instanceof Uint8Array) {
388
+ inner = decoder2.decode(decoded.value);
389
+ } else if (decoded !== null && typeof decoded === "object" && !(decoded instanceof Tag) && !(decoded instanceof Map) && !(decoded instanceof Uint8Array) && "value" in decoded && decoded.value instanceof Uint8Array) {
390
+ inner = decoder2.decode(decoded.value);
391
+ } else if (decoded instanceof Map) {
392
+ inner = decoded;
393
+ } else {
394
+ throw new MalformedCredentialError("IssuerSignedItem must be a tag-24 wrapped CBOR Map");
395
+ }
396
+ if (!(inner instanceof Map)) {
397
+ throw new MalformedCredentialError("IssuerSignedItem must decode to a Map");
398
+ }
399
+ return inner;
400
+ }
401
+ async function verifyAllDigests(nameSpaces, mso) {
402
+ for (const [ns, items] of nameSpaces) {
403
+ const expected = mso.valueDigests.get(ns);
404
+ if (!expected) {
405
+ throw new MalformedCredentialError(`namespace ${ns} has no valueDigests entry in MSO`);
406
+ }
407
+ for (const itemBytes of items) {
408
+ const hash = await computeItemDigest(itemBytes, mso.digestAlgorithm);
409
+ const item = unwrapItemMap(itemBytes);
410
+ const digestID = item.get("digestID");
411
+ if (typeof digestID !== "number") {
412
+ throw new MalformedCredentialError("IssuerSignedItem missing digestID");
413
+ }
414
+ const expectedDigest = expected.get(digestID);
415
+ if (!expectedDigest) {
416
+ throw new MalformedCredentialError(`digestID ${digestID} missing from MSO valueDigests[${ns}]`);
417
+ }
418
+ if (!bytesEqual2(hash, expectedDigest)) {
419
+ throw new MalformedCredentialError(`digest mismatch for ns=${ns} digestID=${digestID}`);
283
420
  }
284
- } catch {
285
421
  }
286
- return new Uint8Array(protectedHeader);
287
422
  }
288
- return new Uint8Array();
289
423
  }
290
- function extractX5Chain(header) {
291
- if (header instanceof Map) {
292
- const cert = header.get(33);
293
- if (isBinaryData(cert)) {
294
- return new Uint8Array(cert);
424
+ var decoder3 = new Encoder({ mapsAsObjects: false, useRecords: false });
425
+ function decodeDateLike(value) {
426
+ if (value instanceof Date) return value;
427
+ if (value instanceof Tag) {
428
+ if (value.tag === 0 && typeof value.value === "string") {
429
+ return new Date(value.value);
430
+ }
431
+ if (value.tag === 1 && typeof value.value === "number") {
432
+ return new Date(value.value * 1e3);
295
433
  }
296
- } else if (typeof header === "object" && header !== null) {
297
- const rec = header;
298
- const cert = rec[33] ?? rec["x5chain"] ?? rec["33"];
299
- if (isBinaryData(cert)) {
300
- return new Uint8Array(cert);
434
+ if (value.tag === 1004 && typeof value.value === "string") {
435
+ return new Date(value.value);
301
436
  }
302
437
  }
303
- return null;
438
+ if (typeof value === "string") {
439
+ return new Date(value);
440
+ }
441
+ throw new MalformedCredentialError(`unsupported date encoding in MSO: ${typeof value}`);
304
442
  }
305
- function mapToCredentialClaims2(items) {
306
- const claims = {};
307
- for (const item of items) {
308
- const key = item.elementIdentifier;
309
- const value = item.elementValue;
310
- switch (key) {
311
- case "age_over_18":
312
- if (typeof value === "boolean") claims.age_over_18 = value;
313
- break;
314
- case "age_over_21":
315
- if (typeof value === "boolean") claims.age_over_21 = value;
316
- break;
317
- case "resident_country":
318
- if (typeof value === "string") claims.resident_country = value;
319
- break;
320
- case "nationality":
321
- if (typeof value === "string") claims.nationality = value;
322
- break;
323
- case "family_name_birth":
324
- if (typeof value === "string") claims.family_name_birth = value;
325
- break;
326
- default:
327
- claims[key] = value;
328
- break;
443
+ function decodeMso(payload) {
444
+ let raw;
445
+ try {
446
+ raw = decoder3.decode(payload);
447
+ if (raw instanceof Tag && raw.tag === 24 && raw.value instanceof Uint8Array) {
448
+ raw = decoder3.decode(raw.value);
449
+ } else if (raw !== null && typeof raw === "object" && !(raw instanceof Tag) && !(raw instanceof Map) && !(raw instanceof Uint8Array) && "value" in raw && raw.value instanceof Uint8Array) {
450
+ raw = decoder3.decode(raw.value);
329
451
  }
452
+ } catch (err) {
453
+ if (err instanceof MalformedCredentialError) throw err;
454
+ throw new MalformedCredentialError("MSO payload is not decodable CBOR");
330
455
  }
331
- return claims;
456
+ if (!(raw instanceof Map)) {
457
+ throw new MalformedCredentialError("MSO must be a CBOR Map");
458
+ }
459
+ const m = raw;
460
+ const version = m.get("version");
461
+ if (typeof version !== "string") {
462
+ throw new MalformedCredentialError("MSO.version must be a string");
463
+ }
464
+ const digestAlgorithm = m.get("digestAlgorithm");
465
+ if (typeof digestAlgorithm !== "string") {
466
+ throw new MalformedCredentialError("MSO.digestAlgorithm must be a string");
467
+ }
468
+ const docType = m.get("docType");
469
+ if (typeof docType !== "string") {
470
+ throw new MalformedCredentialError("MSO.docType must be a string");
471
+ }
472
+ const vdRaw = m.get("valueDigests");
473
+ if (!(vdRaw instanceof Map)) {
474
+ throw new MalformedCredentialError("MSO.valueDigests must be a Map");
475
+ }
476
+ const valueDigests = /* @__PURE__ */ new Map();
477
+ for (const [ns, inner] of vdRaw.entries()) {
478
+ if (typeof ns !== "string" || !(inner instanceof Map)) {
479
+ throw new MalformedCredentialError("MSO.valueDigests entry malformed");
480
+ }
481
+ const perNs = /* @__PURE__ */ new Map();
482
+ for (const [id, digest] of inner.entries()) {
483
+ if (typeof id !== "number" || !(digest instanceof Uint8Array)) {
484
+ throw new MalformedCredentialError("MSO digest entry must map number\u2192bstr");
485
+ }
486
+ perNs.set(id, digest);
487
+ }
488
+ valueDigests.set(ns, perNs);
489
+ }
490
+ const viRaw = m.get("validityInfo");
491
+ if (!(viRaw instanceof Map)) {
492
+ throw new MalformedCredentialError("MSO.validityInfo must be a Map");
493
+ }
494
+ const vi = viRaw;
495
+ const validityInfo = {
496
+ signed: decodeDateLike(vi.get("signed")),
497
+ validFrom: decodeDateLike(vi.get("validFrom")),
498
+ validUntil: decodeDateLike(vi.get("validUntil"))
499
+ };
500
+ if (vi.has("expectedUpdate")) {
501
+ validityInfo.expectedUpdate = decodeDateLike(vi.get("expectedUpdate"));
502
+ }
503
+ return { version, digestAlgorithm, valueDigests, docType, validityInfo };
332
504
  }
333
- function extractValidityPeriod(issuerAuth) {
334
- const payload = issuerAuth[2];
335
- if (!payload) return {};
336
- try {
337
- const decoded = isBinaryData(payload) ? decode(payload) : payload;
338
- if (decoded && typeof decoded === "object") {
339
- const obj = decoded;
340
- const validityInfo = obj["validityInfo"];
341
- if (validityInfo) {
342
- return {
343
- validFrom: validityInfo["validFrom"] instanceof Date ? validityInfo["validFrom"] : typeof validityInfo["validFrom"] === "string" ? new Date(validityInfo["validFrom"]) : void 0,
344
- validUntil: validityInfo["validUntil"] instanceof Date ? validityInfo["validUntil"] : typeof validityInfo["validUntil"] === "string" ? new Date(validityInfo["validUntil"]) : void 0
345
- };
505
+ function validateMsoValidity(mso, now) {
506
+ const { signed, validFrom, validUntil } = mso.validityInfo;
507
+ if (signed.getTime() > now.getTime()) {
508
+ throw new MalformedCredentialError(`MSO signed=${signed.toISOString()} is in the future`);
509
+ }
510
+ if (validFrom.getTime() > now.getTime()) {
511
+ throw new MalformedCredentialError(`MSO validFrom=${validFrom.toISOString()} is in the future`);
512
+ }
513
+ if (validFrom.getTime() > validUntil.getTime()) {
514
+ throw new MalformedCredentialError("MSO validFrom is after validUntil");
515
+ }
516
+ if (signed.getTime() < validFrom.getTime() || signed.getTime() > validUntil.getTime()) {
517
+ throw new MalformedCredentialError("MSO signed is outside the validity window");
518
+ }
519
+ if (validUntil.getTime() < now.getTime()) {
520
+ throw new ExpiredCredentialError(`MSO validUntil=${validUntil.toISOString()} is in the past`);
521
+ }
522
+ }
523
+ function validateMsoDocType(mso, expectedDocType) {
524
+ if (mso.docType !== expectedDocType) {
525
+ throw new MalformedCredentialError(`MSO docType=${mso.docType} does not match expected ${expectedDocType}`);
526
+ }
527
+ }
528
+
529
+ // src/parsers/mdoc.parser.ts
530
+ var decoder4 = new Encoder({ mapsAsObjects: false, useRecords: false });
531
+ var encoder = new Encoder({ mapsAsObjects: false, useRecords: false });
532
+ var DEFAULT_ALLOWED_ALGORITHMS2 = ["ES256", "ES384", "ES512"];
533
+ function bytesEqual3(a, b) {
534
+ if (a.length !== b.length) return false;
535
+ for (let i = 0; i < a.length; i++) {
536
+ if (a[i] !== b[i]) return false;
537
+ }
538
+ return true;
539
+ }
540
+ function derToPem2(der) {
541
+ let binary = "";
542
+ for (const b of der) binary += String.fromCharCode(b);
543
+ const b64 = globalThis.btoa(binary);
544
+ const lines = b64.match(/.{1,64}/g) || [];
545
+ return `-----BEGIN CERTIFICATE-----
546
+ ${lines.join("\n")}
547
+ -----END CERTIFICATE-----`;
548
+ }
549
+ function extractCountryHintFromCert(_certDer) {
550
+ return "";
551
+ }
552
+ function extractRawItemBytes(item) {
553
+ if (item instanceof Tag && item.tag === 24 && item.value instanceof Uint8Array) {
554
+ return item.value;
555
+ }
556
+ if (item instanceof Uint8Array) {
557
+ return item;
558
+ }
559
+ if (item !== null && typeof item === "object" && !(item instanceof Tag) && !(item instanceof Map) && "value" in item && item.value instanceof Uint8Array) {
560
+ return item.value;
561
+ }
562
+ throw new MalformedCredentialError("IssuerSignedItem must be a tag-24 byte string");
563
+ }
564
+ function mapRawToClaims(nameSpaces) {
565
+ const out = {};
566
+ for (const items of nameSpaces.values()) {
567
+ for (const itemBytes of items) {
568
+ let decoded;
569
+ try {
570
+ decoded = decoder4.decode(itemBytes);
571
+ } catch {
572
+ continue;
573
+ }
574
+ let inner;
575
+ if (decoded instanceof Tag && decoded.tag === 24 && decoded.value instanceof Uint8Array) {
576
+ inner = decoder4.decode(decoded.value);
577
+ } else if (decoded !== null && typeof decoded === "object" && !(decoded instanceof Tag) && !(decoded instanceof Map) && !(decoded instanceof Uint8Array) && "value" in decoded && decoded.value instanceof Uint8Array) {
578
+ inner = decoder4.decode(decoded.value);
579
+ } else if (decoded instanceof Map) {
580
+ inner = decoded;
581
+ } else {
582
+ continue;
583
+ }
584
+ if (inner instanceof Map) {
585
+ const id = inner.get("elementIdentifier");
586
+ const val = inner.get("elementValue");
587
+ if (typeof id === "string") {
588
+ out[id] = val;
589
+ }
346
590
  }
347
591
  }
348
- } catch {
349
592
  }
350
- return {};
593
+ return out;
351
594
  }
352
595
  var MdocParser = class {
353
596
  format = "mdoc";
354
597
  /**
355
598
  * Returns true when the token is binary data (Uint8Array or Buffer),
356
599
  * indicating a CBOR-encoded mDOC DeviceResponse.
600
+ * Buffer extends Uint8Array, so `instanceof Uint8Array` catches both.
357
601
  */
358
602
  canParse(vpToken) {
359
- return isBinaryData(vpToken);
603
+ return vpToken instanceof Uint8Array;
360
604
  }
361
605
  /**
362
606
  * Parses and validates an mDOC DeviceResponse token.
@@ -365,66 +609,131 @@ var MdocParser = class {
365
609
  * or does not match the expected DeviceResponse format
366
610
  */
367
611
  async parse(vpToken, options) {
368
- if (!isBinaryData(vpToken)) {
369
- throw new MalformedCredentialError("VP token must be a Uint8Array or Buffer");
612
+ if (!(vpToken instanceof Uint8Array)) {
613
+ throw new MalformedCredentialError("mDOC vpToken must be a Uint8Array");
370
614
  }
371
- const invalidResult = (error) => ({
372
- valid: false,
373
- format: this.format,
374
- claims: {},
375
- issuer: { certificate: new Uint8Array(), country: "" },
376
- error
377
- });
615
+ const allowedAlgorithms = options.allowedAlgorithms ?? DEFAULT_ALLOWED_ALGORITHMS2;
378
616
  let deviceResponse;
379
617
  try {
380
- deviceResponse = decode(vpToken);
618
+ const decoded = decoder4.decode(vpToken);
619
+ if (!(decoded instanceof Map)) throw new Error();
620
+ deviceResponse = decoded;
381
621
  } catch {
382
- throw new MalformedCredentialError("Failed to decode CBOR DeviceResponse");
622
+ throw new MalformedCredentialError("vpToken is not a CBOR DeviceResponse Map");
383
623
  }
384
- if (!deviceResponse || typeof deviceResponse !== "object" || !Array.isArray(deviceResponse.documents) || deviceResponse.documents.length === 0) {
385
- throw new MalformedCredentialError("Invalid DeviceResponse structure: missing or empty documents array");
624
+ const docs = deviceResponse.get("documents");
625
+ if (!Array.isArray(docs) || docs.length === 0) {
626
+ throw new MalformedCredentialError("DeviceResponse has no documents");
386
627
  }
387
- const document = deviceResponse.documents[0];
388
- if (!document.issuerSigned || !document.issuerSigned.nameSpaces || !document.issuerSigned.issuerAuth) {
389
- throw new MalformedCredentialError("Invalid mDOC document: missing issuerSigned data");
628
+ const document = docs[0];
629
+ if (!(document instanceof Map)) {
630
+ throw new MalformedCredentialError("document must be a Map");
390
631
  }
391
- const issuerAuth = document.issuerSigned.issuerAuth;
392
- if (!Array.isArray(issuerAuth) || issuerAuth.length < 4) {
393
- throw new MalformedCredentialError("Invalid issuerAuth: expected COSE_Sign1 array with 4 elements");
632
+ const issuerSigned = document.get("issuerSigned");
633
+ if (!(issuerSigned instanceof Map)) {
634
+ throw new MalformedCredentialError("document.issuerSigned must be a Map");
394
635
  }
395
- const issuerCertBytes = extractCertificateFromIssuerAuth(issuerAuth);
396
- const issuerInfo = {
397
- certificate: issuerCertBytes,
398
- country: ""
399
- };
400
- if (options.trustedCertificates.length > 0) {
401
- const isTrusted = options.trustedCertificates.some((trusted) => bytesEqual2(trusted, issuerCertBytes));
402
- if (!isTrusted) {
403
- return invalidResult("Issuer certificate is not trusted");
636
+ const issuerAuth = issuerSigned.get("issuerAuth");
637
+ if (!Array.isArray(issuerAuth)) {
638
+ throw new MalformedCredentialError("issuerAuth must be a COSE_Sign1 array");
639
+ }
640
+ const issuerAuthBytes = encoder.encode(issuerAuth);
641
+ const cose = decodeCoseSign1(issuerAuthBytes);
642
+ if (!allowedAlgorithms.includes(cose.alg)) {
643
+ return {
644
+ valid: false,
645
+ format: this.format,
646
+ claims: {},
647
+ issuer: { certificate: new Uint8Array(), country: "" },
648
+ error: `Unsupported algorithm: ${cose.alg}`
649
+ };
650
+ }
651
+ const issuerCertBytes = cose.x5chain[0];
652
+ if (!issuerCertBytes) {
653
+ throw new MalformedCredentialError("Missing issuer certificate in x5chain");
654
+ }
655
+ if (options.trustedCertificates.length === 0) {
656
+ if (options.skipTrustCheck !== true) {
657
+ throw new MalformedCredentialError(
658
+ "trustedCertificates must not be empty unless skipTrustCheck is true"
659
+ );
404
660
  }
405
- } else if (issuerCertBytes.length === 0) {
406
- return invalidResult("No issuer certificate found and no trusted certificates configured");
407
- }
408
- const { validUntil } = extractValidityPeriod(issuerAuth);
409
- if (validUntil) {
410
- const now = /* @__PURE__ */ new Date();
411
- if (validUntil < now) {
412
- return invalidResult("Credential has expired");
661
+ } else {
662
+ const trusted = options.trustedCertificates.some((t) => bytesEqual3(t, issuerCertBytes));
663
+ if (!trusted) {
664
+ return {
665
+ valid: false,
666
+ format: this.format,
667
+ claims: {},
668
+ issuer: { certificate: issuerCertBytes, country: "" },
669
+ error: "Issuer certificate is not trusted"
670
+ };
413
671
  }
414
672
  }
415
- const pidNamespace = "eu.europa.ec.eudi.pid.1";
416
- const nameSpaces = document.issuerSigned.nameSpaces;
417
- const items = nameSpaces[pidNamespace] ?? nameSpaces[document.docType] ?? Object.values(nameSpaces)[0] ?? [];
418
- const claims = mapToCredentialClaims2(items);
419
- if (claims.resident_country && typeof claims.resident_country === "string") {
420
- issuerInfo.country = claims.resident_country;
673
+ const publicKey = await importX509(derToPem2(issuerCertBytes), cose.alg);
674
+ try {
675
+ await verifyCoseSign1(cose, publicKey, allowedAlgorithms);
676
+ } catch (err) {
677
+ if (err instanceof MalformedCredentialError) {
678
+ return {
679
+ valid: false,
680
+ format: this.format,
681
+ claims: {},
682
+ issuer: { certificate: issuerCertBytes, country: "" },
683
+ error: err.message
684
+ };
685
+ }
686
+ throw err;
421
687
  }
422
- return {
423
- valid: true,
424
- format: this.format,
425
- claims,
426
- issuer: issuerInfo
688
+ const mso = decodeMso(cose.payload);
689
+ try {
690
+ validateMsoValidity(mso, /* @__PURE__ */ new Date());
691
+ } catch (err) {
692
+ if (err instanceof ExpiredCredentialError) {
693
+ return {
694
+ valid: false,
695
+ format: this.format,
696
+ claims: {},
697
+ issuer: { certificate: issuerCertBytes, country: "" },
698
+ error: "Credential expired"
699
+ };
700
+ }
701
+ throw err;
702
+ }
703
+ if (options.expectedDocType !== void 0) {
704
+ try {
705
+ validateMsoDocType(mso, options.expectedDocType);
706
+ } catch (err) {
707
+ if (err instanceof MalformedCredentialError) {
708
+ return {
709
+ valid: false,
710
+ format: this.format,
711
+ claims: {},
712
+ issuer: { certificate: issuerCertBytes, country: "" },
713
+ error: err.message
714
+ };
715
+ }
716
+ throw err;
717
+ }
718
+ }
719
+ const nsRaw = issuerSigned.get("nameSpaces");
720
+ if (!(nsRaw instanceof Map)) {
721
+ throw new MalformedCredentialError("issuerSigned.nameSpaces must be a Map");
722
+ }
723
+ const nameSpaces = /* @__PURE__ */ new Map();
724
+ for (const [ns, items] of nsRaw.entries()) {
725
+ if (typeof ns !== "string" || !Array.isArray(items)) {
726
+ throw new MalformedCredentialError("nameSpaces entry malformed");
727
+ }
728
+ nameSpaces.set(ns, items.map(extractRawItemBytes));
729
+ }
730
+ await verifyAllDigests(nameSpaces, mso);
731
+ const claims = mapRawToClaims(nameSpaces);
732
+ const issuer = {
733
+ certificate: issuerCertBytes,
734
+ country: extractCountryHintFromCert()
427
735
  };
736
+ return { valid: true, format: this.format, claims, issuer };
428
737
  }
429
738
  };
430
739
  function createAuthorizationRequest(input) {
@@ -484,7 +793,7 @@ async function parsePresentation(vpToken, options) {
484
793
  }
485
794
 
486
795
  // src/index.ts
487
- var VERSION = "0.2.0";
796
+ var VERSION = "0.3.0";
488
797
 
489
798
  export { ExpiredCredentialError, InvalidSignatureError, MalformedCredentialError, MdocParser, NonceValidationError, SdJwtParser, UnsupportedFormatError, VERSION, createAuthorizationRequest, parsePresentation };
490
799
  //# sourceMappingURL=index.js.map