@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/README.md CHANGED
@@ -338,6 +338,10 @@ This library implements the **verifier side** of OpenID4VP for SD-JWT VC and mDO
338
338
 
339
339
  EUDI Architecture Reference Framework (ARF) alignment: tracks OpenID4VP 1.0 final. Full ARF 1.4+ profile compliance will be added before a stable 1.0.
340
340
 
341
+ ## OIDF Conformance Testing
342
+
343
+ Verifier-side conformance is automated against a self-hosted OpenID Foundation conformance suite. See `docs/manual-testing/oidf-interop.md` for both the CI orchestrator (`npm run oidf:ci -- --profile=happy-flow|full`) and the manual hosted-demo escape hatch.
344
+
341
345
  ## Related packages
342
346
 
343
347
  - **[@openeudi/core](https://www.npmjs.com/package/@openeudi/core)** -- Framework-agnostic EUDI Wallet verification protocol engine with session management and QR code generation.
package/dist/index.cjs CHANGED
@@ -8159,7 +8159,10 @@ async function createSignedAuthorizationRequest(input, query) {
8159
8159
  }
8160
8160
  const jwk = { ...input.encryptionKey.publicJwk, use: "enc" };
8161
8161
  clientMetadata.jwks = { keys: [jwk] };
8162
- clientMetadata.encrypted_response_enc_values_supported = enc ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
8162
+ const encValues = enc ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
8163
+ clientMetadata.encrypted_response_enc_values_supported = encValues;
8164
+ clientMetadata.authorization_encrypted_response_alg = "ECDH-ES";
8165
+ clientMetadata.authorization_encrypted_response_enc = encValues[0];
8163
8166
  }
