@openeudi/openid4vp 0.6.0 → 0.7.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 +90 -0
- package/dist/index.cjs +266 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -2
- package/dist/index.d.ts +103 -2
- package/dist/index.js +264 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,96 @@ Mismatches return `valid: false` — they do not throw. Only crypto/structural f
|
|
|
124
124
|
|
|
125
125
|
> **Privacy — diagnostics are verifier-internal.** `match.unmatched[].reason` and `detail` (including `value_mismatch`) are intended for verifier-side logging, debugging, and admin UIs. OpenID4VP §11 warns that per-claim verification outcomes can reveal wallet contents to observers. Do NOT echo these diagnostics into the OpenID4VP wire response sent back to the wallet, into end-user-visible error messages that another party could correlate, or into public analytics/third-party logs. The protocol's own error codes are the public interface; these fields are your internal instrumentation.
|
|
126
126
|
|
|
127
|
+
## Signed authorization requests (x509_san_dns)
|
|
128
|
+
|
|
129
|
+
For flows that require a signed request object (JAR) per OpenID4VP 1.0 §5.10, use `createSignedAuthorizationRequest`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { createSignedAuthorizationRequest } from "@openeudi/openid4vp";
|
|
133
|
+
|
|
134
|
+
const req = await createSignedAuthorizationRequest({
|
|
135
|
+
hostname: "verifier.example.com",
|
|
136
|
+
requestUri: "https://verifier.example.com/request.jwt",
|
|
137
|
+
responseUri: "https://verifier.example.com/response",
|
|
138
|
+
nonce,
|
|
139
|
+
signer: verifierKeyPair, // CryptoKeyPair with public+private
|
|
140
|
+
certificateChain: [leafCertDer], // DER-encoded, leaf SAN DNSName must equal hostname
|
|
141
|
+
encryptionKey: {
|
|
142
|
+
publicJwk: encryptionPublicJwk, // must include alg, e.g. "ECDH-ES"
|
|
143
|
+
},
|
|
144
|
+
vpFormatsSupported: {
|
|
145
|
+
"dc+sd-jwt": { "sd-jwt_alg_values": ["ES256"] },
|
|
146
|
+
},
|
|
147
|
+
}, dcqlQuery);
|
|
148
|
+
|
|
149
|
+
// req.uri — the short URI to hand to the wallet
|
|
150
|
+
// req.requestObject — the JWS the verifier must host at requestUri
|
|
151
|
+
// (Content-Type: application/oauth-authz-req+jwt)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The caller hosts `req.requestObject` at `requestUri` (the library does not host HTTP). The library verifies that the signing key's public SPKI matches the leaf certificate's public key — an attempt to sign with a mismatched key fails with `SignedRequestBuildError: signing_key_cert_mismatch`.
|
|
155
|
+
|
|
156
|
+
## Authorization responses (direct_post and direct_post.jwt)
|
|
157
|
+
|
|
158
|
+
Wallets POST the Authorization Response to your `responseUri`. The library is stateless — you MUST compare the envelope's `state` against the value you issued before treating the response as trustworthy. The recommended pattern differs slightly between the unencrypted and encrypted modes.
|
|
159
|
+
|
|
160
|
+
### Unencrypted (`direct_post`)
|
|
161
|
+
|
|
162
|
+
The envelope arrives as form-encoded JSON; parse it, check `state`, then verify:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { verifyAuthorizationResponse } from "@openeudi/openid4vp";
|
|
166
|
+
|
|
167
|
+
const envelope = parsedVpTokenObject; // { vp_token, state, ... }
|
|
168
|
+
|
|
169
|
+
if (envelope.state !== submittedState) {
|
|
170
|
+
throw new Error("state mismatch — possible CSRF / replay");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await verifyAuthorizationResponse(envelope, dcqlQuery, {
|
|
174
|
+
trustedCertificates: [issuerCertDer],
|
|
175
|
+
nonce,
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Encrypted (`direct_post.jwt`)
|
|
180
|
+
|
|
181
|
+
The wallet wraps the envelope in a JWE. Decrypt explicitly so you can check `state` against the decrypted envelope **before** verification runs:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import {
|
|
185
|
+
decryptAuthorizationResponse,
|
|
186
|
+
verifyAuthorizationResponse,
|
|
187
|
+
} from "@openeudi/openid4vp";
|
|
188
|
+
|
|
189
|
+
const decrypted = await decryptAuthorizationResponse(
|
|
190
|
+
form.get("response"), // the JWE string
|
|
191
|
+
verifierEncryptionPrivateKey,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (decrypted.state !== submittedState) {
|
|
195
|
+
throw new Error("state mismatch — possible CSRF / replay");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const result = await verifyAuthorizationResponse(decrypted, dcqlQuery, {
|
|
199
|
+
trustedCertificates: [issuerCertDer],
|
|
200
|
+
nonce,
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`verifyAuthorizationResponse` also accepts the JWE directly via `{ response: jwe }` together with `options.decryptionKey` — but that path makes the `state` check easy to skip, since the caller never holds the decrypted envelope. Prefer the explicit two-step pattern above.
|
|
205
|
+
|
|
206
|
+
`verifyAuthorizationResponse` accepts the OpenID4VP 1.0 §8.1 envelope shape: `vp_token` is always an object keyed by DCQL credential query id, with arrays of presentations. This release supports **single-credential single-presentation only** — multi-credential queries or multi-presentation arrays throw `MultipleCredentialsNotSupportedError`.
|
|
207
|
+
|
|
208
|
+
### Supported JWE algorithms
|
|
209
|
+
|
|
210
|
+
`direct_post.jwt` decryption supports:
|
|
211
|
+
|
|
212
|
+
- `alg`: `ECDH-ES` (driven by the encryption JWK's `alg` parameter)
|
|
213
|
+
- `enc`: `A128GCM`, `A256GCM` (HAIP requires both)
|
|
214
|
+
|
|
215
|
+
Other algorithms throw `UnsupportedJweError`.
|
|
216
|
+
|
|
127
217
|
### ParseOptions / VerifyOptions
|
|
128
218
|
|
|
129
219
|
Both `parsePresentation` and `verifyPresentation` accept:
|
package/dist/index.cjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
var x509 = require('@peculiar/x509');
|
|
4
4
|
var uuid = require('uuid');
|
|
5
|
-
var cborX = require('cbor-x');
|
|
6
5
|
var jose = require('jose');
|
|
6
|
+
var cborX = require('cbor-x');
|
|
7
7
|
var decode = require('@sd-jwt/decode');
|
|
8
8
|
var dcql = require('@openeudi/dcql');
|
|
9
9
|
var xmldom = require('@xmldom/xmldom');
|
|
@@ -40,7 +40,7 @@ var __export = (target, all) => {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
// src/errors.ts
|
|
43
|
-
exports.InvalidSignatureError = void 0; exports.ExpiredCredentialError = void 0; exports.UnsupportedFormatError = void 0; exports.MalformedCredentialError = void 0; exports.NonceValidationError = void 0; exports.HaipValidationError = void 0; exports.OpenID4VPError = void 0; exports.TrustAnchorNotFoundError = void 0; exports.CertificateChainError = void 0; exports.RevokedCertificateError = void 0; exports.RevocationCheckFailedError = void 0; exports.LotlFetchError = void 0; exports.LotlSignatureError = void 0;
|
|
43
|
+
exports.InvalidSignatureError = void 0; exports.ExpiredCredentialError = void 0; exports.UnsupportedFormatError = void 0; exports.MalformedCredentialError = void 0; exports.NonceValidationError = void 0; exports.HaipValidationError = void 0; exports.OpenID4VPError = void 0; exports.TrustAnchorNotFoundError = void 0; exports.CertificateChainError = void 0; exports.RevokedCertificateError = void 0; exports.RevocationCheckFailedError = void 0; exports.LotlFetchError = void 0; exports.LotlSignatureError = void 0; exports.SignedRequestBuildError = void 0; exports.UnsupportedJweError = void 0; exports.DecryptionFailedError = void 0; exports.MissingDecryptionKeyError = void 0; exports.MultipleCredentialsNotSupportedError = void 0;
|
|
44
44
|
var init_errors = __esm({
|
|
45
45
|
"src/errors.ts"() {
|
|
46
46
|
exports.InvalidSignatureError = class extends Error {
|
|
@@ -136,6 +136,47 @@ var init_errors = __esm({
|
|
|
136
136
|
exports.LotlSignatureError = class extends exports.OpenID4VPError {
|
|
137
137
|
code = "lotl_signature_invalid";
|
|
138
138
|
};
|
|
139
|
+
exports.SignedRequestBuildError = class extends exports.OpenID4VPError {
|
|
140
|
+
code;
|
|
141
|
+
constructor(code, message) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.code = code;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
exports.UnsupportedJweError = class extends exports.OpenID4VPError {
|
|
147
|
+
code = "unsupported_jwe";
|
|
148
|
+
alg;
|
|
149
|
+
enc;
|
|
150
|
+
constructor(alg, enc) {
|
|
151
|
+
super(`Unsupported JWE algorithms: alg=${alg}, enc=${enc}`);
|
|
152
|
+
this.alg = alg;
|
|
153
|
+
this.enc = enc;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
exports.DecryptionFailedError = class extends exports.OpenID4VPError {
|
|
157
|
+
code = "decryption_failed";
|
|
158
|
+
constructor(message = "Failed to decrypt authorization response", options) {
|
|
159
|
+
super(message, options);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
exports.MissingDecryptionKeyError = class extends exports.OpenID4VPError {
|
|
163
|
+
code = "missing_decryption_key";
|
|
164
|
+
constructor(message = "decryptionKey is required to verify a direct_post.jwt response") {
|
|
165
|
+
super(message);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
exports.MultipleCredentialsNotSupportedError = class extends exports.OpenID4VPError {
|
|
169
|
+
code = "multi_credential_unsupported";
|
|
170
|
+
entryCount;
|
|
171
|
+
presentationCount;
|
|
172
|
+
constructor(entryCount, presentationCount) {
|
|
173
|
+
super(
|
|
174
|
+
`Multi-credential presentations are not yet supported: ${entryCount} queryId entries, ${presentationCount} presentations`
|
|
175
|
+
);
|
|
176
|
+
this.entryCount = entryCount;
|
|
177
|
+
this.presentationCount = presentationCount;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
139
180
|
}
|
|
140
181
|
});
|
|
141
182
|
|
|
@@ -8045,6 +8086,135 @@ function createAuthorizationRequest(input, query) {
|
|
|
8045
8086
|
};
|
|
8046
8087
|
}
|
|
8047
8088
|
|
|
8089
|
+
// src/signed-authorization.ts
|
|
8090
|
+
init_errors();
|
|
8091
|
+
var DEFAULT_SUPPORTED_ENC_VALUES = ["A128GCM", "A256GCM"];
|
|
8092
|
+
var SUPPORTED_SIGNING_ALGS = /* @__PURE__ */ new Set(["ES256", "ES384", "RS256"]);
|
|
8093
|
+
async function createSignedAuthorizationRequest(input, query) {
|
|
8094
|
+
if (input.certificateChain.length === 0) {
|
|
8095
|
+
throw new exports.SignedRequestBuildError(
|
|
8096
|
+
"empty_cert_chain",
|
|
8097
|
+
"certificateChain must contain at least the leaf certificate"
|
|
8098
|
+
);
|
|
8099
|
+
}
|
|
8100
|
+
const signingAlg = input.signingAlgorithm ?? "ES256";
|
|
8101
|
+
if (!SUPPORTED_SIGNING_ALGS.has(signingAlg)) {
|
|
8102
|
+
throw new exports.SignedRequestBuildError(
|
|
8103
|
+
"unsupported_signing_alg",
|
|
8104
|
+
`signingAlgorithm ${signingAlg} is not supported (expected ES256/ES384/RS256)`
|
|
8105
|
+
);
|
|
8106
|
+
}
|
|
8107
|
+
if (!input.vpFormatsSupported || Object.keys(input.vpFormatsSupported).length === 0) {
|
|
8108
|
+
throw new exports.SignedRequestBuildError(
|
|
8109
|
+
"missing_vp_formats",
|
|
8110
|
+
"vpFormatsSupported is required and must be non-empty"
|
|
8111
|
+
);
|
|
8112
|
+
}
|
|
8113
|
+
const leafCert = new x509.X509Certificate(toArrayBuffer(input.certificateChain[0]));
|
|
8114
|
+
const sanDnsNames = extractDnsNames(leafCert);
|
|
8115
|
+
if (!sanDnsNames.includes(input.hostname)) {
|
|
8116
|
+
throw new exports.SignedRequestBuildError(
|
|
8117
|
+
"hostname_cert_mismatch",
|
|
8118
|
+
`leaf cert SAN DNSName values [${sanDnsNames.join(", ")}] do not include hostname "${input.hostname}"`
|
|
8119
|
+
);
|
|
8120
|
+
}
|
|
8121
|
+
const signerPublicSpki = new Uint8Array(
|
|
8122
|
+
await crypto.subtle.exportKey("spki", input.signer.publicKey)
|
|
8123
|
+
);
|
|
8124
|
+
const leafSpki = new Uint8Array(leafCert.publicKey.rawData);
|
|
8125
|
+
if (!bytesEqual(signerPublicSpki, leafSpki)) {
|
|
8126
|
+
throw new exports.SignedRequestBuildError(
|
|
8127
|
+
"signing_key_cert_mismatch",
|
|
8128
|
+
"signer.publicKey SPKI does not match leaf certificate SPKI"
|
|
8129
|
+
);
|
|
8130
|
+
}
|
|
8131
|
+
const responseMode = input.responseMode ?? "direct_post.jwt";
|
|
8132
|
+
if (responseMode === "direct_post.jwt") {
|
|
8133
|
+
if (!input.encryptionKey?.publicJwk) {
|
|
8134
|
+
throw new exports.SignedRequestBuildError(
|
|
8135
|
+
"missing_encryption_jwk",
|
|
8136
|
+
"direct_post.jwt response mode requires encryptionKey.publicJwk"
|
|
8137
|
+
);
|
|
8138
|
+
}
|
|
8139
|
+
if (!input.encryptionKey.publicJwk.alg) {
|
|
8140
|
+
throw new exports.SignedRequestBuildError(
|
|
8141
|
+
"missing_encryption_alg",
|
|
8142
|
+
"encryptionKey.publicJwk must carry alg (OpenID4VP 1.0 \xA78.3)"
|
|
8143
|
+
);
|
|
8144
|
+
}
|
|
8145
|
+
}
|
|
8146
|
+
const state = input.state ?? uuid.v4();
|
|
8147
|
+
const clientId = `x509_san_dns:${input.hostname}`;
|
|
8148
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
8149
|
+
const clientMetadata = {
|
|
8150
|
+
vp_formats_supported: input.vpFormatsSupported
|
|
8151
|
+
};
|
|
8152
|
+
if (responseMode === "direct_post.jwt" && input.encryptionKey) {
|
|
8153
|
+
const enc = input.encryptionKey.supportedEncValues;
|
|
8154
|
+
if (enc !== void 0 && enc.length === 0) {
|
|
8155
|
+
throw new exports.SignedRequestBuildError(
|
|
8156
|
+
"empty_supported_enc_values",
|
|
8157
|
+
"encryptionKey.supportedEncValues must not be empty (omit to use defaults [A128GCM, A256GCM])"
|
|
8158
|
+
);
|
|
8159
|
+
}
|
|
8160
|
+
const jwk = { ...input.encryptionKey.publicJwk, use: "enc" };
|
|
8161
|
+
clientMetadata.jwks = { keys: [jwk] };
|
|
8162
|
+
clientMetadata.encrypted_response_enc_values_supported = enc ?? [...DEFAULT_SUPPORTED_ENC_VALUES];
|
|
8163
|
+
}
|
|
8164
|
+
const payload = {
|
|
8165
|
+
iss: clientId,
|
|
8166
|
+
aud: "https://self-issued.me/v2",
|
|
8167
|
+
response_type: "vp_token",
|
|
8168
|
+
response_mode: responseMode,
|
|
8169
|
+
client_id: clientId,
|
|
8170
|
+
response_uri: input.responseUri,
|
|
8171
|
+
nonce: input.nonce,
|
|
8172
|
+
state,
|
|
8173
|
+
dcql_query: query,
|
|
8174
|
+
client_metadata: clientMetadata,
|
|
8175
|
+
iat: now,
|
|
8176
|
+
exp: now + 120
|
|
8177
|
+
};
|
|
8178
|
+
const x5c = input.certificateChain.map((der) => bytesToBase64(der));
|
|
8179
|
+
const jws = await new jose.SignJWT(payload).setProtectedHeader({ typ: "oauth-authz-req+jwt", alg: signingAlg, x5c }).sign(input.signer.privateKey);
|
|
8180
|
+
const uriParams = new URLSearchParams({
|
|
8181
|
+
client_id: clientId,
|
|
8182
|
+
request_uri: input.requestUri
|
|
8183
|
+
});
|
|
8184
|
+
return {
|
|
8185
|
+
uri: `openid4vp://authorize?${uriParams.toString()}`,
|
|
8186
|
+
requestObject: jws,
|
|
8187
|
+
dcqlQuery: query,
|
|
8188
|
+
nonce: input.nonce,
|
|
8189
|
+
state
|
|
8190
|
+
};
|
|
8191
|
+
}
|
|
8192
|
+
function extractDnsNames(cert) {
|
|
8193
|
+
const SAN_OID = "2.5.29.17";
|
|
8194
|
+
const ext = cert.extensions.find((e) => e.type === SAN_OID);
|
|
8195
|
+
if (!ext) return [];
|
|
8196
|
+
const names = ext.names;
|
|
8197
|
+
const items = names?.items ?? [];
|
|
8198
|
+
return items.filter((e) => e.type === "dns").map((e) => e.value);
|
|
8199
|
+
}
|
|
8200
|
+
function bytesEqual(a, b) {
|
|
8201
|
+
if (a.length !== b.length) return false;
|
|
8202
|
+
for (let i = 0; i < a.length; i++) {
|
|
8203
|
+
if (a[i] !== b[i]) return false;
|
|
8204
|
+
}
|
|
8205
|
+
return true;
|
|
8206
|
+
}
|
|
8207
|
+
function bytesToBase64(bytes) {
|
|
8208
|
+
let s = "";
|
|
8209
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
8210
|
+
return btoa(s);
|
|
8211
|
+
}
|
|
8212
|
+
function toArrayBuffer(bytes) {
|
|
8213
|
+
const out = new ArrayBuffer(bytes.byteLength);
|
|
8214
|
+
new Uint8Array(out).set(bytes);
|
|
8215
|
+
return out;
|
|
8216
|
+
}
|
|
8217
|
+
|
|
8048
8218
|
// src/haip.ts
|
|
8049
8219
|
init_errors();
|
|
8050
8220
|
var HAIP_DOCTYPE_NAMESPACES = {
|
|
@@ -8271,7 +8441,7 @@ async function computeItemDigest(itemBytes, digestAlgorithm) {
|
|
|
8271
8441
|
const buf = await crypto.subtle.digest(digestAlgorithm, new Uint8Array(itemBytes));
|
|
8272
8442
|
return new Uint8Array(buf);
|
|
8273
8443
|
}
|
|
8274
|
-
function
|
|
8444
|
+
function bytesEqual2(a, b) {
|
|
8275
8445
|
if (a.length !== b.length) return false;
|
|
8276
8446
|
for (let i = 0; i < a.length; i++) {
|
|
8277
8447
|
if (a[i] !== b[i]) return false;
|
|
@@ -8317,7 +8487,7 @@ async function verifyAllDigests(nameSpaces, mso) {
|
|
|
8317
8487
|
if (!expectedDigest) {
|
|
8318
8488
|
throw new exports.MalformedCredentialError(`digestID ${digestID} missing from MSO valueDigests[${ns}]`);
|
|
8319
8489
|
}
|
|
8320
|
-
if (!
|
|
8490
|
+
if (!bytesEqual2(hash, expectedDigest)) {
|
|
8321
8491
|
throw new exports.MalformedCredentialError(`digest mismatch for ns=${ns} digestID=${digestID}`);
|
|
8322
8492
|
}
|
|
8323
8493
|
}
|
|
@@ -8436,7 +8606,7 @@ init_errors();
|
|
|
8436
8606
|
var decoder4 = new cborX.Encoder({ mapsAsObjects: false, useRecords: false });
|
|
8437
8607
|
var encoder = new cborX.Encoder({ mapsAsObjects: false, useRecords: false });
|
|
8438
8608
|
var DEFAULT_ALLOWED_ALGORITHMS = ["ES256", "ES384", "ES512"];
|
|
8439
|
-
function
|
|
8609
|
+
function bytesEqual3(a, b) {
|
|
8440
8610
|
if (a.length !== b.length) return false;
|
|
8441
8611
|
for (let i = 0; i < a.length; i++) {
|
|
8442
8612
|
if (a[i] !== b[i]) return false;
|
|
@@ -8572,8 +8742,8 @@ var MdocParser = class {
|
|
|
8572
8742
|
cache: options.cache,
|
|
8573
8743
|
clockSkewTolerance: options.clockSkewTolerance
|
|
8574
8744
|
});
|
|
8575
|
-
const { X509Certificate:
|
|
8576
|
-
const leaf = new
|
|
8745
|
+
const { X509Certificate: X509Certificate9 } = await import('@peculiar/x509');
|
|
8746
|
+
const leaf = new X509Certificate9(issuerCertBytes);
|
|
8577
8747
|
trustResult = await evaluator.evaluate(leaf);
|
|
8578
8748
|
} else {
|
|
8579
8749
|
if (options.trustedCertificates.length === 0) {
|
|
@@ -8583,7 +8753,7 @@ var MdocParser = class {
|
|
|
8583
8753
|
);
|
|
8584
8754
|
}
|
|
8585
8755
|
} else {
|
|
8586
|
-
const trusted = options.trustedCertificates.some((t) =>
|
|
8756
|
+
const trusted = options.trustedCertificates.some((t) => bytesEqual3(t, issuerCertBytes));
|
|
8587
8757
|
if (!trusted) {
|
|
8588
8758
|
return {
|
|
8589
8759
|
valid: false,
|
|
@@ -8698,7 +8868,7 @@ function base64ToBytes(base64) {
|
|
|
8698
8868
|
}
|
|
8699
8869
|
return bytes;
|
|
8700
8870
|
}
|
|
8701
|
-
function
|
|
8871
|
+
function bytesEqual4(a, b) {
|
|
8702
8872
|
if (a.length !== b.length) return false;
|
|
8703
8873
|
for (let i = 0; i < a.length; i++) {
|
|
8704
8874
|
if (a[i] !== b[i]) return false;
|
|
@@ -8821,8 +8991,8 @@ var SdJwtParser = class {
|
|
|
8821
8991
|
cache: options.cache,
|
|
8822
8992
|
clockSkewTolerance: options.clockSkewTolerance
|
|
8823
8993
|
});
|
|
8824
|
-
const { X509Certificate:
|
|
8825
|
-
const leaf = new
|
|
8994
|
+
const { X509Certificate: X509Certificate9 } = await import('@peculiar/x509');
|
|
8995
|
+
const leaf = new X509Certificate9(issuerCertBytes);
|
|
8826
8996
|
trustResult = await evaluator.evaluate(leaf);
|
|
8827
8997
|
} else {
|
|
8828
8998
|
if (options.trustedCertificates.length === 0) {
|
|
@@ -8832,7 +9002,7 @@ var SdJwtParser = class {
|
|
|
8832
9002
|
);
|
|
8833
9003
|
}
|
|
8834
9004
|
} else {
|
|
8835
|
-
const isTrusted = options.trustedCertificates.some((trusted) =>
|
|
9005
|
+
const isTrusted = options.trustedCertificates.some((trusted) => bytesEqual4(trusted, issuerCertBytes));
|
|
8836
9006
|
if (!isTrusted) {
|
|
8837
9007
|
return invalidResult("Issuer certificate is not trusted");
|
|
8838
9008
|
}
|
|
@@ -8929,6 +9099,44 @@ async function parsePresentation(vpToken, options) {
|
|
|
8929
9099
|
error: "Unsupported credential format"
|
|
8930
9100
|
};
|
|
8931
9101
|
}
|
|
9102
|
+
|
|
9103
|
+
// src/decrypt-response.ts
|
|
9104
|
+
init_errors();
|
|
9105
|
+
var SUPPORTED_ALG = "ECDH-ES";
|
|
9106
|
+
var SUPPORTED_ENC = /* @__PURE__ */ new Set(["A128GCM", "A256GCM"]);
|
|
9107
|
+
async function decryptAuthorizationResponse(jwe, privateKey) {
|
|
9108
|
+
let header;
|
|
9109
|
+
try {
|
|
9110
|
+
header = jose.decodeProtectedHeader(jwe);
|
|
9111
|
+
} catch (err) {
|
|
9112
|
+
throw new exports.DecryptionFailedError("Malformed JWE header", {
|
|
9113
|
+
cause: err
|
|
9114
|
+
});
|
|
9115
|
+
}
|
|
9116
|
+
const alg = typeof header.alg === "string" ? header.alg : "";
|
|
9117
|
+
const enc = typeof header.enc === "string" ? header.enc : "";
|
|
9118
|
+
if (alg !== SUPPORTED_ALG || !SUPPORTED_ENC.has(enc)) {
|
|
9119
|
+
throw new exports.UnsupportedJweError(alg, enc);
|
|
9120
|
+
}
|
|
9121
|
+
let plaintext;
|
|
9122
|
+
try {
|
|
9123
|
+
const result = await jose.compactDecrypt(jwe, privateKey);
|
|
9124
|
+
plaintext = result.plaintext;
|
|
9125
|
+
} catch (err) {
|
|
9126
|
+
throw new exports.DecryptionFailedError(void 0, { cause: err });
|
|
9127
|
+
}
|
|
9128
|
+
try {
|
|
9129
|
+
const text = new TextDecoder().decode(plaintext);
|
|
9130
|
+
return JSON.parse(text);
|
|
9131
|
+
} catch (err) {
|
|
9132
|
+
throw new exports.DecryptionFailedError("Decrypted plaintext is not valid JSON", {
|
|
9133
|
+
cause: err
|
|
9134
|
+
});
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
|
|
9138
|
+
// src/verify.ts
|
|
9139
|
+
init_errors();
|
|
8932
9140
|
var INTERNAL_TO_DCQL_FORMAT = {
|
|
8933
9141
|
"sd-jwt-vc": "dc+sd-jwt",
|
|
8934
9142
|
mdoc: "mso_mdoc"
|
|
@@ -8960,6 +9168,48 @@ async function verifyPresentation(vpToken, query, options) {
|
|
|
8960
9168
|
valid: parsed.valid && match.satisfied
|
|
8961
9169
|
};
|
|
8962
9170
|
}
|
|
9171
|
+
function isEncryptedResponse(x) {
|
|
9172
|
+
return typeof x === "object" && x !== null && "response" in x && typeof x.response === "string" && Object.keys(x).length === 1;
|
|
9173
|
+
}
|
|
9174
|
+
function isAuthorizationResponse(x) {
|
|
9175
|
+
return typeof x === "object" && x !== null && "vp_token" in x && typeof x.vp_token === "object" && x.vp_token !== null;
|
|
9176
|
+
}
|
|
9177
|
+
async function verifyAuthorizationResponse(envelope, query, options) {
|
|
9178
|
+
if (isEncryptedResponse(envelope)) {
|
|
9179
|
+
if (!options.decryptionKey) {
|
|
9180
|
+
throw new exports.MissingDecryptionKeyError();
|
|
9181
|
+
}
|
|
9182
|
+
const decrypted = await decryptAuthorizationResponse(
|
|
9183
|
+
envelope.response,
|
|
9184
|
+
options.decryptionKey
|
|
9185
|
+
);
|
|
9186
|
+
return verifyAuthorizationResponse(decrypted, query, options);
|
|
9187
|
+
}
|
|
9188
|
+
if (!isAuthorizationResponse(envelope)) {
|
|
9189
|
+
throw new TypeError("envelope must contain vp_token as a JSON object per OpenID4VP 1.0 \xA78.1");
|
|
9190
|
+
}
|
|
9191
|
+
const vpToken = envelope.vp_token;
|
|
9192
|
+
const queryIds = Object.keys(vpToken);
|
|
9193
|
+
if (queryIds.length === 0) {
|
|
9194
|
+
throw new TypeError("vp_token cannot be empty per OpenID4VP 1.0 \xA78.1");
|
|
9195
|
+
}
|
|
9196
|
+
for (const id of queryIds) {
|
|
9197
|
+
if (!Array.isArray(vpToken[id])) {
|
|
9198
|
+
throw new TypeError(
|
|
9199
|
+
`vp_token entry "${id}" must be an array of presentations per OpenID4VP 1.0 \xA78.1`
|
|
9200
|
+
);
|
|
9201
|
+
}
|
|
9202
|
+
}
|
|
9203
|
+
const presentationCount = queryIds.reduce(
|
|
9204
|
+
(sum, id) => sum + vpToken[id].length,
|
|
9205
|
+
0
|
|
9206
|
+
);
|
|
9207
|
+
if (queryIds.length > 1 || presentationCount > 1) {
|
|
9208
|
+
throw new exports.MultipleCredentialsNotSupportedError(queryIds.length, presentationCount);
|
|
9209
|
+
}
|
|
9210
|
+
const presentation = vpToken[queryIds[0]][0];
|
|
9211
|
+
return verifyPresentation(presentation, query, options);
|
|
9212
|
+
}
|
|
8963
9213
|
|
|
8964
9214
|
// src/index.ts
|
|
8965
9215
|
init_errors();
|
|
@@ -9571,7 +9821,7 @@ function equalBytes(a, b) {
|
|
|
9571
9821
|
|
|
9572
9822
|
// src/index.ts
|
|
9573
9823
|
init_errors();
|
|
9574
|
-
var VERSION = "0.
|
|
9824
|
+
var VERSION = "0.7.0";
|
|
9575
9825
|
/*! Bundled license information:
|
|
9576
9826
|
|
|
9577
9827
|
pvtsutils/build/index.es.js:
|
|
@@ -9659,9 +9909,12 @@ exports.StaticTrustStore = StaticTrustStore;
|
|
|
9659
9909
|
exports.VERSION = VERSION;
|
|
9660
9910
|
exports.buildHaipQuery = buildHaipQuery;
|
|
9661
9911
|
exports.createAuthorizationRequest = createAuthorizationRequest;
|
|
9912
|
+
exports.createSignedAuthorizationRequest = createSignedAuthorizationRequest;
|
|
9913
|
+
exports.decryptAuthorizationResponse = decryptAuthorizationResponse;
|
|
9662
9914
|
exports.isHaipQuery = isHaipQuery;
|
|
9663
9915
|
exports.parsePresentation = parsePresentation;
|
|
9664
9916
|
exports.validateHaipQuery = validateHaipQuery;
|
|
9917
|
+
exports.verifyAuthorizationResponse = verifyAuthorizationResponse;
|
|
9665
9918
|
exports.verifyPresentation = verifyPresentation;
|
|
9666
9919
|
//# sourceMappingURL=index.cjs.map
|
|
9667
9920
|
//# sourceMappingURL=index.cjs.map
|