@openeudi/openid4vp 0.7.0 → 0.8.1

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
@@ -6382,6 +6382,33 @@ var init_es20152 = __esm({
6382
6382
  init_validity();
6383
6383
  }
6384
6384
  });
6385
+ function getSkiHex(cert) {
6386
+ const ext = cert.getExtension(x509.SubjectKeyIdentifierExtension);
6387
+ return ext ? ext.keyId.toLowerCase() : null;
6388
+ }
6389
+ function certificatesEqual(a, b) {
6390
+ const ab = new Uint8Array(a.rawData);
6391
+ const bb = new Uint8Array(b.rawData);
6392
+ if (ab.length !== bb.length) return false;
6393
+ for (let i = 0; i < ab.length; i++) {
6394
+ if (ab[i] !== bb[i]) return false;
6395
+ }
6396
+ return true;
6397
+ }
6398
+ function getSkiBytes(cert) {
6399
+ const ext = cert.getExtension(x509.SubjectKeyIdentifierExtension);
6400
+ if (!ext) return null;
6401
+ const hex = ext.keyId;
6402
+ const out = new Uint8Array(hex.length / 2);
6403
+ for (let i = 0; i < out.length; i++) {
6404
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
6405
+ }
6406
+ return out;
6407
+ }
6408
+ var init_x509_utils = __esm({
6409
+ "src/trust/x509-utils.ts"() {
6410
+ }
6411
+ });
6385
6412
  function mapX509AlgoToJwaName(cert) {
6386
6413
  const alg = cert.signatureAlgorithm;
6387
6414
  const name = alg?.name ?? "";
@@ -6391,8 +6418,11 @@ function mapX509AlgoToJwaName(cert) {
6391
6418
  if (name === "ECDSA" && hash === "SHA-512") return "ES512";
6392
6419
  if (name === "Ed25519" || name === "EdDSA") return "EdDSA";
6393
6420
  if (name === "RSASSA-PKCS1-v1_5" && hash === "SHA-256") return "RS256";
6421
+ if (name === "RSASSA-PKCS1-v1_5" && hash === "SHA-384") return "RS384";
6422
+ if (name === "RSASSA-PKCS1-v1_5" && hash === "SHA-512") return "RS512";
6394
6423
  if (name === "RSA-PSS" && hash === "SHA-256") return "PS256";
6395
6424
  if (name === "RSA-PSS" && hash === "SHA-384") return "PS384";
6425
+ if (name === "RSA-PSS" && hash === "SHA-512") return "PS512";
6396
6426
  return `${name}-${hash}`;
6397
6427
  }
6398
6428
  function collectLeafNames(leaf) {
@@ -6485,8 +6515,20 @@ var init_ChainBuilder = __esm({
6485
6515
  init_es2015();
6486
6516
  init_es20152();
6487
6517
  init_errors();
6518
+ init_x509_utils();
6488
6519
  NAME_CONSTRAINTS_OID = "2.5.29.30";
6489
- DEFAULT_ALGORITHMS = ["ES256", "ES384", "EdDSA", "RS256", "PS256"];
6520
+ DEFAULT_ALGORITHMS = [
6521
+ "ES256",
6522
+ "ES384",
6523
+ "ES512",
6524
+ "EdDSA",
6525
+ "RS256",
6526
+ "RS384",
6527
+ "RS512",
6528
+ "PS256",
6529
+ "PS384",
6530
+ "PS512"
6531
+ ];
6490
6532
  ChainBuilder = class {
6491
6533
  constructor(opts = {}) {
6492
6534
  this.opts = opts;
@@ -6545,6 +6587,12 @@ var init_ChainBuilder = __esm({
6545
6587
  current = issuer;
6546
6588
  nonLeafDepth += 1;
6547
6589
  }
6590
+ if (!certificatesEqual(current, anchor)) {
6591
+ throw new exports.CertificateChainError(
6592
+ `certificate ${current.subject} shares the trust anchor's Subject DN but is not the anchor (no signature closure)`,
6593
+ { reason: "signature" }
6594
+ );
6595
+ }
6548
6596
  this.checkNameConstraints(leaf, chain.slice(1));
6549
6597
  return chain;
6550
6598
  }
@@ -7785,7 +7833,10 @@ var init_RevocationChecker = __esm({
7785
7833
  if (hints.ocspUrl) {
7786
7834
  try {
7787
7835
  return await this.ocsp.checkCached(subjectCert, issuerCert, hints.ocspUrl, now);
7788
- } catch {
7836
+ } catch (err) {
7837
+ console.warn(
7838
+ `[openid4vp] OCSP check failed for ${subjectCert.subject} via ${hints.ocspUrl}, falling back to CRL: ${err.message}`
7839
+ );
7789
7840
  }
7790
7841
  }
7791
7842
  for (const url of hints.crlUrls) {
@@ -7813,6 +7864,9 @@ var init_RevocationChecker = __esm({
7813
7864
  return { status: "good", source: "crl", checkedAt: now };
7814
7865
  } catch (err) {
7815
7866
  if (err instanceof exports.RevocationCheckFailedError) throw err;
7867
+ console.warn(
7868
+ `[openid4vp] CRL check failed for ${subjectCert.subject} via ${url}: ${err.message}`
7869
+ );
7816
7870
  continue;
7817
7871
  }
7818
7872
  }
@@ -7831,24 +7885,6 @@ var init_RevocationChecker = __esm({
7831
7885
  };
7832
7886
  }
7833
7887
  });
7834
- function getSkiHex(cert) {
7835
- const ext = cert.getExtension(x509.SubjectKeyIdentifierExtension);
7836
- return ext ? ext.keyId.toLowerCase() : null;
7837
- }
7838
- function getSkiBytes(cert) {
7839
- const ext = cert.getExtension(x509.SubjectKeyIdentifierExtension);
7840
- if (!ext) return null;
7841
- const hex = ext.keyId;
7842
- const out = new Uint8Array(hex.length / 2);
7843
- for (let i = 0; i < out.length; i++) {
7844
- out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
7845
- }
7846
- return out;
7847
- }
7848
- var init_x509_utils = __esm({
7849
- "src/trust/x509-utils.ts"() {
7850
- }
7851
- });
7852
7888
 
7853
7889
  // src/trust/lotl-loa-mapping.ts
7854
7890
  function mapLoA(uris) {
@@ -8030,8 +8066,13 @@ var init_TrustEvaluator = __esm({
8030
8066
  );
8031
8067
  const terminusCert = chain[chain.length - 1];
8032
8068
  const anchor = anchors.find(
8033
- (a) => a.certificate.serialNumber === terminusCert.serialNumber
8034
- ) ?? anchors[0];
8069
+ (a) => certificatesEqual(a.certificate, terminusCert)
8070
+ );
8071
+ if (!anchor) {
8072
+ throw new exports.TrustAnchorNotFoundError(
8073
+ `chain terminus ${terminusCert.subject} does not match any trust anchor certificate`
8074
+ );
8075
+ }
8035
8076
  const issuerForRevocation = chain.length > 1 ? chain[1] : terminusCert;
8036
8077
  const revocation = await this.revocationChecker.check(
8037
8078
  leaf,
@@ -8159,7 +8200,10 @@ async function createSignedAuthorizationRequest(input, query) {
8159
8200
  }
8160
8201
  const jwk = { ...input.encryptionKey.publicJwk, use: "enc" };
8161
8202
  clientMetadata.jwks = { keys: [jwk] };
8162
- clientMetadata.encrypted_response_enc_values_supported = enc ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
8203
+ const encValues = enc ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
8204
+ clientMetadata.encrypted_response_enc_values_supported = encValues;
8205
+ clientMetadata.authorization_encrypted_response_alg = "ECDH-ES";
8206
+ clientMetadata.authorization_encrypted_response_enc = encValues[0];
8163
8207
  }
8164
8208
  const payload = {
8165
8209
  iss: clientId,
@@ -8332,7 +8376,7 @@ function isHaipQuery(query) {
8332
8376
 
8333
8377
  // src/crypto/cose-sign1.ts
8334
8378
  init_errors();
8335
- var decoder = new cborX.Encoder({ mapsAsObjects: false, useRecords: false });
8379
+ var decoder = new cborX.Encoder({ mapsAsObjects: false, useRecords: false, tagUint8Array: false });
8336
8380
  var HEADER_ALG = 1;
8337
8381
  var HEADER_X5CHAIN = 33;
8338
8382
  var COSE_ALG_TO_NAME = {
@@ -8418,7 +8462,7 @@ async function verifyCoseSign1(coseSign1, publicKey, allowedAlgorithms) {
8418
8462
  throw new exports.MalformedCredentialError(`no hash mapping for alg ${coseSign1.alg}`);
8419
8463
  }
8420
8464
  const sigStructure = ["Signature1", coseSign1.protectedHeaderBytes, new Uint8Array(0), coseSign1.payload];
8421
- const sigInput = decoder.encode(sigStructure);
8465
+ const sigInput = new Uint8Array(decoder.encode(sigStructure));
8422
8466
  const ok = await crypto.subtle.verify(
8423
8467
  { name: "ECDSA", hash: hashAlg },
8424
8468
  publicKey,
@@ -8957,32 +9001,74 @@ var SdJwtParser = class {
8957
9001
  return invalidResult(`Unsupported algorithm: ${alg}`);
8958
9002
  }
8959
9003
  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
- }
9004
+ const hasX5c = Array.isArray(x5cArray) && x5cArray.length > 0 && typeof x5cArray[0] === "string";
8963
9005
  let issuerKey;
8964
9006
  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");
9007
+ if (hasX5c) {
9008
+ try {
9009
+ issuerKey = await jose.importX509(derToPem2(x5cArray[0]), alg);
9010
+ issuerCertBytes = base64ToBytes(x5cArray[0]);
9011
+ } catch (err) {
9012
+ if (err instanceof exports.MalformedCredentialError) throw err;
9013
+ throw new exports.MalformedCredentialError("Failed to import issuer certificate");
9014
+ }
9015
+ } else if (options.trustedIssuerJwks && options.trustedIssuerJwks.length > 0) {
9016
+ issuerKey = void 0;
9017
+ } else {
9018
+ throw new exports.MalformedCredentialError("Missing or invalid x5c in JWT header");
8971
9019
  }
8972
9020
  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");
9021
+ if (issuerKey) {
9022
+ try {
9023
+ const result2 = await jose.jwtVerify(parts.jwt, issuerKey, { algorithms: allowedAlgorithms });
9024
+ payload = result2.payload;
9025
+ } catch {
9026
+ return invalidResult("Issuer JWT signature verification failed");
9027
+ }
9028
+ } else {
9029
+ const jwks = options.trustedIssuerJwks;
9030
+ const headerKid = header["kid"];
9031
+ let candidates;
9032
+ if (typeof headerKid === "string") {
9033
+ const matched = jwks.find((k) => k["kid"] === headerKid);
9034
+ if (!matched) {
9035
+ return invalidResult(
9036
+ `No matching JWK found in trustedIssuerJwks for kid=${headerKid}`
9037
+ );
9038
+ }
9039
+ candidates = [matched];
9040
+ } else {
9041
+ candidates = jwks;
9042
+ }
9043
+ let verifiedPayload;
9044
+ for (const jwk of candidates) {
9045
+ let candidateKey;
9046
+ try {
9047
+ candidateKey = await jose.importJWK(jwk, alg);
9048
+ } catch {
9049
+ continue;
9050
+ }
9051
+ try {
9052
+ const result2 = await jose.jwtVerify(parts.jwt, candidateKey, {
9053
+ algorithms: allowedAlgorithms
9054
+ });
9055
+ verifiedPayload = result2.payload;
9056
+ break;
9057
+ } catch {
9058
+ }
9059
+ }
9060
+ if (!verifiedPayload) {
9061
+ return invalidResult("Issuer JWT signature verification failed");
9062
+ }
9063
+ payload = verifiedPayload;
8978
9064
  }
8979
9065
  const issuerString = typeof payload["iss"] === "string" ? payload["iss"] : "";
8980
9066
  const issuerInfo = {
8981
- certificate: issuerCertBytes,
9067
+ certificate: issuerCertBytes ?? new Uint8Array(),
8982
9068
  country: extractCountryHint(issuerString)
8983
9069
  };
8984
9070
  let trustResult;
8985
- if (options.trustStore) {
9071
+ if (issuerCertBytes === void 0) ; else if (options.trustStore) {
8986
9072
  const { TrustEvaluator: TrustEvaluator2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
8987
9073
  const evaluator = new TrustEvaluator2({
8988
9074
  trustStore: options.trustStore,
@@ -9002,24 +9088,56 @@ var SdJwtParser = class {
9002
9088
  );
9003
9089
  }
9004
9090
  } else {
9005
- const isTrusted = options.trustedCertificates.some((trusted) => bytesEqual4(trusted, issuerCertBytes));
9091
+ const isTrusted = options.trustedCertificates.some(
9092
+ (trusted) => bytesEqual4(trusted, issuerCertBytes)
9093
+ );
9006
9094
  if (!isTrusted) {
9007
9095
  return invalidResult("Issuer certificate is not trusted");
9008
9096
  }
9009
9097
  }
9010
9098
  }
9011
- const sdArray = payload["_sd"];
9012
9099
  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
- }
9100
+ const decodedDisclosures = [];
9016
9101
  for (const disclosure of parts.disclosures) {
9102
+ let parsed;
9103
+ try {
9104
+ parsed = JSON.parse(new TextDecoder().decode(base64ToBytes(disclosure)));
9105
+ } catch {
9106
+ return invalidResult("Disclosure is not valid base64url-encoded JSON");
9107
+ }
9108
+ if (!Array.isArray(parsed) || parsed.length !== 2 && parsed.length !== 3) {
9109
+ return invalidResult("Disclosure must be a 2- or 3-element array");
9110
+ }
9017
9111
  const hashBytes = await sha256Hasher(disclosure);
9018
- const hashB64url = bytesToBase64url(hashBytes);
9019
- if (!sdArray.includes(hashB64url)) {
9020
- return invalidResult("Disclosure hash mismatch");
9112
+ decodedDisclosures.push({
9113
+ raw: disclosure,
9114
+ hash: bytesToBase64url(hashBytes),
9115
+ length: parsed.length,
9116
+ value: parsed.length === 3 ? parsed[2] : parsed[1]
9117
+ });
9118
+ }
9119
+ const seed = collectAnchorHashes(payload);
9120
+ const kvHashes = seed.kvHashes;
9121
+ const arrayElemHashes = seed.arrayElemHashes;
9122
+ const remaining = [...decodedDisclosures];
9123
+ for (; ; ) {
9124
+ const anchored = [];
9125
+ for (const d of remaining) {
9126
+ const valid = d.length === 3 ? kvHashes.has(d.hash) : arrayElemHashes.has(d.hash);
9127
+ if (valid) anchored.push(d);
9128
+ }
9129
+ if (anchored.length === 0) break;
9130
+ for (const a of anchored) {
9131
+ const sub = collectAnchorHashes(a.value);
9132
+ for (const h of sub.kvHashes) kvHashes.add(h);
9133
+ for (const h of sub.arrayElemHashes) arrayElemHashes.add(h);
9134
+ const idx = remaining.indexOf(a);
9135
+ if (idx >= 0) remaining.splice(idx, 1);
9021
9136
  }
9022
9137
  }
9138
+ if (remaining.length > 0) {
9139
+ return invalidResult("Disclosure hash mismatch");
9140
+ }
9023
9141
  }
9024
9142
  if (parts.kbJwt) {
9025
9143
  const cnf = payload["cnf"];
@@ -9082,6 +9200,35 @@ var SdJwtParser = class {
9082
9200
  return result;
9083
9201
  }
9084
9202
  };
9203
+ function collectAnchorHashes(node) {
9204
+ const kvHashes = /* @__PURE__ */ new Set();
9205
+ const arrayElemHashes = /* @__PURE__ */ new Set();
9206
+ const visit = (value) => {
9207
+ if (Array.isArray(value)) {
9208
+ for (const item of value) {
9209
+ if (item && typeof item === "object" && !Array.isArray(item)) {
9210
+ const obj = item;
9211
+ const keys = Object.keys(obj);
9212
+ if (keys.length === 1 && keys[0] === "..." && typeof obj["..."] === "string") {
9213
+ arrayElemHashes.add(obj["..."]);
9214
+ continue;
9215
+ }
9216
+ }
9217
+ visit(item);
9218
+ }
9219
+ } else if (value && typeof value === "object") {
9220
+ for (const [k, v] of Object.entries(value)) {
9221
+ if (k === "_sd" && Array.isArray(v)) {
9222
+ for (const h of v) if (typeof h === "string") kvHashes.add(h);
9223
+ continue;
9224
+ }
9225
+ visit(v);
9226
+ }
9227
+ }
9228
+ };
9229
+ visit(node);
9230
+ return { kvHashes, arrayElemHashes };
9231
+ }
9085
9232
 
9086
9233
  // src/presentation.ts
9087
9234
  var parsers = [new SdJwtParser(), new MdocParser()];
@@ -9821,7 +9968,7 @@ function equalBytes(a, b) {
9821
9968
 
9822
9969
  // src/index.ts
9823
9970
  init_errors();
9824
- var VERSION = "0.7.0";
9971
+ var VERSION = "0.8.0";
9825
9972
  /*! Bundled license information:
9826
9973
 
9827
9974
  pvtsutils/build/index.es.js: