PasskeyBridge

Engineering · 2026-06-26

OpenID4VP over the Digital Credentials API: Browser-Native Wallet Selection in Chrome and Safari

By J. W. Bouckaert

OpenID4VP over the Digital Credentials API: Browser-Native Wallet Selection in Chrome and Safari

For a decade, online identity verification has meant one of three things: the relying party operated its own wallet, the relying party integrated one wallet SDK per ecosystem, or the user got punted into a redirect chain that opened, in sequence, every wallet app on their device until one of them claimed the URL. None of these scale. None of them are private. None of them are what a browser is for.

That changed in production this year. The W3C Digital Credentials API shipped enabled-by-default in Chrome 141 on October 3, 2025, and Safari turned it on across Safari 26 on macOS, iOS, and iPadOS under the WebKit Identity Document Services framework. The browser, not the relying party, now mediates which wallet answers an OpenID4VP request. The relying party never enumerates installed wallets, never holds the consent surface, and never sees anything the user did not explicitly disclose.

This article specifies how the new conveyance layer composes with OpenID for Verifiable Presentations 1.0—finalized 9 July 2025—what the browser actually owns versus what stays the verifier's responsibility, the unsolicited-presentation threat model the W3C is building against, and the concrete relying-party shape that lets a single integration accept mdoc driver's licenses, SD-JWT verifiable credentials, and W3C VC-JWT from any conforming wallet on any conforming browser.

The conveyance problem the DC API solves

OpenID4VP is a protocol. Before the Digital Credentials API, three transports existed in practice, each with a structural defect:

