@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 +4 -0
- package/dist/index.cjs +197 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +197 -50
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
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 = [
|
|
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
|
|
8034
|
-
)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
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
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
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.
|
|
9971
|
+
var VERSION = "0.8.0";
|
|
9825
9972
|
/*! Bundled license information:
|
|
9826
9973
|
|
|
9827
9974
|
pvtsutils/build/index.es.js:
|