@openeudi/openid4vp 0.11.1 → 0.13.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.cjs CHANGED
@@ -45,7 +45,7 @@ var init_reflect_polyfill = __esm({
45
45
  });
46
46
 
47
47
  // src/errors.ts
48
- exports.InvalidSignatureError = void 0; exports.ExpiredCredentialError = void 0; exports.UnsupportedFormatError = void 0; exports.MalformedCredentialError = void 0; exports.NonceValidationError = void 0; exports.HaipValidationError = void 0; exports.OpenID4VPError = void 0; exports.TrustAnchorNotFoundError = void 0; exports.CertificateChainError = void 0; exports.RevokedCertificateError = void 0; exports.RevocationCheckFailedError = void 0; exports.LotlFetchError = void 0; exports.LotlSignatureError = void 0; exports.SignedRequestBuildError = void 0; exports.UnsupportedJweError = void 0; exports.DecryptionFailedError = void 0; exports.MissingDecryptionKeyError = void 0; exports.MissingVerifierEncryptionKeyError = void 0; exports.MultipleCredentialsNotSupportedError = void 0;
48
+ exports.InvalidSignatureError = void 0; exports.ExpiredCredentialError = void 0; exports.UnsupportedFormatError = void 0; exports.MalformedCredentialError = void 0; exports.NonceValidationError = void 0; exports.HaipValidationError = void 0; exports.OpenID4VPError = void 0; exports.TrustAnchorNotFoundError = void 0; exports.CertificateChainError = void 0; exports.RevokedCertificateError = void 0; exports.RevocationCheckFailedError = void 0; exports.LotlFetchError = void 0; exports.LotlSignatureError = void 0; exports.LotlConfigurationError = void 0; exports.SignedRequestBuildError = void 0; exports.UnsupportedJweError = void 0; exports.DecryptionFailedError = void 0; exports.MissingDecryptionKeyError = void 0; exports.MissingVerifierEncryptionKeyError = void 0; exports.MultipleCredentialsNotSupportedError = void 0; exports.GuardedFetchError = void 0;
49
49
  var init_errors = __esm({
50
50
  "src/errors.ts"() {
51
51
  init_reflect_polyfill();
@@ -142,6 +142,9 @@ var init_errors = __esm({
142
142
  exports.LotlSignatureError = class extends exports.OpenID4VPError {
143
143
  code = "lotl_signature_invalid";
144
144
  };
145
+ exports.LotlConfigurationError = class extends exports.OpenID4VPError {
146
+ code = "lotl_configuration_invalid";
147
+ };
145
148
  exports.SignedRequestBuildError = class extends exports.OpenID4VPError {
146
149
  code;
147
150
  constructor(code, message) {
@@ -189,6 +192,16 @@ var init_errors = __esm({
189
192
  this.presentationCount = presentationCount;
190
193
  }
191
194
  };
195
+ exports.GuardedFetchError = class extends exports.OpenID4VPError {
196
+ code = "guarded_fetch_rejected";
197
+ reason;
198
+ url;
199
+ constructor(reason, message, options) {
200
+ super(message, { cause: options.cause });
201
+ this.reason = reason;
202
+ this.url = options.url;
203
+ }
204
+ };
192
205
  }
193
206
  });
194
207
 
@@ -6585,7 +6598,7 @@ function dnIsUnder(childDn, subtreeDn) {
6585
6598
  }
6586
6599
  return true;
6587
6600
  }
6588
- var NAME_CONSTRAINTS_OID, DEFAULT_ALGORITHMS, ChainBuilder;
6601
+ var NAME_CONSTRAINTS_OID, ISO_18013_5_DS_EKU, DEFAULT_MAX_CHAIN_LENGTH, DEFAULT_MAX_SIGNATURE_CHECKS, searchAborts, DEFAULT_ALGORITHMS, ChainBuilder;
6589
6602
  var init_ChainBuilder = __esm({
6590
6603
  "src/trust/ChainBuilder.ts"() {
6591
6604
  init_reflect_polyfill();
@@ -6594,6 +6607,10 @@ var init_ChainBuilder = __esm({
6594
6607
  init_errors();
6595
6608
  init_x509_utils();
6596
6609
  NAME_CONSTRAINTS_OID = "2.5.29.30";
6610
+ ISO_18013_5_DS_EKU = "1.0.18013.5.1.2";
6611
+ DEFAULT_MAX_CHAIN_LENGTH = 5;
6612
+ DEFAULT_MAX_SIGNATURE_CHECKS = 32;
6613
+ searchAborts = /* @__PURE__ */ new WeakSet();
6597
6614
  DEFAULT_ALGORITHMS = [
6598
6615
  "ES256",
6599
6616
  "ES384",
@@ -6616,62 +6633,130 @@ var init_ChainBuilder = __esm({
6616
6633
  * optionally using additional intermediates. Returns the ordered chain
6617
6634
  * (leaf → anchor). Throws `CertificateChainError` on any validation
6618
6635
  * failure.
6636
+ *
6637
+ * Trust is by presence in the anchor set and is established ONLY by (a) the
6638
+ * leaf being byte-identical to an anchor, or (b) a verified signature by an
6639
+ * anchor's key on the last certificate of the path. Subject/Issuer DN
6640
+ * equality is used to select candidates, never to conclude trust
6641
+ * (GHSA-4c2f-96cf-f5fc).
6642
+ *
6643
+ * The search is depth-first and backtracks: when several intermediates share
6644
+ * a Subject DN — a re-issued certificate next to its expired predecessor, or
6645
+ * cross-signed copies under different roots — a candidate that fails a check
6646
+ * or leads nowhere does not end the search. The result therefore does not
6647
+ * depend on the order of `intermediates`. Work is bounded by
6648
+ * `maxChainLength` and `maxSignatureChecks`.
6619
6649
  */
6620
- async build(leaf, anchors, intermediates = []) {
6621
- const errors = [];
6650
+ async build(leaf, anchors, intermediates = [], context = {}) {
6651
+ const search = {
6652
+ remaining: this.opts.maxSignatureChecks ?? DEFAULT_MAX_SIGNATURE_CHECKS
6653
+ };
6654
+ let lastFailure;
6622
6655
  for (const anchor of anchors) {
6656
+ search.specific = void 0;
6657
+ search.fallback = void 0;
6623
6658
  try {
6624
- return await this.tryBuild(leaf, anchor, intermediates);
6659
+ const chain = await this.tryBuild(leaf, anchor, intermediates, context, search);
6660
+ if (chain) return chain;
6661
+ lastFailure = search.specific ?? search.fallback;
6625
6662
  } catch (err) {
6626
- errors.push(err);
6663
+ if (searchAborts.has(err)) throw err;
6664
+ lastFailure = err;
6627
6665
  }
6628
6666
  }
6629
- const last = errors[errors.length - 1];
6630
- if (last instanceof exports.CertificateChainError) throw last;
6667
+ if (lastFailure instanceof exports.CertificateChainError) throw lastFailure;
6631
6668
  throw new exports.CertificateChainError("no valid chain to any anchor", {
6632
6669
  reason: "signature",
6633
- cause: last
6670
+ cause: lastFailure
6634
6671
  });
6635
6672
  }
6636
- async tryBuild(leaf, anchor, intermediates) {
6637
- this.checkPerCert(leaf);
6638
- this.checkLeafKeyUsage(leaf);
6639
- const chain = [leaf];
6640
- let current = leaf;
6641
- const pool = new x509.X509Certificates(intermediates);
6642
- let nonLeafDepth = 0;
6643
- while (current.subject !== anchor.subject) {
6644
- const issuer = pool.find((c) => c.subject === current.issuer);
6645
- if (!issuer) {
6646
- if (current.issuer !== anchor.subject) {
6647
- throw new exports.CertificateChainError(`no issuer certificate found for ${current.subject}`, {
6648
- reason: "signature"
6649
- });
6650
- }
6651
- this.checkPerCert(anchor);
6652
- this.checkCaAndPathLen(anchor, nonLeafDepth);
6653
- this.checkAkiSkiMatch(current, anchor);
6654
- await this.verifySignature(current, anchor);
6655
- chain.push(anchor);
6656
- this.checkNameConstraints(leaf, chain.slice(1));
6657
- return chain;
6673
+ /** Search for a path from `leaf` that closes at `anchor`. */
6674
+ async tryBuild(leaf, anchor, intermediates, context, search) {
6675
+ this.checkPerCert(leaf, context);
6676
+ if (certificatesEqual(leaf, anchor)) {
6677
+ this.checkLeafKeyUsage(leaf, "generic");
6678
+ return [leaf];
6679
+ }
6680
+ const profile = this.opts.profile ?? "generic";
6681
+ this.checkLeafKeyUsage(leaf, profile);
6682
+ this.checkLeafExtendedKeyUsage(leaf, profile);
6683
+ const pool = intermediates.filter(
6684
+ (c) => !certificatesEqual(c, anchor) && !certificatesEqual(c, leaf)
6685
+ );
6686
+ return this.extendPath([leaf], anchor, pool, context, search);
6687
+ }
6688
+ /**
6689
+ * Try to complete `chain` (leaf first) to `anchor`. Returns the completed
6690
+ * chain, or `undefined` when no extension of this prefix works — failures
6691
+ * are recorded on `search` so the caller can report the most specific one.
6692
+ * Throws only to abandon the whole search (signature budget exhausted).
6693
+ */
6694
+ async extendPath(chain, anchor, pool, context, search) {
6695
+ const leaf = chain[0];
6696
+ const current = chain[chain.length - 1];
6697
+ const maxLength = this.opts.maxChainLength ?? DEFAULT_MAX_CHAIN_LENGTH;
6698
+ if (chain.length + 1 > maxLength) {
6699
+ search.specific = new exports.CertificateChainError(
6700
+ `chain from ${leaf.subject} exceeds the maximum length of ${maxLength} certificates`,
6701
+ { reason: "path_length" }
6702
+ );
6703
+ return void 0;
6704
+ }
6705
+ const nonLeafDepth = chain.length - 1;
6706
+ if (current.issuer === anchor.subject) {
6707
+ try {
6708
+ await this.closeAtAnchor(current, anchor, nonLeafDepth, context, search);
6709
+ const closed = [...chain, anchor];
6710
+ this.checkNameConstraints(leaf, closed.slice(1));
6711
+ return closed;
6712
+ } catch (err) {
6713
+ if (searchAborts.has(err)) throw err;
6714
+ search.specific = err;
6658
6715
  }
6659
- this.checkPerCert(issuer);
6660
- this.checkCaAndPathLen(issuer, nonLeafDepth);
6661
- this.checkAkiSkiMatch(current, issuer);
6662
- await this.verifySignature(current, issuer);
6663
- chain.push(issuer);
6664
- current = issuer;
6665
- nonLeafDepth += 1;
6666
- }
6667
- if (!certificatesEqual(current, anchor)) {
6668
- throw new exports.CertificateChainError(
6669
- `certificate ${current.subject} shares the trust anchor's Subject DN but is not the anchor (no signature closure)`,
6670
- { reason: "signature" }
6716
+ }
6717
+ const candidates = pool.filter(
6718
+ (c) => c.subject === current.issuer && !chain.some((x) => certificatesEqual(x, c))
6719
+ );
6720
+ let verified = 0;
6721
+ for (const candidate of candidates) {
6722
+ if (!await this.signatureVerifies(current, candidate, search)) continue;
6723
+ verified++;
6724
+ try {
6725
+ this.checkPerCert(candidate, context);
6726
+ this.checkCaAndPathLen(candidate, nonLeafDepth);
6727
+ this.checkAkiSkiMatch(current, candidate);
6728
+ } catch (err) {
6729
+ search.specific = err;
6730
+ continue;
6731
+ }
6732
+ const found = await this.extendPath([...chain, candidate], anchor, pool, context, search);
6733
+ if (found) return found;
6734
+ }
6735
+ search.fallback = new exports.CertificateChainError(
6736
+ candidates.length > 0 && verified === 0 ? `signature verification failed for ${current.subject}` : `no issuer certificate found for ${current.subject}`,
6737
+ { reason: "signature" }
6738
+ );
6739
+ return void 0;
6740
+ }
6741
+ async closeAtAnchor(current, anchor, nonLeafDepth, context, search) {
6742
+ this.checkPerCert(anchor, context);
6743
+ this.checkCaAndPathLen(anchor, nonLeafDepth);
6744
+ this.checkAkiSkiMatch(current, anchor);
6745
+ this.spendSignatureCheck(search);
6746
+ await this.verifySignature(current, anchor);
6747
+ }
6748
+ /** Charge one signature verification to the search, or abandon it. */
6749
+ spendSignatureCheck(search) {
6750
+ if (search.remaining <= 0) {
6751
+ const limit = this.opts.maxSignatureChecks ?? DEFAULT_MAX_SIGNATURE_CHECKS;
6752
+ const err = new exports.CertificateChainError(
6753
+ `certificate path search exceeded ${limit} signature checks`,
6754
+ { reason: "path_length" }
6671
6755
  );
6756
+ searchAborts.add(err);
6757
+ throw err;
6672
6758
  }
6673
- this.checkNameConstraints(leaf, chain.slice(1));
6674
- return chain;
6759
+ search.remaining--;
6675
6760
  }
6676
6761
  /**
6677
6762
  * Enforce nameConstraints asserted by any CA above the leaf against the
@@ -6720,7 +6805,7 @@ var init_ChainBuilder = __esm({
6720
6805
  }
6721
6806
  }
6722
6807
  checkCaAndPathLen(cert, nonLeafDepth) {
6723
- this.checkCaKeyUsage(cert);
6808
+ this.checkCaKeyUsage(cert, this.opts.profile ?? "generic");
6724
6809
  const bc = cert.getExtension(x509.BasicConstraintsExtension);
6725
6810
  if (!bc || !bc.ca) {
6726
6811
  throw new exports.CertificateChainError(
@@ -6735,9 +6820,17 @@ var init_ChainBuilder = __esm({
6735
6820
  );
6736
6821
  }
6737
6822
  }
6738
- checkLeafKeyUsage(leaf) {
6823
+ checkLeafKeyUsage(leaf, profile) {
6739
6824
  const ext = leaf.getExtension(x509.KeyUsagesExtension);
6740
- if (!ext) return;
6825
+ if (!ext) {
6826
+ if (profile === "iso18013-5") {
6827
+ throw new exports.CertificateChainError(
6828
+ `document signer ${leaf.subject} has no keyUsage extension (ISO 18013-5 requires digitalSignature)`,
6829
+ { reason: "key_usage" }
6830
+ );
6831
+ }
6832
+ return;
6833
+ }
6741
6834
  if ((ext.usages & x509.KeyUsageFlags.digitalSignature) === 0) {
6742
6835
  throw new exports.CertificateChainError(
6743
6836
  `leaf ${leaf.subject} does not assert digitalSignature key usage`,
@@ -6745,9 +6838,28 @@ var init_ChainBuilder = __esm({
6745
6838
  );
6746
6839
  }
6747
6840
  }
6748
- checkCaKeyUsage(cert) {
6841
+ checkLeafExtendedKeyUsage(leaf, profile) {
6842
+ if (profile !== "iso18013-5") return;
6843
+ const ext = leaf.getExtension(x509.ExtendedKeyUsageExtension);
6844
+ const usages = (ext?.usages ?? []).map(String);
6845
+ if (!usages.includes(ISO_18013_5_DS_EKU)) {
6846
+ throw new exports.CertificateChainError(
6847
+ `document signer ${leaf.subject} lacks extended key usage ${ISO_18013_5_DS_EKU} (id-mdl-kp-mdlDS)`,
6848
+ { reason: "extended_key_usage" }
6849
+ );
6850
+ }
6851
+ }
6852
+ checkCaKeyUsage(cert, profile) {
6749
6853
  const ext = cert.getExtension(x509.KeyUsagesExtension);
6750
- if (!ext) return;
6854
+ if (!ext) {
6855
+ if (profile === "iso18013-5") {
6856
+ throw new exports.CertificateChainError(
6857
+ `CA ${cert.subject} has no keyUsage extension (ISO 18013-5 requires keyCertSign)`,
6858
+ { reason: "key_usage" }
6859
+ );
6860
+ }
6861
+ return;
6862
+ }
6751
6863
  if ((ext.usages & x509.KeyUsageFlags.keyCertSign) === 0) {
6752
6864
  throw new exports.CertificateChainError(
6753
6865
  `CA ${cert.subject} does not assert keyCertSign key usage`,
@@ -6768,30 +6880,46 @@ var init_ChainBuilder = __esm({
6768
6880
  );
6769
6881
  }
6770
6882
  }
6883
+ /**
6884
+ * Signature check used to select among DN-matching candidates. Never throws
6885
+ * on a bad signature; throws only when the search budget is exhausted.
6886
+ */
6887
+ async signatureVerifies(child, issuer, search) {
6888
+ this.spendSignatureCheck(search);
6889
+ try {
6890
+ return await child.verify({ publicKey: issuer.publicKey, signatureOnly: true });
6891
+ } catch {
6892
+ return false;
6893
+ }
6894
+ }
6771
6895
  async verifySignature(child, issuer) {
6772
6896
  const ok = await child.verify({ publicKey: issuer.publicKey, signatureOnly: true });
6773
6897
  if (!ok) {
6774
6898
  throw new exports.CertificateChainError(`signature verification failed for ${child.subject}`, { reason: "signature" });
6775
6899
  }
6776
6900
  }
6777
- checkPerCert(cert) {
6778
- this.checkValidity(cert);
6901
+ checkPerCert(cert, context = {}) {
6902
+ const now = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))();
6903
+ this.checkValidity(cert, now, "");
6904
+ if (context.issuedAt) {
6905
+ this.checkValidity(cert, context.issuedAt, ` at issuance time ${context.issuedAt.toISOString()}`);
6906
+ }
6779
6907
  this.checkAlgorithm(cert);
6780
6908
  }
6781
- checkValidity(cert) {
6782
- const now = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
6909
+ checkValidity(cert, at, label) {
6910
+ const time = at.getTime();
6783
6911
  const skewMs = (this.opts.clockSkewTolerance ?? 60) * 1e3;
6784
6912
  const notBefore = cert.notBefore.getTime();
6785
6913
  const notAfter = cert.notAfter.getTime();
6786
- if (now + skewMs < notBefore) {
6914
+ if (time + skewMs < notBefore) {
6787
6915
  throw new exports.CertificateChainError(
6788
- `certificate ${cert.subject} not yet valid (notBefore=${cert.notBefore.toISOString()})`,
6916
+ `certificate ${cert.subject} not yet valid${label} (notBefore=${cert.notBefore.toISOString()})`,
6789
6917
  { reason: "validity" }
6790
6918
  );
6791
6919
  }
6792
- if (now - skewMs > notAfter) {
6920
+ if (time - skewMs > notAfter) {
6793
6921
  throw new exports.CertificateChainError(
6794
- `certificate ${cert.subject} expired (notAfter=${cert.notAfter.toISOString()})`,
6922
+ `certificate ${cert.subject} expired${label} (notAfter=${cert.notAfter.toISOString()})`,
6795
6923
  { reason: "validity" }
6796
6924
  );
6797
6925
  }
@@ -6838,6 +6966,349 @@ var init_ecdsa_util = __esm({
6838
6966
  }
6839
6967
  });
6840
6968
 
6969
+ // src/http/ip-address.ts
6970
+ function parseIPv4(input) {
6971
+ const parts = input.split(".");
6972
+ if (parts.length !== 4) return void 0;
6973
+ const bytes = [];
6974
+ for (const part of parts) {
6975
+ if (!/^\d{1,3}$/.test(part)) return void 0;
6976
+ const value = Number(part);
6977
+ if (value > 255) return void 0;
6978
+ bytes.push(value);
6979
+ }
6980
+ return bytes;
6981
+ }
6982
+ function parseIPv6(input) {
6983
+ const address = input.split("%")[0];
6984
+ if (!address.includes(":")) return void 0;
6985
+ let head = address;
6986
+ let embeddedV4;
6987
+ const lastColon = address.lastIndexOf(":");
6988
+ const tail = address.slice(lastColon + 1);
6989
+ if (tail.includes(".")) {
6990
+ embeddedV4 = parseIPv4(tail);
6991
+ if (!embeddedV4) return void 0;
6992
+ head = address.slice(0, lastColon + 1) + "0:0";
6993
+ }
6994
+ const doubleColon = head.split("::");
6995
+ if (doubleColon.length > 2) return void 0;
6996
+ const toGroups = (s) => s === "" ? [] : s.split(":");
6997
+ const left = toGroups(doubleColon[0]);
6998
+ const right = doubleColon.length === 2 ? toGroups(doubleColon[1]) : [];
6999
+ const missing = 8 - left.length - right.length;
7000
+ if (doubleColon.length === 2 ? missing < 1 : missing !== 0) return void 0;
7001
+ const groups = [...left, ...Array(doubleColon.length === 2 ? missing : 0).fill("0"), ...right];
7002
+ const bytes = [];
7003
+ for (const group of groups) {
7004
+ if (!/^[0-9a-fA-F]{1,4}$/.test(group)) return void 0;
7005
+ const value = parseInt(group, 16);
7006
+ bytes.push(value >> 8, value & 255);
7007
+ }
7008
+ if (embeddedV4) bytes.splice(12, 4, ...embeddedV4);
7009
+ return bytes;
7010
+ }
7011
+ function inPrefix(bytes, prefix, bits) {
7012
+ for (let i = 0; i < bits; i++) {
7013
+ const byte = i >> 3;
7014
+ const mask = 128 >> (i & 7);
7015
+ if ((bytes[byte] & mask) !== ((prefix[byte] ?? 0) & mask)) return false;
7016
+ }
7017
+ return true;
7018
+ }
7019
+ function isPublicV4(bytes) {
7020
+ return !BLOCKED_V4.some(([prefix, bits]) => inPrefix(bytes, prefix, bits));
7021
+ }
7022
+ function isPublicV6(bytes) {
7023
+ const first10Zero = bytes.slice(0, 10).every((b) => b === 0);
7024
+ if (first10Zero && bytes[10] === 0 && bytes[11] === 0) {
7025
+ const v4 = bytes.slice(12);
7026
+ if (v4.slice(0, 3).every((b) => b === 0)) return false;
7027
+ return isPublicV4(v4);
7028
+ }
7029
+ if (first10Zero && bytes[10] === 255 && bytes[11] === 255) return isPublicV4(bytes.slice(12));
7030
+ if (inPrefix(bytes, [0, 100, 255, 155], 96)) return isPublicV4(bytes.slice(12));
7031
+ if (inPrefix(bytes, [32, 2], 16)) return isPublicV4(bytes.slice(2, 6));
7032
+ return !BLOCKED_V6.some(([prefix, bits]) => inPrefix(bytes, prefix, bits));
7033
+ }
7034
+ function isPublicIpAddress(ip) {
7035
+ const v4 = parseIPv4(ip);
7036
+ if (v4) return isPublicV4(v4);
7037
+ const v6 = parseIPv6(ip);
7038
+ if (v6) return isPublicV6(v6);
7039
+ return false;
7040
+ }
7041
+ function isIpLiteral(host) {
7042
+ return parseIPv4(host) !== void 0 || parseIPv6(host) !== void 0;
7043
+ }
7044
+ var BLOCKED_V4, BLOCKED_V6;
7045
+ var init_ip_address = __esm({
7046
+ "src/http/ip-address.ts"() {
7047
+ init_reflect_polyfill();
7048
+ BLOCKED_V4 = [
7049
+ [[0, 0, 0, 0], 8],
7050
+ [[10, 0, 0, 0], 8],
7051
+ [[100, 64, 0, 0], 10],
7052
+ [[127, 0, 0, 0], 8],
7053
+ [[169, 254, 0, 0], 16],
7054
+ [[172, 16, 0, 0], 12],
7055
+ [[192, 0, 0, 0], 24],
7056
+ [[192, 0, 2, 0], 24],
7057
+ [[192, 88, 99, 0], 24],
7058
+ [[192, 168, 0, 0], 16],
7059
+ [[198, 18, 0, 0], 15],
7060
+ [[198, 51, 100, 0], 24],
7061
+ [[203, 0, 113, 0], 24],
7062
+ [[224, 0, 0, 0], 4],
7063
+ [[240, 0, 0, 0], 4]
7064
+ ];
7065
+ BLOCKED_V6 = [
7066
+ [[1, 0], 64],
7067
+ // 100::/64 discard-only
7068
+ [[32, 1, 0, 0], 32],
7069
+ // 2001::/32 Teredo (embeds an obfuscated IPv4)
7070
+ [[32, 1, 13, 184], 32],
7071
+ // 2001:db8::/32 documentation
7072
+ [[252], 7],
7073
+ // fc00::/7 unique local (incl. fd00:ec2::254 metadata)
7074
+ [[254, 128], 10],
7075
+ // fe80::/10 link-local
7076
+ [[254, 192], 10],
7077
+ // fec0::/10 deprecated site-local
7078
+ [[255], 8]
7079
+ // ff00::/8 multicast
7080
+ ];
7081
+ }
7082
+ });
7083
+
7084
+ // src/http/guarded-fetch.ts
7085
+ function loadDns() {
7086
+ dnsModule ??= (async () => {
7087
+ try {
7088
+ const specifier = "node:dns";
7089
+ const mod = await import(
7090
+ /* webpackIgnore: true */
7091
+ /* @vite-ignore */
7092
+ specifier
7093
+ );
7094
+ return mod.promises ?? mod.default?.promises;
7095
+ } catch {
7096
+ return void 0;
7097
+ }
7098
+ })();
7099
+ return dnsModule;
7100
+ }
7101
+ function toError(err) {
7102
+ return err instanceof Error ? err : new Error(String(err));
7103
+ }
7104
+ function createGuardedFetcher(options = {}) {
7105
+ const allowHttp = options.allowHttp ?? false;
7106
+ const allowPrivate = options.allowPrivateNetworks ?? false;
7107
+ const maxRedirects = options.maxRedirects ?? exports.GUARDED_FETCH_DEFAULTS.maxRedirects;
7108
+ const timeoutMs = options.timeoutMs ?? exports.GUARDED_FETCH_DEFAULTS.timeoutMs;
7109
+ const maxBytes = options.maxResponseBytes ?? exports.GUARDED_FETCH_DEFAULTS.maxResponseBytes;
7110
+ const lookup = options.lookup ?? defaultLookup;
7111
+ const transport = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
7112
+ return async (input, init = {}) => {
7113
+ const controller = new AbortController();
7114
+ const timer = setTimeout(() => {
7115
+ controller.abort(
7116
+ new exports.GuardedFetchError("timeout", `request to ${input} exceeded ${timeoutMs} ms`, { url: input })
7117
+ );
7118
+ }, timeoutMs);
7119
+ const callerSignal = init.signal ?? void 0;
7120
+ const onCallerAbort = () => controller.abort(callerSignal?.reason);
7121
+ if (callerSignal?.aborted) onCallerAbort();
7122
+ else callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
7123
+ const aborted = new Promise((_, reject) => {
7124
+ const fail = () => reject(toError(controller.signal.reason));
7125
+ if (controller.signal.aborted) fail();
7126
+ else controller.signal.addEventListener("abort", fail, { once: true });
7127
+ });
7128
+ aborted.catch(() => {
7129
+ });
7130
+ const race = (p) => Promise.race([p, aborted]);
7131
+ try {
7132
+ let url = await race(validateTarget(input, void 0));
7133
+ let method = (init.method ?? "GET").toUpperCase();
7134
+ let body = init.body;
7135
+ const headers = new Headers(init.headers);
7136
+ for (let hop = 0; ; hop++) {
7137
+ const response = await race(
7138
+ transport(url.href, { ...init, method, body, headers, redirect: "manual", signal: controller.signal })
7139
+ );
7140
+ if (response.type === "opaqueredirect") {
7141
+ throw new exports.GuardedFetchError(
7142
+ "invalid_redirect",
7143
+ `redirect from ${url.href} cannot be inspected by this transport`,
7144
+ { url: url.href }
7145
+ );
7146
+ }
7147
+ if (!REDIRECT_STATUSES.has(response.status)) {
7148
+ const bytes = await readBounded(response, url.href, maxBytes, race);
7149
+ return new Response(NULL_BODY_STATUSES.has(response.status) ? null : bytes, {
7150
+ status: response.status,
7151
+ statusText: response.statusText,
7152
+ headers: response.headers
7153
+ });
7154
+ }
7155
+ await response.body?.cancel().catch(() => {
7156
+ });
7157
+ if (hop >= maxRedirects) {
7158
+ throw new exports.GuardedFetchError(
7159
+ "too_many_redirects",
7160
+ `more than ${maxRedirects} redirects starting at ${input}`,
7161
+ { url: url.href }
7162
+ );
7163
+ }
7164
+ const location = response.headers.get("location");
7165
+ if (!location) {
7166
+ throw new exports.GuardedFetchError(
7167
+ "invalid_redirect",
7168
+ `HTTP ${response.status} from ${url.href} has no Location header`,
7169
+ { url: url.href }
7170
+ );
7171
+ }
7172
+ let next;
7173
+ try {
7174
+ next = new URL(location, url);
7175
+ } catch (err) {
7176
+ throw new exports.GuardedFetchError("invalid_redirect", `unparseable Location from ${url.href}`, {
7177
+ url: url.href,
7178
+ cause: toError(err)
7179
+ });
7180
+ }
7181
+ const nextUrl = await race(validateTarget(next.href, url));
7182
+ if (response.status === 303 || (response.status === 301 || response.status === 302) && method === "POST") {
7183
+ if (method !== "HEAD") method = "GET";
7184
+ body = void 0;
7185
+ for (const h of BODY_HEADERS) headers.delete(h);
7186
+ }
7187
+ if (nextUrl.origin !== url.origin) {
7188
+ for (const h of CROSS_ORIGIN_STRIPPED_HEADERS) headers.delete(h);
7189
+ }
7190
+ url = nextUrl;
7191
+ }
7192
+ } catch (err) {
7193
+ if (controller.signal.aborted) throw toError(controller.signal.reason);
7194
+ throw err;
7195
+ } finally {
7196
+ clearTimeout(timer);
7197
+ callerSignal?.removeEventListener("abort", onCallerAbort);
7198
+ }
7199
+ };
7200
+ async function validateTarget(raw, previous) {
7201
+ let url;
7202
+ try {
7203
+ url = new URL(raw);
7204
+ } catch (err) {
7205
+ throw new exports.GuardedFetchError("invalid_url", `not an absolute URL: ${raw}`, { url: raw, cause: toError(err) });
7206
+ }
7207
+ if (url.protocol !== "https:" && !(allowHttp && url.protocol === "http:")) {
7208
+ throw new exports.GuardedFetchError("insecure_scheme", `refusing ${url.protocol} URL ${url.href}: HTTPS is required`, {
7209
+ url: url.href
7210
+ });
7211
+ }
7212
+ if (previous?.protocol === "https:" && url.protocol !== "https:") {
7213
+ throw new exports.GuardedFetchError("insecure_scheme", `refusing https -> http redirect to ${url.href}`, {
7214
+ url: url.href
7215
+ });
7216
+ }
7217
+ if (allowPrivate) return url;
7218
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
7219
+ if (host === "localhost" || host.endsWith(".localhost")) {
7220
+ throw new exports.GuardedFetchError("private_address", `refusing loopback host ${host}`, { url: url.href });
7221
+ }
7222
+ if (isIpLiteral(host)) {
7223
+ if (!isPublicIpAddress(host)) {
7224
+ throw new exports.GuardedFetchError("private_address", `refusing non-public address ${host}`, { url: url.href });
7225
+ }
7226
+ return url;
7227
+ }
7228
+ let addresses;
7229
+ try {
7230
+ addresses = await lookup(host);
7231
+ } catch (err) {
7232
+ throw new exports.GuardedFetchError("dns_resolution_failed", `could not resolve ${host}`, {
7233
+ url: url.href,
7234
+ cause: toError(err)
7235
+ });
7236
+ }
7237
+ if (addresses === void 0) return url;
7238
+ if (addresses.length === 0) {
7239
+ throw new exports.GuardedFetchError("dns_resolution_failed", `${host} resolved to no addresses`, {
7240
+ url: url.href
7241
+ });
7242
+ }
7243
+ const blocked = addresses.find((a) => !isPublicIpAddress(a));
7244
+ if (blocked !== void 0) {
7245
+ throw new exports.GuardedFetchError("private_address", `${host} resolves to non-public address ${blocked}`, {
7246
+ url: url.href
7247
+ });
7248
+ }
7249
+ return url;
7250
+ }
7251
+ }
7252
+ async function readBounded(response, url, maxBytes, race) {
7253
+ const tooLarge = () => new exports.GuardedFetchError("response_too_large", `response from ${url} exceeds ${maxBytes} bytes`, { url });
7254
+ const declared = response.headers.get("content-length");
7255
+ if (declared !== null && Number(declared) > maxBytes) {
7256
+ await response.body?.cancel().catch(() => {
7257
+ });
7258
+ throw tooLarge();
7259
+ }
7260
+ if (!response.body) return null;
7261
+ const reader = response.body.getReader();
7262
+ const chunks = [];
7263
+ let total = 0;
7264
+ try {
7265
+ for (; ; ) {
7266
+ const { done, value } = await race(reader.read());
7267
+ if (done) break;
7268
+ total += value.byteLength;
7269
+ if (total > maxBytes) throw tooLarge();
7270
+ chunks.push(value);
7271
+ }
7272
+ } catch (err) {
7273
+ reader.cancel().catch(() => {
7274
+ });
7275
+ throw err;
7276
+ }
7277
+ const out = new Uint8Array(new ArrayBuffer(total));
7278
+ let offset = 0;
7279
+ for (const chunk of chunks) {
7280
+ out.set(chunk, offset);
7281
+ offset += chunk.byteLength;
7282
+ }
7283
+ return out;
7284
+ }
7285
+ function createDefaultTrustFetcher() {
7286
+ return createGuardedFetcher({ allowHttp: true });
7287
+ }
7288
+ exports.GUARDED_FETCH_DEFAULTS = void 0; var REDIRECT_STATUSES, NULL_BODY_STATUSES, CROSS_ORIGIN_STRIPPED_HEADERS, BODY_HEADERS, dnsModule, defaultLookup;
7289
+ var init_guarded_fetch = __esm({
7290
+ "src/http/guarded-fetch.ts"() {
7291
+ init_reflect_polyfill();
7292
+ init_errors();
7293
+ init_ip_address();
7294
+ exports.GUARDED_FETCH_DEFAULTS = {
7295
+ maxRedirects: 3,
7296
+ timeoutMs: 15e3,
7297
+ maxResponseBytes: 16 * 1024 * 1024
7298
+ };
7299
+ REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
7300
+ NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
7301
+ CROSS_ORIGIN_STRIPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
7302
+ BODY_HEADERS = ["content-type", "content-length", "content-encoding", "content-language", "content-location"];
7303
+ defaultLookup = async (hostname) => {
7304
+ const dns = await loadDns();
7305
+ if (!dns) return void 0;
7306
+ const records = await dns.lookup(hostname, { all: true, verbatim: true });
7307
+ return records.map((r) => r.address);
7308
+ };
7309
+ }
7310
+ });
7311
+
6841
7312
  // src/trust/CrlFetcher.ts
6842
7313
  function normalizeCrl(parsed, der) {
6843
7314
  const thisUpdate = parsed.tbsCertList.thisUpdate.getTime();
@@ -6873,11 +7344,12 @@ var init_CrlFetcher = __esm({
6873
7344
  init_es2015();
6874
7345
  init_es20152();
6875
7346
  init_ecdsa_util();
7347
+ init_guarded_fetch();
6876
7348
  CrlFetcher = class {
6877
7349
  fetcher;
6878
7350
  cache;
6879
7351
  constructor(opts = {}) {
6880
- this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
7352
+ this.fetcher = opts.fetcher ?? createDefaultTrustFetcher();
6881
7353
  this.cache = opts.cache;
6882
7354
  }
6883
7355
  async fetchAndParse(url) {
@@ -7609,12 +8081,13 @@ var init_OcspClient = __esm({
7609
8081
  init_es20152();
7610
8082
  init_ChainBuilder();
7611
8083
  init_ecdsa_util();
8084
+ init_guarded_fetch();
7612
8085
  DEFAULT_MAX_RESPONSE_AGE_MS = 7 * 24 * 3600 * 1e3;
7613
8086
  OcspClient = class {
7614
8087
  fetcher;
7615
8088
  cache;
7616
8089
  constructor(opts = {}) {
7617
- this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
8090
+ this.fetcher = opts.fetcher ?? createDefaultTrustFetcher();
7618
8091
  this.cache = opts.cache;
7619
8092
  }
7620
8093
  /**
@@ -8080,7 +8553,9 @@ var init_ProvenanceResolver = __esm({
8080
8553
  // src/trust/TrustEvaluator.ts
8081
8554
  var TrustEvaluator_exports = {};
8082
8555
  __export(TrustEvaluator_exports, {
8083
- TrustEvaluator: () => TrustEvaluator
8556
+ MAX_CREDENTIAL_CERTIFICATES: () => MAX_CREDENTIAL_CERTIFICATES,
8557
+ TrustEvaluator: () => TrustEvaluator,
8558
+ evaluateCredentialIssuer: () => evaluateCredentialIssuer
8084
8559
  });
8085
8560
  function deriveHint(leaf) {
8086
8561
  const aki = leaf.getExtension(x509.AuthorityKeyIdentifierExtension);
@@ -8091,6 +8566,13 @@ function deriveHint(leaf) {
8091
8566
  aki: akiBytes
8092
8567
  };
8093
8568
  }
8569
+ function deriveSelfHint(cert) {
8570
+ const skiHex = getSkiHex(cert);
8571
+ return {
8572
+ issuer: cert.subject,
8573
+ aki: skiHex ? hexToBytes(skiHex) : void 0
8574
+ };
8575
+ }
8094
8576
  function hexToBytes(s) {
8095
8577
  const clean = s.replace(/[^0-9a-f]/gi, "");
8096
8578
  const out = new Uint8Array(clean.length / 2);
@@ -8122,7 +8604,31 @@ async function resolveProvenance(store, anchor) {
8122
8604
  return void 0;
8123
8605
  }
8124
8606
  }
8125
- var TrustEvaluator;
8607
+ async function evaluateCredentialIssuer(input) {
8608
+ const [signerDer, ...rest] = input.chain;
8609
+ if (!signerDer) {
8610
+ throw new exports.MalformedCredentialError("Missing issuer certificate");
8611
+ }
8612
+ if (input.chain.length > MAX_CREDENTIAL_CERTIFICATES) {
8613
+ throw new exports.MalformedCredentialError(
8614
+ `x5c/x5chain carries ${input.chain.length} certificates; at most ${MAX_CREDENTIAL_CERTIFICATES} are accepted`
8615
+ );
8616
+ }
8617
+ const parse = (bytes, position) => {
8618
+ try {
8619
+ return new x509.X509Certificate(bytes);
8620
+ } catch (err) {
8621
+ throw new exports.MalformedCredentialError(
8622
+ `Invalid certificate at x5c/x5chain position ${position}: ${err.message}`
8623
+ );
8624
+ }
8625
+ };
8626
+ const leaf = parse(signerDer, 0);
8627
+ const intermediates = rest.map((bytes, i) => parse(bytes, i + 1));
8628
+ const { chain, issuedAt, ...evaluatorOptions } = input;
8629
+ return new TrustEvaluator(evaluatorOptions).evaluate(leaf, { intermediates, issuedAt });
8630
+ }
8631
+ var TrustEvaluator, MAX_CREDENTIAL_CERTIFICATES;
8126
8632
  var init_TrustEvaluator = __esm({
8127
8633
  "src/trust/TrustEvaluator.ts"() {
8128
8634
  init_reflect_polyfill();
@@ -8148,13 +8654,10 @@ var init_TrustEvaluator = __esm({
8148
8654
  revocationChecker;
8149
8655
  async evaluate(leaf, context = {}) {
8150
8656
  const hint = deriveHint(leaf);
8151
- const seen = /* @__PURE__ */ new Set();
8152
8657
  const anchors = [];
8153
8658
  const pushUnique = (batch) => {
8154
8659
  for (const a of batch) {
8155
- const key = a.certificate.serialNumber;
8156
- if (seen.has(key)) continue;
8157
- seen.add(key);
8660
+ if (anchors.some((x) => certificatesEqual(x.certificate, a.certificate))) continue;
8158
8661
  anchors.push(a);
8159
8662
  }
8160
8663
  };
@@ -8162,6 +8665,7 @@ var init_TrustEvaluator = __esm({
8162
8665
  for (const inter of context.intermediates ?? []) {
8163
8666
  pushUnique(await this.trustStore.getAnchors(deriveHint(inter)));
8164
8667
  }
8668
+ pushUnique(await this.trustStore.getAnchors(deriveSelfHint(leaf)));
8165
8669
  if (anchors.length === 0) {
8166
8670
  throw new exports.TrustAnchorNotFoundError(
8167
8671
  `no trust anchor for issuer=${hint.issuer ?? "(none)"}`
@@ -8171,7 +8675,8 @@ var init_TrustEvaluator = __esm({
8171
8675
  const chain = await this.chainBuilder.build(
8172
8676
  leaf,
8173
8677
  anchorCerts,
8174
- context.intermediates ?? []
8678
+ context.intermediates ?? [],
8679
+ { issuedAt: context.issuedAt }
8175
8680
  );
8176
8681
  const terminusCert = chain[chain.length - 1];
8177
8682
  const anchor = anchors.find(
@@ -8211,6 +8716,7 @@ var init_TrustEvaluator = __esm({
8211
8716
  };
8212
8717
  }
8213
8718
  };
8719
+ MAX_CREDENTIAL_CERTIFICATES = 10;
8214
8720
  }
8215
8721
  });
8216
8722
 
@@ -8443,6 +8949,33 @@ function buildHaipQuery(input) {
8443
8949
  }
8444
8950
  return { credentials: [credential] };
8445
8951
  }
8952
+ function buildCredentialSetQuery(input) {
8953
+ if (input.options.length === 0) {
8954
+ throw new exports.HaipValidationError(
8955
+ "EMPTY_OPTIONS",
8956
+ "a credential set must offer at least one option"
8957
+ );
8958
+ }
8959
+ const seen = /* @__PURE__ */ new Set();
8960
+ for (const option of input.options) {
8961
+ if (seen.has(option.credentialId)) {
8962
+ throw new exports.HaipValidationError(
8963
+ "DUPLICATE_CREDENTIAL_ID",
8964
+ `credential '${option.credentialId}' is declared by more than one option; credential ids must be unique for credential_sets to reference them`,
8965
+ option.credentialId
8966
+ );
8967
+ }
8968
+ seen.add(option.credentialId);
8969
+ }
8970
+ const credentials = input.options.map((option) => buildHaipQuery(option).credentials[0]);
8971
+ const credentialSet = {
8972
+ options: input.options.map((option) => [option.credentialId])
8973
+ };
8974
+ if (input.required === false) {
8975
+ credentialSet.required = false;
8976
+ }
8977
+ return { credentials, credential_sets: [credentialSet] };
8978
+ }
8446
8979
  var HAIP_FORMATS = /* @__PURE__ */ new Set(["dc+sd-jwt", "mso_mdoc"]);
8447
8980
  function validateHaipQuery(query) {
8448
8981
  if (!query.credentials || query.credentials.length === 0) {
@@ -8458,46 +8991,86 @@ function validateHaipQuery(query) {
8458
8991
  );
8459
8992
  }
8460
8993
  for (const credential of query.credentials) {
8461
- if (!HAIP_FORMATS.has(credential.format)) {
8994
+ validateCredential(credential);
8995
+ }
8996
+ }
8997
+ function validateCredential(credential) {
8998
+ if (!HAIP_FORMATS.has(credential.format)) {
8999
+ throw new exports.HaipValidationError(
9000
+ "UNSUPPORTED_FORMAT",
9001
+ `credential '${credential.id}' uses unsupported format '${credential.format}' (allowed: dc+sd-jwt, mso_mdoc)`,
9002
+ credential.id
9003
+ );
9004
+ }
9005
+ if (credential.format === "dc+sd-jwt") {
9006
+ const vctValues = credential.meta?.vct_values;
9007
+ if (!vctValues || vctValues.length === 0) {
8462
9008
  throw new exports.HaipValidationError(
8463
- "UNSUPPORTED_FORMAT",
8464
- `credential '${credential.id}' uses unsupported format '${credential.format}' (allowed: dc+sd-jwt, mso_mdoc)`,
9009
+ "MISSING_SDJWT_META",
9010
+ `credential '${credential.id}' with format 'dc+sd-jwt' must set meta.vct_values`,
8465
9011
  credential.id
8466
9012
  );
8467
9013
  }
8468
- if (credential.format === "dc+sd-jwt") {
8469
- const vctValues = credential.meta?.vct_values;
8470
- if (!vctValues || vctValues.length === 0) {
8471
- throw new exports.HaipValidationError(
8472
- "MISSING_SDJWT_META",
8473
- `credential '${credential.id}' with format 'dc+sd-jwt' must set meta.vct_values`,
8474
- credential.id
8475
- );
8476
- }
8477
- }
8478
- if (credential.format === "mso_mdoc") {
8479
- if (typeof credential.meta?.doctype_value !== "string") {
8480
- throw new exports.HaipValidationError(
8481
- "MISSING_MDOC_META",
8482
- `credential '${credential.id}' with format 'mso_mdoc' must set meta.doctype_value`,
8483
- credential.id
8484
- );
8485
- }
8486
- }
8487
- if (!credential.claims || credential.claims.length === 0) {
9014
+ }
9015
+ if (credential.format === "mso_mdoc") {
9016
+ if (typeof credential.meta?.doctype_value !== "string") {
8488
9017
  throw new exports.HaipValidationError(
8489
- "NO_CLAIMS",
8490
- `credential '${credential.id}' must declare at least one claim`,
9018
+ "MISSING_MDOC_META",
9019
+ `credential '${credential.id}' with format 'mso_mdoc' must set meta.doctype_value`,
8491
9020
  credential.id
8492
9021
  );
8493
9022
  }
8494
- if (credential.claim_sets !== void 0) {
9023
+ }
9024
+ if (!credential.claims || credential.claims.length === 0) {
9025
+ throw new exports.HaipValidationError(
9026
+ "NO_CLAIMS",
9027
+ `credential '${credential.id}' must declare at least one claim`,
9028
+ credential.id
9029
+ );
9030
+ }
9031
+ if (credential.claim_sets !== void 0) {
9032
+ throw new exports.HaipValidationError(
9033
+ "CLAIM_SETS_DISALLOWED",
9034
+ `credential '${credential.id}' uses claim_sets, not permitted in HAIP-minimal`,
9035
+ credential.id
9036
+ );
9037
+ }
9038
+ }
9039
+ function validateCredentialSetQuery(query) {
9040
+ if (!query.credentials || query.credentials.length === 0) {
9041
+ throw new exports.HaipValidationError(
9042
+ "EMPTY_QUERY",
9043
+ "query.credentials must have at least one entry"
9044
+ );
9045
+ }
9046
+ if (!query.credential_sets || query.credential_sets.length === 0) {
9047
+ throw new exports.HaipValidationError(
9048
+ "MISSING_CREDENTIAL_SETS",
9049
+ "a credential-set query must declare at least one credential_sets entry"
9050
+ );
9051
+ }
9052
+ for (const credential of query.credentials) {
9053
+ validateCredential(credential);
9054
+ }
9055
+ const declaredIds = new Set(query.credentials.map((c) => c.id));
9056
+ for (const credentialSet of query.credential_sets) {
9057
+ if (!credentialSet.options || credentialSet.options.length === 0) {
8495
9058
  throw new exports.HaipValidationError(
8496
- "CLAIM_SETS_DISALLOWED",
8497
- `credential '${credential.id}' uses claim_sets, not permitted in HAIP-minimal`,
8498
- credential.id
9059
+ "EMPTY_OPTIONS",
9060
+ "a credential set must offer at least one option"
8499
9061
  );
8500
9062
  }
9063
+ for (const option of credentialSet.options) {
9064
+ for (const id of option) {
9065
+ if (!declaredIds.has(id)) {
9066
+ throw new exports.HaipValidationError(
9067
+ "UNKNOWN_OPTION_REFERENCE",
9068
+ `credential_sets option references '${id}', which is not declared in query.credentials`,
9069
+ id
9070
+ );
9071
+ }
9072
+ }
9073
+ }
8501
9074
  }
8502
9075
  }
8503
9076
  function isHaipQuery(query) {
@@ -8935,6 +9508,14 @@ function bytesEqual3(a, b) {
8935
9508
  }
8936
9509
  return true;
8937
9510
  }
9511
+ function peekMsoSigned(payload) {
9512
+ try {
9513
+ const signed = decodeMso(payload).validityInfo.signed;
9514
+ return Number.isFinite(signed.getTime()) ? signed : void 0;
9515
+ } catch {
9516
+ return void 0;
9517
+ }
9518
+ }
8938
9519
  function derToPem(der) {
8939
9520
  let binary = "";
8940
9521
  for (const b of der) binary += String.fromCharCode(b);
@@ -9095,17 +9676,17 @@ var MdocParser = class {
9095
9676
  }
9096
9677
  let trustResult;
9097
9678
  if (options.trustStore) {
9098
- const { TrustEvaluator: TrustEvaluator2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
9099
- const evaluator = new TrustEvaluator2({
9679
+ const { evaluateCredentialIssuer: evaluateCredentialIssuer2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
9680
+ trustResult = await evaluateCredentialIssuer2({
9681
+ chain: cose.x5chain,
9682
+ profile: "iso18013-5",
9683
+ issuedAt: peekMsoSigned(cose.payload),
9100
9684
  trustStore: options.trustStore,
9101
9685
  revocationPolicy: options.revocationPolicy ?? "skip",
9102
9686
  fetcher: options.fetcher,
9103
9687
  cache: options.cache,
9104
9688
  clockSkewTolerance: options.clockSkewTolerance
9105
9689
  });
9106
- const { X509Certificate: X509Certificate9 } = await import('@peculiar/x509');
9107
- const leaf = new X509Certificate9(issuerCertBytes);
9108
- trustResult = await evaluator.evaluate(leaf);
9109
9690
  } else {
9110
9691
  if (options.trustedCertificates.length === 0) {
9111
9692
  if (options.skipTrustCheck !== true) {
@@ -9254,6 +9835,13 @@ var MdocParser = class {
9254
9835
  init_reflect_polyfill();
9255
9836
  init_errors();
9256
9837
  var DEFAULT_ALLOWED_ALGORITHMS2 = ["ES256", "ES384", "ES512"];
9838
+ var SD_JWT_VC_TYP = "dc+sd-jwt";
9839
+ var LEGACY_SD_JWT_VC_TYP = "vc+sd-jwt";
9840
+ function normalizeTyp(typ) {
9841
+ if (typeof typ !== "string") return void 0;
9842
+ const lower = typ.toLowerCase();
9843
+ return lower.startsWith("application/") ? lower.slice("application/".length) : lower;
9844
+ }
9257
9845
  var sha256Hasher = async (data, _alg) => {
9258
9846
  const encoder2 = new TextEncoder();
9259
9847
  const input = typeof data === "string" ? encoder2.encode(data) : data;
@@ -9366,6 +9954,18 @@ var SdJwtParser = class {
9366
9954
  if (typeof alg !== "string" || !allowedAlgorithms.includes(alg)) {
9367
9955
  return invalidResult(`Unsupported algorithm: ${alg}`);
9368
9956
  }
9957
+ const typ = normalizeTyp(header.typ);
9958
+ if (typ === LEGACY_SD_JWT_VC_TYP) {
9959
+ if (options.allowLegacyVcSdJwtTyp !== true) {
9960
+ return invalidResult(
9961
+ `Legacy SD-JWT VC typ '${LEGACY_SD_JWT_VC_TYP}' is not accepted (removed in draft-ietf-oauth-sd-jwt-vc-19; expected '${SD_JWT_VC_TYP}'). Set allowLegacyVcSdJwtTyp to accept it during migration.`
9962
+ );
9963
+ }
9964
+ } else if (typ !== SD_JWT_VC_TYP) {
9965
+ return invalidResult(
9966
+ `Unsupported SD-JWT VC typ: ${header.typ === void 0 ? "(missing)" : String(header.typ)} (expected '${SD_JWT_VC_TYP}')`
9967
+ );
9968
+ }
9369
9969
  const x5cArray = header.x5c;
9370
9970
  const hasX5c = Array.isArray(x5cArray) && x5cArray.length > 0 && typeof x5cArray[0] === "string";
9371
9971
  let issuerKey;
@@ -9435,17 +10035,22 @@ var SdJwtParser = class {
9435
10035
  };
9436
10036
  let trustResult;
9437
10037
  if (issuerCertBytes === void 0) ; else if (options.trustStore) {
9438
- const { TrustEvaluator: TrustEvaluator2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
9439
- const evaluator = new TrustEvaluator2({
10038
+ const { evaluateCredentialIssuer: evaluateCredentialIssuer2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
10039
+ const x5cRest = x5cArray.slice(1);
10040
+ if (!x5cRest.every((entry) => typeof entry === "string")) {
10041
+ throw new exports.MalformedCredentialError("x5c entries must be base64 strings");
10042
+ }
10043
+ const iat = payload["iat"];
10044
+ trustResult = await evaluateCredentialIssuer2({
10045
+ chain: [issuerCertBytes, ...x5cRest.map((entry) => base64ToBytes(entry))],
10046
+ profile: "generic",
10047
+ issuedAt: typeof iat === "number" && Number.isFinite(iat) ? new Date(iat * 1e3) : void 0,
9440
10048
  trustStore: options.trustStore,
9441
10049
  revocationPolicy: options.revocationPolicy ?? "skip",
9442
10050
  fetcher: options.fetcher,
9443
10051
  cache: options.cache,
9444
10052
  clockSkewTolerance: options.clockSkewTolerance
9445
10053
  });
9446
- const { X509Certificate: X509Certificate9 } = await import('@peculiar/x509');
9447
- const leaf = new X509Certificate9(issuerCertBytes);
9448
- trustResult = await evaluator.evaluate(leaf);
9449
10054
  } else {
9450
10055
  if (options.trustedCertificates.length === 0) {
9451
10056
  if (options.skipTrustCheck !== true) {
@@ -9881,6 +10486,9 @@ var InMemoryCache = class {
9881
10486
  }
9882
10487
  };
9883
10488
 
10489
+ // src/index.ts
10490
+ init_guarded_fetch();
10491
+
9884
10492
  // src/trust/index.ts
9885
10493
  init_reflect_polyfill();
9886
10494
 
@@ -9928,14 +10536,10 @@ var CompositeTrustStore = class {
9928
10536
  const results = await Promise.all(
9929
10537
  this.stores.map((store) => store.getAnchors(hint))
9930
10538
  );
9931
- const seen = /* @__PURE__ */ new Set();
9932
10539
  const out = [];
9933
10540
  for (const batch of results) {
9934
10541
  for (const anchor of batch) {
9935
- const ski = getSkiHex(anchor.certificate);
9936
- const key = ski ?? anchor.certificate.serialNumber;
9937
- if (seen.has(key)) continue;
9938
- seen.add(key);
10542
+ if (out.some((a) => certificatesEqual(a.certificate, anchor.certificate))) continue;
9939
10543
  out.push(anchor);
9940
10544
  }
9941
10545
  }
@@ -9967,6 +10571,7 @@ function bytesToHex3(bytes) {
9967
10571
 
9968
10572
  // src/trust/LotlTrustStore.ts
9969
10573
  init_reflect_polyfill();
10574
+ init_errors();
9970
10575
 
9971
10576
  // src/trust/lotl-signing-anchors.ts
9972
10577
  init_reflect_polyfill();
@@ -10028,13 +10633,15 @@ var EU_LOTL_SIGNING_ANCHORS = Object.freeze(
10028
10633
  // src/trust/LotlFetcher.ts
10029
10634
  init_reflect_polyfill();
10030
10635
  init_errors();
10636
+ init_guarded_fetch();
10031
10637
  var XMLDSIG_NS = "http://www.w3.org/2000/09/xmldsig#";
10032
10638
  var LotlFetcher = class {
10033
10639
  fetcher;
10034
10640
  constructor(opts = {}) {
10035
- this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
10641
+ this.fetcher = opts.fetcher ?? createDefaultTrustFetcher();
10036
10642
  }
10037
10643
  async fetchSigned(url, signingAnchors) {
10644
+ assertCryptoEngineRegistered();
10038
10645
  let xmlText;
10039
10646
  try {
10040
10647
  const response = await this.fetcher(url, { method: "GET" });
@@ -10087,6 +10694,22 @@ var LotlFetcher = class {
10087
10694
  );
10088
10695
  }
10089
10696
  };
10697
+ function assertCryptoEngineRegistered() {
10698
+ let engine;
10699
+ try {
10700
+ engine = xmldsig__namespace.Application.crypto;
10701
+ } catch (err) {
10702
+ throw new exports.LotlConfigurationError(
10703
+ `no xmldsigjs crypto engine registered \u2014 trusted-list signatures cannot be verified. Call xmldsig.Application.setEngine('NodeJS', new Crypto()) (e.g. from @peculiar/webcrypto) once at startup, before using LotlTrustStore. Underlying error: ${err instanceof Error ? err.message : String(err)}`,
10704
+ { cause: err instanceof Error ? err : void 0 }
10705
+ );
10706
+ }
10707
+ if (!engine) {
10708
+ throw new exports.LotlConfigurationError(
10709
+ "no xmldsigjs crypto engine registered \u2014 trusted-list signatures cannot be verified. Call xmldsig.Application.setEngine('NodeJS', new Crypto()) (e.g. from @peculiar/webcrypto) once at startup, before using LotlTrustStore."
10710
+ );
10711
+ }
10712
+ }
10090
10713
  async function verifyAgainst(doc, sigEl, anchor) {
10091
10714
  try {
10092
10715
  const signed = new xmldsig__namespace.SignedXml(doc);
@@ -10234,6 +10857,7 @@ function parsePointer(el) {
10234
10857
 
10235
10858
  // src/trust/NationalTlResolver.ts
10236
10859
  init_reflect_polyfill();
10860
+ init_errors();
10237
10861
  var TSL_NS2 = "http://uri.etsi.org/02231/v2#";
10238
10862
  var NationalTlResolver = class {
10239
10863
  constructor(opts) {
@@ -10249,6 +10873,7 @@ var NationalTlResolver = class {
10249
10873
  );
10250
10874
  return parseNationalTl(doc, pointer.country);
10251
10875
  } catch (err) {
10876
+ if (err instanceof exports.LotlConfigurationError) throw err;
10252
10877
  console.warn(
10253
10878
  `[openid4vp] national TL for ${pointer.country} failed: ${err.message}`
10254
10879
  );
@@ -10427,6 +11052,7 @@ var LotlTrustStore = class {
10427
11052
  };
10428
11053
  return this.snapshot;
10429
11054
  } catch (err) {
11055
+ if (err instanceof exports.LotlConfigurationError) throw err;
10430
11056
  if (this.snapshot) {
10431
11057
  console.warn(
10432
11058
  `[openid4vp] LOTL refresh failed; serving cached snapshot (${err.message})`
@@ -10474,7 +11100,7 @@ function equalBytes(a, b) {
10474
11100
 
10475
11101
  // src/index.ts
10476
11102
  init_errors();
10477
- var VERSION = "0.11.1";
11103
+ var VERSION = "0.13.0";
10478
11104
  /*! Bundled license information:
10479
11105
 
10480
11106
  pvtsutils/build/index.es.js:
@@ -10560,14 +11186,17 @@ exports.MdocParser = MdocParser;
10560
11186
  exports.SdJwtParser = SdJwtParser;
10561
11187
  exports.StaticTrustStore = StaticTrustStore;
10562
11188
  exports.VERSION = VERSION;
11189
+ exports.buildCredentialSetQuery = buildCredentialSetQuery;
10563
11190
  exports.buildHaipQuery = buildHaipQuery;
10564
11191
  exports.buildOid4vpSessionTranscript = buildOid4vpSessionTranscript;
10565
11192
  exports.buildOpenID4VPHandoverSessionTranscript = buildOpenID4VPHandoverSessionTranscript;
10566
11193
  exports.createAuthorizationRequest = createAuthorizationRequest;
11194
+ exports.createGuardedFetcher = createGuardedFetcher;
10567
11195
  exports.createSignedAuthorizationRequest = createSignedAuthorizationRequest;
10568
11196
  exports.decryptAuthorizationResponse = decryptAuthorizationResponse;
10569
11197
  exports.isHaipQuery = isHaipQuery;
10570
11198
  exports.parsePresentation = parsePresentation;
11199
+ exports.validateCredentialSetQuery = validateCredentialSetQuery;
10571
11200
  exports.validateHaipQuery = validateHaipQuery;
10572
11201
  exports.verifyAuthorizationResponse = verifyAuthorizationResponse;
10573
11202
  exports.verifyPresentation = verifyPresentation;