Docs
Verification

BLE Proximity Verification

Verify credentials over Bluetooth Low Energy for in-person scenarios

Proximity verification uses Bluetooth Low Energy (BLE) to verify credentials in face-to-face scenarios, following the ISO 18013-5 standard. The holder presents a QR code containing device engagement data, the verifier scans it and establishes a BLE connection, then the credential is transferred and verified locally.

Flow overview

The proximity exchange itself (device engagement, BLE session, DeviceRequest/DeviceResponse) happens entirely between the verifier app and the holder's wallet, per ISO 18013-5. The backend's role is a single endpoint, POST /proximity/verify, that cryptographically verifies the DeviceResponse your app received.

1. Device engagement

The holder's wallet displays a QR code containing the ISO 18013-5 device engagement (CBOR encoded as a URI). Your verifier app scans it to extract:

  • The BLE UUID for the connection
  • The holder's ephemeral public key (for session encryption)

Keep the raw device engagement bytes: they are required later for the session transcript.


2. BLE connection

After scanning, the wallet and verifier establish a BLE connection:

  1. Service discovery: The verifier connects to the BLE service UUID from the engagement.
  2. Session establishment: Both sides derive a shared session key using HKDF over their ephemeral ECDH keys.
  3. Encrypted transfer: The credential response is encrypted with AES-256-GCM and transferred over BLE.

All BLE communication is encrypted end-to-end. The session keys are ephemeral and never reused.


3. Submit the response

Once your app receives the BLE data, forward it to the backend for cryptographic verification.

POST /proximity/verify

Authentication: API key (publishable or secret)

POST /proximity/verify
Authorization: Bearer oat_your_publishable_key
Content-Type: application/json

{
  "deviceResponse": "omdkb2N1bWVudHOh...",
  "sessionTranscript": {
    "deviceEngagementBytes": "pGpEZXZpY2VFbmdhZ2VtZW50...",
    "eReaderKeyBytes": "pAECIAEhWCA..."
  }
}
FieldTypeRequiredDescription
deviceResponsestringYesBase64-encoded CBOR DeviceResponse received over BLE
sessionTranscript.deviceEngagementBytesstringYesBase64-encoded device engagement bytes from the scanned QR
sessionTranscript.eReaderKeyBytesstringYesBase64-encoded reader ephemeral key bytes from session establishment

There is no request ID: the endpoint is stateless and everything needed for verification is in the body. Both parts of sessionTranscript are required; the endpoint returns 400 if either is missing.

Response 200

{
  "status": "valid",
  "verification": {
    "ISSUER_AUTH": [
      { "status": "✅", "check": "Issuer certificate must be valid" },
      { "status": "✅", "check": "The MSO must be valid at the time of verification" }
    ],
    "DEVICE_AUTH": [
      { "status": "✅", "check": "Device signature must be valid" }
    ]
  },
  "presentation": {
    "eu.europa.ec.eudi.pid.1": {
      "given_name": "Marie",
      "family_name": "Dupont",
      "portrait": "data:image/jpeg;base64,/9j/4AAQ..."
    }
  }
}

Check statuses are ✅ (passed) or 🛑 (failed).

Error responses

CodeBodyWhen
400{ "error": "deviceResponse is required" }Missing deviceResponse
400{ "error": "sessionTranscript with deviceEngagementBytes and eReaderKeyBytes is required" }Missing or incomplete sessionTranscript
422{ "status": "failed", "verification": { ... }, "presentation": { ... }, "error": "..." }Verification hit an error; the result object is returned with an error message

Proximity-specific checks

Beyond the standard mDoc verification (issuer signature, certificate chain, device binding), proximity verification adds:

CheckDescription
Session transcriptVerifies the device response was created for this specific session
BLE session encryptionEnsures data was not tampered with during transfer
Ephemeral key bindingConfirms the device that signed the response is the one that established the BLE session

Reader authentication (RICAL)

When the verifier acts as an mDoc reader (sending a DeviceRequest to a wallet over proximity), the wallet may verify the reader's identity by checking the readerAuth certificate chain against a RICAL (Reader Identification CA List, ISO/IEC 18013-5 second edition Annex F).

A RICAL is a signed COSE list of CA certificates that are trusted to issue mDoc reader certificates within a given ecosystem (e.g. France Titres' driving-licence reader PKI). The wallet loads a RICAL out-of-band, then on every inbound proximity request:

  1. Parses the readerAuth from the DeviceRequest.
  2. Validates the COSE_Sign1 signature on the reader cert.
  3. Builds the cert chain from the leaf up to a CA listed in the RICAL.
  4. If no anchor matches, the wallet shows the request as "Reader not trusted" (the user may still consent; RICAL is informational, not blocking).

If the wallet has no RICAL configured, it falls back to first-edition behaviour: signature is verified but chain trust is skipped.

For verifier integrators

To have your reader cert validate on wallets that consume a given RICAL:

  • Register your reader CA with the ecosystem operator (e.g. France Titres for the FR mDL test event).
  • Once published, wallets that load that RICAL will see your reader as trusted.
  • No code change on the verifier side: the reader cert is what already travels in your readerAuth.

The Verifiables wallet exposes a "Reader Trust (RICAL)" entry in Settings → Connectivity to load the active RICAL by URL.


Use cases

  • Age verification at point of sale: Verify age_over_18 without collecting personal data
  • Identity checks at reception: Verify visitor identity with portrait comparison
  • Access control: Verify employee credentials at building entry points
  • Document inspection: Law enforcement or border control identity verification

Implementation notes

  • BLE transfer supports payloads up to ~512 KB, sufficient for credentials with portrait images.
  • The BLE connection timeout is 30 seconds. If the transfer is not completed, the session expires.
  • For mobile verifier apps, the BLE proximity module handles connection management. See the SDK documentation for integration details.