@openeudi/openid4vp 0.8.0 → 0.9.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 +91 -30
- package/dist/index.cjs +400 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -1
- package/dist/index.d.ts +101 -1
- package/dist/index.js +399 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -147,10 +147,10 @@ var init_errors = __esm({
|
|
|
147
147
|
code = "unsupported_jwe";
|
|
148
148
|
alg;
|
|
149
149
|
enc;
|
|
150
|
-
constructor(alg,
|
|
151
|
-
super(`Unsupported JWE algorithms: alg=${alg}, enc=${
|
|
150
|
+
constructor(alg, enc3) {
|
|
151
|
+
super(`Unsupported JWE algorithms: alg=${alg}, enc=${enc3}`);
|
|
152
152
|
this.alg = alg;
|
|
153
|
-
this.enc =
|
|
153
|
+
this.enc = enc3;
|
|
154
154
|
}
|
|
155
155
|
};
|
|
156
156
|
exports.DecryptionFailedError = class extends exports.OpenID4VPError {
|
|
@@ -334,9 +334,9 @@ var init_index_es = __esm({
|
|
|
334
334
|
static isBase64Url(data) {
|
|
335
335
|
return typeof data === STRING_TYPE && BASE64URL_REGEX.test(data);
|
|
336
336
|
}
|
|
337
|
-
static ToString(buffer,
|
|
337
|
+
static ToString(buffer, enc3 = "utf8") {
|
|
338
338
|
const buf = BufferSourceConverter.toUint8Array(buffer);
|
|
339
|
-
switch (
|
|
339
|
+
switch (enc3.toLowerCase()) {
|
|
340
340
|
case "utf8":
|
|
341
341
|
return this.ToUtf8String(buf);
|
|
342
342
|
case "binary":
|
|
@@ -353,14 +353,14 @@ var init_index_es = __esm({
|
|
|
353
353
|
case "utf16be":
|
|
354
354
|
return Utf16Converter.toString(buf);
|
|
355
355
|
default:
|
|
356
|
-
throw new Error(`Unknown type of encoding '${
|
|
356
|
+
throw new Error(`Unknown type of encoding '${enc3}'`);
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
|
-
static FromString(str,
|
|
359
|
+
static FromString(str, enc3 = "utf8") {
|
|
360
360
|
if (!str) {
|
|
361
361
|
return new ArrayBuffer(0);
|
|
362
362
|
}
|
|
363
|
-
switch (
|
|
363
|
+
switch (enc3.toLowerCase()) {
|
|
364
364
|
case "utf8":
|
|
365
365
|
return this.FromUtf8String(str);
|
|
366
366
|
case "binary":
|
|
@@ -377,7 +377,7 @@ var init_index_es = __esm({
|
|
|
377
377
|
case "utf16be":
|
|
378
378
|
return Utf16Converter.fromString(str);
|
|
379
379
|
default:
|
|
380
|
-
throw new Error(`Unknown type of encoding '${
|
|
380
|
+
throw new Error(`Unknown type of encoding '${enc3}'`);
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
static ToBase64(buffer) {
|
|
@@ -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,
|
|
@@ -8150,8 +8191,8 @@ async function createSignedAuthorizationRequest(input, query) {
|
|
|
8150
8191
|
vp_formats_supported: input.vpFormatsSupported
|
|
8151
8192
|
};
|
|
8152
8193
|
if (responseMode === "direct_post.jwt" && input.encryptionKey) {
|
|
8153
|
-
const
|
|
8154
|
-
if (
|
|
8194
|
+
const enc3 = input.encryptionKey.supportedEncValues;
|
|
8195
|
+
if (enc3 !== void 0 && enc3.length === 0) {
|
|
8155
8196
|
throw new exports.SignedRequestBuildError(
|
|
8156
8197
|
"empty_supported_enc_values",
|
|
8157
8198
|
"encryptionKey.supportedEncValues must not be empty (omit to use defaults [A128GCM, A256GCM])"
|
|
@@ -8159,7 +8200,7 @@ async function createSignedAuthorizationRequest(input, query) {
|
|
|
8159
8200
|
}
|
|
8160
8201
|
const jwk = { ...input.encryptionKey.publicJwk, use: "enc" };
|
|
8161
8202
|
clientMetadata.jwks = { keys: [jwk] };
|
|
8162
|
-
const encValues =
|
|
8203
|
+
const encValues = enc3 ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
|
|
8163
8204
|
clientMetadata.encrypted_response_enc_values_supported = encValues;
|
|
8164
8205
|
clientMetadata.authorization_encrypted_response_alg = "ECDH-ES";
|
|
8165
8206
|
clientMetadata.authorization_encrypted_response_enc = encValues[0];
|
|
@@ -8335,7 +8376,7 @@ function isHaipQuery(query) {
|
|
|
8335
8376
|
|
|
8336
8377
|
// src/crypto/cose-sign1.ts
|
|
8337
8378
|
init_errors();
|
|
8338
|
-
var decoder = new cborX.Encoder({ mapsAsObjects: false, useRecords: false });
|
|
8379
|
+
var decoder = new cborX.Encoder({ mapsAsObjects: false, useRecords: false, tagUint8Array: false });
|
|
8339
8380
|
var HEADER_ALG = 1;
|
|
8340
8381
|
var HEADER_X5CHAIN = 33;
|
|
8341
8382
|
var COSE_ALG_TO_NAME = {
|
|
@@ -8433,6 +8474,131 @@ async function verifyCoseSign1(coseSign1, publicKey, allowedAlgorithms) {
|
|
|
8433
8474
|
}
|
|
8434
8475
|
}
|
|
8435
8476
|
|
|
8477
|
+
// src/crypto/device-auth.ts
|
|
8478
|
+
init_errors();
|
|
8479
|
+
var enc = new cborX.Encoder({ mapsAsObjects: false, useRecords: false, tagUint8Array: false });
|
|
8480
|
+
var HEADER_ALG2 = 1;
|
|
8481
|
+
var COSE_ALG_TO_NAME2 = {
|
|
8482
|
+
[-7]: "ES256",
|
|
8483
|
+
[-35]: "ES384",
|
|
8484
|
+
[-36]: "ES512"
|
|
8485
|
+
};
|
|
8486
|
+
var ALG_TO_HASH2 = { ES256: "SHA-256", ES384: "SHA-384", ES512: "SHA-512" };
|
|
8487
|
+
var COSE_KEY_KTY = 1;
|
|
8488
|
+
var COSE_KEY_CRV = -1;
|
|
8489
|
+
var COSE_KEY_X = -2;
|
|
8490
|
+
var COSE_KEY_Y = -3;
|
|
8491
|
+
var COSE_KTY_EC2 = 2;
|
|
8492
|
+
var COSE_CRV_TO_JWK = { 1: "P-256", 2: "P-384", 3: "P-521" };
|
|
8493
|
+
function concatBytes(...chunks) {
|
|
8494
|
+
let len = 0;
|
|
8495
|
+
for (const c of chunks) len += c.length;
|
|
8496
|
+
const out = new Uint8Array(len);
|
|
8497
|
+
let off = 0;
|
|
8498
|
+
for (const c of chunks) {
|
|
8499
|
+
out.set(c, off);
|
|
8500
|
+
off += c.length;
|
|
8501
|
+
}
|
|
8502
|
+
return out;
|
|
8503
|
+
}
|
|
8504
|
+
function bytesToBase64Url(bytes) {
|
|
8505
|
+
let bin = "";
|
|
8506
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
8507
|
+
return globalThis.btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8508
|
+
}
|
|
8509
|
+
function encodeTag24(inner) {
|
|
8510
|
+
return concatBytes(new Uint8Array([216, 24]), new Uint8Array(enc.encode(inner)));
|
|
8511
|
+
}
|
|
8512
|
+
function buildDeviceAuthenticationBytes(params) {
|
|
8513
|
+
const deviceAuthentication = concatBytes(
|
|
8514
|
+
new Uint8Array([132]),
|
|
8515
|
+
// array(4)
|
|
8516
|
+
new Uint8Array(enc.encode("DeviceAuthentication")),
|
|
8517
|
+
params.sessionTranscript,
|
|
8518
|
+
new Uint8Array(enc.encode(params.docType)),
|
|
8519
|
+
encodeTag24(params.deviceNameSpacesInner)
|
|
8520
|
+
);
|
|
8521
|
+
return encodeTag24(deviceAuthentication);
|
|
8522
|
+
}
|
|
8523
|
+
function encodeDeviceSigStructure(protectedBytes, deviceAuthBytes) {
|
|
8524
|
+
const sig = ["Signature1", protectedBytes, new Uint8Array(0), deviceAuthBytes];
|
|
8525
|
+
return new Uint8Array(enc.encode(sig));
|
|
8526
|
+
}
|
|
8527
|
+
async function importDeviceKeyFromCose(coseKey) {
|
|
8528
|
+
if (!(coseKey instanceof Map)) {
|
|
8529
|
+
throw new exports.MalformedCredentialError("MSO deviceKeyInfo.deviceKey is not a COSE_Key map");
|
|
8530
|
+
}
|
|
8531
|
+
const kty = coseKey.get(COSE_KEY_KTY);
|
|
8532
|
+
if (kty !== COSE_KTY_EC2) {
|
|
8533
|
+
throw new exports.MalformedCredentialError(`Unsupported COSE key type for device key: ${String(kty)}`);
|
|
8534
|
+
}
|
|
8535
|
+
const crv = coseKey.get(COSE_KEY_CRV);
|
|
8536
|
+
const crvName = typeof crv === "number" ? COSE_CRV_TO_JWK[crv] : void 0;
|
|
8537
|
+
const x = coseKey.get(COSE_KEY_X);
|
|
8538
|
+
const y = coseKey.get(COSE_KEY_Y);
|
|
8539
|
+
if (!crvName || !(x instanceof Uint8Array) || !(y instanceof Uint8Array)) {
|
|
8540
|
+
throw new exports.MalformedCredentialError("Malformed EC2 COSE_Key in MSO deviceKeyInfo");
|
|
8541
|
+
}
|
|
8542
|
+
return crypto.subtle.importKey(
|
|
8543
|
+
"jwk",
|
|
8544
|
+
{ kty: "EC", crv: crvName, x: bytesToBase64Url(x), y: bytesToBase64Url(y) },
|
|
8545
|
+
{ name: "ECDSA", namedCurve: crvName },
|
|
8546
|
+
false,
|
|
8547
|
+
["verify"]
|
|
8548
|
+
);
|
|
8549
|
+
}
|
|
8550
|
+
async function verifyDeviceAuth(params) {
|
|
8551
|
+
const { deviceSigned, deviceKeyCose, docType, sessionTranscript, allowedAlgorithms } = params;
|
|
8552
|
+
const deviceAuth = deviceSigned.get("deviceAuth");
|
|
8553
|
+
if (!(deviceAuth instanceof Map)) {
|
|
8554
|
+
throw new exports.MalformedCredentialError("deviceSigned.deviceAuth is missing or malformed");
|
|
8555
|
+
}
|
|
8556
|
+
if (deviceAuth.has("deviceMac") && !deviceAuth.has("deviceSignature")) {
|
|
8557
|
+
throw new exports.MalformedCredentialError("mDOC DeviceMac authentication is not supported");
|
|
8558
|
+
}
|
|
8559
|
+
const deviceSignature = deviceAuth.get("deviceSignature");
|
|
8560
|
+
if (!Array.isArray(deviceSignature) || deviceSignature.length !== 4) {
|
|
8561
|
+
throw new exports.MalformedCredentialError("deviceAuth.deviceSignature must be a COSE_Sign1 array");
|
|
8562
|
+
}
|
|
8563
|
+
const protectedBytes = deviceSignature[0];
|
|
8564
|
+
const signature = deviceSignature[3];
|
|
8565
|
+
if (!(protectedBytes instanceof Uint8Array) || !(signature instanceof Uint8Array)) {
|
|
8566
|
+
throw new exports.MalformedCredentialError("DeviceSignature protected header / signature must be byte strings");
|
|
8567
|
+
}
|
|
8568
|
+
let alg;
|
|
8569
|
+
try {
|
|
8570
|
+
const ph = enc.decode(protectedBytes);
|
|
8571
|
+
const label = ph instanceof Map ? ph.get(HEADER_ALG2) : void 0;
|
|
8572
|
+
alg = typeof label === "number" ? COSE_ALG_TO_NAME2[label] : void 0;
|
|
8573
|
+
} catch {
|
|
8574
|
+
throw new exports.MalformedCredentialError("DeviceSignature protected header is not decodable CBOR");
|
|
8575
|
+
}
|
|
8576
|
+
if (!alg || !allowedAlgorithms.includes(alg)) {
|
|
8577
|
+
throw new exports.MalformedCredentialError(`DeviceSignature uses a disallowed algorithm: ${String(alg)}`);
|
|
8578
|
+
}
|
|
8579
|
+
const deviceNameSpacesInner = extractTag24Inner(deviceSigned.get("nameSpaces"));
|
|
8580
|
+
const deviceAuthBytes = buildDeviceAuthenticationBytes({ sessionTranscript, docType, deviceNameSpacesInner });
|
|
8581
|
+
const sigInput = encodeDeviceSigStructure(protectedBytes, deviceAuthBytes);
|
|
8582
|
+
const deviceKey = await importDeviceKeyFromCose(deviceKeyCose);
|
|
8583
|
+
const ok = await crypto.subtle.verify(
|
|
8584
|
+
{ name: "ECDSA", hash: ALG_TO_HASH2[alg] },
|
|
8585
|
+
deviceKey,
|
|
8586
|
+
new Uint8Array(signature),
|
|
8587
|
+
sigInput
|
|
8588
|
+
);
|
|
8589
|
+
if (!ok) {
|
|
8590
|
+
throw new exports.MalformedCredentialError("mDOC device authentication (DeviceSignature) failed to verify");
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
function extractTag24Inner(value) {
|
|
8594
|
+
if (value instanceof Uint8Array) return value;
|
|
8595
|
+
if (value !== null && typeof value === "object" && "value" in value && value.value instanceof Uint8Array && // Tag(24,bytes) and EmbeddedCbor both expose .value; both are fine here.
|
|
8596
|
+
("tag" in value ? value.tag === 24 : true)) {
|
|
8597
|
+
return value.value;
|
|
8598
|
+
}
|
|
8599
|
+
throw new exports.MalformedCredentialError("deviceSigned.nameSpaces must be tag-24 wrapped CBOR");
|
|
8600
|
+
}
|
|
8601
|
+
|
|
8436
8602
|
// src/crypto/digest.ts
|
|
8437
8603
|
init_errors();
|
|
8438
8604
|
var decoder2 = new cborX.Encoder({ mapsAsObjects: false, useRecords: false });
|
|
@@ -8565,6 +8731,14 @@ function decodeMso(payload) {
|
|
|
8565
8731
|
}
|
|
8566
8732
|
valueDigests.set(ns, perNs);
|
|
8567
8733
|
}
|
|
8734
|
+
let deviceKey;
|
|
8735
|
+
const dkiRaw = m.get("deviceKeyInfo");
|
|
8736
|
+
if (dkiRaw instanceof Map) {
|
|
8737
|
+
const dk = dkiRaw.get("deviceKey");
|
|
8738
|
+
if (dk instanceof Map) {
|
|
8739
|
+
deviceKey = dk;
|
|
8740
|
+
}
|
|
8741
|
+
}
|
|
8568
8742
|
const viRaw = m.get("validityInfo");
|
|
8569
8743
|
if (!(viRaw instanceof Map)) {
|
|
8570
8744
|
throw new exports.MalformedCredentialError("MSO.validityInfo must be a Map");
|
|
@@ -8578,7 +8752,7 @@ function decodeMso(payload) {
|
|
|
8578
8752
|
if (vi.has("expectedUpdate")) {
|
|
8579
8753
|
validityInfo.expectedUpdate = decodeDateLike(vi.get("expectedUpdate"));
|
|
8580
8754
|
}
|
|
8581
|
-
return { version, digestAlgorithm, valueDigests, docType, validityInfo };
|
|
8755
|
+
return { version, digestAlgorithm, valueDigests, docType, deviceKey, validityInfo };
|
|
8582
8756
|
}
|
|
8583
8757
|
function validateMsoValidity(mso, now) {
|
|
8584
8758
|
const { signed, validFrom, validUntil } = mso.validityInfo;
|
|
@@ -8628,15 +8802,54 @@ ${lines.join("\n")}
|
|
|
8628
8802
|
function extractCountryHintFromCert(_certDer) {
|
|
8629
8803
|
return "";
|
|
8630
8804
|
}
|
|
8805
|
+
function cborByteStringHeader(length) {
|
|
8806
|
+
if (length < 24) return Uint8Array.of(64 | length);
|
|
8807
|
+
if (length < 256) return Uint8Array.of(88, length);
|
|
8808
|
+
if (length < 65536) return Uint8Array.of(89, length >> 8 & 255, length & 255);
|
|
8809
|
+
if (length < 4294967296) {
|
|
8810
|
+
return Uint8Array.of(
|
|
8811
|
+
90,
|
|
8812
|
+
length >>> 24 & 255,
|
|
8813
|
+
length >>> 16 & 255,
|
|
8814
|
+
length >>> 8 & 255,
|
|
8815
|
+
length & 255
|
|
8816
|
+
);
|
|
8817
|
+
}
|
|
8818
|
+
const hi = Math.floor(length / 4294967296);
|
|
8819
|
+
const lo = length >>> 0;
|
|
8820
|
+
return Uint8Array.of(
|
|
8821
|
+
91,
|
|
8822
|
+
hi >>> 24 & 255,
|
|
8823
|
+
hi >>> 16 & 255,
|
|
8824
|
+
hi >>> 8 & 255,
|
|
8825
|
+
hi & 255,
|
|
8826
|
+
lo >>> 24 & 255,
|
|
8827
|
+
lo >>> 16 & 255,
|
|
8828
|
+
lo >>> 8 & 255,
|
|
8829
|
+
lo & 255
|
|
8830
|
+
);
|
|
8831
|
+
}
|
|
8832
|
+
function wrapIssuerSignedItemBytes(inner) {
|
|
8833
|
+
const header = cborByteStringHeader(inner.length);
|
|
8834
|
+
const out = new Uint8Array(2 + header.length + inner.length);
|
|
8835
|
+
out[0] = 216;
|
|
8836
|
+
out[1] = 24;
|
|
8837
|
+
out.set(header, 2);
|
|
8838
|
+
out.set(inner, 2 + header.length);
|
|
8839
|
+
return out;
|
|
8840
|
+
}
|
|
8631
8841
|
function extractRawItemBytes(item) {
|
|
8632
8842
|
if (item instanceof cborX.Tag && item.tag === 24 && item.value instanceof Uint8Array) {
|
|
8633
|
-
return item.value;
|
|
8843
|
+
return wrapIssuerSignedItemBytes(item.value);
|
|
8634
8844
|
}
|
|
8635
8845
|
if (item instanceof Uint8Array) {
|
|
8636
|
-
|
|
8846
|
+
if (item.length >= 2 && item[0] === 216 && item[1] === 24) {
|
|
8847
|
+
return item;
|
|
8848
|
+
}
|
|
8849
|
+
return wrapIssuerSignedItemBytes(item);
|
|
8637
8850
|
}
|
|
8638
8851
|
if (item !== null && typeof item === "object" && !(item instanceof cborX.Tag) && !(item instanceof Map) && "value" in item && item.value instanceof Uint8Array) {
|
|
8639
|
-
return item.value;
|
|
8852
|
+
return wrapIssuerSignedItemBytes(item.value);
|
|
8640
8853
|
}
|
|
8641
8854
|
throw new exports.MalformedCredentialError("IssuerSignedItem must be a tag-24 byte string");
|
|
8642
8855
|
}
|
|
@@ -8826,6 +9039,54 @@ var MdocParser = class {
|
|
|
8826
9039
|
nameSpaces.set(ns, items.map(extractRawItemBytes));
|
|
8827
9040
|
}
|
|
8828
9041
|
await verifyAllDigests(nameSpaces, mso);
|
|
9042
|
+
const deviceSigned = document.get("deviceSigned");
|
|
9043
|
+
if (!(deviceSigned instanceof Map)) {
|
|
9044
|
+
return {
|
|
9045
|
+
valid: false,
|
|
9046
|
+
format: this.format,
|
|
9047
|
+
claims: {},
|
|
9048
|
+
issuer: { certificate: issuerCertBytes, country: "" },
|
|
9049
|
+
error: "Missing deviceSigned \u2014 mDOC device authentication is required"
|
|
9050
|
+
};
|
|
9051
|
+
}
|
|
9052
|
+
if (!(options.mdocSessionTranscript instanceof Uint8Array) || options.mdocSessionTranscript.length === 0) {
|
|
9053
|
+
return {
|
|
9054
|
+
valid: false,
|
|
9055
|
+
format: this.format,
|
|
9056
|
+
claims: {},
|
|
9057
|
+
issuer: { certificate: issuerCertBytes, country: "" },
|
|
9058
|
+
error: "mdocSessionTranscript is required to verify mDOC device authentication"
|
|
9059
|
+
};
|
|
9060
|
+
}
|
|
9061
|
+
if (!mso.deviceKey) {
|
|
9062
|
+
return {
|
|
9063
|
+
valid: false,
|
|
9064
|
+
format: this.format,
|
|
9065
|
+
claims: {},
|
|
9066
|
+
issuer: { certificate: issuerCertBytes, country: "" },
|
|
9067
|
+
error: "MSO is missing deviceKeyInfo.deviceKey; cannot verify device authentication"
|
|
9068
|
+
};
|
|
9069
|
+
}
|
|
9070
|
+
try {
|
|
9071
|
+
await verifyDeviceAuth({
|
|
9072
|
+
deviceSigned,
|
|
9073
|
+
deviceKeyCose: mso.deviceKey,
|
|
9074
|
+
docType: mso.docType,
|
|
9075
|
+
sessionTranscript: options.mdocSessionTranscript,
|
|
9076
|
+
allowedAlgorithms
|
|
9077
|
+
});
|
|
9078
|
+
} catch (err) {
|
|
9079
|
+
if (err instanceof exports.MalformedCredentialError) {
|
|
9080
|
+
return {
|
|
9081
|
+
valid: false,
|
|
9082
|
+
format: this.format,
|
|
9083
|
+
claims: {},
|
|
9084
|
+
issuer: { certificate: issuerCertBytes, country: "" },
|
|
9085
|
+
error: err.message
|
|
9086
|
+
};
|
|
9087
|
+
}
|
|
9088
|
+
throw err;
|
|
9089
|
+
}
|
|
8829
9090
|
const { flat: claims, namespaced: namespacedClaims } = mapRawToClaims(nameSpaces);
|
|
8830
9091
|
const issuer = {
|
|
8831
9092
|
certificate: issuerCertBytes,
|
|
@@ -9098,11 +9359,21 @@ var SdJwtParser = class {
|
|
|
9098
9359
|
return invalidResult("Disclosure hash mismatch");
|
|
9099
9360
|
}
|
|
9100
9361
|
}
|
|
9362
|
+
const cnf = payload["cnf"];
|
|
9363
|
+
const isHolderBound = !!cnf && typeof cnf === "object" && !!cnf["jwk"];
|
|
9364
|
+
const keyBindingRequired = isHolderBound || options.requireKeyBinding === true;
|
|
9365
|
+
if (keyBindingRequired && !parts.kbJwt) {
|
|
9366
|
+
return invalidResult(
|
|
9367
|
+
"Key binding JWT is required for holder-bound credentials but was not presented"
|
|
9368
|
+
);
|
|
9369
|
+
}
|
|
9101
9370
|
if (parts.kbJwt) {
|
|
9102
|
-
|
|
9103
|
-
if (!cnf || typeof cnf !== "object" || !cnf["jwk"]) {
|
|
9371
|
+
if (!isHolderBound) {
|
|
9104
9372
|
throw new exports.MalformedCredentialError("Missing cnf.jwk in issuer JWT for key binding");
|
|
9105
9373
|
}
|
|
9374
|
+
if (typeof options.nonce !== "string" || options.nonce.length === 0) {
|
|
9375
|
+
return invalidResult("A nonce is required to verify the key binding JWT");
|
|
9376
|
+
}
|
|
9106
9377
|
let holderKey;
|
|
9107
9378
|
try {
|
|
9108
9379
|
const kbHeader = jose.decodeProtectedHeader(parts.kbJwt);
|
|
@@ -9220,9 +9491,9 @@ async function decryptAuthorizationResponse(jwe, privateKey) {
|
|
|
9220
9491
|
});
|
|
9221
9492
|
}
|
|
9222
9493
|
const alg = typeof header.alg === "string" ? header.alg : "";
|
|
9223
|
-
const
|
|
9224
|
-
if (alg !== SUPPORTED_ALG || !SUPPORTED_ENC.has(
|
|
9225
|
-
throw new exports.UnsupportedJweError(alg,
|
|
9494
|
+
const enc3 = typeof header.enc === "string" ? header.enc : "";
|
|
9495
|
+
if (alg !== SUPPORTED_ALG || !SUPPORTED_ENC.has(enc3)) {
|
|
9496
|
+
throw new exports.UnsupportedJweError(alg, enc3);
|
|
9226
9497
|
}
|
|
9227
9498
|
let plaintext;
|
|
9228
9499
|
try {
|
|
@@ -9240,6 +9511,29 @@ async function decryptAuthorizationResponse(jwe, privateKey) {
|
|
|
9240
9511
|
});
|
|
9241
9512
|
}
|
|
9242
9513
|
}
|
|
9514
|
+
var enc2 = new cborX.Encoder({ mapsAsObjects: false, useRecords: false, tagUint8Array: false });
|
|
9515
|
+
async function sha256(bytes) {
|
|
9516
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
9517
|
+
}
|
|
9518
|
+
async function buildOid4vpSessionTranscript(params) {
|
|
9519
|
+
const { clientId, responseUri, nonce, mdocGeneratedNonce } = params;
|
|
9520
|
+
const clientIdHash = await sha256(new Uint8Array(enc2.encode([clientId, mdocGeneratedNonce])));
|
|
9521
|
+
const responseUriHash = await sha256(new Uint8Array(enc2.encode([responseUri, mdocGeneratedNonce])));
|
|
9522
|
+
return new Uint8Array(enc2.encode([null, null, [clientIdHash, responseUriHash, nonce]]));
|
|
9523
|
+
}
|
|
9524
|
+
async function jwkSha256Thumbprint(jwk) {
|
|
9525
|
+
if (jwk.kty !== "EC") {
|
|
9526
|
+
throw new Error(`Unsupported JWK kty for thumbprint: ${jwk.kty}`);
|
|
9527
|
+
}
|
|
9528
|
+
const canonical = `{"crv":"${jwk.crv}","kty":"EC","x":"${jwk.x}","y":"${jwk.y}"}`;
|
|
9529
|
+
return sha256(new TextEncoder().encode(canonical));
|
|
9530
|
+
}
|
|
9531
|
+
async function buildOpenID4VPHandoverSessionTranscript(params) {
|
|
9532
|
+
const { clientId, nonce, responseUri, verifierEncryptionJwk } = params;
|
|
9533
|
+
const thumb = verifierEncryptionJwk ? await jwkSha256Thumbprint(verifierEncryptionJwk) : null;
|
|
9534
|
+
const infoHash = await sha256(new Uint8Array(enc2.encode([clientId, nonce, thumb, responseUri])));
|
|
9535
|
+
return new Uint8Array(enc2.encode([null, null, ["OpenID4VPHandover", infoHash]]));
|
|
9536
|
+
}
|
|
9243
9537
|
|
|
9244
9538
|
// src/verify.ts
|
|
9245
9539
|
init_errors();
|
|
@@ -9277,6 +9571,42 @@ async function verifyPresentation(vpToken, query, options) {
|
|
|
9277
9571
|
function isEncryptedResponse(x) {
|
|
9278
9572
|
return typeof x === "object" && x !== null && "response" in x && typeof x.response === "string" && Object.keys(x).length === 1;
|
|
9279
9573
|
}
|
|
9574
|
+
var BASE64URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
9575
|
+
function base64UrlToBytes(input) {
|
|
9576
|
+
const cleaned = input.replace(/=+$/, "");
|
|
9577
|
+
const byteLength = Math.floor(cleaned.length * 3 / 4);
|
|
9578
|
+
const bytes = new Uint8Array(byteLength);
|
|
9579
|
+
let byteIndex = 0;
|
|
9580
|
+
for (let i = 0; i < cleaned.length; i += 4) {
|
|
9581
|
+
const a = BASE64URL_CHARS.indexOf(cleaned[i]);
|
|
9582
|
+
const b = i + 1 < cleaned.length ? BASE64URL_CHARS.indexOf(cleaned[i + 1]) : 0;
|
|
9583
|
+
const c = i + 2 < cleaned.length ? BASE64URL_CHARS.indexOf(cleaned[i + 2]) : 0;
|
|
9584
|
+
const d = i + 3 < cleaned.length ? BASE64URL_CHARS.indexOf(cleaned[i + 3]) : 0;
|
|
9585
|
+
if (a === -1 || b === -1 || c === -1 || d === -1) {
|
|
9586
|
+
throw new TypeError("Invalid base64url character in vp_token/apu");
|
|
9587
|
+
}
|
|
9588
|
+
bytes[byteIndex++] = a << 2 | b >> 4;
|
|
9589
|
+
if (byteIndex < byteLength) bytes[byteIndex++] = (b & 15) << 4 | c >> 2;
|
|
9590
|
+
if (byteIndex < byteLength) bytes[byteIndex++] = (c & 3) << 6 | d;
|
|
9591
|
+
}
|
|
9592
|
+
return bytes;
|
|
9593
|
+
}
|
|
9594
|
+
function extractMdocGeneratedNonce(jwe) {
|
|
9595
|
+
let apu;
|
|
9596
|
+
try {
|
|
9597
|
+
apu = jose.decodeProtectedHeader(jwe).apu;
|
|
9598
|
+
} catch {
|
|
9599
|
+
return void 0;
|
|
9600
|
+
}
|
|
9601
|
+
if (typeof apu !== "string" || apu.length === 0) {
|
|
9602
|
+
return void 0;
|
|
9603
|
+
}
|
|
9604
|
+
try {
|
|
9605
|
+
return new TextDecoder().decode(base64UrlToBytes(apu));
|
|
9606
|
+
} catch {
|
|
9607
|
+
return void 0;
|
|
9608
|
+
}
|
|
9609
|
+
}
|
|
9280
9610
|
function isAuthorizationResponse(x) {
|
|
9281
9611
|
return typeof x === "object" && x !== null && "vp_token" in x && typeof x.vp_token === "object" && x.vp_token !== null;
|
|
9282
9612
|
}
|
|
@@ -9285,11 +9615,32 @@ async function verifyAuthorizationResponse(envelope, query, options) {
|
|
|
9285
9615
|
if (!options.decryptionKey) {
|
|
9286
9616
|
throw new exports.MissingDecryptionKeyError();
|
|
9287
9617
|
}
|
|
9618
|
+
const nextOptions = { ...options };
|
|
9619
|
+
if (options.mdocSessionTranscript === void 0 && options.clientId !== void 0 && options.responseUri !== void 0 && options.nonce !== void 0) {
|
|
9620
|
+
if (options.sessionTranscriptProfile === "openid4vp-1.0") {
|
|
9621
|
+
nextOptions.mdocSessionTranscript = await buildOpenID4VPHandoverSessionTranscript({
|
|
9622
|
+
clientId: options.clientId,
|
|
9623
|
+
nonce: options.nonce,
|
|
9624
|
+
responseUri: options.responseUri,
|
|
9625
|
+
verifierEncryptionJwk: options.verifierEncryptionJwk
|
|
9626
|
+
});
|
|
9627
|
+
} else {
|
|
9628
|
+
const mdocGeneratedNonce = extractMdocGeneratedNonce(envelope.response);
|
|
9629
|
+
if (mdocGeneratedNonce !== void 0) {
|
|
9630
|
+
nextOptions.mdocSessionTranscript = await buildOid4vpSessionTranscript({
|
|
9631
|
+
clientId: options.clientId,
|
|
9632
|
+
responseUri: options.responseUri,
|
|
9633
|
+
nonce: options.nonce,
|
|
9634
|
+
mdocGeneratedNonce
|
|
9635
|
+
});
|
|
9636
|
+
}
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9288
9639
|
const decrypted = await decryptAuthorizationResponse(
|
|
9289
9640
|
envelope.response,
|
|
9290
9641
|
options.decryptionKey
|
|
9291
9642
|
);
|
|
9292
|
-
return verifyAuthorizationResponse(decrypted, query,
|
|
9643
|
+
return verifyAuthorizationResponse(decrypted, query, nextOptions);
|
|
9293
9644
|
}
|
|
9294
9645
|
if (!isAuthorizationResponse(envelope)) {
|
|
9295
9646
|
throw new TypeError("envelope must contain vp_token as a JSON object per OpenID4VP 1.0 \xA78.1");
|
|
@@ -9314,7 +9665,8 @@ async function verifyAuthorizationResponse(envelope, query, options) {
|
|
|
9314
9665
|
throw new exports.MultipleCredentialsNotSupportedError(queryIds.length, presentationCount);
|
|
9315
9666
|
}
|
|
9316
9667
|
const presentation = vpToken[queryIds[0]][0];
|
|
9317
|
-
|
|
9668
|
+
const decodedPresentation = query.credentials[0]?.format === "mso_mdoc" && typeof presentation === "string" ? base64UrlToBytes(presentation) : presentation;
|
|
9669
|
+
return verifyPresentation(decodedPresentation, query, options);
|
|
9318
9670
|
}
|
|
9319
9671
|
|
|
9320
9672
|
// src/index.ts
|
|
@@ -10014,6 +10366,8 @@ exports.SdJwtParser = SdJwtParser;
|
|
|
10014
10366
|
exports.StaticTrustStore = StaticTrustStore;
|
|
10015
10367
|
exports.VERSION = VERSION;
|
|
10016
10368
|
exports.buildHaipQuery = buildHaipQuery;
|
|
10369
|
+
exports.buildOid4vpSessionTranscript = buildOid4vpSessionTranscript;
|
|
10370
|
+
exports.buildOpenID4VPHandoverSessionTranscript = buildOpenID4VPHandoverSessionTranscript;
|
|
10017
10371
|
exports.createAuthorizationRequest = createAuthorizationRequest;
|
|
10018
10372
|
exports.createSignedAuthorizationRequest = createSignedAuthorizationRequest;
|
|
10019
10373
|
exports.decryptAuthorizationResponse = decryptAuthorizationResponse;
|