8164
8167
  const payload = {
8165
8168
  iss: clientId,
@@ -8418,7 +8421,7 @@ async function verifyCoseSign1(coseSign1, publicKey, allowedAlgorithms) {
8418
8421
  throw new exports.MalformedCredentialError(`no hash mapping for alg ${coseSign1.alg}`);
8419
8422
  }
8420
8423
  const sigStructure = ["Signature1", coseSign1.protectedHeaderBytes, new Uint8Array(0), coseSign1.payload];
8421
- const sigInput = decoder.encode(sigStructure);
8424
+ const sigInput = new Uint8Array(decoder.encode(sigStructure));
8422
8425
  const ok = await crypto.subtle.verify(
8423
8426
  { name: "ECDSA", hash: hashAlg },
8424
8427
  publicKey,
@@ -8957,32 +8960,74 @@ var SdJwtParser = class {
8957
8960
  return invalidResult(`Unsupported algorithm: ${alg}`);
8958
8961
  }
8959
8962
  const x5cArray = header.x5c;
8960
- if (!Array.isArray(x5cArray) || x5cArray.length === 0 || typeof x5cArray[0] !== "string") {
8961
- throw new exports.MalformedCredentialError("Missing or invalid x5c in JWT header");
8962
- }
8963
+ const hasX5c = Array.isArray(x5cArray) && x5cArray.length > 0 && typeof x5cArray[0] === "string";
8963
8964
  let issuerKey;
8964
8965
  let issuerCertBytes;
8965
- try {
8966
- issuerKey = await jose.importX509(derToPem2(x5cArray[0]), alg);
8967
- issuerCertBytes = base64ToBytes(x5cArray[0]);
8968
- } catch (err) {
8969
- if (err instanceof exports.MalformedCredentialError) throw err;
8970
- throw new exports.MalformedCredentialError("Failed to import issuer certificate");
8966
+ if (hasX5c) {
8967
+ try {
8968
+ issuerKey = await jose.importX509(derToPem2(x5cArray[0]), alg);
8969
+ issuerCertBytes = base64ToBytes(x5cArray[0]);
8970
+ } catch (err) {
8971
+ if (err instanceof exports.MalformedCredentialError) throw err;
8972
+ throw new exports.MalformedCredentialError("Failed to import issuer certificate");
8973
+ }
8974
+ } else if (options.trustedIssuerJwks && options.trustedIssuerJwks.length > 0) {
8975
+ issuerKey = void 0;
8976
+ } else {
8977
+ throw new exports.MalformedCredentialError("Missing or invalid x5c in JWT header");
8971
8978
  }
8972
8979
  let payload;
8973
- try {
8974
- const result2 = await jose.jwtVerify(parts.jwt, issuerKey, { algorithms: allowedAlgorithms });
8975
- payload = result2.payload;
8976
- } catch {
8977
- return invalidResult("Issuer JWT signature verification failed");
8980
+ if (issuerKey) {
8981
+ try {
8982
+ const result2 = await jose.jwtVerify(parts.jwt, issuerKey, { algorithms: allowedAlgorithms });
8983
+ payload = result2.payload;
8984
+ } catch {
8985
+ return invalidResult("Issuer JWT signature verification failed");
8986
+ }
8987
+ } else {
8988
+ const jwks = options.trustedIssuerJwks;
8989
+ const headerKid = header["kid"];
8990
+ let candidates;
8991
+ if (typeof headerKid === "string") {
8992
+ const matched = jwks.find((k) => k["kid"] === headerKid);
8993
+ if (!matched) {
8994
+ return invalidResult(
8995
+ `No matching JWK found in trustedIssuerJwks for kid=${headerKid}`
8996
+ );
8997
+ }
8998
+ candidates = [matched];
8999
+ } else {
9000
+ candidates = jwks;
9001
+ }
9002
+ let verifiedPayload;
9003
+ for (const jwk of candidates) {
9004
+ let candidateKey;
9005
+ try {
9006
+ candidateKey = await jose.importJWK(jwk, alg);
9007
+ } catch {
9008
+ continue;
9009
+ }
9010
+ try {
9011
+ const result2 = await jose.jwtVerify(parts.jwt, candidateKey, {
9012
+ algorithms: allowedAlgorithms
9013
+ });
9014
+ verifiedPayload = result2.payload;
9015
+ break;
9016
+ } catch {
9017
+ }
9018
+ }
9019
+ if (!verifiedPayload) {
9020
+ return invalidResult("Issuer JWT signature verification failed");
9021
+ }
9022
+ payload = verifiedPayload;
8978
9023
  }
8979
9024
  const issuerString = typeof payload["iss"] === "string" ? payload["iss"] : "";
8980
9025
  const issuerInfo = {
8981
- certificate: issuerCertBytes,
9026
+ certificate: issuerCertBytes ?? new Uint8Array(),
8982
9027
  country: extractCountryHint(issuerString)
8983
9028
  };
8984
9029
  let trustResult;
8985
- if (options.trustStore) {
9030
+ if (issuerCertBytes === void 0) ; else if (options.trustStore) {
8986
9031
  const { TrustEvaluator: TrustEvaluator2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
8987
9032
  const evaluator = new TrustEvaluator2({
8988
9033
  trustStore: options.trustStore,
@@ -9002,24 +9047,56 @@ var SdJwtParser = class {
9002
9047
  );
9003
9048
  }
9004
9049
  } else {
9005
- const isTrusted = options.trustedCertificates.some((trusted) => bytesEqual4(trusted, issuerCertBytes));
9050
+ const isTrusted = options.trustedCertificates.some(
9051
+ (trusted) => bytesEqual4(trusted, issuerCertBytes)
9052
+ );
9006
9053
  if (!isTrusted) {
9007
9054
  return invalidResult("Issuer certificate is not trusted");
9008
9055
  }
9009
9056
  }
9010
9057
  }
9011
- const sdArray = payload["_sd"];
9012
9058
  if (parts.disclosures.length > 0) {
9013
- if (!Array.isArray(sdArray) || sdArray.length === 0) {
9014
- return invalidResult("Disclosures present but no _sd array in issuer JWT");
9015
- }
9059
+ const decodedDisclosures = [];
9016
9060
  for (const disclosure of parts.disclosures) {
9061
+ let parsed;
9062
+ try {
9063
+ parsed = JSON.parse(new TextDecoder().decode(base64ToBytes(disclosure)));
9064
+ } catch {
9065
+ return invalidResult("Disclosure is not valid base64url-encoded JSON");
9066
+ }
9067
+ if (!Array.isArray(parsed) || parsed.length !== 2 && parsed.length !== 3) {
9068
+ return invalidResult("Disclosure must be a 2- or 3-element array");
9069
+ }
9017
9070
  const hashBytes = await sha256Hasher(disclosure);
9018
- const hashB64url = bytesToBase64url(hashBytes);
9019
- if (!sdArray.includes(hashB64url)) {
9020
- return invalidResult("Disclosure hash mismatch");
9071
+ decodedDisclosures.push({
9072
+ raw: disclosure,
9073
+ hash: bytesToBase64url(hashBytes),
9074
+ length: parsed.length,
9075
+ value: parsed.length === 3 ? parsed[2] : parsed[1]
9076
+ });
9077
+ }
9078
+ const seed = collectAnchorHashes(payload);
9079
+ const kvHashes = seed.kvHashes;
9080
+ const arrayElemHashes = seed.arrayElemHashes;
9081
+ const remaining = [...decodedDisclosures];
9082
+ for (; ; ) {
9083
+ const anchored = [];
9084
+ for (const d of remaining) {
9085
+ const valid = d.length === 3 ? kvHashes.has(d.hash) : arrayElemHashes.has(d.hash);
9086
+ if (valid) anchored.push(d);
9087
+ }
9088
+ if (anchored.length === 0) break;
9089
+ for (const a of anchored) {
9090
+ const sub = collectAnchorHashes(a.value);
9091
+ for (const h of sub.kvHashes) kvHashes.add(h);
9092
+ for (const h of sub.arrayElemHashes) arrayElemHashes.add(h);
9093
+ const idx = remaining.indexOf(a);
9094
+ if (idx >= 0) remaining.splice(idx, 1);
9021
9095
  }
9022
9096
  }
9097
+ if (remaining.length > 0) {
9098
+ return invalidResult("Disclosure hash mismatch");
9099
+ }
9023
9100
  }
9024
9101
  if (parts.kbJwt) {
9025
9102
  const cnf = payload["cnf"];
@@ -9082,6 +9159,35 @@ var SdJwtParser = class {
9082
9159
  return result;
9083
9160
  }
9084
9161
  };
9162
+ function collectAnchorHashes(node) {
9163
+ const kvHashes = /* @__PURE__ */ new Set();
9164
+ const arrayElemHashes = /* @__PURE__ */ new Set();
9165
+ const visit = (value) => {
9166
+ if (Array.isArray(value)) {
9167
+ for (const item of value) {
9168
+ if (item && typeof item === "object" && !Array.isArray(item)) {
9169
+ const obj = item;
9170
+ const keys = Object.keys(obj);
9171
+ if (keys.length === 1 && keys[0] === "..." && typeof obj["..."] === "string") {
9172
+ arrayElemHashes.add(obj["..."]);
9173
+ continue;
9174
+ }
9175
+ }
9176
+ visit(item);
9177
+ }
9178
+ } else if (value && typeof value === "object") {
9179
+ for (const [k, v] of Object.entries(value)) {
9180
+ if (k === "_sd" && Array.isArray(v)) {
9181
+ for (const h of v) if (typeof h === "string") kvHashes.add(h);
9182
+ continue;
9183
+ }
9184
+ visit(v);
9185
+ }
9186
+ }
9187
+ };
9188
+ visit(node);
9189
+ return { kvHashes, arrayElemHashes };
9190
+ }
9085
9191
 
9086
9192
  // src/presentation.ts
9087
9193
  var parsers = [new SdJwtParser(), new MdocParser()];
@@ -9821,7 +9927,7 @@ function equalBytes(a, b) {
9821
9927
 
9822
9928
  // src/index.ts
9823
9929
  init_errors();
9824
- var VERSION = "0.7.0";
9930
+ var VERSION = "0.8.0";
9825
9931
  /*! Bundled license information:
9826
9932
 
9827
9933
  pvtsutils/build/index.es.js: