@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 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
- - `trustedCertificates` (required) — the set of trusted issuer certificates for crypto verification.
223
- - `audience?` — expected audience.
224
- - `allowedAlgorithms?` — restrict signature algorithms.
225
- - `skipTrustCheck?` — skip trust-list checks (dev/test only).
226
- - `expectedDocType?` — for mDOC verification.
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 (v0.4.x):**
317
-
318
- - SD-JWT VC: full cryptographic verification (issuer JWT signature via x5c, disclosure hashes, key binding JWT signature + sd_hash, nonce check)
319
- - mDOC / ISO 18013-5 *mso_mdoc* format: CBOR decoding and claim extraction
320
- - mDOC / COSE_Sign1 cryptographic signature verification
321
- - mDOC MobileSecurityObject validity enforcement (strict ISO 18013-5)
322
- - mDOC IssuerSignedItem digest verification
323
- - `expectedDocType` ParseOptions to lock the credential type
324
- - Algorithm allowlist (ES256/384/512 — ECDSA only per EUDI policy)
325
- - Authorization request builder with DCQL query
326
- - DCQL query matching via [@openeudi/dcql](https://www.npmjs.com/package/@openeudi/dcql)
327
- - HAIP query build/validate helpers
328
- - `verifyPresentation` — combined crypto + DCQL match in one call
329
- - Certificate trust check via byte-equality against a caller-supplied trusted set
330
-
331
- **What is NOT yet implemented** (planned for follow-up releases — do not assume compliance in production until present):
332
-
333
- - X.509 certificate chain building and validation beyond leaf-byte-equality
334
- - EU List of Trusted Lists (LOTL) / ETSI TL resolution
335
- - Certificate revocation (CRL, OCSP)
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 from 0.3.x
406
+ ## Migration
407
+
408
+ See [CHANGELOG.md](./CHANGELOG.md) for per-release changes. Key migration moments:
352
409
 
353
- See [CHANGELOG.md](./CHANGELOG.md) for the full 0.4.0 migration guide (breaking changes and new APIs).
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