@openeudi/openid4vp 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -30
- package/dist/index.cjs +349 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +348 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,6 +153,13 @@ const req = await createSignedAuthorizationRequest({
|
|
|
153
153
|
|
|
154
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
155
|
|
|
156
|
+
The emitted `client_metadata` carries both shapes for compatibility:
|
|
157
|
+
|
|
158
|
+
- 1.0 Final plural — `encrypted_response_enc_values_supported: ["A128GCM", ...]`
|
|
159
|
+
- ID3 singular — `authorization_encrypted_response_alg: "ECDH-ES"`, `authorization_encrypted_response_enc: "A128GCM"`
|
|
160
|
+
|
|
161
|
+
Verifiers reading either shape (e.g. the OIDF conformance suite reads ID3 directly) work without bespoke configuration.
|
|
162
|
+
|
|
156
163
|
## Authorization responses (direct_post and direct_post.jwt)
|
|
157
164
|
|
|
158
165
|
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.
|
|
@@ -205,6 +212,40 @@ const result = await verifyAuthorizationResponse(decrypted, dcqlQuery, {
|
|
|
205
212
|
|
|
206
213
|
`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
214
|
|
|
215
|
+
#### mDOC SessionTranscript on the encrypted path
|
|
216
|
+
|
|
217
|
+
Verifying an mDOC credential requires the ISO 18013-5 `SessionTranscript` the device signed over (see [mDOC](#mdoc) below). For `direct_post.jwt`, `verifyAuthorizationResponse` can auto-build the `SessionTranscript` for you, in one of two profiles selected via `options.sessionTranscriptProfile`:
|
|
218
|
+
|
|
219
|
+
- **`'iso-18013-7'` (default)** — the ISO 18013-7 Annex B OID4VP transcript, matching id2/id3-era wallets. Pass `options.clientId` and `options.responseUri` alongside the usual `options.nonce`, and the library derives the `mdoc-generated-nonce` from the JWE's `apu` header to construct the transcript before verification runs.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const result = await verifyAuthorizationResponse(envelope, dcqlQuery, {
|
|
223
|
+
trustedCertificates: [issuerCertDer],
|
|
224
|
+
nonce,
|
|
225
|
+
decryptionKey: verifierEncryptionPrivateKey,
|
|
226
|
+
clientId: verifierClientId,
|
|
227
|
+
responseUri: verifierResponseUri,
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- **`'openid4vp-1.0'`** — the OpenID4VP 1.0-Final `OpenID4VPHandover` transcript, for wallets that implement the final spec's SessionTranscript shape instead of the Annex B one. Pass `sessionTranscriptProfile: 'openid4vp-1.0'` together with `clientId`, `responseUri`, `nonce`, and — for the encrypted path — `verifierEncryptionJwk` (the verifier's response-encryption public JWK, used to derive the handover's JWK thumbprint). This profile does **not** read the JWE `apu` header.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const result = await verifyAuthorizationResponse(envelope, dcqlQuery, {
|
|
235
|
+
trustedCertificates: [issuerCertDer],
|
|
236
|
+
nonce,
|
|
237
|
+
decryptionKey: verifierEncryptionPrivateKey,
|
|
238
|
+
clientId: verifierClientId,
|
|
239
|
+
responseUri: verifierResponseUri,
|
|
240
|
+
sessionTranscriptProfile: 'openid4vp-1.0',
|
|
241
|
+
verifierEncryptionJwk, // verifier's response-encryption public JWK
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The transcript is `[null, null, ["OpenID4VPHandover", SHA-256(cbor([client_id, nonce, jwk_thumbprint | null, response_uri]))]]`, where `jwk_thumbprint` is the RFC 7638 SHA-256 thumbprint of `verifierEncryptionJwk` (or `null` when the response is unencrypted). Callers who need this transcript outside `verifyAuthorizationResponse` can use the exported `buildOpenID4VPHandoverSessionTranscript({ clientId, nonce, responseUri, verifierEncryptionJwk? })`.
|
|
246
|
+
|
|
247
|
+
If you already have the transcript bytes (or are verifying an mDOC outside either auto-build path, e.g. the unencrypted `direct_post` flow), pass `options.mdocSessionTranscript: Uint8Array` explicitly — it always takes precedence over either auto-built value. `buildOid4vpSessionTranscript({ clientId, responseUri, nonce, mdocGeneratedNonce })` (Annex B) and `buildOpenID4VPHandoverSessionTranscript({ clientId, nonce, responseUri, verifierEncryptionJwk? })` (1.0-Final) are both exported for callers who need to construct a transcript themselves. Without a transcript, the mDOC parser fails closed — see [mDOC](#mdoc).
|
|
248
|
+
|
|
208
249
|
### Supported JWE algorithms
|
|
209
250
|
|
|
210
251
|
`direct_post.jwt` decryption supports:
|
|
@@ -219,11 +260,21 @@ Other algorithms throw `UnsupportedJweError`.
|
|
|
219
260
|
Both `parsePresentation` and `verifyPresentation` accept:
|
|
220
261
|
|
|
221
262
|
- `nonce` (required) — the nonce bound into the VP token at creation time.
|
|
222
|
-
- `
|
|
223
|
-
- `
|
|
224
|
-
- `
|
|
225
|
-
- `
|
|
226
|
-
- `
|
|
263
|
+
- `requireKeyBinding?` — force SD-JWT holder-binding verification even when the issuer JWT carries no `cnf` claim. When the credential **is** holder-bound (`cnf` present), a KB-JWT is **always** required regardless of this flag — fail-closed, not opt-in. This flag only extends the requirement to credentials that lack `cnf`. Default `false`. See [SD-JWT VC](#sd-jwt-vc).
|
|
264
|
+
- `mdocSessionTranscript?` — CBOR bytes of the ISO 18013-5 `SessionTranscript` for the current OpenID4VP exchange. **Required** to verify mDOC device authentication; the mDOC parser fails closed without it. For `direct_post.jwt`, `verifyAuthorizationResponse` can build this for you from `clientId`/`responseUri` — see [mDOC SessionTranscript on the encrypted path](#mdoc-sessiontranscript-on-the-encrypted-path). See [mDOC](#mdoc).
|
|
265
|
+
- `sessionTranscriptProfile?` — *(`VerifyAuthorizationResponseOptions` only — the `direct_post`/`direct_post.jwt` auto-build described above)* which `SessionTranscript` shape to build from `clientId`/`responseUri`/`nonce`: `'iso-18013-7'` (default) builds the Annex B, `apu`-derived transcript for id2/id3-era wallets; `'openid4vp-1.0'` builds the OpenID4VP 1.0-Final `OpenID4VPHandover` transcript instead. See [mDOC SessionTranscript on the encrypted path](#mdoc-sessiontranscript-on-the-encrypted-path).
|
|
266
|
+
- `verifierEncryptionJwk?` — *(`VerifyAuthorizationResponseOptions` only)* the verifier's response-encryption public JWK. Required on the encrypted path when `sessionTranscriptProfile: 'openid4vp-1.0'` is used, to derive the handover's JWK thumbprint; ignored for the `'iso-18013-7'` profile.
|
|
267
|
+
- `trustedCertificates` (required when `trustStore` is unset) — DER-encoded issuer leaf certificates for the 0.4.x byte-equality trust check. Deprecated since 0.5.0 — pass an empty array and supply `trustStore` for production deployments.
|
|
268
|
+
- `trustStore?` — `TrustStore` instance for full RFC 5280 chain validation (e.g. `LotlTrustStore`, `StaticTrustStore`, or `CompositeTrustStore`). When set, takes precedence over `trustedCertificates`.
|
|
269
|
+
- `revocationPolicy?` — `'skip'` (default) | `'prefer'` | `'require'`. Controls whether the chain validator consults OCSP / CRL.
|
|
270
|
+
- `fetcher?` — HTTP transport for CRL/OCSP/LOTL fetches. Defaults to `globalThis.fetch`.
|
|
271
|
+
- `cache?` — cache for CRL/OCSP/LOTL artefacts. Defaults to `new InMemoryCache()`.
|
|
272
|
+
- `clockSkewTolerance?` — seconds of slack applied to certificate validity checks. Default 60.
|
|
273
|
+
- `trustedIssuerJwks?` — opt-in alternate trust path for SD-JWT VCs whose issuer JWT lacks an `x5c` header. The library matches by `kid` (or iterates the array when no `kid` is present) and skips chain validation entirely. Intended for harness setups (e.g. OIDF conformance suite) where the wallet signs without `x5c` and the verifier knows the signing key out-of-band. **Not recommended for production verifiers** — `trustStore` is the secure path.
|
|
274
|
+
- `audience?` — expected audience for key binding JWT verification.
|
|
275
|
+
- `allowedAlgorithms?` — restrict signature algorithms. Defaults to `['ES256','ES384','ES512']`.
|
|
276
|
+
- `skipTrustCheck?` — skip trust checks entirely (dev/test only).
|
|
277
|
+
- `expectedDocType?` — for mDOC verification, lock the credential `docType` (or SD-JWT `vct`).
|
|
227
278
|
|
|
228
279
|
## Supported formats
|
|
229
280
|
|
|
@@ -234,8 +285,10 @@ Selective Disclosure JSON Web Token Verifiable Credentials. The token is a strin
|
|
|
234
285
|
- Decodes the issuer JWT and extracts the `x5c` certificate chain
|
|
235
286
|
- Verifies the issuer certificate against your trusted set
|
|
236
287
|
- Checks credential expiry from the `exp` claim
|
|
237
|
-
- Validates the nonce in the key binding JWT
|
|
238
288
|
- Resolves selective disclosures using SHA-256
|
|
289
|
+
- Enforces holder binding (key binding JWT / KB-JWT): **mandatory whenever the issuer JWT carries a `cnf` claim** — the credential is holder-bound and verification fails closed if the KB-JWT is missing. Set `requireKeyBinding: true` to extend this requirement to credentials without `cnf`. When a KB-JWT is required, the parser verifies its signature against the `cnf.jwk` holder key and validates its claims: a non-empty `nonce` (matched against `options.nonce`), `sd_hash` (over the SD-JWT + disclosures), `audience` (when `options.audience` is set), and standard JWT claims including `iat`.
|
|
290
|
+
|
|
291
|
+
> **Breaking change (0.9.0):** prior releases only validated the KB-JWT's nonce when a KB-JWT happened to be present and silently accepted holder-bound credentials presented *without* one. As of 0.9.0, a holder-bound credential missing its KB-JWT is rejected outright — see [CHANGELOG](./CHANGELOG.md).
|
|
239
292
|
|
|
240
293
|
### mDOC
|
|
241
294
|
|
|
@@ -246,6 +299,10 @@ Mobile Document credentials as defined in ISO 18013-5. The token is a CBOR-encod
|
|
|
246
299
|
- Verifies the certificate against your trusted set
|
|
247
300
|
- Checks the validity period from `validityInfo`
|
|
248
301
|
- Extracts claims from the `eu.europa.ec.eudi.pid.1` namespace
|
|
302
|
+
- Verifies each `IssuerSignedItem`'s digest against the MSO's `valueDigests`, computed over the full tag-24 `IssuerSignedItemBytes` (`#6.24(bstr .cbor IssuerSignedItem)`) per ISO 18013-5 §9.1.2.4 — not just the inner CBOR — matching how real wallets encode mdocs. mDOC verification, including this digest check and device authentication below, is validated against the OIDF conformance suite acting as an independent ISO 18013-5 mdl wallet.
|
|
303
|
+
- Performs full ISO 18013-5 §9.1.3 device authentication: verifies the `DeviceSignature` (COSE_Sign1) over `DeviceAuthentication`, which binds the `SessionTranscript`/nonce, using the EC2 device key committed in the MSO's `deviceKeyInfo`. **`DeviceMac` (COSE_Mac0) is not supported and is rejected.** The parser fails closed if `deviceSigned`, the `SessionTranscript` (`options.mdocSessionTranscript`), or the device key is missing — a captured `issuerSigned` payload alone is no longer accepted as proof of presentation.
|
|
304
|
+
|
|
305
|
+
> **Breaking change (0.9.0):** prior releases verified only the issuer-signed data (`issuerSigned`), so a replayed or intercepted mDOC device response — without any proof the presenting device held the credentialed key — was accepted. As of 0.9.0, device authentication is mandatory and fails closed; see [CHANGELOG](./CHANGELOG.md).
|
|
249
306
|
|
|
250
307
|
## Custom parsers
|
|
251
308
|
|
|
@@ -313,28 +370,26 @@ try {
|
|
|
313
370
|
|
|
314
371
|
This library implements the **verifier side** of OpenID4VP for SD-JWT VC and mDOC credentials.
|
|
315
372
|
|
|
316
|
-
**What is implemented
|
|
317
|
-
|
|
318
|
-
- SD-JWT VC
|
|
319
|
-
- mDOC / ISO 18013-5
|
|
320
|
-
-
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
- `
|
|
324
|
-
-
|
|
325
|
-
-
|
|
326
|
-
-
|
|
327
|
-
-
|
|
328
|
-
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
-
|
|
334
|
-
-
|
|
335
|
-
-
|
|
336
|
-
- OpenID Foundation conformance test suite integration
|
|
337
|
-
- SIOPv2 (Self-Issued OpenID Provider) identity flows
|
|
373
|
+
**What is implemented:**
|
|
374
|
+
|
|
375
|
+
- **SD-JWT VC** — full cryptographic verification (issuer JWT signature via `x5c`, transitive disclosure-hash check, key binding JWT signature + `sd_hash`, nonce check). Holder binding (KB-JWT) is **mandatory and fails closed** whenever the issuer JWT carries a `cnf` claim; `requireKeyBinding` extends the requirement to credentials without `cnf`. Optional `trustedIssuerJwks` alternate trust path for VCs without `x5c`.
|
|
376
|
+
- **mDOC / ISO 18013-5** `mso_mdoc` format — CBOR decoding, claim extraction, COSE_Sign1 signature verification, MobileSecurityObject validity enforcement, IssuerSignedItem digest verification. Device authentication (ISO 18013-5 §9.1.3) is **mandatory and fails closed**: the `DeviceSignature` (COSE_Sign1) over `DeviceAuthentication`/`SessionTranscript` is verified against the MSO-committed device key. `DeviceMac` (COSE_Mac0) is **not supported and is rejected**.
|
|
377
|
+
- **DCQL** — authorization request builder with DCQL query, matching via [@openeudi/dcql](https://www.npmjs.com/package/@openeudi/dcql), `verifyPresentation` for combined crypto + match.
|
|
378
|
+
- **HAIP** — `buildHaipQuery` / `validateHaipQuery` helpers for the High Assurance Interoperability Profile.
|
|
379
|
+
- **Signed authorization requests (JAR)** — `createSignedAuthorizationRequest` per RFC 9101 / OpenID4VP 1.0 §5.10 with `x509_san_dns` client-id binding. Emits both 1.0 Final and ID3 `client_metadata` shapes for verifier interop.
|
|
380
|
+
- **Encrypted responses** — `decryptAuthorizationResponse` for `direct_post.jwt` (ECDH-ES + A128GCM/A256GCM), `verifyAuthorizationResponse` for the 1.0 §8.1 object-keyed `vp_token` envelope.
|
|
381
|
+
- **X.509 chain validation** — RFC 5280 chain building including `nameConstraints`, `StaticTrustStore`, `CompositeTrustStore`.
|
|
382
|
+
- **Revocation checking** — OCSP-first with CRL fallback (`revocationPolicy: 'skip' | 'prefer' | 'require'`).
|
|
383
|
+
- **EU List of Trusted Lists** — `LotlTrustStore` resolves national trust lists via signed XML fetch + XAdES verification; populates `provenance` (LoA, qualified status, country, service name) on verified presentations.
|
|
384
|
+
- **OIDF conformance** — automated against the OpenID Foundation conformance suite in CI (`oidf-pr.yml` happy-flow gate, `oidf-release.yml` full plan).
|
|
385
|
+
- **Algorithm allowlist** — ES256/384/512 (ECDSA only per EUDI policy); configurable via `allowedAlgorithms`.
|
|
386
|
+
|
|
387
|
+
**What is NOT yet implemented** (planned for follow-up releases):
|
|
388
|
+
|
|
389
|
+
- Multi-credential DCQL queries (multiple query ids) and multi-presentation arrays per query id — currently rejected with `MultipleCredentialsNotSupportedError`.
|
|
390
|
+
- `client_id_scheme: x509_hash` (HAIP 1.0 final's mandated scheme) — only `x509_san_dns` is supported today.
|
|
391
|
+
- Self-signed-leaf rejection per HAIP 1.0 final's strict constraint (current behaviour accepts self-signed leaves for the verifier's own identity).
|
|
392
|
+
- SIOPv2 (Self-Issued OpenID Provider) identity flows.
|
|
338
393
|
|
|
339
394
|
EUDI Architecture Reference Framework (ARF) alignment: tracks OpenID4VP 1.0 final. Full ARF 1.4+ profile compliance will be added before a stable 1.0.
|
|
340
395
|
|
|
@@ -348,9 +403,15 @@ Verifier-side conformance is automated against a self-hosted OpenID Foundation c
|
|
|
348
403
|
- **[@openeudi/dcql](https://www.npmjs.com/package/@openeudi/dcql)** -- DCQL query matching engine used internally by `verifyPresentation`.
|
|
349
404
|
- **[eIDAS Pro](https://eidas-pro.eu)** -- Managed verification service with admin dashboard, webhook integrations, and plugin support for WooCommerce and Shopify.
|
|
350
405
|
|
|
351
|
-
## Migration
|
|
406
|
+
## Migration
|
|
407
|
+
|
|
408
|
+
See [CHANGELOG.md](./CHANGELOG.md) for per-release changes. Key migration moments:
|
|
352
409
|
|
|
353
|
-
|
|
410
|
+
- **0.4.0** — `presentationDefinition` (PEX) replaced by DCQL queries; `verifyPresentation` introduced.
|
|
411
|
+
- **0.5.0** — `trustStore` option added for RFC 5280 chain validation; `trustedCertificates` deprecated.
|
|
412
|
+
- **0.6.0** — DCQL surfaces specific `UnmatchedReason` values via `@openeudi/dcql@0.2.0` (BREAKING for callers reading `match.unmatched[].reason`).
|
|
413
|
+
- **0.7.0** — `createSignedAuthorizationRequest`, `decryptAuthorizationResponse`, `verifyAuthorizationResponse` for HAIP / 1.0 §8.1 envelopes.
|
|
414
|
+
- **0.8.0** — additive: ID3 `client_metadata` bridge, `trustedIssuerJwks` opt-in, transitive SD-JWT disclosure check.
|
|
354
415
|
|
|
355
416
|
## License
|
|
356
417
|
|