TransportMechanismDefect
Cross-device QRRP renders a QR; mobile wallet scans it; response returns over response_uriWorks, but requires two devices and adds 5 to 15 seconds of UX latency
Custom URL scheme (openid4vp://)Same-device deep link claimed by whichever wallet registered the schemeWallet collisions; OS prompts; silent enumeration of installed wallets
Vendor SDKDirect JS or native SDK from a specific wallet vendorRP locked to that wallet; interoperability lost; ships third-party code into the auth path

The DC API replaces all three for the same-device, in-browser case. The relying party invokes navigator.credentials.get({ digital: { requests: [...] } }). The browser inspects the request, finds wallets installed on the device or available through OS-level credential providers, presents only the ones that can satisfy the query, and routes the chosen wallet's response back to the page as a DigitalCredential object.

The relying party learns nothing about which wallets the user has, what the picker looked like, or which wallets declined. The only output is the credential the user chose to release, in the format the wallet returned it.

The browser's responsibilities

The DC API draws a hard line between platform responsibility and verifier responsibility. The browser owns:

  • Wallet discovery. OS-registered credential providers on iOS 26 and Android 15+, plus browser-installed wallet PWAs and extensions, are enumerated by the user agent. The site never sees the list.
  • Matching. The browser inspects each request's protocol envelope and credential query, asks each candidate wallet whether it has a matching credential, and only surfaces wallets that say yes. Negative answers are not exposed to the RP.
  • The picker UI. The OS or browser draws the chooser, including wallet branding. The site cannot style, intercept, or pre-select.
  • User consent. The wallet draws the disclosure sheet (which claims, to which RP origin, for which purpose). The site cannot dismiss or skip it.
  • Transport security. The browser carries the request and response between site and wallet over a channel the page cannot eavesdrop on.

What the browser deliberately does not do:

  • Verify the response. The vp_token arrives unvalidated. The relying party must verify the issuer signature, the holder binding, the nonce, and the audience.
  • Authorize the request. A page can call the API with no user-initiated reason. The browser shows the prompt; the user has to refuse.
  • Cache credentials. Each call is a fresh disclosure. There is no navigator.credentials.preventSilentAccess() analogue that persists wallet state across calls.

This division is the right one. The browser is the only party in the loop that can see every wallet on the device without leaking that view to the page; the verifier is the only party that can decide whether a particular presentation satisfies a particular policy. Centralising both in either place would break the model.

OpenID4VP over the DC API

OpenID4VP 1.0 added the OpenID4VP over the W3C Digital Credentials API profile specifically for this conveyance. The on-the-wire shape inside navigator.credentials.get({ digital }) is, end-to-end:

              ┌────────────────────────────────────────────┐
   RP page ──►│ navigator.credentials.get({ digital: {     │
              │   requests: [{                             │
              │     protocol: "openid4vp-v1-signed",       │
              │     data: {                                │
              │       client_id: "https://verifier.example",│
              │       nonce: "<server-issued, single-use>", │
              │       dcql_query: { credentials: [ ... ] } │
              │     }                                      │
              │   }]                                       │
              │ }})                                        │
              └──────────────────┬─────────────────────────┘
                                 ▼
                       ┌─────────────────┐
                       │ User agent      │   discovery, matching, picker, consent
                       └────────┬────────┘
                                ▼
                       ┌─────────────────┐
                       │ Selected wallet │   builds vp_token bound to nonce + client_id
                       └────────┬────────┘
                                ▼
              ┌────────────────────────────────────────────┐
              │ DigitalCredential.data = {                 │
              │   vp_token: { ... }                        │
              │ }  →  verify on the server                 │
              └────────────────────────────────────────────┘

Two query languages are in scope. DCQL—Digital Credentials Query Language—is the OpenID4VP 1.0 native form and is the one to ship against in 2026. Presentation Exchange 2.0 remains supported for wallets that have not yet upgraded; PasskeyBridge's verifier accepts both and normalises them server-side.

The credential formats the browser is format-agnostic about—and that PasskeyBridge's shield-vc-present verifier handles natively—are:

FormatIssuer signatureHolder bindingWhere it dominates
ISO/IEC 18013-5 mdocCOSE_Sign1 over MSODevice key in MSOMobile driver's licenses (US, EU, AU)
IETF SD-JWT VC (draft-ietf-oauth-sd-jwt-vc)JWS over SD-JWTcnf claim with holder JWKEUDI Wallet, enterprise issuers
W3C VC-JWT (VC Data Model 2.0)JWS over VCcnf or proof propertyLong tail of W3C-VC issuers

A wallet-agnostic relying party accepts whichever the wallet returns and runs format-specific verification on the server. Hard-coding to one is the architectural mistake that locks the integration to a single wallet ecosystem.

The unsolicited-presentation threat model

The DC API is reachable from any HTTPS origin. That is the protocol's strength and its central threat surface. The W3C Threat Model for Decentralized Credentials catalogs the relevant adversaries; the one that matters most for relying parties is the unsolicited presentation request.

Shape of the attack:

  1. A malicious page (phishing kit, ad iframe in a permissive context, compromised script in a third-party dependency) calls navigator.credentials.get({ digital }) with a query for a high-value credential—mdoc driver's license, age-over-21, national ID.
  2. The browser shows the user-agent picker and the wallet's consent sheet.
  3. A habituated user taps through, expecting it to be the site they meant to verify on.
  4. The attacker receives a valid, freshly-signed presentation they did not earn.

Three structural defenses live in different parts of the stack:

Browser-side—Chrome and Safari gate the call behind user activation, render an origin-clear picker, and the wallet renders a strong consent sheet that names the requesting origin and the claims being asked for. Neither browser allows silent invocation.

Wallet-side—Conforming wallets show the RP's client_id (an HTTPS origin in the unsigned variant, or a verified identifier in the signed variant) and the exact claim set being released. The OpenID4VP client_id_scheme = web-origin variant binds the response to the origin the browser observed, so a phished origin cannot pass the binding check on a verifier that enforces it.

Verifier-side—The relying party's verifier must reject any vp_token whose audience does not exactly match the verifier's expected client_id, whose nonce does not match the one the verifier issued less than 60 seconds prior, or whose holder binding does not chain to a key the wallet asserts owns the credential. Skipping any of these turns a per-page disclosure into a replayable token.

PasskeyBridge's shield-vc-present edge function enforces all three checks before returning an attestation, and emits a zero-PII verification event regardless of which format the wallet returned. The verification primitives are the same ones we documented in Selective Disclosure with SD-JWT and Verifiable Credentials Without Blockchain; the DC API just changes how the request reaches the wallet.

Adoption posture: What ships where, today

SurfaceDC API stateNotes
Chrome 141+ desktop & AndroidStable, on by defaultShipped 3 Oct 2025
Safari 26+ macOS & iOS 26+StableWebKit: Identity Verification with DC API
iOS 26 third-party browsers (Chrome iOS, Edge iOS, Firefox iOS)Available via iOS Identity Document ServicesPer Chrome blog announcement
Firefox desktopNot yet shippedTracking in Bugzilla; no stable date
Apple Wallet mdoc driver's licensesAvailable in supporting US states + EU national rolloutsFirst-party issuer for iOS
Google WalletAndroid credential provider roleWallet ecosystem dependent on state/country issuer
EUDI Wallet (reference)DC API conveyance in scope for ARFEUDI Wallet ARF

The cross-section of users who can present a real mdoc credential to an in-browser verifier through the DC API in 2026 is still narrower than the cross-section who can use a passkey. But it is the only architecture in the W3C and OpenID rec stack where the same code path serves a US state mobile driver's license, a German national ID, and an enterprise-issued SD-JWT employment credential without the relying party committing to one wallet vendor.

The wallet-agnostic relying-party pattern

The integration shape that survives wallet churn looks like this:

                  ┌─────────────────────────┐
   Browser ──────►│  RP page                │
                  │  • mints server nonce   │
                  │  • assembles DCQL       │
                  │  • calls DC API         │
                  └────────────┬────────────┘
                               ▼
                  ┌─────────────────────────┐
                  │  RP verifier (server)   │
                  │  • format-detect        │  mdoc | SD-JWT VC | VC-JWT
                  │  • verify issuer sig    │  cose | jws  | jws
                  │  • verify holder bind   │  device key | cnf | cnf
                  │  • check audience+nonce │
                  │  • emit zero-PII attest │
                  └─────────────────────────┘

The three behaviours that make this RP wallet-agnostic, in order of how often we see them missed in customer audits:

  1. Mint the nonce server-side, every time. A nonce generated in JS, in the browser, lasts exactly as long as the attacker's XSS. The verifier issues a 128-bit single-use nonce, returns it to the page over an authenticated channel, and rejects any vp_token whose nonce it did not just issue.
  1. Accept every format the wallet might return. Hard-coding to mdoc shuts out every SD-JWT issuer and every W3C VC-JWT credential. The format is in the vp_token's outer shape; detect it and dispatch. The PasskeyBridge shield-vc-present endpoint normalizes all three into the same canonical attestation envelope.
  1. Bind the audience to the exact client_id the verifier expects. Wallets that implement client_id_scheme = web-origin will refuse to release credentials to a mismatched origin. Verifiers that do not enforce the audience check on the way back have no way to catch the case where the wallet did not enforce it on the way out.

This is the spine of an integration that survives the next wallet vendor change. The DC API is the conveyance; OpenID4VP is the protocol; PasskeyBridge handles the verification.

Consequences for the identity stack

The browser-mediated layer changes three things about the identity-verification market in the next 18 months.

The wallet-lock-in window is closing. A relying party that picks a single wallet SDK in 2026 is committing to a 12-month integration debt against a layer the browsers now own. The cost of swapping wallets falls to zero on the RP side, because the RP no longer integrates against any one of them. The competition moves to wallets themselves—which ones users install, which ones their states and employers issue.

The redirect-only architectures look obsolete. Same-device redirect flows survive on iOS Safari pre-26 and on Firefox desktop, but the architectural pull is toward DC API everywhere a browser supports it. Verifiers that ship only the redirect path in 2026 will be asked, in 2027, why the wallet picker bounces the user out of the page.

The verifier becomes the differentiator. When every wallet is reachable through the same API, the only place identity-trust vendors compete is on the verifier—format coverage, latency, zero-PII posture, freshness, compliance evidence. PasskeyBridge sits on this side of the line by design.

The W3C draft is still moving (the current snapshot is the 16 June 2026 Working Draft). The conveyance is shipping. The protocol is final. The verifier is where the engineering attention belongs.

---

Further reading:

Start free · Test the API