@openeudi/openid4vp 0.7.0 → 0.8.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.d.cts CHANGED
@@ -317,6 +317,27 @@ interface ParseOptions {
317
317
  cache?: Cache;
318
318
  /** Clock-skew tolerance in seconds for certificate validity checks. Default 60. */
319
319
  clockSkewTolerance?: number;
320
+ /**
321
+ * An explicit set of trusted issuer JWKs used as an alternate trust path when the
322
+ * SD-JWT VC's issuer JWT lacks an `x5c` header. When the header has no `x5c`:
323
+ * - If this array is provided, the parser looks up the issuer key by `kid`
324
+ * (or by exact `kty`/`crv`/`x`/`y` match when no `kid` is present). The
325
+ * matched JWK is the trust anchor — cert-chain validation is skipped.
326
+ * - If this array is absent (or empty), the parser falls back to the existing
327
+ * behaviour and throws `MalformedCredentialError('Missing or invalid x5c in JWT header')`.
328
+ *
329
+ * **When the JWT header DOES include `x5c`** this option is ignored — the existing
330
+ * x5c/`trustedCertificates` / `trustStore` path runs unchanged.
331
+ *
332
+ * **Security notice**: opting in to this path skips certificate-chain trust
333
+ * evaluation entirely. The caller is explicitly asserting that they trust the
334
+ * supplied JWK(s) out-of-band. This is appropriate for CI harness setups (e.g.
335
+ * OIDF conformance suite, where the wallet signs without x5c and the verifier
336
+ * knows the signing key from the test-plan config). It is **not recommended for
337
+ * production verifiers** — cert-chain trust evaluation via `trustStore` is the
338
+ * secure path.
339
+ */
340
+ trustedIssuerJwks?: JsonWebKey[];
320
341
  }
