@openeudi/openid4vp 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/dist/index.cjs +396 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -2
- package/dist/index.d.ts +124 -2
- package/dist/index.js +394 -38
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { X509Certificate, SubjectKeyIdentifierExtension, X509Certificates, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags as KeyUsageFlags$1, AuthorityKeyIdentifierExtension, SubjectAlternativeNameExtension, ExtendedKeyUsageExtension, Name as Name$1 } from '@peculiar/x509';
|
|
2
2
|
import { v4 } from 'uuid';
|
|
3
|
+
import { SignJWT, importX509, decodeProtectedHeader, jwtVerify, importJWK, compactDecrypt } from 'jose';
|
|
3
4
|
import { Encoder, Tag } from 'cbor-x';
|
|
4
|
-
import { importX509, decodeProtectedHeader, jwtVerify, importJWK } from 'jose';
|
|
5
5
|
import { splitSdJwt, decodeSdJwt, getClaims } from '@sd-jwt/decode';
|
|
6
6
|
import { matchQuery, buildSubmission } from '@openeudi/dcql';
|
|
7
7
|
export { DcqlMatchError, DcqlValidationError } from '@openeudi/dcql';
|
|
@@ -19,7 +19,7 @@ var __export = (target, all) => {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
// src/errors.ts
|
|
22
|
-
var InvalidSignatureError, ExpiredCredentialError, UnsupportedFormatError, MalformedCredentialError, NonceValidationError, HaipValidationError, OpenID4VPError, TrustAnchorNotFoundError, CertificateChainError, RevokedCertificateError, RevocationCheckFailedError, LotlFetchError, LotlSignatureError;
|
|
22
|
+
var InvalidSignatureError, ExpiredCredentialError, UnsupportedFormatError, MalformedCredentialError, NonceValidationError, HaipValidationError, OpenID4VPError, TrustAnchorNotFoundError, CertificateChainError, RevokedCertificateError, RevocationCheckFailedError, LotlFetchError, LotlSignatureError, SignedRequestBuildError, UnsupportedJweError, DecryptionFailedError, MissingDecryptionKeyError, MultipleCredentialsNotSupportedError;
|
|
23
23
|
var init_errors = __esm({
|
|
24
24
|
"src/errors.ts"() {
|
|
25
25
|
InvalidSignatureError = class extends Error {
|
|
@@ -115,6 +115,47 @@ var init_errors = __esm({
|
|
|
115
115
|
LotlSignatureError = class extends OpenID4VPError {
|
|
116
116
|
code = "lotl_signature_invalid";
|
|
117
117
|
};
|
|
118
|
+
SignedRequestBuildError = class extends OpenID4VPError {
|
|
119
|
+
code;
|
|
120
|
+
constructor(code, message) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.code = code;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
UnsupportedJweError = class extends OpenID4VPError {
|
|
126
|
+
code = "unsupported_jwe";
|
|
127
|
+
alg;
|
|
128
|
+
enc;
|
|
129
|
+
constructor(alg, enc) {
|
|
130
|
+
super(`Unsupported JWE algorithms: alg=${alg}, enc=${enc}`);
|
|
131
|
+
this.alg = alg;
|
|
132
|
+
this.enc = enc;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
DecryptionFailedError = class extends OpenID4VPError {
|
|
136
|
+
code = "decryption_failed";
|
|
137
|
+
constructor(message = "Failed to decrypt authorization response", options) {
|
|
138
|
+
super(message, options);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
MissingDecryptionKeyError = class extends OpenID4VPError {
|
|
142
|
+
code = "missing_decryption_key";
|
|
143
|
+
constructor(message = "decryptionKey is required to verify a direct_post.jwt response") {
|
|
144
|
+
super(message);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
MultipleCredentialsNotSupportedError = class extends OpenID4VPError {
|
|
148
|
+
code = "multi_credential_unsupported";
|
|
149
|
+
entryCount;
|
|
150
|
+
presentationCount;
|
|
151
|
+
constructor(entryCount, presentationCount) {
|
|
152
|
+
super(
|
|
153
|
+
`Multi-credential presentations are not yet supported: ${entryCount} queryId entries, ${presentationCount} presentations`
|
|
154
|
+
);
|
|
155
|
+
this.entryCount = entryCount;
|
|
156
|
+
this.presentationCount = presentationCount;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
118
159
|
}
|
|
119
160
|
});
|
|
120
161
|
|
|
@@ -8024,6 +8065,138 @@ function createAuthorizationRequest(input, query) {
|
|
|
8024
8065
|
};
|
|
8025
8066
|
}
|
|
8026
8067
|
|
|
8068
|
+
// src/signed-authorization.ts
|
|
8069
|
+
init_errors();
|
|
8070
|
+
var DEFAULT_SUPPORTED_ENC_VALUES = ["A128GCM", "A256GCM"];
|
|
8071
|
+
var SUPPORTED_SIGNING_ALGS = /* @__PURE__ */ new Set(["ES256", "ES384", "RS256"]);
|
|
8072
|
+
async function createSignedAuthorizationRequest(input, query) {
|
|
8073
|
+
if (input.certificateChain.length === 0) {
|
|
8074
|
+
throw new SignedRequestBuildError(
|
|
8075
|
+
"empty_cert_chain",
|
|
8076
|
+
"certificateChain must contain at least the leaf certificate"
|
|
8077
|
+
);
|
|
8078
|
+
}
|
|
8079
|
+
const signingAlg = input.signingAlgorithm ?? "ES256";
|
|
8080
|
+
if (!SUPPORTED_SIGNING_ALGS.has(signingAlg)) {
|
|
8081
|
+
throw new SignedRequestBuildError(
|
|
8082
|
+
"unsupported_signing_alg",
|
|
8083
|
+
`signingAlgorithm ${signingAlg} is not supported (expected ES256/ES384/RS256)`
|
|
8084
|
+
);
|
|
8085
|
+
}
|
|
8086
|
+
if (!input.vpFormatsSupported || Object.keys(input.vpFormatsSupported).length === 0) {
|
|
8087
|
+
throw new SignedRequestBuildError(
|
|
8088
|
+
"missing_vp_formats",
|
|
8089
|
+
"vpFormatsSupported is required and must be non-empty"
|
|
8090
|
+
);
|
|
8091
|
+
}
|
|
8092
|
+
const leafCert = new X509Certificate(toArrayBuffer(input.certificateChain[0]));
|
|
8093
|
+
const sanDnsNames = extractDnsNames(leafCert);
|
|
8094
|
+
if (!sanDnsNames.includes(input.hostname)) {
|
|
8095
|
+
throw new SignedRequestBuildError(
|
|
8096
|
+
"hostname_cert_mismatch",
|
|
8097
|
+
`leaf cert SAN DNSName values [${sanDnsNames.join(", ")}] do not include hostname "${input.hostname}"`
|
|
8098
|
+
);
|
|
8099
|
+
}
|
|
8100
|
+
const signerPublicSpki = new Uint8Array(
|
|
8101
|
+
await crypto.subtle.exportKey("spki", input.signer.publicKey)
|
|
8102
|
+
);
|
|
8103
|
+
const leafSpki = new Uint8Array(leafCert.publicKey.rawData);
|
|
8104
|
+
if (!bytesEqual(signerPublicSpki, leafSpki)) {
|
|
8105
|
+
throw new SignedRequestBuildError(
|
|
8106
|
+
"signing_key_cert_mismatch",
|
|
8107
|
+
"signer.publicKey SPKI does not match leaf certificate SPKI"
|
|
8108
|
+
);
|
|
8109
|
+
}
|
|
8110
|
+
const responseMode = input.responseMode ?? "direct_post.jwt";
|
|
8111
|
+
if (responseMode === "direct_post.jwt") {
|
|
8112
|
+
if (!input.encryptionKey?.publicJwk) {
|
|
8113
|
+
throw new SignedRequestBuildError(
|
|
8114
|
+
"missing_encryption_jwk",
|
|
8115
|
+
"direct_post.jwt response mode requires encryptionKey.publicJwk"
|
|
8116
|
+
);
|
|
8117
|
+
}
|
|
8118
|
+
if (!input.encryptionKey.publicJwk.alg) {
|
|
8119
|
+
throw new SignedRequestBuildError(
|
|
8120
|
+
"missing_encryption_alg",
|
|
8121
|
+
"encryptionKey.publicJwk must carry alg (OpenID4VP 1.0 \xA78.3)"
|
|
8122
|
+
);
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
const state = input.state ?? v4();
|
|
8126
|
+
const clientId = `x509_san_dns:${input.hostname}`;
|
|
8127
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
8128
|
+
const clientMetadata = {
|
|
8129
|
+
vp_formats_supported: input.vpFormatsSupported
|
|
8130
|
+
};
|
|
8131
|
+
if (responseMode === "direct_post.jwt" && input.encryptionKey) {
|
|
8132
|
+
const enc = input.encryptionKey.supportedEncValues;
|
|
8133
|
+
if (enc !== void 0 && enc.length === 0) {
|
|
8134
|
+
throw new SignedRequestBuildError(
|
|
8135
|
+
"empty_supported_enc_values",
|
|
8136
|
+
"encryptionKey.supportedEncValues must not be empty (omit to use defaults [A128GCM, A256GCM])"
|
|
8137
|
+
);
|
|
8138
|
+
}
|
|
8139
|
+
const jwk = { ...input.encryptionKey.publicJwk, use: "enc" };
|
|
8140
|
+
clientMetadata.jwks = { keys: [jwk] };
|
|
8141
|
+
const encValues = enc ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
|
|
8142
|
+
clientMetadata.encrypted_response_enc_values_supported = encValues;
|
|
8143
|
+
clientMetadata.authorization_encrypted_response_alg = "ECDH-ES";
|
|
8144
|
+
clientMetadata.authorization_encrypted_response_enc = encValues[0];
|
|
8145
|
+
}
|
|
8146
|
+
const payload = {
|
|
8147
|
+
iss: clientId,
|
|
8148
|
+
aud: "https://self-issued.me/v2",
|
|
8149
|
+
response_type: "vp_token",
|
|
8150
|
+
response_mode: responseMode,
|
|
8151
|
+
client_id: clientId,
|
|
8152
|
+
response_uri: input.responseUri,
|
|
8153
|
+
nonce: input.nonce,
|
|
8154
|
+
state,
|
|
8155
|
+
dcql_query: query,
|
|
8156
|
+
client_metadata: clientMetadata,
|
|
8157
|
+
iat: now,
|
|
8158
|
+
exp: now + 120
|
|
8159
|
+
};
|
|
8160
|
+
const x5c = input.certificateChain.map((der) => bytesToBase64(der));
|
|
8161
|
+
const jws = await new SignJWT(payload).setProtectedHeader({ typ: "oauth-authz-req+jwt", alg: signingAlg, x5c }).sign(input.signer.privateKey);
|
|
8162
|
+
const uriParams = new URLSearchParams({
|
|
8163
|
+
client_id: clientId,
|
|
8164
|
+
request_uri: input.requestUri
|
|
8165
|
+
});
|
|
8166
|
+
return {
|
|
8167
|
+
uri: `openid4vp://authorize?${uriParams.toString()}`,
|
|
8168
|
+
requestObject: jws,
|
|
8169
|
+
dcqlQuery: query,
|
|
8170
|
+
nonce: input.nonce,
|
|
8171
|
+
state
|
|
8172
|
+
};
|
|
8173
|
+
}
|
|
8174
|
+
function extractDnsNames(cert) {
|
|
8175
|
+
const SAN_OID = "2.5.29.17";
|
|
8176
|
+
const ext = cert.extensions.find((e) => e.type === SAN_OID);
|
|
8177
|
+
if (!ext) return [];
|
|
8178
|
+
const names = ext.names;
|
|
8179
|
+
const items = names?.items ?? [];
|
|
8180
|
+
return items.filter((e) => e.type === "dns").map((e) => e.value);
|
|
8181
|
+
}
|
|
8182
|
+
function bytesEqual(a, b) {
|
|
8183
|
+
if (a.length !== b.length) return false;
|
|
8184
|
+
for (let i = 0; i < a.length; i++) {
|
|
8185
|
+
if (a[i] !== b[i]) return false;
|
|
8186
|
+
}
|
|
8187
|
+
return true;
|
|
8188
|
+
}
|
|
8189
|
+
function bytesToBase64(bytes) {
|
|
8190
|
+
let s = "";
|
|
8191
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
8192
|
+
return btoa(s);
|
|
8193
|
+
}
|
|
8194
|
+
function toArrayBuffer(bytes) {
|
|
8195
|
+
const out = new ArrayBuffer(bytes.byteLength);
|
|
8196
|
+
new Uint8Array(out).set(bytes);
|
|
8197
|
+
return out;
|
|
8198
|
+
}
|
|
8199
|
+
|
|
8027
8200
|
// src/haip.ts
|
|
8028
8201
|
init_errors();
|
|
8029
8202
|
var HAIP_DOCTYPE_NAMESPACES = {
|
|
@@ -8227,7 +8400,7 @@ async function verifyCoseSign1(coseSign1, publicKey, allowedAlgorithms) {
|
|
|
8227
8400
|
throw new MalformedCredentialError(`no hash mapping for alg ${coseSign1.alg}`);
|
|
8228
8401
|
}
|
|
8229
8402
|
const sigStructure = ["Signature1", coseSign1.protectedHeaderBytes, new Uint8Array(0), coseSign1.payload];
|
|
8230
|
-
const sigInput = decoder.encode(sigStructure);
|
|
8403
|
+
const sigInput = new Uint8Array(decoder.encode(sigStructure));
|
|
8231
8404
|
const ok = await crypto.subtle.verify(
|
|
8232
8405
|
{ name: "ECDSA", hash: hashAlg },
|
|
8233
8406
|
publicKey,
|
|
@@ -8250,7 +8423,7 @@ async function computeItemDigest(itemBytes, digestAlgorithm) {
|
|
|
8250
8423
|
const buf = await crypto.subtle.digest(digestAlgorithm, new Uint8Array(itemBytes));
|
|
8251
8424
|
return new Uint8Array(buf);
|
|
8252
8425
|
}
|
|
8253
|
-
function
|
|
8426
|
+
function bytesEqual2(a, b) {
|
|
8254
8427
|
if (a.length !== b.length) return false;
|
|
8255
8428
|
for (let i = 0; i < a.length; i++) {
|
|
8256
8429
|
if (a[i] !== b[i]) return false;
|
|
@@ -8296,7 +8469,7 @@ async function verifyAllDigests(nameSpaces, mso) {
|
|
|
8296
8469
|
if (!expectedDigest) {
|
|
8297
8470
|
throw new MalformedCredentialError(`digestID ${digestID} missing from MSO valueDigests[${ns}]`);
|
|
8298
8471
|
}
|
|
8299
|
-
if (!
|
|
8472
|
+
if (!bytesEqual2(hash, expectedDigest)) {
|
|
8300
8473
|
throw new MalformedCredentialError(`digest mismatch for ns=${ns} digestID=${digestID}`);
|
|
8301
8474
|
}
|
|
8302
8475
|
}
|
|
@@ -8415,7 +8588,7 @@ init_errors();
|
|
|
8415
8588
|
var decoder4 = new Encoder({ mapsAsObjects: false, useRecords: false });
|
|
8416
8589
|
var encoder = new Encoder({ mapsAsObjects: false, useRecords: false });
|
|
8417
8590
|
var DEFAULT_ALLOWED_ALGORITHMS = ["ES256", "ES384", "ES512"];
|
|
8418
|
-
function
|
|
8591
|
+
function bytesEqual3(a, b) {
|
|
8419
8592
|
if (a.length !== b.length) return false;
|
|
8420
8593
|
for (let i = 0; i < a.length; i++) {
|
|
8421
8594
|
if (a[i] !== b[i]) return false;
|
|
@@ -8551,8 +8724,8 @@ var MdocParser = class {
|
|
|
8551
8724
|
cache: options.cache,
|
|
8552
8725
|
clockSkewTolerance: options.clockSkewTolerance
|
|
8553
8726
|
});
|
|
8554
|
-
const { X509Certificate:
|
|
8555
|
-
const leaf = new
|
|
8727
|
+
const { X509Certificate: X509Certificate9 } = await import('@peculiar/x509');
|
|
8728
|
+
const leaf = new X509Certificate9(issuerCertBytes);
|
|
8556
8729
|
trustResult = await evaluator.evaluate(leaf);
|
|
8557
8730
|
} else {
|
|
8558
8731
|
if (options.trustedCertificates.length === 0) {
|
|
@@ -8562,7 +8735,7 @@ var MdocParser = class {
|
|
|
8562
8735
|
);
|
|
8563
8736
|
}
|
|
8564
8737
|
} else {
|
|
8565
|
-
const trusted = options.trustedCertificates.some((t) =>
|
|
8738
|
+
const trusted = options.trustedCertificates.some((t) => bytesEqual3(t, issuerCertBytes));
|
|
8566
8739
|
if (!trusted) {
|
|
8567
8740
|
return {
|
|
8568
8741
|
valid: false,
|
|
@@ -8677,7 +8850,7 @@ function base64ToBytes(base64) {
|
|
|
8677
8850
|
}
|
|
8678
8851
|
return bytes;
|
|
8679
8852
|
}
|
|
8680
|
-
function
|
|
8853
|
+
function bytesEqual4(a, b) {
|
|
8681
8854
|
if (a.length !== b.length) return false;
|
|
8682
8855
|
for (let i = 0; i < a.length; i++) {
|
|
8683
8856
|
if (a[i] !== b[i]) return false;
|
|
@@ -8766,32 +8939,74 @@ var SdJwtParser = class {
|
|
|
8766
8939
|
return invalidResult(`Unsupported algorithm: ${alg}`);
|
|
8767
8940
|
}
|
|
8768
8941
|
const x5cArray = header.x5c;
|
|
8769
|
-
|
|
8770
|
-
throw new MalformedCredentialError("Missing or invalid x5c in JWT header");
|
|
8771
|
-
}
|
|
8942
|
+
const hasX5c = Array.isArray(x5cArray) && x5cArray.length > 0 && typeof x5cArray[0] === "string";
|
|
8772
8943
|
let issuerKey;
|
|
8773
8944
|
let issuerCertBytes;
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8945
|
+
if (hasX5c) {
|
|
8946
|
+
try {
|
|
8947
|
+
issuerKey = await importX509(derToPem2(x5cArray[0]), alg);
|
|
8948
|
+
issuerCertBytes = base64ToBytes(x5cArray[0]);
|
|
8949
|
+
} catch (err) {
|
|
8950
|
+
if (err instanceof MalformedCredentialError) throw err;
|
|
8951
|
+
throw new MalformedCredentialError("Failed to import issuer certificate");
|
|
8952
|
+
}
|
|
8953
|
+
} else if (options.trustedIssuerJwks && options.trustedIssuerJwks.length > 0) {
|
|
8954
|
+
issuerKey = void 0;
|
|
8955
|
+
} else {
|
|
8956
|
+
throw new MalformedCredentialError("Missing or invalid x5c in JWT header");
|
|
8780
8957
|
}
|
|
8781
8958
|
let payload;
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8959
|
+
if (issuerKey) {
|
|
8960
|
+
try {
|
|
8961
|
+
const result2 = await jwtVerify(parts.jwt, issuerKey, { algorithms: allowedAlgorithms });
|
|
8962
|
+
payload = result2.payload;
|
|
8963
|
+
} catch {
|
|
8964
|
+
return invalidResult("Issuer JWT signature verification failed");
|
|
8965
|
+
}
|
|
8966
|
+
} else {
|
|
8967
|
+
const jwks = options.trustedIssuerJwks;
|
|
8968
|
+
const headerKid = header["kid"];
|
|
8969
|
+
let candidates;
|
|
8970
|
+
if (typeof headerKid === "string") {
|
|
8971
|
+
const matched = jwks.find((k) => k["kid"] === headerKid);
|
|
8972
|
+
if (!matched) {
|
|
8973
|
+
return invalidResult(
|
|
8974
|
+
`No matching JWK found in trustedIssuerJwks for kid=${headerKid}`
|
|
8975
|
+
);
|
|
8976
|
+
}
|
|
8977
|
+
candidates = [matched];
|
|
8978
|
+
} else {
|
|
8979
|
+
candidates = jwks;
|
|
8980
|
+
}
|
|
8981
|
+
let verifiedPayload;
|
|
8982
|
+
for (const jwk of candidates) {
|
|
8983
|
+
let candidateKey;
|
|
8984
|
+
try {
|
|
8985
|
+
candidateKey = await importJWK(jwk, alg);
|
|
8986
|
+
} catch {
|
|
8987
|
+
continue;
|
|
8988
|
+
}
|
|
8989
|
+
try {
|
|
8990
|
+
const result2 = await jwtVerify(parts.jwt, candidateKey, {
|
|
8991
|
+
algorithms: allowedAlgorithms
|
|
8992
|
+
});
|
|
8993
|
+
verifiedPayload = result2.payload;
|
|
8994
|
+
break;
|
|
8995
|
+
} catch {
|
|
8996
|
+
}
|
|
8997
|
+
}
|
|
8998
|
+
if (!verifiedPayload) {
|
|
8999
|
+
return invalidResult("Issuer JWT signature verification failed");
|
|
9000
|
+
}
|
|
9001
|
+
payload = verifiedPayload;
|
|
8787
9002
|
}
|
|
8788
9003
|
const issuerString = typeof payload["iss"] === "string" ? payload["iss"] : "";
|
|
8789
9004
|
const issuerInfo = {
|
|
8790
|
-
certificate: issuerCertBytes,
|
|
9005
|
+
certificate: issuerCertBytes ?? new Uint8Array(),
|
|
8791
9006
|
country: extractCountryHint(issuerString)
|
|
8792
9007
|
};
|
|
8793
9008
|
let trustResult;
|
|
8794
|
-
if (options.trustStore) {
|
|
9009
|
+
if (issuerCertBytes === void 0) ; else if (options.trustStore) {
|
|
8795
9010
|
const { TrustEvaluator: TrustEvaluator2 } = await Promise.resolve().then(() => (init_TrustEvaluator(), TrustEvaluator_exports));
|
|
8796
9011
|
const evaluator = new TrustEvaluator2({
|
|
8797
9012
|
trustStore: options.trustStore,
|
|
@@ -8800,8 +9015,8 @@ var SdJwtParser = class {
|
|
|
8800
9015
|
cache: options.cache,
|
|
8801
9016
|
clockSkewTolerance: options.clockSkewTolerance
|
|
8802
9017
|
});
|
|
8803
|
-
const { X509Certificate:
|
|
8804
|
-
const leaf = new
|
|
9018
|
+
const { X509Certificate: X509Certificate9 } = await import('@peculiar/x509');
|
|
9019
|
+
const leaf = new X509Certificate9(issuerCertBytes);
|
|
8805
9020
|
trustResult = await evaluator.evaluate(leaf);
|
|
8806
9021
|
} else {
|
|
8807
9022
|
if (options.trustedCertificates.length === 0) {
|
|
@@ -8811,23 +9026,55 @@ var SdJwtParser = class {
|
|
|
8811
9026
|
);
|
|
8812
9027
|
}
|
|
8813
9028
|
} else {
|
|
8814
|
-
const isTrusted = options.trustedCertificates.some(
|
|
9029
|
+
const isTrusted = options.trustedCertificates.some(
|
|
9030
|
+
(trusted) => bytesEqual4(trusted, issuerCertBytes)
|
|
9031
|
+
);
|
|
8815
9032
|
if (!isTrusted) {
|
|
8816
9033
|
return invalidResult("Issuer certificate is not trusted");
|
|
8817
9034
|
}
|
|
8818
9035
|
}
|
|
8819
9036
|
}
|
|
8820
|
-
const sdArray = payload["_sd"];
|
|
8821
9037
|
if (parts.disclosures.length > 0) {
|
|
8822
|
-
|
|
8823
|
-
return invalidResult("Disclosures present but no _sd array in issuer JWT");
|
|
8824
|
-
}
|
|
9038
|
+
const decodedDisclosures = [];
|
|
8825
9039
|
for (const disclosure of parts.disclosures) {
|
|
9040
|
+
let parsed;
|
|
9041
|
+
try {
|
|
9042
|
+
parsed = JSON.parse(new TextDecoder().decode(base64ToBytes(disclosure)));
|
|
9043
|
+
} catch {
|
|
9044
|
+
return invalidResult("Disclosure is not valid base64url-encoded JSON");
|
|
9045
|
+
}
|
|
9046
|
+
if (!Array.isArray(parsed) || parsed.length !== 2 && parsed.length !== 3) {
|
|
9047
|
+
return invalidResult("Disclosure must be a 2- or 3-element array");
|
|
9048
|
+
}
|
|
8826
9049
|
const hashBytes = await sha256Hasher(disclosure);
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
9050
|
+
decodedDisclosures.push({
|
|
9051
|
+
raw: disclosure,
|
|
9052
|
+
hash: bytesToBase64url(hashBytes),
|
|
9053
|
+
length: parsed.length,
|
|
9054
|
+
value: parsed.length === 3 ? parsed[2] : parsed[1]
|
|
9055
|
+
});
|
|
9056
|
+
}
|
|
9057
|
+
const seed = collectAnchorHashes(payload);
|
|
9058
|
+
const kvHashes = seed.kvHashes;
|
|
9059
|
+
const arrayElemHashes = seed.arrayElemHashes;
|
|
9060
|
+
const remaining = [...decodedDisclosures];
|
|
9061
|
+
for (; ; ) {
|
|
9062
|
+
const anchored = [];
|
|
9063
|
+
for (const d of remaining) {
|
|
9064
|
+
const valid = d.length === 3 ? kvHashes.has(d.hash) : arrayElemHashes.has(d.hash);
|
|
9065
|
+
if (valid) anchored.push(d);
|
|
8830
9066
|
}
|
|
9067
|
+
if (anchored.length === 0) break;
|
|
9068
|
+
for (const a of anchored) {
|
|
9069
|
+
const sub = collectAnchorHashes(a.value);
|
|
9070
|
+
for (const h of sub.kvHashes) kvHashes.add(h);
|
|
9071
|
+
for (const h of sub.arrayElemHashes) arrayElemHashes.add(h);
|
|
9072
|
+
const idx = remaining.indexOf(a);
|
|
9073
|
+
if (idx >= 0) remaining.splice(idx, 1);
|
|
9074
|
+
}
|
|
9075
|
+
}
|
|
9076
|
+
if (remaining.length > 0) {
|
|
9077
|
+
return invalidResult("Disclosure hash mismatch");
|
|
8831
9078
|
}
|
|
8832
9079
|
}
|
|
8833
9080
|
if (parts.kbJwt) {
|
|
@@ -8891,6 +9138,35 @@ var SdJwtParser = class {
|
|
|
8891
9138
|
return result;
|
|
8892
9139
|
}
|
|
8893
9140
|
};
|
|
9141
|
+
function collectAnchorHashes(node) {
|
|
9142
|
+
const kvHashes = /* @__PURE__ */ new Set();
|
|
9143
|
+
const arrayElemHashes = /* @__PURE__ */ new Set();
|
|
9144
|
+
const visit = (value) => {
|
|
9145
|
+
if (Array.isArray(value)) {
|
|
9146
|
+
for (const item of value) {
|
|
9147
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
9148
|
+
const obj = item;
|
|
9149
|
+
const keys = Object.keys(obj);
|
|
9150
|
+
if (keys.length === 1 && keys[0] === "..." && typeof obj["..."] === "string") {
|
|
9151
|
+
arrayElemHashes.add(obj["..."]);
|
|
9152
|
+
continue;
|
|
9153
|
+
}
|
|
9154
|
+
}
|
|
9155
|
+
visit(item);
|
|
9156
|
+
}
|
|
9157
|
+
} else if (value && typeof value === "object") {
|
|
9158
|
+
for (const [k, v] of Object.entries(value)) {
|
|
9159
|
+
if (k === "_sd" && Array.isArray(v)) {
|
|
9160
|
+
for (const h of v) if (typeof h === "string") kvHashes.add(h);
|
|
9161
|
+
continue;
|
|
9162
|
+
}
|
|
9163
|
+
visit(v);
|
|
9164
|
+
}
|
|
9165
|
+
}
|
|
9166
|
+
};
|
|
9167
|
+
visit(node);
|
|
9168
|
+
return { kvHashes, arrayElemHashes };
|
|
9169
|
+
}
|
|
8894
9170
|
|
|
8895
9171
|
// src/presentation.ts
|
|
8896
9172
|
var parsers = [new SdJwtParser(), new MdocParser()];
|
|
@@ -8908,6 +9184,44 @@ async function parsePresentation(vpToken, options) {
|
|
|
8908
9184
|
error: "Unsupported credential format"
|
|
8909
9185
|
};
|
|
8910
9186
|
}
|
|
9187
|
+
|
|
9188
|
+
// src/decrypt-response.ts
|
|
9189
|
+
init_errors();
|
|
9190
|
+
var SUPPORTED_ALG = "ECDH-ES";
|
|
9191
|
+
var SUPPORTED_ENC = /* @__PURE__ */ new Set(["A128GCM", "A256GCM"]);
|
|
9192
|
+
async function decryptAuthorizationResponse(jwe, privateKey) {
|
|
9193
|
+
let header;
|
|
9194
|
+
try {
|
|
9195
|
+
header = decodeProtectedHeader(jwe);
|
|
9196
|
+
} catch (err) {
|
|
9197
|
+
throw new DecryptionFailedError("Malformed JWE header", {
|
|
9198
|
+
cause: err
|
|
9199
|
+
});
|
|
9200
|
+
}
|
|
9201
|
+
const alg = typeof header.alg === "string" ? header.alg : "";
|
|
9202
|
+
const enc = typeof header.enc === "string" ? header.enc : "";
|
|
9203
|
+
if (alg !== SUPPORTED_ALG || !SUPPORTED_ENC.has(enc)) {
|
|
9204
|
+
throw new UnsupportedJweError(alg, enc);
|
|
9205
|
+
}
|
|
9206
|
+
let plaintext;
|
|
9207
|
+
try {
|
|
9208
|
+
const result = await compactDecrypt(jwe, privateKey);
|
|
9209
|
+
plaintext = result.plaintext;
|
|
9210
|
+
} catch (err) {
|
|
9211
|
+
throw new DecryptionFailedError(void 0, { cause: err });
|
|
9212
|
+
}
|
|
9213
|
+
try {
|
|
9214
|
+
const text = new TextDecoder().decode(plaintext);
|
|
9215
|
+
return JSON.parse(text);
|
|
9216
|
+
} catch (err) {
|
|
9217
|
+
throw new DecryptionFailedError("Decrypted plaintext is not valid JSON", {
|
|
9218
|
+
cause: err
|
|
9219
|
+
});
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
|
|
9223
|
+
// src/verify.ts
|
|
9224
|
+
init_errors();
|
|
8911
9225
|
var INTERNAL_TO_DCQL_FORMAT = {
|
|
8912
9226
|
"sd-jwt-vc": "dc+sd-jwt",
|
|
8913
9227
|
mdoc: "mso_mdoc"
|
|
@@ -8939,6 +9253,48 @@ async function verifyPresentation(vpToken, query, options) {
|
|
|
8939
9253
|
valid: parsed.valid && match.satisfied
|
|
8940
9254
|
};
|
|
8941
9255
|
}
|
|
9256
|
+
function isEncryptedResponse(x) {
|
|
9257
|
+
return typeof x === "object" && x !== null && "response" in x && typeof x.response === "string" && Object.keys(x).length === 1;
|
|
9258
|
+
}
|
|
9259
|
+
function isAuthorizationResponse(x) {
|
|
9260
|
+
return typeof x === "object" && x !== null && "vp_token" in x && typeof x.vp_token === "object" && x.vp_token !== null;
|
|
9261
|
+
}
|
|
9262
|
+
async function verifyAuthorizationResponse(envelope, query, options) {
|
|
9263
|
+
if (isEncryptedResponse(envelope)) {
|
|
9264
|
+
if (!options.decryptionKey) {
|
|
9265
|
+
throw new MissingDecryptionKeyError();
|
|
9266
|
+
}
|
|
9267
|
+
const decrypted = await decryptAuthorizationResponse(
|
|
9268
|
+
envelope.response,
|
|
9269
|
+
options.decryptionKey
|
|
9270
|
+
);
|
|
9271
|
+
return verifyAuthorizationResponse(decrypted, query, options);
|
|
9272
|
+
}
|
|
9273
|
+
if (!isAuthorizationResponse(envelope)) {
|
|
9274
|
+
throw new TypeError("envelope must contain vp_token as a JSON object per OpenID4VP 1.0 \xA78.1");
|
|
9275
|
+
}
|
|
9276
|
+
const vpToken = envelope.vp_token;
|
|
9277
|
+
const queryIds = Object.keys(vpToken);
|
|
9278
|
+
if (queryIds.length === 0) {
|
|
9279
|
+
throw new TypeError("vp_token cannot be empty per OpenID4VP 1.0 \xA78.1");
|
|
9280
|
+
}
|
|
9281
|
+
for (const id of queryIds) {
|
|
9282
|
+
if (!Array.isArray(vpToken[id])) {
|
|
9283
|
+
throw new TypeError(
|
|
9284
|
+
`vp_token entry "${id}" must be an array of presentations per OpenID4VP 1.0 \xA78.1`
|
|
9285
|
+
);
|
|
9286
|
+
}
|
|
9287
|
+
}
|
|
9288
|
+
const presentationCount = queryIds.reduce(
|
|
9289
|
+
(sum, id) => sum + vpToken[id].length,
|
|
9290
|
+
0
|
|
9291
|
+
);
|
|
9292
|
+
if (queryIds.length > 1 || presentationCount > 1) {
|
|
9293
|
+
throw new MultipleCredentialsNotSupportedError(queryIds.length, presentationCount);
|
|
9294
|
+
}
|
|
9295
|
+
const presentation = vpToken[queryIds[0]][0];
|
|
9296
|
+
return verifyPresentation(presentation, query, options);
|
|
9297
|
+
}
|
|
8942
9298
|
|
|
8943
9299
|
// src/index.ts
|
|
8944
9300
|
init_errors();
|
|
@@ -9550,7 +9906,7 @@ function equalBytes(a, b) {
|
|
|
9550
9906
|
|
|
9551
9907
|
// src/index.ts
|
|
9552
9908
|
init_errors();
|
|
9553
|
-
var VERSION = "0.
|
|
9909
|
+
var VERSION = "0.8.0";
|
|
9554
9910
|
/*! Bundled license information:
|
|
9555
9911
|
|
|
9556
9912
|
pvtsutils/build/index.es.js:
|
|
@@ -9620,6 +9976,6 @@ asn1js/build/index.es.js:
|
|
|
9620
9976
|
*)
|
|
9621
9977
|
*/
|
|
9622
9978
|
|
|
9623
|
-
export { CertificateChainError, CompositeTrustStore, ExpiredCredentialError, HAIP_DOCTYPE_NAMESPACES, HaipValidationError, InMemoryCache, InvalidSignatureError, LotlFetchError, LotlSignatureError, LotlTrustStore, MalformedCredentialError, MdocParser, NonceValidationError, OpenID4VPError, RevocationCheckFailedError, RevokedCertificateError, SdJwtParser, StaticTrustStore, TrustAnchorNotFoundError, UnsupportedFormatError, VERSION, buildHaipQuery, createAuthorizationRequest, isHaipQuery, parsePresentation, validateHaipQuery, verifyPresentation };
|
|
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 };
|
|
9624
9980
|
//# sourceMappingURL=index.js.map
|
|
9625
9981
|
//# sourceMappingURL=index.js.map
|