@openeudi/openid4vp 0.9.2 → 0.10.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 CHANGED
@@ -124,7 +124,7 @@ 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)
127
+ ## Signed authorization requests (x509_san_dns, x509_hash)
128
128
 
129
129
  For flows that require a signed request object (JAR) per OpenID4VP 1.0 §5.10, use `createSignedAuthorizationRequest`:
130
130
 
@@ -132,12 +132,13 @@ For flows that require a signed request object (JAR) per OpenID4VP 1.0 §5.10, u
132
132
  import { createSignedAuthorizationRequest } from "@openeudi/openid4vp";
133
133
 
134
134
  const req = await createSignedAuthorizationRequest({
135
- hostname: "verifier.example.com",
135
+ clientIdPrefix: "x509_san_dns", // or "x509_hash"; defaults to "x509_san_dns"
136
+ hostname: "verifier.example.com", // required for x509_san_dns; optional for x509_hash
136
137
  requestUri: "https://verifier.example.com/request.jwt",
137
138
  responseUri: "https://verifier.example.com/response",
138
139
  nonce,
139
140
  signer: verifierKeyPair, // CryptoKeyPair with public+private
140
- certificateChain: [leafCertDer], // DER-encoded, leaf SAN DNSName must equal hostname
141
+ certificateChain: [leafCertDer], // DER-encoded, leaf SAN DNSName must include hostname when one is given
141
142
  encryptionKey: {
142
143
  publicJwk: encryptionPublicJwk, // must include alg, e.g. "ECDH-ES"
143
144
  },
@@ -151,14 +152,11 @@ const req = await createSignedAuthorizationRequest({
151
152
  // (Content-Type: application/oauth-authz-req+jwt)
152
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
-
156
- The emitted `client_metadata` carries both shapes for compatibility:
155
+ `clientIdPrefix: "x509_hash"` sets `client_id` to `x509_hash:` followed by the base64url-encoded SHA-256 hash of the DER-encoded leaf certificate, per OpenID4VP 1.0 §5.9.3 (referenced by HAIP 1.0 Final for its mandated Client Identifier Prefix). `hostname` is not required in this mode, since the `client_id` is derived from the certificate rather than the host. If you do supply one it is still checked against the leaf SAN DNSName values — a supplied hostname that the certificate does not cover is treated as a misconfiguration (`hostname_cert_mismatch`) rather than silently ignored.
157
156
 
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"`
157
+ 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`.
160
158
 
161
- Verifiers reading either shape (e.g. the OIDF conformance suite reads ID3 directly) work without bespoke configuration.
159
+ The emitted `client_metadata` carries the 1.0 Final shape: `encrypted_response_enc_values_supported: ["A128GCM", ...]`.
162
160
 
163
161
  ## Authorization responses (direct_post and direct_post.jwt)
164
162
 
@@ -166,10 +164,15 @@ Wallets POST the Authorization Response to your `responseUri`. The library is st
166
164
 
167
165
  ### Unencrypted (`direct_post`)
168
166
 
169
- The envelope arrives as form-encoded JSON; parse it, check `state`, then verify:
167
+ The envelope arrives as form-encoded JSON; parse it, check `state`, then verify.
168
+
169
+ **mDOC SessionTranscript wall:** plain `direct_post` has no JWE, so there is no `apu` header and `verifyAuthorizationResponse` cannot auto-build the mDOC `SessionTranscript`. You must pass `options.mdocSessionTranscript` yourself. For wallets that use the OpenID4VP 1.0 Final unencrypted handover, omit `verifierEncryptionJwk` so the handover's `jwkThumbprint` is CBOR `null`:
170
170
 
171
171
  ```ts
172
- import { verifyAuthorizationResponse } from "@openeudi/openid4vp";
172
+ import {
173
+ buildOpenID4VPHandoverSessionTranscript,
174
+ verifyAuthorizationResponse,
175
+ } from "@openeudi/openid4vp";
173
176
 
174
177
  const envelope = parsedVpTokenObject; // { vp_token, state, ... }
175
178
 
@@ -177,12 +180,26 @@ if (envelope.state !== submittedState) {
177
180
  throw new Error("state mismatch — possible CSRF / replay");
178
181
  }
179
182
 
183
+ const mdocSessionTranscript = await buildOpenID4VPHandoverSessionTranscript({
184
+ clientId: verifierClientId,
185
+ nonce,
186
+ responseUri: verifierResponseUri,
187
+ // omit verifierEncryptionJwk → jwkThumbprint = null (unencrypted handover)
188
+ });
189
+
180
190
  const result = await verifyAuthorizationResponse(envelope, dcqlQuery, {
181
191
  trustedCertificates: [issuerCertDer],
182
192
  nonce,
193
+ clientId: verifierClientId,
194
+ responseUri: verifierResponseUri,
195
+ mdocSessionTranscript,
183
196
  });
184
197
  ```
185
198
 
199
+ Without a transcript, the mDOC parser fails closed (`claims: {}`, no `docType` on the failure result). DCQL matching against that doctype-less parse can then surface a misleading `doctype_mismatch` even when the wallet sent the correct credential — prefer `parsed.error` when diagnosing.
200
+
201
+ For `direct_post.jwt` auto-build (Annex B / `openid4vp-1.0`), see [mDOC SessionTranscript on the encrypted path](#mdoc-sessiontranscript-on-the-encrypted-path).
202
+
186
203
  ### Encrypted (`direct_post.jwt`)
187
204
 
188
205
  The wallet wraps the envelope in a JWE. Decrypt explicitly so you can check `state` against the decrypted envelope **before** verification runs:
@@ -214,7 +231,7 @@ const result = await verifyAuthorizationResponse(decrypted, dcqlQuery, {
214
231
 
215
232
  #### mDOC SessionTranscript on the encrypted path
216
233
 
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`:
234
+ Verifying an mDOC credential requires the ISO 18013-5 `SessionTranscript` the device signed over (see [mDOC](#mdoc) below). For **unencrypted** `direct_post` (no JWE / no `apu`), auto-build does not run — pass `mdocSessionTranscript` explicitly as shown in [Unencrypted (`direct_post`)](#unencrypted-direct_post). For `direct_post.jwt`, `verifyAuthorizationResponse` can auto-build the `SessionTranscript` for you, in one of two profiles selected via `options.sessionTranscriptProfile`:
218
235
 
219
236
  - **`'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
237
 
@@ -244,7 +261,7 @@ Verifying an mDOC credential requires the ISO 18013-5 `SessionTranscript` the de
244
261
 
245
262
  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
263
 
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).
264
+ If you already have the transcript bytes (or are verifying an mDOC outside either auto-build path), 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). For the plain `direct_post` pattern, see [Unencrypted (`direct_post`)](#unencrypted-direct_post).
248
265
 
249
266
  ### Supported JWE algorithms
250
267
 
@@ -376,7 +393,7 @@ This library implements the **verifier side** of OpenID4VP for SD-JWT VC and mDO
376
393
  - **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
394
  - **DCQL** — authorization request builder with DCQL query, matching via [@openeudi/dcql](https://www.npmjs.com/package/@openeudi/dcql), `verifyPresentation` for combined crypto + match.
378
395
  - **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.
396
+ - **Signed authorization requests (JAR)** — `createSignedAuthorizationRequest` per RFC 9101 / OpenID4VP 1.0 §5.10 with `x509_san_dns` or `x509_hash` client-id binding. Emits the OpenID4VP 1.0 Final `client_metadata` shape.
380
397
  - **Encrypted responses** — `decryptAuthorizationResponse` for `direct_post.jwt` (ECDH-ES + A128GCM/A256GCM), `verifyAuthorizationResponse` for the 1.0 §8.1 object-keyed `vp_token` envelope.
381
398
  - **X.509 chain validation** — RFC 5280 chain building including `nameConstraints`, `StaticTrustStore`, `CompositeTrustStore`.
382
399
  - **Revocation checking** — OCSP-first with CRL fallback (`revocationPolicy: 'skip' | 'prefer' | 'require'`).
@@ -387,7 +404,6 @@ This library implements the **verifier side** of OpenID4VP for SD-JWT VC and mDO
387
404
  **What is NOT yet implemented** (planned for follow-up releases):
388
405
 
389
406
  - 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
407
  - 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
408
  - SIOPv2 (Self-Issued OpenID Provider) identity flows.
393
409
 
@@ -403,6 +419,14 @@ Verifier-side conformance is automated against a self-hosted OpenID Foundation c
403
419
  - **[@openeudi/dcql](https://www.npmjs.com/package/@openeudi/dcql)** -- DCQL query matching engine used internally by `verifyPresentation`.
404
420
  - **[eIDAS Pro](https://eidas-pro.eu)** -- Managed verification service with admin dashboard, webhook integrations, and plugin support for WooCommerce and Shopify.
405
421
 
422
+ ## Community integrations
423
+
424
+ Third-party projects built on this library. These are **not** maintained or audited by OpenEUDI -- evaluate them on their own merits.
425
+
426
+ - **[eudi-verify](https://github.com/eudi-verify/eudi-verify)** -- Relying-party widget and API, running `@openeudi/openid4vp` as one of two pluggable verifier engines. Includes public [interop notes](https://github.com/eudi-verify/eudi-verify/blob/main/docs/INTEROP.md) covering `mso_mdoc` age-over-18 against an EU Age Verification reference wallet. Demo: [demo.eudi-verify.eu](https://demo.eudi-verify.eu/).
427
+
428
+ Built something on `@openeudi/openid4vp`? Open an issue and we're happy to consider listing it here.
429
+
406
430
  ## Migration
407
431
 
408
432
  See [CHANGELOG.md](./CHANGELOG.md) for per-release changes. Key migration moments:
@@ -412,6 +436,8 @@ See [CHANGELOG.md](./CHANGELOG.md) for per-release changes. Key migration moment
412
436
  - **0.6.0** — DCQL surfaces specific `UnmatchedReason` values via `@openeudi/dcql@0.2.0` (BREAKING for callers reading `match.unmatched[].reason`).
413
437
  - **0.7.0** — `createSignedAuthorizationRequest`, `decryptAuthorizationResponse`, `verifyAuthorizationResponse` for HAIP / 1.0 §8.1 envelopes.
414
438
  - **0.8.0** — additive: ID3 `client_metadata` bridge, `trustedIssuerJwks` opt-in, transitive SD-JWT disclosure check.
439
+ - **0.9.3** — `reflect-metadata` is loaded by the package itself in both ESM and CJS builds. If you added a manual `import "reflect-metadata"` before importing this library to work around the 0.9.2 bug, you can drop it.
440
+ - **0.10.0** — `createSignedAuthorizationRequest` gains `clientIdPrefix` (`x509_san_dns` default, or `x509_hash`), and `hostname` becomes optional — required only for `x509_san_dns`, but still validated against the leaf SAN whenever supplied. **Wire change for existing callers:** the emitted `client_metadata` no longer carries the singular ID3 `authorization_encrypted_response_alg` / `authorization_encrypted_response_enc` fields added in 0.8.0. Verifiers that read the 1.0 Final `encrypted_response_enc_values_supported` array (and `alg` from `client_metadata.jwks`) are unaffected; anything still reading the singular fields needs updating.
415
441
 
416
442
  ## License
417
443