321
342
  interface ICredentialParser {
322
343
  readonly format: CredentialFormat;
@@ -555,6 +576,6 @@ declare class LotlTrustStore implements TrustStore {
555
576
  private refresh;
556
577
  }
557
578
 
558
- declare const VERSION = "0.7.0";
579
+ declare const VERSION = "0.8.0";
559
580
 
560
581
  export { type AuthorizationRequest, type AuthorizationRequestInput, type AuthorizationResponse, type Cache, CertificateChainError, type ChainErrorReason, CompositeTrustStore, type CredentialClaims, type CredentialFormat, DecryptionFailedError, type EncryptedResponse, ExpiredCredentialError, type Fetcher, HAIP_DOCTYPE_NAMESPACES, type HaipQueryInput, type HaipValidationCode, HaipValidationError, type ICredentialParser, InMemoryCache, InvalidSignatureError, type IssuerInfo, type LotlAnchorMetadata, LotlFetchError, LotlSignatureError, LotlTrustStore, type LotlTrustStoreOptions, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, type ParseOptions, type PresentationResult, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, type SignedAuthorizationRequest, type SignedAuthorizationRequestInput, SignedRequestBuildError, type SignedRequestBuildErrorCode, StaticTrustStore, type TrustAnchor, TrustAnchorNotFoundError, type TrustStore, type TrustStoreHint, type TrustStoreInput, UnsupportedFormatError, UnsupportedJweError, VERSION, type VerifyAuthorizationResponseOptions, type VerifyOptions, type VerifyResult, buildHaipQuery, createAuthorizationRequest, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
package/dist/index.d.ts CHANGED
@@ -317,6 +317,27 @@ interface ParseOptions {
317
317
  cache?: Cache;
318
318
  /** Clock-skew tolerance in seconds for certificate validity checks. Default 60. */
319
319
  clockSkewTolerance?: number;
320
+ /**
321
+ * An explicit set of trusted issuer JWKs used as an alternate trust path when the
322
+ * SD-JWT VC's issuer JWT lacks an `x5c` header. When the header has no `x5c`:
323
+ * - If this array is provided, the parser looks up the issuer key by `kid`
324
+ * (or by exact `kty`/`crv`/`x`/`y` match when no `kid` is present). The
325
+ * matched JWK is the trust anchor — cert-chain validation is skipped.
326
+ * - If this array is absent (or empty), the parser falls back to the existing
327
+ * behaviour and throws `MalformedCredentialError('Missing or invalid x5c in JWT header')`.
328
+ *
329
+ * **When the JWT header DOES include `x5c`** this option is ignored — the existing
330
+ * x5c/`trustedCertificates` / `trustStore` path runs unchanged.
331
+ *
332
+ * **Security notice**: opting in to this path skips certificate-chain trust
333
+ * evaluation entirely. The caller is explicitly asserting that they trust the
334
+ * supplied JWK(s) out-of-band. This is appropriate for CI harness setups (e.g.
335
+ * OIDF conformance suite, where the wallet signs without x5c and the verifier
336
+ * knows the signing key from the test-plan config). It is **not recommended for
337
+ * production verifiers** — cert-chain trust evaluation via `trustStore` is the
338
+ * secure path.
339
+ */
340
+ trustedIssuerJwks?: JsonWebKey[];
320
341
  }
321
342
  interface ICredentialParser {
322
343
  readonly format: CredentialFormat;
@@ -555,6 +576,6 @@ declare class LotlTrustStore implements TrustStore {
555
576
  private refresh;
556
577
  }
557
578
 
558
- declare const VERSION = "0.7.0";
579
+ declare const VERSION = "0.8.0";
559
580
 
560
581
  export { type AuthorizationRequest, type AuthorizationRequestInput, type AuthorizationResponse, type Cache, CertificateChainError, type ChainErrorReason, CompositeTrustStore, type CredentialClaims, type CredentialFormat, DecryptionFailedError, type EncryptedResponse, ExpiredCredentialError, type Fetcher, HAIP_DOCTYPE_NAMESPACES, type HaipQueryInput, type HaipValidationCode, HaipValidationError, type ICredentialParser, InMemoryCache, InvalidSignatureError, type IssuerInfo, type LotlAnchorMetadata, LotlFetchError, LotlSignatureError, LotlTrustStore, type LotlTrustStoreOptions, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, type ParseOptions, type PresentationResult, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, type SignedAuthorizationRequest, type SignedAuthorizationRequestInput, SignedRequestBuildError, type SignedRequestBuildErrorCode, StaticTrustStore, type TrustAnchor, TrustAnchorNotFoundError, type TrustStore, type TrustStoreHint, type TrustStoreInput, UnsupportedFormatError, UnsupportedJweError, VERSION, type VerifyAuthorizationResponseOptions, type VerifyOptions, type VerifyResult, buildHaipQuery, createAuthorizationRequest, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
package/dist/index.js CHANGED
@@ -8138,7 +8138,10 @@ async function createSignedAuthorizationRequest(input, query) {
8138
8138
  }
8139
8139
  const jwk = { ...input.encryptionKey.publicJwk, use: "enc" };
8140
8140
  clientMetadata.jwks = { keys: [jwk] };
8141
- clientMetadata.encrypted_response_enc_values_supported = enc ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
8141
+ const encValues = enc ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
8142
+ clientMetadata.encrypted_response_enc_values_supported = encValues;
8143
+ clientMetadata.authorization_encrypted_response_alg = "ECDH-ES";
8144
+ clientMetadata.authorization_encrypted_response_enc = encValues[0];
8142
8145
  }
8143
8146
  const payload = {
8144
8147
  iss: clientId,
@@ -8397,7 +8400,7 @@ async function verifyCoseSign1(coseSign1, publicKey, allowedAlgorithms) {
8397
8400
  throw new MalformedCredentialError(`no hash mapping for alg ${coseSign1.alg}`);
8398
8401
  }
8399
8402
  const sigStructure = ["Signature1", coseSign1.protectedHeaderBytes, new Uint8Array(0), coseSign1.payload];
8400
- const sigInput = decoder.encode(sigStructure);
8403
+ const sigInput = new Uint8Array(decoder.encode(sigStructure));
8401
8404
  const ok = await crypto.subtle.verify(
8402
8405
  { name: "ECDSA", hash: hashAlg },
8403
8406
  publicKey,
@@ -8936,32 +8939,74 @@ var SdJwtParser = class {
8936
8939
  return invalidResult(`Unsupported algorithm: ${alg}`);
8937
8940
  }
8938
8941
  const x5cArray = header.x5c;
8939
- if (!Array.isArray(x5cArray) || x5cArray.length === 0 || typeof x5cArray[0] !== "string") {
8940
- throw new MalformedCredentialError("Missing or invalid x5c in JWT header");
8941
- }
8942
+ const hasX5c = Array.isArray(x5cArray) && x5cArray.length > 0 && typeof x5cArray[0] === "string";
8942
8943
  let issuerKey;
8943
8944
  let issuerCertBytes;
8944
- try {
8945
- issuerKey = await importX509(derToPem2(x5cArray[0]), alg);
8946
- issuerCertBytes = base64ToBytes(x5cArray[0]);
8947
- } catch (err) {
8948
- if (err instanceof MalformedCredentialError) throw err;
8949
- throw new MalformedCredentialError("Failed to import issuer certificate");
8945
+ if (hasX5c) {
8946
+ try {
8947
+ issuerKey = await importX509(derToPem2(x5cArray[0]), alg);
8948
+ issuerCertBytes = base64ToBytes(x5cArray[0]);
8949
+ } catch (err) {
8950
+ if (err instanceof MalformedCredentialError) throw err;
8951
+ throw new MalformedCredentialError("Failed to import issuer certificate");
8952
+ }
8953
+ } else if (options.trustedIssuerJwks && options.trustedIssuerJwks.length > 0) {
8954
+ issuerKey = void 0;
8955
+ } else {
8956
+ throw new MalformedCredentialError("Missing or invalid x5c in JWT header");
8950
8957
  }
8951
8958
  let payload;
8952
- try {
8953
- const result2 = await jwtVerify(parts.jwt, issuerKey, { algorithms: allowedAlgorithms });
8954
- payload = result2.payload;
8955
- } catch {
8956
- return invalidResult("Issuer JWT signature verification failed");
8959
+ if (issuerKey) {
8960
+ try {
8961
+ const result2 = await jwtVerify(parts.jwt, issuerKey, { algorithms: allowedAlgorithms });
8962
+ payload = result2.payload;
8963
+ } catch {
8964
+ return invalidResult("Issuer JWT signature verification failed");
8965
+ }
8966
+ } else {
8967
+ const jwks = options.trustedIssuerJwks;
8968
+ const headerKid = header["kid"];
8969
+ let candidates;
8970
+ if (typeof headerKid === "string") {
8971
+ const matched = jwks.find((k) => k["kid"] === headerKid);
8972
+ if (!matched) {
8973
+ return invalidResult(
8974
+ `No matching JWK found in trustedIssuerJwks for kid=${headerKid}`
8975
+ );
8976
+ }
8977
+ candidates = [matched];
8978
+ } else {
8979
+ candidates = jwks;
8980
+ }
8981
+ let verifiedPayload;
8982
+ for (const jwk of candidates) {
8983
+ let candidateKey;
8984
+ try {
8985
+ candidateKey = await importJWK(jwk, alg);
8986
+ } catch {
8987
+ continue;
8988
+ }
8989
+ try {
8990
+ const result2 = await jwtVerify(parts.jwt, candidateKey, {
8991
+ algorithms: allowedAlgorithms
8992
+ });
8993
+ verifiedPayload = result2.payload;
8994
+ break;
8995
+ } catch {
8996
+ }
8997
+ }
8998
+ if (!verifiedPayload) {
8999
+ return invalidResult("Issuer JWT signature verification failed");
9000
+ }
9001
+ payload = verifiedPayload;
8957
9002
  }
8958
9003
  const issuerString = typeof payload["iss"] === "string" ? payload["iss"] : "";
8959
9004
  const issuerInfo = {
8960
- certificate: issuerCertBytes,
9005
+ certificate: issuerCertBytes ?? new Uint8Array(),
8961
9006
  country: extractCountryHint(issuerString)
8962
9007
  };
8963
9008
  let trustResult;
8964
- if (options.trustStore) {
9009
+ if (issuerCertBytes === void 0) ; else if (options.trustStore) {
8965
9010
  const { TrustEvaluator: TrustEvaluator2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
8966
9011
  const evaluator = new TrustEvaluator2({
8967
9012
  trustStore: options.trustStore,
@@ -8981,24 +9026,56 @@ var SdJwtParser = class {
8981
9026
  );
8982
9027
  }
8983
9028
  } else {
8984
- const isTrusted = options.trustedCertificates.some((trusted) => bytesEqual4(trusted, issuerCertBytes));
9029
+ const isTrusted = options.trustedCertificates.some(
9030
+ (trusted) => bytesEqual4(trusted, issuerCertBytes)
9031
+ );
8985
9032
  if (!isTrusted) {
8986
9033
  return invalidResult("Issuer certificate is not trusted");
8987
9034
  }
8988
9035
  }
8989
9036
  }
8990
- const sdArray = payload["_sd"];
8991
9037
  if (parts.disclosures.length > 0) {
8992
- if (!Array.isArray(sdArray) || sdArray.length === 0) {
8993
- return invalidResult("Disclosures present but no _sd array in issuer JWT");
8994
- }
9038
+ const decodedDisclosures = [];
8995
9039
  for (const disclosure of parts.disclosures) {
9040
+ let parsed;
9041
+ try {
9042
+ parsed = JSON.parse(new TextDecoder().decode(base64ToBytes(disclosure)));
9043
+ } catch {
9044
+ return invalidResult("Disclosure is not valid base64url-encoded JSON");
9045
+ }
9046
+ if (!Array.isArray(parsed) || parsed.length !== 2 && parsed.length !== 3) {
9047
+ return invalidResult("Disclosure must be a 2- or 3-element array");
9048
+ }
8996
9049
  const hashBytes = await sha256Hasher(disclosure);
8997
- const hashB64url = bytesToBase64url(hashBytes);
8998
- if (!sdArray.includes(hashB64url)) {
8999
- return invalidResult("Disclosure hash mismatch");
9050
+ decodedDisclosures.push({
9051
+ raw: disclosure,
9052
+ hash: bytesToBase64url(hashBytes),
9053
+ length: parsed.length,
9054
+ value: parsed.length === 3 ? parsed[2] : parsed[1]
9055
+ });
9056
+ }
9057
+ const seed = collectAnchorHashes(payload);
9058
+ const kvHashes = seed.kvHashes;
9059
+ const arrayElemHashes = seed.arrayElemHashes;
9060
+ const remaining = [...decodedDisclosures];
9061
+ for (; ; ) {
9062
+ const anchored = [];
9063
+ for (const d of remaining) {
9064
+ const valid = d.length === 3 ? kvHashes.has(d.hash) : arrayElemHashes.has(d.hash);
9065
+ if (valid) anchored.push(d);
9066
+ }
9067
+ if (anchored.length === 0) break;
9068
+ for (const a of anchored) {
9069
+ const sub = collectAnchorHashes(a.value);
9070
+ for (const h of sub.kvHashes) kvHashes.add(h);
9071
+ for (const h of sub.arrayElemHashes) arrayElemHashes.add(h);
9072
+ const idx = remaining.indexOf(a);
9073
+ if (idx >= 0) remaining.splice(idx, 1);
9000
9074
  }
9001
9075
  }
9076
+ if (remaining.length > 0) {
9077
+ return invalidResult("Disclosure hash mismatch");
9078
+ }
9002
9079
  }
9003
9080
  if (parts.kbJwt) {
9004
9081
  const cnf = payload["cnf"];
@@ -9061,6 +9138,35 @@ var SdJwtParser = class {
9061
9138
  return result;
9062
9139
  }
9063
9140
  };
9141
+ function collectAnchorHashes(node) {
9142
+ const kvHashes = /* @__PURE__ */ new Set();
9143
+ const arrayElemHashes = /* @__PURE__ */ new Set();
9144
+ const visit = (value) => {
9145
+ if (Array.isArray(value)) {
9146
+ for (const item of value) {
9147
+ if (item && typeof item === "object" && !Array.isArray(item)) {
9148
+ const obj = item;
9149
+ const keys = Object.keys(obj);
9150
+ if (keys.length === 1 && keys[0] === "..." && typeof obj["..."] === "string") {
9151
+ arrayElemHashes.add(obj["..."]);
9152
+ continue;
9153
+ }
9154
+ }
9155
+ visit(item);
9156
+ }
9157
+ } else if (value && typeof value === "object") {
9158
+ for (const [k, v] of Object.entries(value)) {
9159
+ if (k === "_sd" && Array.isArray(v)) {
9160
+ for (const h of v) if (typeof h === "string") kvHashes.add(h);
9161
+ continue;
9162
+ }
9163
+ visit(v);
9164
+ }
9165
+ }
9166
+ };
9167
+ visit(node);
9168
+ return { kvHashes, arrayElemHashes };
9169
+ }
9064
9170
 
9065
9171
  // src/presentation.ts
9066
9172
  var parsers = [new SdJwtParser(), new MdocParser()];
@@ -9800,7 +9906,7 @@ function equalBytes(a, b) {
9800
9906
 
9801
9907
  // src/index.ts
9802
9908
  init_errors();
9803
- var VERSION = "0.7.0";
9909
+ var VERSION = "0.8.0";
9804
9910
  /*! Bundled license information:
9805
9911
 
9806
9912
  pvtsutils/build/index.es.js: