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:
- Service discovery: The verifier connects to the BLE service UUID from the engagement.
- Session establishment: Both sides derive a shared session key using HKDF over their ephemeral ECDH keys.
- 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..."
}
}| Field | Type | Required | Description |
|---|---|---|---|
deviceResponse | string | Yes | Base64-encoded CBOR DeviceResponse received over BLE |
sessionTranscript.deviceEngagementBytes | string | Yes | Base64-encoded device engagement bytes from the scanned QR |
sessionTranscript.eReaderKeyBytes | string | Yes | Base64-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
| Code | Body | When |
|---|---|---|
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:
| Check | Description |
|---|---|
| Session transcript | Verifies the device response was created for this specific session |
| BLE session encryption | Ensures data was not tampered with during transfer |
| Ephemeral key binding | Confirms 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:
- Parses the
readerAuthfrom theDeviceRequest. - Validates the COSE_Sign1 signature on the reader cert.
- Builds the cert chain from the leaf up to a CA listed in the RICAL.
- 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_18without 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.