@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.js
CHANGED
|
@@ -126,10 +126,10 @@ var init_errors = __esm({
|
|
|
126
126
|
code = "unsupported_jwe";
|
|
127
127
|
alg;
|
|
128
128
|
enc;
|
|
129
|
-
constructor(alg,
|
|
130
|
-
super(`Unsupported JWE algorithms: alg=${alg}, enc=${
|
|
129
|
+
constructor(alg, enc3) {
|
|
130
|
+
super(`Unsupported JWE algorithms: alg=${alg}, enc=${enc3}`);
|
|
131
131
|
this.alg = alg;
|
|
132
|
-
this.enc =
|
|
132
|
+
this.enc = enc3;
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
135
|
DecryptionFailedError = class extends OpenID4VPError {
|
|
@@ -313,9 +313,9 @@ var init_index_es = __esm({
|
|
|
313
313
|
static isBase64Url(data) {
|
|
314
314
|
return typeof data === STRING_TYPE && BASE64URL_REGEX.test(data);
|
|
315
315
|
}
|
|
316
|
-
static ToString(buffer,
|
|
316
|
+
static ToString(buffer, enc3 = "utf8") {
|
|
317
317
|
const buf = BufferSourceConverter.toUint8Array(buffer);
|
|
318
|
-
switch (
|
|
318
|
+
switch (enc3.toLowerCase()) {
|
|
319
319
|
case "utf8":
|
|
320
320
|
return this.ToUtf8String(buf);
|
|
321
321
|
case "binary":
|
|
@@ -332,14 +332,14 @@ var init_index_es = __esm({
|
|
|
332
332
|
case "utf16be":
|
|
333
333
|
return Utf16Converter.toString(buf);
|
|
334
334
|
default:
|
|
335
|
-
throw new Error(`Unknown type of encoding '${
|
|
335
|
+
throw new Error(`Unknown type of encoding '${enc3}'`);
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
|
-
static FromString(str,
|
|
338
|
+
static FromString(str, enc3 = "utf8") {
|
|
339
339
|
if (!str) {
|
|
340
340
|
return new ArrayBuffer(0);
|
|
341
341
|
}
|
|
342
|
-
switch (
|
|
342
|
+
switch (enc3.toLowerCase()) {
|
|
343
343
|
case "utf8":
|
|
344
344
|
return this.FromUtf8String(str);
|
|
345
345
|
case "binary":
|
|
@@ -356,7 +356,7 @@ var init_index_es = __esm({
|
|
|
356
356
|
case "utf16be":
|
|
357
357
|
return Utf16Converter.fromString(str);
|
|
358
358
|
default:
|
|
359
|
-
throw new Error(`Unknown type of encoding '${
|
|
359
|
+
throw new Error(`Unknown type of encoding '${enc3}'`);
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
static ToBase64(buffer) {
|
|
@@ -6361,6 +6361,33 @@ var init_es20152 = __esm({
|
|
|
6361
6361
|
init_validity();
|
|
6362
6362
|
}
|
|
6363
6363
|
});
|
|
6364
|
+
function getSkiHex(cert) {
|
|
6365
|
+
const ext = cert.getExtension(SubjectKeyIdentifierExtension);
|
|
6366
|
+
return ext ? ext.keyId.toLowerCase() : null;
|
|
6367
|
+
}
|
|
6368
|
+
function certificatesEqual(a, b) {
|
|
6369
|
+
const ab = new Uint8Array(a.rawData);
|
|
6370
|
+
const bb = new Uint8Array(b.rawData);
|
|
6371
|
+
if (ab.length !== bb.length) return false;
|
|
6372
|
+
for (let i = 0; i < ab.length; i++) {
|
|
6373
|
+
if (ab[i] !== bb[i]) return false;
|
|
6374
|
+
}
|
|
6375
|
+
return true;
|
|
6376
|
+
}
|
|
6377
|
+
function getSkiBytes(cert) {
|
|
6378
|
+
const ext = cert.getExtension(SubjectKeyIdentifierExtension);
|
|
6379
|
+
if (!ext) return null;
|
|
6380
|
+
const hex = ext.keyId;
|
|
6381
|
+
const out = new Uint8Array(hex.length / 2);
|
|
6382
|
+
for (let i = 0; i < out.length; i++) {
|
|
6383
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
6384
|
+
}
|
|
6385
|
+
return out;
|
|
6386
|
+
}
|
|
6387
|
+
var init_x509_utils = __esm({
|
|
6388
|
+
"src/trust/x509-utils.ts"() {
|
|
6389
|
+
}
|
|
6390
|
+
});
|
|
6364
6391
|
function mapX509AlgoToJwaName(cert) {
|
|
6365
6392
|
const alg = cert.signatureAlgorithm;
|
|
6366
6393
|
const name = alg?.name ?? "";
|
|
@@ -6370,8 +6397,11 @@ function mapX509AlgoToJwaName(cert) {
|
|
|
6370
6397
|
if (name === "ECDSA" && hash === "SHA-512") return "ES512";
|
|
6371
6398
|
if (name === "Ed25519" || name === "EdDSA") return "EdDSA";
|
|
6372
6399
|
if (name === "RSASSA-PKCS1-v1_5" && hash === "SHA-256") return "RS256";
|
|
6400
|
+
if (name === "RSASSA-PKCS1-v1_5" && hash === "SHA-384") return "RS384";
|
|
6401
|
+
if (name === "RSASSA-PKCS1-v1_5" && hash === "SHA-512") return "RS512";
|
|
6373
6402
|
if (name === "RSA-PSS" && hash === "SHA-256") return "PS256";
|
|
6374
6403
|
if (name === "RSA-PSS" && hash === "SHA-384") return "PS384";
|
|
6404
|
+
if (name === "RSA-PSS" && hash === "SHA-512") return "PS512";
|
|
6375
6405
|
return `${name}-${hash}`;
|
|
6376
6406
|
}
|
|
6377
6407
|
function collectLeafNames(leaf) {
|
|
@@ -6464,8 +6494,20 @@ var init_ChainBuilder = __esm({
|
|
|
6464
6494
|
init_es2015();
|
|
6465
6495
|
init_es20152();
|
|
6466
6496
|
init_errors();
|
|
6497
|
+
init_x509_utils();
|
|
6467
6498
|
NAME_CONSTRAINTS_OID = "2.5.29.30";
|
|
6468
|
-
DEFAULT_ALGORITHMS = [
|
|
6499
|
+
DEFAULT_ALGORITHMS = [
|
|
6500
|
+
"ES256",
|
|
6501
|
+
"ES384",
|
|
6502
|
+
"ES512",
|
|
6503
|
+
"EdDSA",
|
|
6504
|
+
"RS256",
|
|
6505
|
+
"RS384",
|
|
6506
|
+
"RS512",
|
|
6507
|
+
"PS256",
|
|
6508
|
+
"PS384",
|
|
6509
|
+
"PS512"
|
|
6510
|
+
];
|
|
6469
6511
|
ChainBuilder = class {
|
|
6470
6512
|
constructor(opts = {}) {
|
|
6471
6513
|
this.opts = opts;
|
|
@@ -6524,6 +6566,12 @@ var init_ChainBuilder = __esm({
|
|
|
6524
6566
|
current = issuer;
|
|
6525
6567
|
nonLeafDepth += 1;
|
|
6526
6568
|
}
|
|
6569
|
+
if (!certificatesEqual(current, anchor)) {
|
|
6570
|
+
throw new CertificateChainError(
|
|
6571
|
+
`certificate ${current.subject} shares the trust anchor's Subject DN but is not the anchor (no signature closure)`,
|
|
6572
|
+
{ reason: "signature" }
|
|
6573
|
+
);
|
|
6574
|
+
}
|
|
6527
6575
|
this.checkNameConstraints(leaf, chain.slice(1));
|
|
6528
6576
|
return chain;
|
|
6529
6577
|
}
|
|
@@ -7764,7 +7812,10 @@ var init_RevocationChecker = __esm({
|
|
|
7764
7812
|
if (hints.ocspUrl) {
|
|
7765
7813
|
try {
|
|
7766
7814
|
return await this.ocsp.checkCached(subjectCert, issuerCert, hints.ocspUrl, now);
|
|
7767
|
-
} catch {
|
|
7815
|
+
} catch (err) {
|
|
7816
|
+
console.warn(
|
|
7817
|
+
`[openid4vp] OCSP check failed for ${subjectCert.subject} via ${hints.ocspUrl}, falling back to CRL: ${err.message}`
|
|
7818
|
+
);
|
|
7768
7819
|
}
|
|
7769
7820
|
}
|
|
7770
7821
|
for (const url of hints.crlUrls) {
|
|
@@ -7792,6 +7843,9 @@ var init_RevocationChecker = __esm({
|
|
|
7792
7843
|
return { status: "good", source: "crl", checkedAt: now };
|
|
7793
7844
|
} catch (err) {
|
|
7794
7845
|
if (err instanceof RevocationCheckFailedError) throw err;
|
|
7846
|
+
console.warn(
|
|
7847
|
+
`[openid4vp] CRL check failed for ${subjectCert.subject} via ${url}: ${err.message}`
|
|
7848
|
+
);
|
|
7795
7849
|
continue;
|
|
7796
7850
|
}
|
|
7797
7851
|
}
|
|
@@ -7810,24 +7864,6 @@ var init_RevocationChecker = __esm({
|
|
|
7810
7864
|
};
|
|
7811
7865
|
}
|
|
7812
7866
|
});
|
|
7813
|
-
function getSkiHex(cert) {
|
|
7814
|
-
const ext = cert.getExtension(SubjectKeyIdentifierExtension);
|
|
7815
|
-
return ext ? ext.keyId.toLowerCase() : null;
|
|
7816
|
-
}
|
|
7817
|
-
function getSkiBytes(cert) {
|
|
7818
|
-
const ext = cert.getExtension(SubjectKeyIdentifierExtension);
|
|
7819
|
-
if (!ext) return null;
|
|
7820
|
-
const hex = ext.keyId;
|
|
7821
|
-
const out = new Uint8Array(hex.length / 2);
|
|
7822
|
-
for (let i = 0; i < out.length; i++) {
|
|
7823
|
-
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
7824
|
-
}
|
|
7825
|
-
return out;
|
|
7826
|
-
}
|
|
7827
|
-
var init_x509_utils = __esm({
|
|
7828
|
-
"src/trust/x509-utils.ts"() {
|
|
7829
|
-
}
|
|
7830
|
-
});
|
|
7831
7867
|
|
|
7832
7868
|
// src/trust/lotl-loa-mapping.ts
|
|
7833
7869
|
function mapLoA(uris) {
|
|
@@ -8009,8 +8045,13 @@ var init_TrustEvaluator = __esm({
|
|
|
8009
8045
|
);
|
|
8010
8046
|
const terminusCert = chain[chain.length - 1];
|
|
8011
8047
|
const anchor = anchors.find(
|
|
8012
|
-
(a) => a.certificate
|
|
8013
|
-
)
|
|
8048
|
+
(a) => certificatesEqual(a.certificate, terminusCert)
|
|
8049
|
+
);
|
|
8050
|
+
if (!anchor) {
|
|
8051
|
+
throw new TrustAnchorNotFoundError(
|
|
8052
|
+
`chain terminus ${terminusCert.subject} does not match any trust anchor certificate`
|
|
8053
|
+
);
|
|
8054
|
+
}
|
|
8014
8055
|
const issuerForRevocation = chain.length > 1 ? chain[1] : terminusCert;
|
|
8015
8056
|
const revocation = await this.revocationChecker.check(
|
|
8016
8057
|
leaf,
|
|
@@ -8129,8 +8170,8 @@ async function createSignedAuthorizationRequest(input, query) {
|
|
|
8129
8170
|
vp_formats_supported: input.vpFormatsSupported
|
|
8130
8171
|
};
|
|
8131
8172
|
if (responseMode === "direct_post.jwt" && input.encryptionKey) {
|
|
8132
|
-
const
|
|
8133
|
-
if (
|
|
8173
|
+
const enc3 = input.encryptionKey.supportedEncValues;
|
|
8174
|
+
if (enc3 !== void 0 && enc3.length === 0) {
|
|
8134
8175
|
throw new SignedRequestBuildError(
|
|
8135
8176
|
"empty_supported_enc_values",
|
|
8136
8177
|
"encryptionKey.supportedEncValues must not be empty (omit to use defaults [A128GCM, A256GCM])"
|
|
@@ -8138,7 +8179,7 @@ async function createSignedAuthorizationRequest(input, query) {
|
|
|
8138
8179
|
}
|
|
8139
8180
|
const jwk = { ...input.encryptionKey.publicJwk, use: "enc" };
|
|
8140
8181
|
clientMetadata.jwks = { keys: [jwk] };
|
|
8141
|
-
const encValues =
|
|
8182
|
+
const encValues = enc3 ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
|
|
8142
8183
|
clientMetadata.encrypted_response_enc_values_supported = encValues;
|
|
8143
8184
|
clientMetadata.authorization_encrypted_response_alg = "ECDH-ES";
|
|
8144
8185
|
clientMetadata.authorization_encrypted_response_enc = encValues[0];
|
|
@@ -8314,7 +8355,7 @@ function isHaipQuery(query) {
|
|
|
8314
8355
|
|
|
8315
8356
|
// src/crypto/cose-sign1.ts
|
|
8316
8357
|
init_errors();
|
|
8317
|
-
var decoder = new Encoder({ mapsAsObjects: false, useRecords: false });
|
|
8358
|
+
var decoder = new Encoder({ mapsAsObjects: false, useRecords: false, tagUint8Array: false });
|
|
8318
8359
|
var HEADER_ALG = 1;
|
|
8319
8360
|
var HEADER_X5CHAIN = 33;
|
|
8320
8361
|
var COSE_ALG_TO_NAME = {
|
|
@@ -8412,6 +8453,131 @@ async function verifyCoseSign1(coseSign1, publicKey, allowedAlgorithms) {
|
|
|
8412
8453
|
}
|
|
8413
8454
|
}
|
|
8414
8455
|
|
|
8456
|
+
// src/crypto/device-auth.ts
|
|
8457
|
+
init_errors();
|
|
8458
|
+
var enc = new Encoder({ mapsAsObjects: false, useRecords: false, tagUint8Array: false });
|
|
8459
|
+
var HEADER_ALG2 = 1;
|
|
8460
|
+
var COSE_ALG_TO_NAME2 = {
|
|
8461
|
+
[-7]: "ES256",
|
|
8462
|
+
[-35]: "ES384",
|
|
8463
|
+
[-36]: "ES512"
|
|
8464
|
+
};
|
|
8465
|
+
var ALG_TO_HASH2 = { ES256: "SHA-256", ES384: "SHA-384", ES512: "SHA-512" };
|
|
8466
|
+
var COSE_KEY_KTY = 1;
|
|
8467
|
+
var COSE_KEY_CRV = -1;
|
|
8468
|
+
var COSE_KEY_X = -2;
|
|
8469
|
+
var COSE_KEY_Y = -3;
|
|
8470
|
+
var COSE_KTY_EC2 = 2;
|
|
8471
|
+
var COSE_CRV_TO_JWK = { 1: "P-256", 2: "P-384", 3: "P-521" };
|
|
8472
|
+
function concatBytes(...chunks) {
|
|
8473
|
+
let len = 0;
|
|
8474
|
+
for (const c of chunks) len += c.length;
|
|
8475
|
+
const out = new Uint8Array(len);
|
|
8476
|
+
let off = 0;
|
|
8477
|
+
for (const c of chunks) {
|
|
8478
|
+
out.set(c, off);
|
|
8479
|
+
off += c.length;
|
|
8480
|
+
}
|
|
8481
|
+
return out;
|
|
8482
|
+
}
|
|
8483
|
+
function bytesToBase64Url(bytes) {
|
|
8484
|
+
let bin = "";
|
|
8485
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
8486
|
+
return globalThis.btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8487
|
+
}
|
|
8488
|
+
function encodeTag24(inner) {
|
|
8489
|
+
return concatBytes(new Uint8Array([216, 24]), new Uint8Array(enc.encode(inner)));
|
|
8490
|
+
}
|
|
8491
|
+
function buildDeviceAuthenticationBytes(params) {
|
|
8492
|
+
const deviceAuthentication = concatBytes(
|
|
8493
|
+
new Uint8Array([132]),
|
|
8494
|
+
// array(4)
|
|
8495
|
+
new Uint8Array(enc.encode("DeviceAuthentication")),
|
|
8496
|
+
params.sessionTranscript,
|
|
8497
|
+
new Uint8Array(enc.encode(params.docType)),
|
|
8498
|
+
encodeTag24(params.deviceNameSpacesInner)
|
|
8499
|
+
);
|
|
8500
|
+
return encodeTag24(deviceAuthentication);
|
|
8501
|
+
}
|
|
8502
|
+
function encodeDeviceSigStructure(protectedBytes, deviceAuthBytes) {
|
|
8503
|
+
const sig = ["Signature1", protectedBytes, new Uint8Array(0), deviceAuthBytes];
|
|
8504
|
+
return new Uint8Array(enc.encode(sig));
|
|
8505
|
+
}
|
|
8506
|
+
async function importDeviceKeyFromCose(coseKey) {
|
|
8507
|
+
if (!(coseKey instanceof Map)) {
|
|
8508
|
+
throw new MalformedCredentialError("MSO deviceKeyInfo.deviceKey is not a COSE_Key map");
|
|
8509
|
+
}
|
|
8510
|
+
const kty = coseKey.get(COSE_KEY_KTY);
|
|
8511
|
+
if (kty !== COSE_KTY_EC2) {
|
|
8512
|
+
throw new MalformedCredentialError(`Unsupported COSE key type for device key: ${String(kty)}`);
|
|
8513
|
+
}
|
|
8514
|
+
const crv = coseKey.get(COSE_KEY_CRV);
|
|
8515
|
+
const crvName = typeof crv === "number" ? COSE_CRV_TO_JWK[crv] : void 0;
|
|
8516
|
+
const x = coseKey.get(COSE_KEY_X);
|
|
8517
|
+
const y = coseKey.get(COSE_KEY_Y);
|
|
8518
|
+
if (!crvName || !(x instanceof Uint8Array) || !(y instanceof Uint8Array)) {
|
|
8519
|
+
throw new MalformedCredentialError("Malformed EC2 COSE_Key in MSO deviceKeyInfo");
|
|
8520
|
+
}
|
|
8521
|
+
return crypto.subtle.importKey(
|
|
8522
|
+
"jwk",
|
|
8523
|
+
{ kty: "EC", crv: crvName, x: bytesToBase64Url(x), y: bytesToBase64Url(y) },
|
|
8524
|
+
{ name: "ECDSA", namedCurve: crvName },
|
|
8525
|
+
false,
|
|
8526
|
+
["verify"]
|
|
8527
|
+
);
|
|
8528
|
+
}
|
|
8529
|
+
async function verifyDeviceAuth(params) {
|
|
8530
|
+
const { deviceSigned, deviceKeyCose, docType, sessionTranscript, allowedAlgorithms } = params;
|
|
8531
|
+
const deviceAuth = deviceSigned.get("deviceAuth");
|
|
8532
|
+
if (!(deviceAuth instanceof Map)) {
|
|
8533
|
+
throw new MalformedCredentialError("deviceSigned.deviceAuth is missing or malformed");
|
|
8534
|
+
}
|
|
8535
|
+
if (deviceAuth.has("deviceMac") && !deviceAuth.has("deviceSignature")) {
|
|
8536
|
+
throw new MalformedCredentialError("mDOC DeviceMac authentication is not supported");
|
|
8537
|
+
}
|
|
8538
|
+
const deviceSignature = deviceAuth.get("deviceSignature");
|
|
8539
|
+
if (!Array.isArray(deviceSignature) || deviceSignature.length !== 4) {
|
|
8540
|
+
throw new MalformedCredentialError("deviceAuth.deviceSignature must be a COSE_Sign1 array");
|
|
8541
|
+
}
|
|
8542
|
+
const protectedBytes = deviceSignature[0];
|
|
8543
|
+
const signature = deviceSignature[3];
|
|
8544
|
+
if (!(protectedBytes instanceof Uint8Array) || !(signature instanceof Uint8Array)) {
|
|
8545
|
+
throw new MalformedCredentialError("DeviceSignature protected header / signature must be byte strings");
|
|
8546
|
+
}
|
|
8547
|
+
let alg;
|
|
8548
|
+
try {
|
|
8549
|
+
const ph = enc.decode(protectedBytes);
|
|
8550
|
+
const label = ph instanceof Map ? ph.get(HEADER_ALG2) : void 0;
|
|
8551
|
+
alg = typeof label === "number" ? COSE_ALG_TO_NAME2[label] : void 0;
|
|
8552
|
+
} catch {
|
|
8553
|
+
throw new MalformedCredentialError("DeviceSignature protected header is not decodable CBOR");
|
|
8554
|
+
}
|
|
8555
|
+
if (!alg || !allowedAlgorithms.includes(alg)) {
|
|
8556
|
+
throw new MalformedCredentialError(`DeviceSignature uses a disallowed algorithm: ${String(alg)}`);
|
|
8557
|
+
}
|
|
8558
|
+
const deviceNameSpacesInner = extractTag24Inner(deviceSigned.get("nameSpaces"));
|
|
8559
|
+
const deviceAuthBytes = buildDeviceAuthenticationBytes({ sessionTranscript, docType, deviceNameSpacesInner });
|
|
8560
|
+
const sigInput = encodeDeviceSigStructure(protectedBytes, deviceAuthBytes);
|
|
8561
|
+
const deviceKey = await importDeviceKeyFromCose(deviceKeyCose);
|
|
8562
|
+
const ok = await crypto.subtle.verify(
|
|
8563
|
+
{ name: "ECDSA", hash: ALG_TO_HASH2[alg] },
|
|
8564
|
+
deviceKey,
|
|
8565
|
+
new Uint8Array(signature),
|
|
8566
|
+
sigInput
|
|
8567
|
+
);
|
|
8568
|
+
if (!ok) {
|
|
8569
|
+
throw new MalformedCredentialError("mDOC device authentication (DeviceSignature) failed to verify");
|
|
8570
|
+
}
|
|
8571
|
+
}
|
|
8572
|
+
function extractTag24Inner(value) {
|
|
8573
|
+
if (value instanceof Uint8Array) return value;
|
|
8574
|
+
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.
|
|
8575
|
+
("tag" in value ? value.tag === 24 : true)) {
|
|
8576
|
+
return value.value;
|
|
8577
|
+
}
|
|
8578
|
+
throw new MalformedCredentialError("deviceSigned.nameSpaces must be tag-24 wrapped CBOR");
|
|
8579
|
+
}
|
|
8580
|
+
|
|
8415
8581
|
// src/crypto/digest.ts
|
|
8416
8582
|
init_errors();
|
|
8417
8583
|
var decoder2 = new Encoder({ mapsAsObjects: false, useRecords: false });
|
|
@@ -8544,6 +8710,14 @@ function decodeMso(payload) {
|
|
|
8544
8710
|
}
|
|
8545
8711
|
valueDigests.set(ns, perNs);
|
|
8546
8712
|
}
|
|
8713
|
+
let deviceKey;
|
|
8714
|
+
const dkiRaw = m.get("deviceKeyInfo");
|
|
8715
|
+
if (dkiRaw instanceof Map) {
|
|
8716
|
+
const dk = dkiRaw.get("deviceKey");
|
|
8717
|
+
if (dk instanceof Map) {
|
|
8718
|
+
deviceKey = dk;
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8547
8721
|
const viRaw = m.get("validityInfo");
|
|
8548
8722
|
if (!(viRaw instanceof Map)) {
|
|
8549
8723
|
throw new MalformedCredentialError("MSO.validityInfo must be a Map");
|
|
@@ -8557,7 +8731,7 @@ function decodeMso(payload) {
|
|
|
8557
8731
|
if (vi.has("expectedUpdate")) {
|
|
8558
8732
|
validityInfo.expectedUpdate = decodeDateLike(vi.get("expectedUpdate"));
|
|
8559
8733
|
}
|
|
8560
|
-
return { version, digestAlgorithm, valueDigests, docType, validityInfo };
|
|
8734
|
+
return { version, digestAlgorithm, valueDigests, docType, deviceKey, validityInfo };
|
|
8561
8735
|
}
|
|
8562
8736
|
function validateMsoValidity(mso, now) {
|
|
8563
8737
|
const { signed, validFrom, validUntil } = mso.validityInfo;
|
|
@@ -8607,15 +8781,54 @@ ${lines.join("\n")}
|
|
|
8607
8781
|
function extractCountryHintFromCert(_certDer) {
|
|
8608
8782
|
return "";
|
|
8609
8783
|
}
|
|
8784
|
+
function cborByteStringHeader(length) {
|
|
8785
|
+
if (length < 24) return Uint8Array.of(64 | length);
|
|
8786
|
+
if (length < 256) return Uint8Array.of(88, length);
|
|
8787
|
+
if (length < 65536) return Uint8Array.of(89, length >> 8 & 255, length & 255);
|
|
8788
|
+
if (length < 4294967296) {
|
|
8789
|
+
return Uint8Array.of(
|
|
8790
|
+
90,
|
|
8791
|
+
length >>> 24 & 255,
|
|
8792
|
+
length >>> 16 & 255,
|
|
8793
|
+
length >>> 8 & 255,
|
|
8794
|
+
length & 255
|
|
8795
|
+
);
|
|
8796
|
+
}
|
|
8797
|
+
const hi = Math.floor(length / 4294967296);
|
|
8798
|
+
const lo = length >>> 0;
|
|
8799
|
+
return Uint8Array.of(
|
|
8800
|
+
91,
|
|
8801
|
+
hi >>> 24 & 255,
|
|
8802
|
+
hi >>> 16 & 255,
|
|
8803
|
+
hi >>> 8 & 255,
|
|
8804
|
+
hi & 255,
|
|
8805
|
+
lo >>> 24 & 255,
|
|
8806
|
+
lo >>> 16 & 255,
|
|
8807
|
+
lo >>> 8 & 255,
|
|
8808
|
+
lo & 255
|
|
8809
|
+
);
|
|
8810
|
+
}
|
|
8811
|
+
function wrapIssuerSignedItemBytes(inner) {
|
|
8812
|
+
const header = cborByteStringHeader(inner.length);
|
|
8813
|
+
const out = new Uint8Array(2 + header.length + inner.length);
|
|
8814
|
+
out[0] = 216;
|
|
8815
|
+
out[1] = 24;
|
|
8816
|
+
out.set(header, 2);
|
|
8817
|
+
out.set(inner, 2 + header.length);
|
|
8818
|
+
return out;
|
|
8819
|
+
}
|
|
8610
8820
|
function extractRawItemBytes(item) {
|
|
8611
8821
|
if (item instanceof Tag && item.tag === 24 && item.value instanceof Uint8Array) {
|
|
8612
|
-
return item.value;
|
|
8822
|
+
return wrapIssuerSignedItemBytes(item.value);
|
|
8613
8823
|
}
|
|
8614
8824
|
if (item instanceof Uint8Array) {
|
|
8615
|
-
|
|
8825
|
+
if (item.length >= 2 && item[0] === 216 && item[1] === 24) {
|
|
8826
|
+
return item;
|
|
8827
|
+
}
|
|
8828
|
+
return wrapIssuerSignedItemBytes(item);
|
|
8616
8829
|
}
|
|
8617
8830
|
if (item !== null && typeof item === "object" && !(item instanceof Tag) && !(item instanceof Map) && "value" in item && item.value instanceof Uint8Array) {
|
|
8618
|
-
return item.value;
|
|
8831
|
+
return wrapIssuerSignedItemBytes(item.value);
|
|
8619
8832
|
}
|
|
8620
8833
|
throw new MalformedCredentialError("IssuerSignedItem must be a tag-24 byte string");
|
|
8621
8834
|
}
|
|
@@ -8805,6 +9018,54 @@ var MdocParser = class {
|
|
|
8805
9018
|
nameSpaces.set(ns, items.map(extractRawItemBytes));
|
|
8806
9019
|
}
|
|
8807
9020
|
await verifyAllDigests(nameSpaces, mso);
|
|
9021
|
+
const deviceSigned = document.get("deviceSigned");
|
|
9022
|
+
if (!(deviceSigned instanceof Map)) {
|
|
9023
|
+
return {
|
|
9024
|
+
valid: false,
|
|
9025
|
+
format: this.format,
|
|
9026
|
+
claims: {},
|
|
9027
|
+
issuer: { certificate: issuerCertBytes, country: "" },
|
|
9028
|
+
error: "Missing deviceSigned \u2014 mDOC device authentication is required"
|
|
9029
|
+
};
|
|
9030
|
+
}
|
|
9031
|
+
if (!(options.mdocSessionTranscript instanceof Uint8Array) || options.mdocSessionTranscript.length === 0) {
|
|
9032
|
+
return {
|
|
9033
|
+
valid: false,
|
|
9034
|
+
format: this.format,
|
|
9035
|
+
claims: {},
|
|
9036
|
+
issuer: { certificate: issuerCertBytes, country: "" },
|
|
9037
|
+
error: "mdocSessionTranscript is required to verify mDOC device authentication"
|
|
9038
|
+
};
|
|
9039
|
+
}
|
|
9040
|
+
if (!mso.deviceKey) {
|
|
9041
|
+
return {
|
|
9042
|
+
valid: false,
|
|
9043
|
+
format: this.format,
|
|
9044
|
+
claims: {},
|
|
9045
|
+
issuer: { certificate: issuerCertBytes, country: "" },
|
|
9046
|
+
error: "MSO is missing deviceKeyInfo.deviceKey; cannot verify device authentication"
|
|
9047
|
+
};
|
|
9048
|
+
}
|
|
9049
|
+
try {
|
|
9050
|
+
await verifyDeviceAuth({
|
|
9051
|
+
deviceSigned,
|
|
9052
|
+
deviceKeyCose: mso.deviceKey,
|
|
9053
|
+
docType: mso.docType,
|
|
9054
|
+
sessionTranscript: options.mdocSessionTranscript,
|
|
9055
|
+
allowedAlgorithms
|
|
9056
|
+
});
|
|
9057
|
+
} catch (err) {
|
|
9058
|
+
if (err instanceof MalformedCredentialError) {
|
|
9059
|
+
return {
|
|
9060
|
+
valid: false,
|
|
9061
|
+
format: this.format,
|
|
9062
|
+
claims: {},
|
|
9063
|
+
issuer: { certificate: issuerCertBytes, country: "" },
|
|
9064
|
+
error: err.message
|
|
9065
|
+
};
|
|
9066
|
+
}
|
|
9067
|
+
throw err;
|
|
9068
|
+
}
|
|
8808
9069
|
const { flat: claims, namespaced: namespacedClaims } = mapRawToClaims(nameSpaces);
|
|
8809
9070
|
const issuer = {
|
|
8810
9071
|
certificate: issuerCertBytes,
|
|
@@ -9077,11 +9338,21 @@ var SdJwtParser = class {
|
|
|
9077
9338
|
return invalidResult("Disclosure hash mismatch");
|
|
9078
9339
|
}
|
|
9079
9340
|
}
|
|
9341
|
+
const cnf = payload["cnf"];
|
|
9342
|
+
const isHolderBound = !!cnf && typeof cnf === "object" && !!cnf["jwk"];
|
|
9343
|
+
const keyBindingRequired = isHolderBound || options.requireKeyBinding === true;
|
|
9344
|
+
if (keyBindingRequired && !parts.kbJwt) {
|
|
9345
|
+
return invalidResult(
|
|
9346
|
+
"Key binding JWT is required for holder-bound credentials but was not presented"
|
|
9347
|
+
);
|
|
9348
|
+
}
|
|
9080
9349
|
if (parts.kbJwt) {
|
|
9081
|
-
|
|
9082
|
-
if (!cnf || typeof cnf !== "object" || !cnf["jwk"]) {
|
|
9350
|
+
if (!isHolderBound) {
|
|
9083
9351
|
throw new MalformedCredentialError("Missing cnf.jwk in issuer JWT for key binding");
|
|
9084
9352
|
}
|
|
9353
|
+
if (typeof options.nonce !== "string" || options.nonce.length === 0) {
|
|
9354
|
+
return invalidResult("A nonce is required to verify the key binding JWT");
|
|
9355
|
+
}
|
|
9085
9356
|
let holderKey;
|
|
9086
9357
|
try {
|
|
9087
9358
|
const kbHeader = decodeProtectedHeader(parts.kbJwt);
|
|
@@ -9199,9 +9470,9 @@ async function decryptAuthorizationResponse(jwe, privateKey) {
|
|
|
9199
9470
|
});
|
|
9200
9471
|
}
|
|
9201
9472
|
const alg = typeof header.alg === "string" ? header.alg : "";
|
|
9202
|
-
const
|
|
9203
|
-
if (alg !== SUPPORTED_ALG || !SUPPORTED_ENC.has(
|
|
9204
|
-
throw new UnsupportedJweError(alg,
|
|
9473
|
+
const enc3 = typeof header.enc === "string" ? header.enc : "";
|
|
9474
|
+
if (alg !== SUPPORTED_ALG || !SUPPORTED_ENC.has(enc3)) {
|
|
9475
|
+
throw new UnsupportedJweError(alg, enc3);
|
|
9205
9476
|
}
|
|
9206
9477
|
let plaintext;
|
|
9207
9478
|
try {
|
|
@@ -9219,6 +9490,29 @@ async function decryptAuthorizationResponse(jwe, privateKey) {
|
|
|
9219
9490
|
});
|
|
9220
9491
|
}
|
|
9221
9492
|
}
|
|
9493
|
+
var enc2 = new Encoder({ mapsAsObjects: false, useRecords: false, tagUint8Array: false });
|
|
9494
|
+
async function sha256(bytes) {
|
|
9495
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
9496
|
+
}
|
|
9497
|
+
async function buildOid4vpSessionTranscript(params) {
|
|
9498
|
+
const { clientId, responseUri, nonce, mdocGeneratedNonce } = params;
|
|
9499
|
+
const clientIdHash = await sha256(new Uint8Array(enc2.encode([clientId, mdocGeneratedNonce])));
|
|
9500
|
+
const responseUriHash = await sha256(new Uint8Array(enc2.encode([responseUri, mdocGeneratedNonce])));
|
|
9501
|
+
return new Uint8Array(enc2.encode([null, null, [clientIdHash, responseUriHash, nonce]]));
|
|
9502
|
+
}
|
|
9503
|
+
async function jwkSha256Thumbprint(jwk) {
|
|
9504
|
+
if (jwk.kty !== "EC") {
|
|
9505
|
+
throw new Error(`Unsupported JWK kty for thumbprint: ${jwk.kty}`);
|
|
9506
|
+
}
|
|
9507
|
+
const canonical = `{"crv":"${jwk.crv}","kty":"EC","x":"${jwk.x}","y":"${jwk.y}"}`;
|
|
9508
|
+
return sha256(new TextEncoder().encode(canonical));
|
|
9509
|
+
}
|
|
9510
|
+
async function buildOpenID4VPHandoverSessionTranscript(params) {
|
|
9511
|
+
const { clientId, nonce, responseUri, verifierEncryptionJwk } = params;
|
|
9512
|
+
const thumb = verifierEncryptionJwk ? await jwkSha256Thumbprint(verifierEncryptionJwk) : null;
|
|
9513
|
+
const infoHash = await sha256(new Uint8Array(enc2.encode([clientId, nonce, thumb, responseUri])));
|
|
9514
|
+
return new Uint8Array(enc2.encode([null, null, ["OpenID4VPHandover", infoHash]]));
|
|
9515
|
+
}
|
|
9222
9516
|
|
|
9223
9517
|
// src/verify.ts
|
|
9224
9518
|
init_errors();
|
|
@@ -9256,6 +9550,42 @@ async function verifyPresentation(vpToken, query, options) {
|
|
|
9256
9550
|
function isEncryptedResponse(x) {
|
|
9257
9551
|
return typeof x === "object" && x !== null && "response" in x && typeof x.response === "string" && Object.keys(x).length === 1;
|
|
9258
9552
|
}
|
|
9553
|
+
var BASE64URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
9554
|
+
function base64UrlToBytes(input) {
|
|
9555
|
+
const cleaned = input.replace(/=+$/, "");
|
|
9556
|
+
const byteLength = Math.floor(cleaned.length * 3 / 4);
|
|
9557
|
+
const bytes = new Uint8Array(byteLength);
|
|
9558
|
+
let byteIndex = 0;
|
|
9559
|
+
for (let i = 0; i < cleaned.length; i += 4) {
|
|
9560
|
+
const a = BASE64URL_CHARS.indexOf(cleaned[i]);
|
|
9561
|
+
const b = i + 1 < cleaned.length ? BASE64URL_CHARS.indexOf(cleaned[i + 1]) : 0;
|
|
9562
|
+
const c = i + 2 < cleaned.length ? BASE64URL_CHARS.indexOf(cleaned[i + 2]) : 0;
|
|
9563
|
+
const d = i + 3 < cleaned.length ? BASE64URL_CHARS.indexOf(cleaned[i + 3]) : 0;
|
|
9564
|
+
if (a === -1 || b === -1 || c === -1 || d === -1) {
|
|
9565
|
+
throw new TypeError("Invalid base64url character in vp_token/apu");
|
|
9566
|
+
}
|
|
9567
|
+
bytes[byteIndex++] = a << 2 | b >> 4;
|
|
9568
|
+
if (byteIndex < byteLength) bytes[byteIndex++] = (b & 15) << 4 | c >> 2;
|
|
9569
|
+
if (byteIndex < byteLength) bytes[byteIndex++] = (c & 3) << 6 | d;
|
|
9570
|
+
}
|
|
9571
|
+
return bytes;
|
|
9572
|
+
}
|
|
9573
|
+
function extractMdocGeneratedNonce(jwe) {
|
|
9574
|
+
let apu;
|
|
9575
|
+
try {
|
|
9576
|
+
apu = decodeProtectedHeader(jwe).apu;
|
|
9577
|
+
} catch {
|
|
9578
|
+
return void 0;
|
|
9579
|
+
}
|
|
9580
|
+
if (typeof apu !== "string" || apu.length === 0) {
|
|
9581
|
+
return void 0;
|
|
9582
|
+
}
|
|
9583
|
+
try {
|
|
9584
|
+
return new TextDecoder().decode(base64UrlToBytes(apu));
|
|
9585
|
+
} catch {
|
|
9586
|
+
return void 0;
|
|
9587
|
+
}
|
|
9588
|
+
}
|
|
9259
9589
|
function isAuthorizationResponse(x) {
|
|
9260
9590
|
return typeof x === "object" && x !== null && "vp_token" in x && typeof x.vp_token === "object" && x.vp_token !== null;
|
|
9261
9591
|
}
|
|
@@ -9264,11 +9594,32 @@ async function verifyAuthorizationResponse(envelope, query, options) {
|
|
|
9264
9594
|
if (!options.decryptionKey) {
|
|
9265
9595
|
throw new MissingDecryptionKeyError();
|
|
9266
9596
|
}
|
|
9597
|
+
const nextOptions = { ...options };
|
|
9598
|
+
if (options.mdocSessionTranscript === void 0 && options.clientId !== void 0 && options.responseUri !== void 0 && options.nonce !== void 0) {
|
|
9599
|
+
if (options.sessionTranscriptProfile === "openid4vp-1.0") {
|
|
9600
|
+
nextOptions.mdocSessionTranscript = await buildOpenID4VPHandoverSessionTranscript({
|
|
9601
|
+
clientId: options.clientId,
|
|
9602
|
+
nonce: options.nonce,
|
|
9603
|
+
responseUri: options.responseUri,
|
|
9604
|
+
verifierEncryptionJwk: options.verifierEncryptionJwk
|
|
9605
|
+
});
|
|
9606
|
+
} else {
|
|
9607
|
+
const mdocGeneratedNonce = extractMdocGeneratedNonce(envelope.response);
|
|
9608
|
+
if (mdocGeneratedNonce !== void 0) {
|
|
9609
|
+
nextOptions.mdocSessionTranscript = await buildOid4vpSessionTranscript({
|
|
9610
|
+
clientId: options.clientId,
|
|
9611
|
+
responseUri: options.responseUri,
|
|
9612
|
+
nonce: options.nonce,
|
|
9613
|
+
mdocGeneratedNonce
|
|
9614
|
+
});
|
|
9615
|
+
}
|
|
9616
|
+
}
|
|
9617
|
+
}
|
|
9267
9618
|
const decrypted = await decryptAuthorizationResponse(
|
|
9268
9619
|
envelope.response,
|
|
9269
9620
|
options.decryptionKey
|
|
9270
9621
|
);
|
|
9271
|
-
return verifyAuthorizationResponse(decrypted, query,
|
|
9622
|
+
return verifyAuthorizationResponse(decrypted, query, nextOptions);
|
|
9272
9623
|
}
|
|
9273
9624
|
if (!isAuthorizationResponse(envelope)) {
|
|
9274
9625
|
throw new TypeError("envelope must contain vp_token as a JSON object per OpenID4VP 1.0 \xA78.1");
|
|
@@ -9293,7 +9644,8 @@ async function verifyAuthorizationResponse(envelope, query, options) {
|
|
|
9293
9644
|
throw new MultipleCredentialsNotSupportedError(queryIds.length, presentationCount);
|
|
9294
9645
|
}
|
|
9295
9646
|
const presentation = vpToken[queryIds[0]][0];
|
|
9296
|
-
|
|
9647
|
+
const decodedPresentation = query.credentials[0]?.format === "mso_mdoc" && typeof presentation === "string" ? base64UrlToBytes(presentation) : presentation;
|
|
9648
|
+
return verifyPresentation(decodedPresentation, query, options);
|
|
9297
9649
|
}
|
|
9298
9650
|
|
|
9299
9651
|
// src/index.ts
|
|
@@ -9976,6 +10328,6 @@ asn1js/build/index.es.js:
|
|
|
9976
10328
|
*)
|
|
9977
10329
|
*/
|
|
9978
10330
|
|
|
9979
|
-
export { CertificateChainError, CompositeTrustStore, DecryptionFailedError, ExpiredCredentialError, HAIP_DOCTYPE_NAMESPACES, HaipValidationError, InMemoryCache, InvalidSignatureError, LotlFetchError, LotlSignatureError, LotlTrustStore, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, SignedRequestBuildError, StaticTrustStore, TrustAnchorNotFoundError, UnsupportedFormatError, UnsupportedJweError, VERSION, buildHaipQuery, createAuthorizationRequest, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
|
|
10331
|
+
export { CertificateChainError, CompositeTrustStore, DecryptionFailedError, ExpiredCredentialError, HAIP_DOCTYPE_NAMESPACES, HaipValidationError, InMemoryCache, InvalidSignatureError, LotlFetchError, LotlSignatureError, LotlTrustStore, MalformedCredentialError, MdocParser, MissingDecryptionKeyError, MultipleCredentialsNotSupportedError, NonceValidationError, OpenID4VPError, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, SignedRequestBuildError, StaticTrustStore, TrustAnchorNotFoundError, UnsupportedFormatError, UnsupportedJweError, VERSION, buildHaipQuery, buildOid4vpSessionTranscript, buildOpenID4VPHandoverSessionTranscript, createAuthorizationRequest, createSignedAuthorizationRequest, decryptAuthorizationResponse, isHaipQuery, parsePresentation, validateHaipQuery, verifyAuthorizationResponse, verifyPresentation };
|
|
9980
10332
|
//# sourceMappingURL=index.js.map
|
|
9981
10333
|
//# sourceMappingURL=index.js.map
|