@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import 'reflect-metadata';
2
- import { X509Certificate, SubjectKeyIdentifierExtension, X509Certificates, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags as KeyUsageFlags$1, AuthorityKeyIdentifierExtension, SubjectAlternativeNameExtension, ExtendedKeyUsageExtension, Name as Name$1 } from '@peculiar/x509';
2
+ import { X509Certificate, SubjectKeyIdentifierExtension, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags as KeyUsageFlags$1, ExtendedKeyUsageExtension, AuthorityKeyIdentifierExtension, SubjectAlternativeNameExtension, Name as Name$1 } from '@peculiar/x509';
3
3
  import { v4 } from 'uuid';
4
4
  import { SignJWT, importX509, decodeProtectedHeader, jwtVerify, importJWK, compactDecrypt } from 'jose';
5
5
  import { Encoder, Tag } from 'cbor-x';
@@ -24,7 +24,7 @@ var init_reflect_polyfill = __esm({
24
24
  });
25
25
 
26
26
  // src/errors.ts
27
- var InvalidSignatureError, ExpiredCredentialError, UnsupportedFormatError, MalformedCredentialError, NonceValidationError, HaipValidationError, OpenID4VPError, TrustAnchorNotFoundError, CertificateChainError, RevokedCertificateError, RevocationCheckFailedError, LotlFetchError, LotlSignatureError, SignedRequestBuildError, UnsupportedJweError, DecryptionFailedError, MissingDecryptionKeyError, MissingVerifierEncryptionKeyError, MultipleCredentialsNotSupportedError;
27
+ var InvalidSignatureError, ExpiredCredentialError, UnsupportedFormatError, MalformedCredentialError, NonceValidationError, HaipValidationError, OpenID4VPError, TrustAnchorNotFoundError, CertificateChainError, RevokedCertificateError, RevocationCheckFailedError, LotlFetchError, LotlSignatureError, LotlConfigurationError, SignedRequestBuildError, UnsupportedJweError, DecryptionFailedError, MissingDecryptionKeyError, MissingVerifierEncryptionKeyError, MultipleCredentialsNotSupportedError, GuardedFetchError;
28
28
  var init_errors = __esm({
29
29
  "src/errors.ts"() {
30
30
  init_reflect_polyfill();
@@ -121,6 +121,9 @@ var init_errors = __esm({
121
121
  LotlSignatureError = class extends OpenID4VPError {
122
122
  code = "lotl_signature_invalid";
123
123
  };
124
+ LotlConfigurationError = class extends OpenID4VPError {
125
+ code = "lotl_configuration_invalid";
126
+ };
124
127
  SignedRequestBuildError = class extends OpenID4VPError {
125
128
  code;
126
129
  constructor(code, message) {
@@ -168,6 +171,16 @@ var init_errors = __esm({
168
171
  this.presentationCount = presentationCount;
169
172
  }
170
173
  };
174
+ GuardedFetchError = class extends OpenID4VPError {
175
+ code = "guarded_fetch_rejected";
176
+ reason;
177
+ url;
178
+ constructor(reason, message, options) {
179
+ super(message, { cause: options.cause });
180
+ this.reason = reason;
181
+ this.url = options.url;
182
+ }
183
+ };
171
184
  }
172
185
  });
173
186
 
@@ -6564,7 +6577,7 @@ function dnIsUnder(childDn, subtreeDn) {
6564
6577
  }
6565
6578
  return true;
6566
6579
  }
6567
- var NAME_CONSTRAINTS_OID, DEFAULT_ALGORITHMS, ChainBuilder;
6580
+ var NAME_CONSTRAINTS_OID, ISO_18013_5_DS_EKU, DEFAULT_MAX_CHAIN_LENGTH, DEFAULT_MAX_SIGNATURE_CHECKS, searchAborts, DEFAULT_ALGORITHMS, ChainBuilder;
6568
6581
  var init_ChainBuilder = __esm({
6569
6582
  "src/trust/ChainBuilder.ts"() {
6570
6583
  init_reflect_polyfill();
@@ -6573,6 +6586,10 @@ var init_ChainBuilder = __esm({
6573
6586
  init_errors();
6574
6587
  init_x509_utils();
6575
6588
  NAME_CONSTRAINTS_OID = "2.5.29.30";
6589
+ ISO_18013_5_DS_EKU = "1.0.18013.5.1.2";
6590
+ DEFAULT_MAX_CHAIN_LENGTH = 5;
6591
+ DEFAULT_MAX_SIGNATURE_CHECKS = 32;
6592
+ searchAborts = /* @__PURE__ */ new WeakSet();
6576
6593
  DEFAULT_ALGORITHMS = [
6577
6594
  "ES256",
6578
6595
  "ES384",
@@ -6595,62 +6612,130 @@ var init_ChainBuilder = __esm({
6595
6612
  * optionally using additional intermediates. Returns the ordered chain
6596
6613
  * (leaf → anchor). Throws `CertificateChainError` on any validation
6597
6614
  * failure.
6615
+ *
6616
+ * Trust is by presence in the anchor set and is established ONLY by (a) the
6617
+ * leaf being byte-identical to an anchor, or (b) a verified signature by an
6618
+ * anchor's key on the last certificate of the path. Subject/Issuer DN
6619
+ * equality is used to select candidates, never to conclude trust
6620
+ * (GHSA-4c2f-96cf-f5fc).
6621
+ *
6622
+ * The search is depth-first and backtracks: when several intermediates share
6623
+ * a Subject DN — a re-issued certificate next to its expired predecessor, or
6624
+ * cross-signed copies under different roots — a candidate that fails a check
6625
+ * or leads nowhere does not end the search. The result therefore does not
6626
+ * depend on the order of `intermediates`. Work is bounded by
6627
+ * `maxChainLength` and `maxSignatureChecks`.
6598
6628
  */
6599
- async build(leaf, anchors, intermediates = []) {
6600
- const errors = [];
6629
+ async build(leaf, anchors, intermediates = [], context = {}) {
6630
+ const search = {
6631
+ remaining: this.opts.maxSignatureChecks ?? DEFAULT_MAX_SIGNATURE_CHECKS
6632
+ };
6633
+ let lastFailure;
6601
6634
  for (const anchor of anchors) {
6635
+ search.specific = void 0;
6636
+ search.fallback = void 0;
6602
6637
  try {
6603
- return await this.tryBuild(leaf, anchor, intermediates);
6638
+ const chain = await this.tryBuild(leaf, anchor, intermediates, context, search);
6639
+ if (chain) return chain;
6640
+ lastFailure = search.specific ?? search.fallback;
6604
6641
  } catch (err) {
6605
- errors.push(err);
6642
+ if (searchAborts.has(err)) throw err;
6643
+ lastFailure = err;
6606
6644
  }
6607
6645
  }
6608
- const last = errors[errors.length - 1];
6609
- if (last instanceof CertificateChainError) throw last;
6646
+ if (lastFailure instanceof CertificateChainError) throw lastFailure;
6610
6647
  throw new CertificateChainError("no valid chain to any anchor", {
6611
6648
  reason: "signature",
6612
- cause: last
6649
+ cause: lastFailure
6613
6650
  });
6614
6651
  }
6615
- async tryBuild(leaf, anchor, intermediates) {
6616
- this.checkPerCert(leaf);
6617
- this.checkLeafKeyUsage(leaf);
6618
- const chain = [leaf];
6619
- let current = leaf;
6620
- const pool = new X509Certificates(intermediates);
6621
- let nonLeafDepth = 0;
6622
- while (current.subject !== anchor.subject) {
6623
- const issuer = pool.find((c) => c.subject === current.issuer);
6624
- if (!issuer) {
6625
- if (current.issuer !== anchor.subject) {
6626
- throw new CertificateChainError(`no issuer certificate found for ${current.subject}`, {
6627
- reason: "signature"
6628
- });
6629
- }
6630
- this.checkPerCert(anchor);
6631
- this.checkCaAndPathLen(anchor, nonLeafDepth);
6632
- this.checkAkiSkiMatch(current, anchor);
6633
- await this.verifySignature(current, anchor);
6634
- chain.push(anchor);
6635
- this.checkNameConstraints(leaf, chain.slice(1));
6636
- return chain;
6652
+ /** Search for a path from `leaf` that closes at `anchor`. */
6653
+ async tryBuild(leaf, anchor, intermediates, context, search) {
6654
+ this.checkPerCert(leaf, context);
6655
+ if (certificatesEqual(leaf, anchor)) {
6656
+ this.checkLeafKeyUsage(leaf, "generic");
6657
+ return [leaf];
6658
+ }
6659
+ const profile = this.opts.profile ?? "generic";
6660
+ this.checkLeafKeyUsage(leaf, profile);
6661
+ this.checkLeafExtendedKeyUsage(leaf, profile);
6662
+ const pool = intermediates.filter(
6663
+ (c) => !certificatesEqual(c, anchor) && !certificatesEqual(c, leaf)
6664
+ );
6665
+ return this.extendPath([leaf], anchor, pool, context, search);
6666
+ }
6667
+ /**
6668
+ * Try to complete `chain` (leaf first) to `anchor`. Returns the completed
6669
+ * chain, or `undefined` when no extension of this prefix works — failures
6670
+ * are recorded on `search` so the caller can report the most specific one.
6671
+ * Throws only to abandon the whole search (signature budget exhausted).
6672
+ */
6673
+ async extendPath(chain, anchor, pool, context, search) {
6674
+ const leaf = chain[0];
6675
+ const current = chain[chain.length - 1];
6676
+ const maxLength = this.opts.maxChainLength ?? DEFAULT_MAX_CHAIN_LENGTH;
6677
+ if (chain.length + 1 > maxLength) {
6678
+ search.specific = new CertificateChainError(
6679
+ `chain from ${leaf.subject} exceeds the maximum length of ${maxLength} certificates`,
6680
+ { reason: "path_length" }
6681
+ );
6682
+ return void 0;
6683
+ }
6684
+ const nonLeafDepth = chain.length - 1;
6685
+ if (current.issuer === anchor.subject) {
6686
+ try {
6687
+ await this.closeAtAnchor(current, anchor, nonLeafDepth, context, search);
6688
+ const closed = [...chain, anchor];
6689
+ this.checkNameConstraints(leaf, closed.slice(1));
6690
+ return closed;
6691
+ } catch (err) {
6692
+ if (searchAborts.has(err)) throw err;
6693
+ search.specific = err;
6637
6694
  }
6638
- this.checkPerCert(issuer);
6639
- this.checkCaAndPathLen(issuer, nonLeafDepth);
6640
- this.checkAkiSkiMatch(current, issuer);
6641
- await this.verifySignature(current, issuer);
6642
- chain.push(issuer);
6643
- current = issuer;
6644
- nonLeafDepth += 1;
6645
- }
6646
- if (!certificatesEqual(current, anchor)) {
6647
- throw new CertificateChainError(
6648
- `certificate ${current.subject} shares the trust anchor's Subject DN but is not the anchor (no signature closure)`,
6649
- { reason: "signature" }
6695
+ }
6696
+ const candidates = pool.filter(
6697
+ (c) => c.subject === current.issuer && !chain.some((x) => certificatesEqual(x, c))
6698
+ );
6699
+ let verified = 0;
6700
+ for (const candidate of candidates) {
6701
+ if (!await this.signatureVerifies(current, candidate, search)) continue;
6702
+ verified++;
6703
+ try {
6704
+ this.checkPerCert(candidate, context);
6705
+ this.checkCaAndPathLen(candidate, nonLeafDepth);
6706
+ this.checkAkiSkiMatch(current, candidate);
6707
+ } catch (err) {
6708
+ search.specific = err;
6709
+ continue;
6710
+ }
6711
+ const found = await this.extendPath([...chain, candidate], anchor, pool, context, search);
6712
+ if (found) return found;
6713
+ }
6714
+ search.fallback = new CertificateChainError(
6715
+ candidates.length > 0 && verified === 0 ? `signature verification failed for ${current.subject}` : `no issuer certificate found for ${current.subject}`,
6716
+ { reason: "signature" }
6717
+ );
6718
+ return void 0;
6719
+ }
6720
+ async closeAtAnchor(current, anchor, nonLeafDepth, context, search) {
6721
+ this.checkPerCert(anchor, context);
6722
+ this.checkCaAndPathLen(anchor, nonLeafDepth);
6723
+ this.checkAkiSkiMatch(current, anchor);
6724
+ this.spendSignatureCheck(search);
6725
+ await this.verifySignature(current, anchor);
6726
+ }
6727
+ /** Charge one signature verification to the search, or abandon it. */
6728
+ spendSignatureCheck(search) {
6729
+ if (search.remaining <= 0) {
6730
+ const limit = this.opts.maxSignatureChecks ?? DEFAULT_MAX_SIGNATURE_CHECKS;
6731
+ const err = new CertificateChainError(
6732
+ `certificate path search exceeded ${limit} signature checks`,
6733
+ { reason: "path_length" }
6650
6734
  );
6735
+ searchAborts.add(err);
6736
+ throw err;
6651
6737
  }
6652
- this.checkNameConstraints(leaf, chain.slice(1));
6653
- return chain;
6738
+ search.remaining--;
6654
6739
  }
6655
6740
  /**
6656
6741
  * Enforce nameConstraints asserted by any CA above the leaf against the
@@ -6699,7 +6784,7 @@ var init_ChainBuilder = __esm({
6699
6784
  }
6700
6785
  }
6701
6786
  checkCaAndPathLen(cert, nonLeafDepth) {
6702
- this.checkCaKeyUsage(cert);
6787
+ this.checkCaKeyUsage(cert, this.opts.profile ?? "generic");
6703
6788
  const bc = cert.getExtension(BasicConstraintsExtension);
6704
6789
  if (!bc || !bc.ca) {
6705
6790
  throw new CertificateChainError(
@@ -6714,9 +6799,17 @@ var init_ChainBuilder = __esm({
6714
6799
  );
6715
6800
  }
6716
6801
  }
6717
- checkLeafKeyUsage(leaf) {
6802
+ checkLeafKeyUsage(leaf, profile) {
6718
6803
  const ext = leaf.getExtension(KeyUsagesExtension);
6719
- if (!ext) return;
6804
+ if (!ext) {
6805
+ if (profile === "iso18013-5") {
6806
+ throw new CertificateChainError(
6807
+ `document signer ${leaf.subject} has no keyUsage extension (ISO 18013-5 requires digitalSignature)`,
6808
+ { reason: "key_usage" }
6809
+ );
6810
+ }
6811
+ return;
6812
+ }
6720
6813
  if ((ext.usages & KeyUsageFlags$1.digitalSignature) === 0) {
6721
6814
  throw new CertificateChainError(
6722
6815
  `leaf ${leaf.subject} does not assert digitalSignature key usage`,
@@ -6724,9 +6817,28 @@ var init_ChainBuilder = __esm({
6724
6817
  );
6725
6818
  }
6726
6819
  }
6727
- checkCaKeyUsage(cert) {
6820
+ checkLeafExtendedKeyUsage(leaf, profile) {
6821
+ if (profile !== "iso18013-5") return;
6822
+ const ext = leaf.getExtension(ExtendedKeyUsageExtension);
6823
+ const usages = (ext?.usages ?? []).map(String);
6824
+ if (!usages.includes(ISO_18013_5_DS_EKU)) {
6825
+ throw new CertificateChainError(
6826
+ `document signer ${leaf.subject} lacks extended key usage ${ISO_18013_5_DS_EKU} (id-mdl-kp-mdlDS)`,
6827
+ { reason: "extended_key_usage" }
6828
+ );
6829
+ }
6830
+ }
6831
+ checkCaKeyUsage(cert, profile) {
6728
6832
  const ext = cert.getExtension(KeyUsagesExtension);
6729
- if (!ext) return;
6833
+ if (!ext) {
6834
+ if (profile === "iso18013-5") {
6835
+ throw new CertificateChainError(
6836
+ `CA ${cert.subject} has no keyUsage extension (ISO 18013-5 requires keyCertSign)`,
6837
+ { reason: "key_usage" }
6838
+ );
6839
+ }
6840
+ return;
6841
+ }
6730
6842
  if ((ext.usages & KeyUsageFlags$1.keyCertSign) === 0) {
6731
6843
  throw new CertificateChainError(
6732
6844
  `CA ${cert.subject} does not assert keyCertSign key usage`,
@@ -6747,30 +6859,46 @@ var init_ChainBuilder = __esm({
6747
6859
  );
6748
6860
  }
6749
6861
  }
6862
+ /**
6863
+ * Signature check used to select among DN-matching candidates. Never throws
6864
+ * on a bad signature; throws only when the search budget is exhausted.
6865
+ */
6866
+ async signatureVerifies(child, issuer, search) {
6867
+ this.spendSignatureCheck(search);
6868
+ try {
6869
+ return await child.verify({ publicKey: issuer.publicKey, signatureOnly: true });
6870
+ } catch {
6871
+ return false;
6872
+ }
6873
+ }
6750
6874
  async verifySignature(child, issuer) {
6751
6875
  const ok = await child.verify({ publicKey: issuer.publicKey, signatureOnly: true });
6752
6876
  if (!ok) {
6753
6877
  throw new CertificateChainError(`signature verification failed for ${child.subject}`, { reason: "signature" });
6754
6878
  }
6755
6879
  }
6756
- checkPerCert(cert) {
6757
- this.checkValidity(cert);
6880
+ checkPerCert(cert, context = {}) {
6881
+ const now = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))();
6882
+ this.checkValidity(cert, now, "");
6883
+ if (context.issuedAt) {
6884
+ this.checkValidity(cert, context.issuedAt, ` at issuance time ${context.issuedAt.toISOString()}`);
6885
+ }
6758
6886
  this.checkAlgorithm(cert);
6759
6887
  }
6760
- checkValidity(cert) {
6761
- const now = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
6888
+ checkValidity(cert, at, label) {
6889
+ const time = at.getTime();
6762
6890
  const skewMs = (this.opts.clockSkewTolerance ?? 60) * 1e3;
6763
6891
  const notBefore = cert.notBefore.getTime();
6764
6892
  const notAfter = cert.notAfter.getTime();
6765
- if (now + skewMs < notBefore) {
6893
+ if (time + skewMs < notBefore) {
6766
6894
  throw new CertificateChainError(
6767
- `certificate ${cert.subject} not yet valid (notBefore=${cert.notBefore.toISOString()})`,
6895
+ `certificate ${cert.subject} not yet valid${label} (notBefore=${cert.notBefore.toISOString()})`,
6768
6896
  { reason: "validity" }
6769
6897
  );
6770
6898
  }
6771
- if (now - skewMs > notAfter) {
6899
+ if (time - skewMs > notAfter) {
6772
6900
  throw new CertificateChainError(
6773
- `certificate ${cert.subject} expired (notAfter=${cert.notAfter.toISOString()})`,
6901
+ `certificate ${cert.subject} expired${label} (notAfter=${cert.notAfter.toISOString()})`,
6774
6902
  { reason: "validity" }
6775
6903
  );
6776
6904
  }
@@ -6817,6 +6945,349 @@ var init_ecdsa_util = __esm({
6817
6945
  }
6818
6946
  });
6819
6947
 
6948
+ // src/http/ip-address.ts
6949
+ function parseIPv4(input) {
6950
+ const parts = input.split(".");
6951
+ if (parts.length !== 4) return void 0;
6952
+ const bytes = [];
6953
+ for (const part of parts) {
6954
+ if (!/^\d{1,3}$/.test(part)) return void 0;
6955
+ const value = Number(part);
6956
+ if (value > 255) return void 0;
6957
+ bytes.push(value);
6958
+ }
6959
+ return bytes;
6960
+ }
6961
+ function parseIPv6(input) {
6962
+ const address = input.split("%")[0];
6963
+ if (!address.includes(":")) return void 0;
6964
+ let head = address;
6965
+ let embeddedV4;
6966
+ const lastColon = address.lastIndexOf(":");
6967
+ const tail = address.slice(lastColon + 1);
6968
+ if (tail.includes(".")) {
6969
+ embeddedV4 = parseIPv4(tail);
6970
+ if (!embeddedV4) return void 0;
6971
+ head = address.slice(0, lastColon + 1) + "0:0";
6972
+ }
6973
+ const doubleColon = head.split("::");
6974
+ if (doubleColon.length > 2) return void 0;
6975
+ const toGroups = (s) => s === "" ? [] : s.split(":");
6976
+ const left = toGroups(doubleColon[0]);
6977
+ const right = doubleColon.length === 2 ? toGroups(doubleColon[1]) : [];
6978
+ const missing = 8 - left.length - right.length;
6979
+ if (doubleColon.length === 2 ? missing < 1 : missing !== 0) return void 0;
6980
+ const groups = [...left, ...Array(doubleColon.length === 2 ? missing : 0).fill("0"), ...right];
6981
+ const bytes = [];
6982
+ for (const group of groups) {
6983
+ if (!/^[0-9a-fA-F]{1,4}$/.test(group)) return void 0;
6984
+ const value = parseInt(group, 16);
6985
+ bytes.push(value >> 8, value & 255);
6986
+ }
6987
+ if (embeddedV4) bytes.splice(12, 4, ...embeddedV4);
6988
+ return bytes;
6989
+ }
6990
+ function inPrefix(bytes, prefix, bits) {
6991
+ for (let i = 0; i < bits; i++) {
6992
+ const byte = i >> 3;
6993
+ const mask = 128 >> (i & 7);
6994
+ if ((bytes[byte] & mask) !== ((prefix[byte] ?? 0) & mask)) return false;
6995
+ }
6996
+ return true;
6997
+ }
6998
+ function isPublicV4(bytes) {
6999
+ return !BLOCKED_V4.some(([prefix, bits]) => inPrefix(bytes, prefix, bits));
7000
+ }
7001
+ function isPublicV6(bytes) {
7002
+ const first10Zero = bytes.slice(0, 10).every((b) => b === 0);
7003
+ if (first10Zero && bytes[10] === 0 && bytes[11] === 0) {
7004
+ const v4 = bytes.slice(12);
7005
+ if (v4.slice(0, 3).every((b) => b === 0)) return false;
7006
+ return isPublicV4(v4);
7007
+ }
7008
+ if (first10Zero && bytes[10] === 255 && bytes[11] === 255) return isPublicV4(bytes.slice(12));
7009
+ if (inPrefix(bytes, [0, 100, 255, 155], 96)) return isPublicV4(bytes.slice(12));
7010
+ if (inPrefix(bytes, [32, 2], 16)) return isPublicV4(bytes.slice(2, 6));
7011
+ return !BLOCKED_V6.some(([prefix, bits]) => inPrefix(bytes, prefix, bits));
7012
+ }
7013
+ function isPublicIpAddress(ip) {
7014
+ const v4 = parseIPv4(ip);
7015
+ if (v4) return isPublicV4(v4);
7016
+ const v6 = parseIPv6(ip);
7017
+ if (v6) return isPublicV6(v6);
7018
+ return false;
7019
+ }
7020
+ function isIpLiteral(host) {
7021
+ return parseIPv4(host) !== void 0 || parseIPv6(host) !== void 0;
7022
+ }
7023
+ var BLOCKED_V4, BLOCKED_V6;
7024
+ var init_ip_address = __esm({
7025
+ "src/http/ip-address.ts"() {
7026
+ init_reflect_polyfill();
7027
+ BLOCKED_V4 = [
7028
+ [[0, 0, 0, 0], 8],
7029
+ [[10, 0, 0, 0], 8],
7030
+ [[100, 64, 0, 0], 10],
7031
+ [[127, 0, 0, 0], 8],
7032
+ [[169, 254, 0, 0], 16],
7033
+ [[172, 16, 0, 0], 12],
7034
+ [[192, 0, 0, 0], 24],
7035
+ [[192, 0, 2, 0], 24],
7036
+ [[192, 88, 99, 0], 24],
7037
+ [[192, 168, 0, 0], 16],
7038
+ [[198, 18, 0, 0], 15],
7039
+ [[198, 51, 100, 0], 24],
7040
+ [[203, 0, 113, 0], 24],
7041
+ [[224, 0, 0, 0], 4],
7042
+ [[240, 0, 0, 0], 4]
7043
+ ];
7044
+ BLOCKED_V6 = [
7045
+ [[1, 0], 64],
7046
+ // 100::/64 discard-only
7047
+ [[32, 1, 0, 0], 32],
7048
+ // 2001::/32 Teredo (embeds an obfuscated IPv4)
7049
+ [[32, 1, 13, 184], 32],
7050
+ // 2001:db8::/32 documentation
7051
+ [[252], 7],
7052
+ // fc00::/7 unique local (incl. fd00:ec2::254 metadata)
7053
+ [[254, 128], 10],
7054
+ // fe80::/10 link-local
7055
+ [[254, 192], 10],
7056
+ // fec0::/10 deprecated site-local
7057
+ [[255], 8]
7058
+ // ff00::/8 multicast
7059
+ ];
7060
+ }
7061
+ });
7062
+
7063
+ // src/http/guarded-fetch.ts
7064
+ function loadDns() {
7065
+ dnsModule ??= (async () => {
7066
+ try {
7067
+ const specifier = "node:dns";
7068
+ const mod = await import(
7069
+ /* webpackIgnore: true */
7070
+ /* @vite-ignore */
7071
+ specifier
7072
+ );
7073
+ return mod.promises ?? mod.default?.promises;
7074
+ } catch {
7075
+ return void 0;
7076
+ }
7077
+ })();
7078
+ return dnsModule;
7079
+ }
7080
+ function toError(err) {
7081
+ return err instanceof Error ? err : new Error(String(err));
7082
+ }
7083
+ function createGuardedFetcher(options = {}) {
7084
+ const allowHttp = options.allowHttp ?? false;
7085
+ const allowPrivate = options.allowPrivateNetworks ?? false;
7086
+ const maxRedirects = options.maxRedirects ?? GUARDED_FETCH_DEFAULTS.maxRedirects;
7087
+ const timeoutMs = options.timeoutMs ?? GUARDED_FETCH_DEFAULTS.timeoutMs;
7088
+ const maxBytes = options.maxResponseBytes ?? GUARDED_FETCH_DEFAULTS.maxResponseBytes;
7089
+ const lookup = options.lookup ?? defaultLookup;
7090
+ const transport = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
7091
+ return async (input, init = {}) => {
7092
+ const controller = new AbortController();
7093
+ const timer = setTimeout(() => {
7094
+ controller.abort(
7095
+ new GuardedFetchError("timeout", `request to ${input} exceeded ${timeoutMs} ms`, { url: input })
7096
+ );
7097
+ }, timeoutMs);
7098
+ const callerSignal = init.signal ?? void 0;
7099
+ const onCallerAbort = () => controller.abort(callerSignal?.reason);
7100
+ if (callerSignal?.aborted) onCallerAbort();
7101
+ else callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
7102
+ const aborted = new Promise((_, reject) => {
7103
+ const fail = () => reject(toError(controller.signal.reason));
7104
+ if (controller.signal.aborted) fail();
7105
+ else controller.signal.addEventListener("abort", fail, { once: true });
7106
+ });
7107
+ aborted.catch(() => {
7108
+ });
7109
+ const race = (p) => Promise.race([p, aborted]);
7110
+ try {
7111
+ let url = await race(validateTarget(input, void 0));
7112
+ let method = (init.method ?? "GET").toUpperCase();
7113
+ let body = init.body;
7114
+ const headers = new Headers(init.headers);
7115
+ for (let hop = 0; ; hop++) {
7116
+ const response = await race(
7117
+ transport(url.href, { ...init, method, body, headers, redirect: "manual", signal: controller.signal })
7118
+ );
7119
+ if (response.type === "opaqueredirect") {
7120
+ throw new GuardedFetchError(
7121
+ "invalid_redirect",
7122
+ `redirect from ${url.href} cannot be inspected by this transport`,
7123
+ { url: url.href }
7124
+ );
7125
+ }
7126
+ if (!REDIRECT_STATUSES.has(response.status)) {
7127
+ const bytes = await readBounded(response, url.href, maxBytes, race);
7128
+ return new Response(NULL_BODY_STATUSES.has(response.status) ? null : bytes, {
7129
+ status: response.status,
7130
+ statusText: response.statusText,
7131
+ headers: response.headers
7132
+ });
7133
+ }
7134
+ await response.body?.cancel().catch(() => {
7135
+ });
7136
+ if (hop >= maxRedirects) {
7137
+ throw new GuardedFetchError(
7138
+ "too_many_redirects",
7139
+ `more than ${maxRedirects} redirects starting at ${input}`,
7140
+ { url: url.href }
7141
+ );
7142
+ }
7143
+ const location = response.headers.get("location");
7144
+ if (!location) {
7145
+ throw new GuardedFetchError(
7146
+ "invalid_redirect",
7147
+ `HTTP ${response.status} from ${url.href} has no Location header`,
7148
+ { url: url.href }
7149
+ );
7150
+ }
7151
+ let next;
7152
+ try {
7153
+ next = new URL(location, url);
7154
+ } catch (err) {
7155
+ throw new GuardedFetchError("invalid_redirect", `unparseable Location from ${url.href}`, {
7156
+ url: url.href,
7157
+ cause: toError(err)
7158
+ });
7159
+ }
7160
+ const nextUrl = await race(validateTarget(next.href, url));
7161
+ if (response.status === 303 || (response.status === 301 || response.status === 302) && method === "POST") {
7162
+ if (method !== "HEAD") method = "GET";
7163
+ body = void 0;
7164
+ for (const h of BODY_HEADERS) headers.delete(h);
7165
+ }
7166
+ if (nextUrl.origin !== url.origin) {
7167
+ for (const h of CROSS_ORIGIN_STRIPPED_HEADERS) headers.delete(h);
7168
+ }
7169
+ url = nextUrl;
7170
+ }
7171
+ } catch (err) {
7172
+ if (controller.signal.aborted) throw toError(controller.signal.reason);
7173
+ throw err;
7174
+ } finally {
7175
+ clearTimeout(timer);
7176
+ callerSignal?.removeEventListener("abort", onCallerAbort);
7177
+ }
7178
+ };
7179
+ async function validateTarget(raw, previous) {
7180
+ let url;
7181
+ try {
7182
+ url = new URL(raw);
7183
+ } catch (err) {
7184
+ throw new GuardedFetchError("invalid_url", `not an absolute URL: ${raw}`, { url: raw, cause: toError(err) });
7185
+ }
7186
+ if (url.protocol !== "https:" && !(allowHttp && url.protocol === "http:")) {
7187
+ throw new GuardedFetchError("insecure_scheme", `refusing ${url.protocol} URL ${url.href}: HTTPS is required`, {
7188
+ url: url.href
7189
+ });
7190
+ }
7191
+ if (previous?.protocol === "https:" && url.protocol !== "https:") {
7192
+ throw new GuardedFetchError("insecure_scheme", `refusing https -> http redirect to ${url.href}`, {
7193
+ url: url.href
7194
+ });
7195
+ }
7196
+ if (allowPrivate) return url;
7197
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
7198
+ if (host === "localhost" || host.endsWith(".localhost")) {
7199
+ throw new GuardedFetchError("private_address", `refusing loopback host ${host}`, { url: url.href });
7200
+ }
7201
+ if (isIpLiteral(host)) {
7202
+ if (!isPublicIpAddress(host)) {
7203
+ throw new GuardedFetchError("private_address", `refusing non-public address ${host}`, { url: url.href });
7204
+ }
7205
+ return url;
7206
+ }
7207
+ let addresses;
7208
+ try {
7209
+ addresses = await lookup(host);
7210
+ } catch (err) {
7211
+ throw new GuardedFetchError("dns_resolution_failed", `could not resolve ${host}`, {
7212
+ url: url.href,
7213
+ cause: toError(err)
7214
+ });
7215
+ }
7216
+ if (addresses === void 0) return url;
7217
+ if (addresses.length === 0) {
7218
+ throw new GuardedFetchError("dns_resolution_failed", `${host} resolved to no addresses`, {
7219
+ url: url.href
7220
+ });
7221
+ }
7222
+ const blocked = addresses.find((a) => !isPublicIpAddress(a));
7223
+ if (blocked !== void 0) {
7224
+ throw new GuardedFetchError("private_address", `${host} resolves to non-public address ${blocked}`, {
7225
+ url: url.href
7226
+ });
7227
+ }
7228
+ return url;
7229
+ }
7230
+ }
7231
+ async function readBounded(response, url, maxBytes, race) {
7232
+ const tooLarge = () => new GuardedFetchError("response_too_large", `response from ${url} exceeds ${maxBytes} bytes`, { url });
7233
+ const declared = response.headers.get("content-length");
7234
+ if (declared !== null && Number(declared) > maxBytes) {
7235
+ await response.body?.cancel().catch(() => {
7236
+ });
7237
+ throw tooLarge();
7238
+ }
7239
+ if (!response.body) return null;
7240
+ const reader = response.body.getReader();
7241
+ const chunks = [];
7242
+ let total = 0;
7243
+ try {
7244
+ for (; ; ) {
7245
+ const { done, value } = await race(reader.read());
7246
+ if (done) break;
7247
+ total += value.byteLength;
7248
+ if (total > maxBytes) throw tooLarge();
7249
+ chunks.push(value);
7250
+ }
7251
+ } catch (err) {
7252
+ reader.cancel().catch(() => {
7253
+ });
7254
+ throw err;
7255
+ }
7256
+ const out = new Uint8Array(new ArrayBuffer(total));
7257
+ let offset = 0;
7258
+ for (const chunk of chunks) {
7259
+ out.set(chunk, offset);
7260
+ offset += chunk.byteLength;
7261
+ }
7262
+ return out;
7263
+ }
7264
+ function createDefaultTrustFetcher() {
7265
+ return createGuardedFetcher({ allowHttp: true });
7266
+ }
7267
+ var GUARDED_FETCH_DEFAULTS, REDIRECT_STATUSES, NULL_BODY_STATUSES, CROSS_ORIGIN_STRIPPED_HEADERS, BODY_HEADERS, dnsModule, defaultLookup;
7268
+ var init_guarded_fetch = __esm({
7269
+ "src/http/guarded-fetch.ts"() {
7270
+ init_reflect_polyfill();
7271
+ init_errors();
7272
+ init_ip_address();
7273
+ GUARDED_FETCH_DEFAULTS = {
7274
+ maxRedirects: 3,
7275
+ timeoutMs: 15e3,
7276
+ maxResponseBytes: 16 * 1024 * 1024
7277
+ };
7278
+ REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
7279
+ NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
7280
+ CROSS_ORIGIN_STRIPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
7281
+ BODY_HEADERS = ["content-type", "content-length", "content-encoding", "content-language", "content-location"];
7282
+ defaultLookup = async (hostname) => {
7283
+ const dns = await loadDns();
7284
+ if (!dns) return void 0;
7285
+ const records = await dns.lookup(hostname, { all: true, verbatim: true });
7286
+ return records.map((r) => r.address);
7287
+ };
7288
+ }
7289
+ });
7290
+
6820
7291
  // src/trust/CrlFetcher.ts
6821
7292
  function normalizeCrl(parsed, der) {
6822
7293
  const thisUpdate = parsed.tbsCertList.thisUpdate.getTime();
@@ -6852,11 +7323,12 @@ var init_CrlFetcher = __esm({
6852
7323
  init_es2015();
6853
7324
  init_es20152();
6854
7325
  init_ecdsa_util();
7326
+ init_guarded_fetch();
6855
7327
  CrlFetcher = class {
6856
7328
  fetcher;
6857
7329
  cache;
6858
7330
  constructor(opts = {}) {
6859
- this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
7331
+ this.fetcher = opts.fetcher ?? createDefaultTrustFetcher();
6860
7332
  this.cache = opts.cache;
6861
7333
  }
6862
7334
  async fetchAndParse(url) {
@@ -7588,12 +8060,13 @@ var init_OcspClient = __esm({
7588
8060
  init_es20152();
7589
8061
  init_ChainBuilder();
7590
8062
  init_ecdsa_util();
8063
+ init_guarded_fetch();
7591
8064
  DEFAULT_MAX_RESPONSE_AGE_MS = 7 * 24 * 3600 * 1e3;
7592
8065
  OcspClient = class {
7593
8066
  fetcher;
7594
8067
  cache;
7595
8068
  constructor(opts = {}) {
7596
- this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
8069
+ this.fetcher = opts.fetcher ?? createDefaultTrustFetcher();
7597
8070
  this.cache = opts.cache;
7598
8071
  }
7599
8072
  /**
@@ -8059,7 +8532,9 @@ var init_ProvenanceResolver = __esm({
8059
8532
  // src/trust/TrustEvaluator.ts
8060
8533
  var TrustEvaluator_exports = {};
8061
8534
  __export(TrustEvaluator_exports, {
8062
- TrustEvaluator: () => TrustEvaluator
8535
+ MAX_CREDENTIAL_CERTIFICATES: () => MAX_CREDENTIAL_CERTIFICATES,
8536
+ TrustEvaluator: () => TrustEvaluator,
8537
+ evaluateCredentialIssuer: () => evaluateCredentialIssuer
8063
8538
  });
8064
8539
  function deriveHint(leaf) {
8065
8540
  const aki = leaf.getExtension(AuthorityKeyIdentifierExtension);
@@ -8070,6 +8545,13 @@ function deriveHint(leaf) {
8070
8545
  aki: akiBytes
8071
8546
  };
8072
8547
  }
8548
+ function deriveSelfHint(cert) {
8549
+ const skiHex = getSkiHex(cert);
8550
+ return {
8551
+ issuer: cert.subject,
8552
+ aki: skiHex ? hexToBytes(skiHex) : void 0
8553
+ };
8554
+ }
8073
8555
  function hexToBytes(s) {
8074
8556
  const clean = s.replace(/[^0-9a-f]/gi, "");
8075
8557
  const out = new Uint8Array(clean.length / 2);
@@ -8101,7 +8583,31 @@ async function resolveProvenance(store, anchor) {
8101
8583
  return void 0;
8102
8584
  }
8103
8585
  }
8104
- var TrustEvaluator;
8586
+ async function evaluateCredentialIssuer(input) {
8587
+ const [signerDer, ...rest] = input.chain;
8588
+ if (!signerDer) {
8589
+ throw new MalformedCredentialError("Missing issuer certificate");
8590
+ }
8591
+ if (input.chain.length > MAX_CREDENTIAL_CERTIFICATES) {
8592
+ throw new MalformedCredentialError(
8593
+ `x5c/x5chain carries ${input.chain.length} certificates; at most ${MAX_CREDENTIAL_CERTIFICATES} are accepted`
8594
+ );
8595
+ }
8596
+ const parse = (bytes, position) => {
8597
+ try {
8598
+ return new X509Certificate(bytes);
8599
+ } catch (err) {
8600
+ throw new MalformedCredentialError(
8601
+ `Invalid certificate at x5c/x5chain position ${position}: ${err.message}`
8602
+ );
8603
+ }
8604
+ };
8605
+ const leaf = parse(signerDer, 0);
8606
+ const intermediates = rest.map((bytes, i) => parse(bytes, i + 1));
8607
+ const { chain, issuedAt, ...evaluatorOptions } = input;
8608
+ return new TrustEvaluator(evaluatorOptions).evaluate(leaf, { intermediates, issuedAt });
8609
+ }
8610
+ var TrustEvaluator, MAX_CREDENTIAL_CERTIFICATES;
8105
8611
  var init_TrustEvaluator = __esm({
8106
8612
  "src/trust/TrustEvaluator.ts"() {
8107
8613
  init_reflect_polyfill();
@@ -8127,13 +8633,10 @@ var init_TrustEvaluator = __esm({
8127
8633
  revocationChecker;
8128
8634
  async evaluate(leaf, context = {}) {
8129
8635
  const hint = deriveHint(leaf);
8130
- const seen = /* @__PURE__ */ new Set();
8131
8636
  const anchors = [];
8132
8637
  const pushUnique = (batch) => {
8133
8638
  for (const a of batch) {
8134
- const key = a.certificate.serialNumber;
8135
- if (seen.has(key)) continue;
8136
- seen.add(key);
8639
+ if (anchors.some((x) => certificatesEqual(x.certificate, a.certificate))) continue;
8137
8640
  anchors.push(a);
8138
8641
  }
8139
8642
  };
@@ -8141,6 +8644,7 @@ var init_TrustEvaluator = __esm({
8141
8644
  for (const inter of context.intermediates ?? []) {
8142
8645
  pushUnique(await this.trustStore.getAnchors(deriveHint(inter)));
8143
8646
  }
8647
+ pushUnique(await this.trustStore.getAnchors(deriveSelfHint(leaf)));
8144
8648
  if (anchors.length === 0) {
8145
8649
  throw new TrustAnchorNotFoundError(
8146
8650
  `no trust anchor for issuer=${hint.issuer ?? "(none)"}`
@@ -8150,7 +8654,8 @@ var init_TrustEvaluator = __esm({
8150
8654
  const chain = await this.chainBuilder.build(
8151
8655
  leaf,
8152
8656
  anchorCerts,
8153
- context.intermediates ?? []
8657
+ context.intermediates ?? [],
8658
+ { issuedAt: context.issuedAt }
8154
8659
  );
8155
8660
  const terminusCert = chain[chain.length - 1];
8156
8661
  const anchor = anchors.find(
@@ -8190,6 +8695,7 @@ var init_TrustEvaluator = __esm({
8190
8695
  };
8191
8696
  }
8192
8697
  };
8698
+ MAX_CREDENTIAL_CERTIFICATES = 10;
8193
8699
  }
8194
8700
  });
8195
8701
 
@@ -8422,6 +8928,33 @@ function buildHaipQuery(input) {
8422
8928
  }
8423
8929
  return { credentials: [credential] };
8424
8930
  }
8931
+ function buildCredentialSetQuery(input) {
8932
+ if (input.options.length === 0) {
8933
+ throw new HaipValidationError(
8934
+ "EMPTY_OPTIONS",
8935
+ "a credential set must offer at least one option"
8936
+ );
8937
+ }
8938
+ const seen = /* @__PURE__ */ new Set();
8939
+ for (const option of input.options) {
8940
+ if (seen.has(option.credentialId)) {
8941
+ throw new HaipValidationError(
8942
+ "DUPLICATE_CREDENTIAL_ID",
8943
+ `credential '${option.credentialId}' is declared by more than one option; credential ids must be unique for credential_sets to reference them`,
8944
+ option.credentialId
8945
+ );
8946
+ }
8947
+ seen.add(option.credentialId);
8948
+ }
8949
+ const credentials = input.options.map((option) => buildHaipQuery(option).credentials[0]);
8950
+ const credentialSet = {
8951
+ options: input.options.map((option) => [option.credentialId])
8952
+ };
8953
+ if (input.required === false) {
8954
+ credentialSet.required = false;
8955
+ }
8956
+ return { credentials, credential_sets: [credentialSet] };
8957
+ }
8425
8958
  var HAIP_FORMATS = /* @__PURE__ */ new Set(["dc+sd-jwt", "mso_mdoc"]);
8426
8959
  function validateHaipQuery(query) {
8427
8960
  if (!query.credentials || query.credentials.length === 0) {
@@ -8437,46 +8970,86 @@ function validateHaipQuery(query) {
8437
8970
  );
8438
8971
  }
8439
8972
  for (const credential of query.credentials) {
8440
- if (!HAIP_FORMATS.has(credential.format)) {
8973
+ validateCredential(credential);
8974
+ }
8975
+ }
8976
+ function validateCredential(credential) {
8977
+ if (!HAIP_FORMATS.has(credential.format)) {
8978
+ throw new HaipValidationError(
8979
+ "UNSUPPORTED_FORMAT",
8980
+ `credential '${credential.id}' uses unsupported format '${credential.format}' (allowed: dc+sd-jwt, mso_mdoc)`,
8981
+ credential.id
8982
+ );
8983
+ }
8984
+ if (credential.format === "dc+sd-jwt") {
8985
+ const vctValues = credential.meta?.vct_values;
8986
+ if (!vctValues || vctValues.length === 0) {
8441
8987
  throw new HaipValidationError(
8442
- "UNSUPPORTED_FORMAT",
8443
- `credential '${credential.id}' uses unsupported format '${credential.format}' (allowed: dc+sd-jwt, mso_mdoc)`,
8988
+ "MISSING_SDJWT_META",
8989
+ `credential '${credential.id}' with format 'dc+sd-jwt' must set meta.vct_values`,
8444
8990
  credential.id
8445
8991
  );
8446
8992
  }
8447
- if (credential.format === "dc+sd-jwt") {
8448
- const vctValues = credential.meta?.vct_values;
8449
- if (!vctValues || vctValues.length === 0) {
8450
- throw new HaipValidationError(
8451
- "MISSING_SDJWT_META",
8452
- `credential '${credential.id}' with format 'dc+sd-jwt' must set meta.vct_values`,
8453
- credential.id
8454
- );
8455
- }
8456
- }
8457
- if (credential.format === "mso_mdoc") {
8458
- if (typeof credential.meta?.doctype_value !== "string") {
8459
- throw new HaipValidationError(
8460
- "MISSING_MDOC_META",
8461
- `credential '${credential.id}' with format 'mso_mdoc' must set meta.doctype_value`,
8462
- credential.id
8463
- );
8464
- }
8465
- }
8466
- if (!credential.claims || credential.claims.length === 0) {
8993
+ }
8994
+ if (credential.format === "mso_mdoc") {
8995
+ if (typeof credential.meta?.doctype_value !== "string") {
8467
8996
  throw new HaipValidationError(
8468
- "NO_CLAIMS",
8469
- `credential '${credential.id}' must declare at least one claim`,
8997
+ "MISSING_MDOC_META",
8998
+ `credential '${credential.id}' with format 'mso_mdoc' must set meta.doctype_value`,
8470
8999
  credential.id
8471
9000
  );
8472
9001
  }
8473
- if (credential.claim_sets !== void 0) {
9002
+ }
9003
+ if (!credential.claims || credential.claims.length === 0) {
9004
+ throw new HaipValidationError(
9005
+ "NO_CLAIMS",
9006
+ `credential '${credential.id}' must declare at least one claim`,
9007
+ credential.id
9008
+ );
9009
+ }
9010
+ if (credential.claim_sets !== void 0) {
9011
+ throw new HaipValidationError(
9012
+ "CLAIM_SETS_DISALLOWED",
9013
+ `credential '${credential.id}' uses claim_sets, not permitted in HAIP-minimal`,
9014
+ credential.id
9015
+ );
9016
+ }
9017
+ }
9018
+ function validateCredentialSetQuery(query) {
9019
+ if (!query.credentials || query.credentials.length === 0) {
9020
+ throw new HaipValidationError(
9021
+ "EMPTY_QUERY",
9022
+ "query.credentials must have at least one entry"
9023
+ );
9024
+ }
9025
+ if (!query.credential_sets || query.credential_sets.length === 0) {
9026
+ throw new HaipValidationError(
9027
+ "MISSING_CREDENTIAL_SETS",
9028
+ "a credential-set query must declare at least one credential_sets entry"
9029
+ );
9030
+ }
9031
+ for (const credential of query.credentials) {
9032
+ validateCredential(credential);
9033
+ }
9034
+ const declaredIds = new Set(query.credentials.map((c) => c.id));
9035
+ for (const credentialSet of query.credential_sets) {
9036
+ if (!credentialSet.options || credentialSet.options.length === 0) {
8474
9037
  throw new HaipValidationError(
8475
- "CLAIM_SETS_DISALLOWED",
8476
- `credential '${credential.id}' uses claim_sets, not permitted in HAIP-minimal`,
8477
- credential.id
9038
+ "EMPTY_OPTIONS",
9039
+ "a credential set must offer at least one option"
8478
9040
  );
8479
9041
  }
9042
+ for (const option of credentialSet.options) {
9043
+ for (const id of option) {
9044
+ if (!declaredIds.has(id)) {
9045
+ throw new HaipValidationError(
9046
+ "UNKNOWN_OPTION_REFERENCE",
9047
+ `credential_sets option references '${id}', which is not declared in query.credentials`,
9048
+ id
9049
+ );
9050
+ }
9051
+ }
9052
+ }
8480
9053
  }
8481
9054
  }
8482
9055
  function isHaipQuery(query) {
@@ -8914,6 +9487,14 @@ function bytesEqual3(a, b) {
8914
9487
  }
8915
9488
  return true;
8916
9489
  }
9490
+ function peekMsoSigned(payload) {
9491
+ try {
9492
+ const signed = decodeMso(payload).validityInfo.signed;
9493
+ return Number.isFinite(signed.getTime()) ? signed : void 0;
9494
+ } catch {
9495
+ return void 0;
9496
+ }
9497
+ }
8917
9498
  function derToPem(der) {
8918
9499
  let binary = "";
8919
9500
  for (const b of der) binary += String.fromCharCode(b);
@@ -9074,17 +9655,17 @@ var MdocParser = class {
9074
9655
  }
9075
9656
  let trustResult;
9076
9657
  if (options.trustStore) {
9077
- const { TrustEvaluator: TrustEvaluator2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
9078
- const evaluator = new TrustEvaluator2({
9658
+ const { evaluateCredentialIssuer: evaluateCredentialIssuer2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
9659
+ trustResult = await evaluateCredentialIssuer2({
9660
+ chain: cose.x5chain,
9661
+ profile: "iso18013-5",
9662
+ issuedAt: peekMsoSigned(cose.payload),
9079
9663
  trustStore: options.trustStore,
9080
9664
  revocationPolicy: options.revocationPolicy ?? "skip",
9081
9665
  fetcher: options.fetcher,
9082
9666
  cache: options.cache,
9083
9667
  clockSkewTolerance: options.clockSkewTolerance
9084
9668
  });
9085
- const { X509Certificate: X509Certificate9 } = await import('@peculiar/x509');
9086
- const leaf = new X509Certificate9(issuerCertBytes);
9087
- trustResult = await evaluator.evaluate(leaf);
9088
9669
  } else {
9089
9670
  if (options.trustedCertificates.length === 0) {
9090
9671
  if (options.skipTrustCheck !== true) {
@@ -9233,6 +9814,13 @@ var MdocParser = class {
9233
9814
  init_reflect_polyfill();
9234
9815
  init_errors();
9235
9816
  var DEFAULT_ALLOWED_ALGORITHMS2 = ["ES256", "ES384", "ES512"];
9817
+ var SD_JWT_VC_TYP = "dc+sd-jwt";
9818
+ var LEGACY_SD_JWT_VC_TYP = "vc+sd-jwt";
9819
+ function normalizeTyp(typ) {
9820
+ if (typeof typ !== "string") return void 0;
9821
+ const lower = typ.toLowerCase();
9822
+ return lower.startsWith("application/") ? lower.slice("application/".length) : lower;
9823
+ }
9236
9824
  var sha256Hasher = async (data, _alg) => {
9237
9825
  const encoder2 = new TextEncoder();
9238
9826
  const input = typeof data === "string" ? encoder2.encode(data) : data;
@@ -9345,6 +9933,18 @@ var SdJwtParser = class {
9345
9933
  if (typeof alg !== "string" || !allowedAlgorithms.includes(alg)) {
9346
9934
  return invalidResult(`Unsupported algorithm: ${alg}`);
9347
9935
  }
9936
+ const typ = normalizeTyp(header.typ);
9937
+ if (typ === LEGACY_SD_JWT_VC_TYP) {
9938
+ if (options.allowLegacyVcSdJwtTyp !== true) {
9939
+ return invalidResult(
9940
+ `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.`
9941
+ );
9942
+ }
9943
+ } else if (typ !== SD_JWT_VC_TYP) {
9944
+ return invalidResult(
9945
+ `Unsupported SD-JWT VC typ: ${header.typ === void 0 ? "(missing)" : String(header.typ)} (expected '${SD_JWT_VC_TYP}')`
9946
+ );
9947
+ }
9348
9948
  const x5cArray = header.x5c;
9349
9949
  const hasX5c = Array.isArray(x5cArray) && x5cArray.length > 0 && typeof x5cArray[0] === "string";
9350
9950
  let issuerKey;
@@ -9414,17 +10014,22 @@ var SdJwtParser = class {
9414
10014
  };
9415
10015
  let trustResult;
9416
10016
  if (issuerCertBytes === void 0) ; else if (options.trustStore) {
9417
- const { TrustEvaluator: TrustEvaluator2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
9418
- const evaluator = new TrustEvaluator2({
10017
+ const { evaluateCredentialIssuer: evaluateCredentialIssuer2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
10018
+ const x5cRest = x5cArray.slice(1);
10019
+ if (!x5cRest.every((entry) => typeof entry === "string")) {
10020
+ throw new MalformedCredentialError("x5c entries must be base64 strings");
10021
+ }
10022
+ const iat = payload["iat"];
10023
+ trustResult = await evaluateCredentialIssuer2({
10024
+ chain: [issuerCertBytes, ...x5cRest.map((entry) => base64ToBytes(entry))],
10025
+ profile: "generic",
10026
+ issuedAt: typeof iat === "number" && Number.isFinite(iat) ? new Date(iat * 1e3) : void 0,
9419
10027
  trustStore: options.trustStore,
9420
10028
  revocationPolicy: options.revocationPolicy ?? "skip",
9421
10029
  fetcher: options.fetcher,
9422
10030
  cache: options.cache,
9423
10031
  clockSkewTolerance: options.clockSkewTolerance
9424
10032
  });
9425
- const { X509Certificate: X509Certificate9 } = await import('@peculiar/x509');
9426
- const leaf = new X509Certificate9(issuerCertBytes);
9427
- trustResult = await evaluator.evaluate(leaf);
9428
10033
  } else {
9429
10034
  if (options.trustedCertificates.length === 0) {
9430
10035
  if (options.skipTrustCheck !== true) {
@@ -9860,6 +10465,9 @@ var InMemoryCache = class {
9860
10465
  }
9861
10466
  };
9862
10467
 
10468
+ // src/index.ts
10469
+ init_guarded_fetch();
10470
+
9863
10471
  // src/trust/index.ts
9864
10472
  init_reflect_polyfill();
9865
10473
 
@@ -9907,14 +10515,10 @@ var CompositeTrustStore = class {
9907
10515
  const results = await Promise.all(
9908
10516
  this.stores.map((store) => store.getAnchors(hint))
9909
10517
  );
9910
- const seen = /* @__PURE__ */ new Set();
9911
10518
  const out = [];
9912
10519
  for (const batch of results) {
9913
10520
  for (const anchor of batch) {
9914
- const ski = getSkiHex(anchor.certificate);
9915
- const key = ski ?? anchor.certificate.serialNumber;
9916
- if (seen.has(key)) continue;
9917
- seen.add(key);
10521
+ if (out.some((a) => certificatesEqual(a.certificate, anchor.certificate))) continue;
9918
10522
  out.push(anchor);
9919
10523
  }
9920
10524
  }
@@ -9946,6 +10550,7 @@ function bytesToHex3(bytes) {
9946
10550
 
9947
10551
  // src/trust/LotlTrustStore.ts
9948
10552
  init_reflect_polyfill();
10553
+ init_errors();
9949
10554
 
9950
10555
  // src/trust/lotl-signing-anchors.ts
9951
10556
  init_reflect_polyfill();
@@ -10007,13 +10612,15 @@ var EU_LOTL_SIGNING_ANCHORS = Object.freeze(
10007
10612
  // src/trust/LotlFetcher.ts
10008
10613
  init_reflect_polyfill();
10009
10614
  init_errors();
10615
+ init_guarded_fetch();
10010
10616
  var XMLDSIG_NS = "http://www.w3.org/2000/09/xmldsig#";
10011
10617
  var LotlFetcher = class {
10012
10618
  fetcher;
10013
10619
  constructor(opts = {}) {
10014
- this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
10620
+ this.fetcher = opts.fetcher ?? createDefaultTrustFetcher();
10015
10621
  }
10016
10622
  async fetchSigned(url, signingAnchors) {
10623
+ assertCryptoEngineRegistered();
10017
10624
  let xmlText;
10018
10625
  try {
10019
10626
  const response = await this.fetcher(url, { method: "GET" });
@@ -10066,6 +10673,22 @@ var LotlFetcher = class {
10066
10673
  );
10067
10674
  }
10068
10675
  };
10676
+ function assertCryptoEngineRegistered() {
10677
+ let engine;
10678
+ try {
10679
+ engine = xmldsig.Application.crypto;
10680
+ } catch (err) {
10681
+ throw new LotlConfigurationError(
10682
+ `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)}`,
10683
+ { cause: err instanceof Error ? err : void 0 }
10684
+ );
10685
+ }
10686
+ if (!engine) {
10687
+ throw new LotlConfigurationError(
10688
+ "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."
10689
+ );
10690
+ }
10691
+ }
10069
10692
  async function verifyAgainst(doc, sigEl, anchor) {
10070
10693
  try {
10071
10694
  const signed = new xmldsig.SignedXml(doc);
@@ -10213,6 +10836,7 @@ function parsePointer(el) {
10213
10836
 
10214
10837
  // src/trust/NationalTlResolver.ts
10215
10838
  init_reflect_polyfill();
10839
+ init_errors();
10216
10840
  var TSL_NS2 = "http://uri.etsi.org/02231/v2#";
10217
10841
  var NationalTlResolver = class {
10218
10842
  constructor(opts) {
@@ -10228,6 +10852,7 @@ var NationalTlResolver = class {
10228
10852
  );
10229
10853
  return parseNationalTl(doc, pointer.country);
10230
10854
  } catch (err) {
10855
+ if (err instanceof LotlConfigurationError) throw err;
10231
10856
  console.warn(
10232
10857
  `[openid4vp] national TL for ${pointer.country} failed: ${err.message}`
10233
10858
  );
@@ -10406,6 +11031,7 @@ var LotlTrustStore = class {
10406
11031
  };
10407
11032
  return this.snapshot;
10408
11033
  } catch (err) {
11034
+ if (err instanceof LotlConfigurationError) throw err;
10409
11035
  if (this.snapshot) {
10410
11036
  console.warn(
10411
11037
  `[openid4vp] LOTL refresh failed; serving cached snapshot (${err.message})`
@@ -10453,7 +11079,7 @@ function equalBytes(a, b) {
10453
11079
 
10454
11080
  // src/index.ts
10455
11081
  init_errors();
10456
- var VERSION = "0.11.1";
11082
+ var VERSION = "0.13.0";
10457
11083
  /*! Bundled license information:
10458
11084
 
10459
11085
  pvtsutils/build/index.es.js:
@@ -10523,6 +11149,6 @@ asn1js/build/index.es.js:
10523
11149
  *)
10524
11150
  */
10525
11151
 
10526
- export { CertificateChainError, CompositeTrustStore, DecryptionFailedError, ExpiredCredentialError, HAIP_DOCTYPE_NAMESPACES, HaipValidationError, InMemoryCache, InvalidSignatureError, LotlFetchError, LotlSignatureError, LotlTrustStore, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MissingVerifierEncryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, SignedRequestBuildError, StaticTrustStore, TrustAnchorNotFoundError, UnsupportedFormatError, UnsupportedJweError, VERSION, buildHaipQuery, buildOid4vpSessionTranscript, buildOpenID4VPHandoverSessionTranscript, createAuthorizationRequest, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
11152
+ export { CertificateChainError, CompositeTrustStore, DecryptionFailedError, ExpiredCredentialError, GUARDED_FETCH_DEFAULTS, GuardedFetchError, HAIP_DOCTYPE_NAMESPACES, HaipValidationError, InMemoryCache, InvalidSignatureError, LotlConfigurationError, LotlFetchError, LotlSignatureError, LotlTrustStore, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MissingVerifierEncryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, SignedRequestBuildError, StaticTrustStore, TrustAnchorNotFoundError, UnsupportedFormatError, UnsupportedJweError, VERSION, buildCredentialSetQuery, buildHaipQuery, buildOid4vpSessionTranscript, buildOpenID4VPHandoverSessionTranscript, createAuthorizationRequest, createGuardedFetcher, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateCredentialSetQuery, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
10527
11153
  //# sourceMappingURL=index.js.map
10528
11154
  //# sourceMappingURL=index.js.map