@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 +41 -15
- package/dist/index.cjs +168 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +168 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|