Skip to main content
Version: Next

Presenting an ISO mdoc Offline

The SDK can present an ISO mdoc to a verifier in person, using the proximity protocol defined in ISO/IEC 18013-5. The wallet and the verifier connect directly to each other over Bluetooth Low Energy (BLE), so neither device needs an internet connection during the exchange.

This guide walks through the flow from the wallet side.

Prerequisites​

How the flow works​

In a proximity presentation, the wallet is the initiator. The holder starts the flow in their app, and the verifier responds to it. The exchange has two phases.

Device engagement. The wallet shares the information the verifier needs to connect: an ephemeral public key for the session, the BLE modes the wallet supports, and a service identifier (UUID) for the BLE connection. The holder shares this either by displaying a QR code for the verifier to scan, or through an NFC tap.

Data retrieval. The two devices connect over BLE. The verifier sends its request, the holder reviews it, and the wallet either sends the requested data or rejects the request. The BLE connection then closes.

Session encryption​

The devices do not use BLE pairing. Instead, each side generates an ephemeral key pair for the session, and the two derive a shared key from them. Every message in the data retrieval phase is encrypted with that key, so the BLE connection itself carries only encrypted content.

The wallet's choice of key algorithm for this is set by sessionEncryptionAlgorithm in your configuration.

BLE modes​

BLE always has one device that advertises its presence and hosts a service (the peripheral and server), and one that scans for it and connects (the central and client). ISO/IEC 18013-5 defines two modes for which device takes which role.

Mode names always describe the wallet's role, never the verifier's:

mdoc peripheral server modemdoc central client mode
Advertises and hosts the serviceWalletVerifier
Scans and connectsVerifierWallet

The wallet always chooses the service identifier and includes it in its device engagement data, whichever device ends up advertising. In peripheral server mode, the wallet advertises this identifier and the verifier scans for it. In central client mode, the roles reverse: the verifier advertises the identifier it received from the wallet, and the wallet scans for it.

You choose which modes your wallet supports with bleMode. When you choose BOTH, it runs both at the same time and the verifier chooses which one to use. ISO/IEC 18013-5 directs verifiers to prefer central client mode in this case, and verifiers built on the Core SDK always choose it.

Mandatory and optional claims​

The ISO/IEC 18013-5 request format cannot mark individual claims as mandatory, so every requested claim reaches the wallet as optional. In the response from getPresentationDefinitionV2, every claim of an ISO_MDL request has required: false.

The verifier still enforces its own requirements when it validates the presentation: if the holder withholds a claim the verifier requires, the verifier rejects the presentation.

Configuration​

The flow uses two parts of your configuration: the device engagement methods, and the ISO_MDL verification protocol.

verificationEngagement:
QR_CODE:
display: verificationEngagement.qrCode
order: 1
enabled: true
NFC:
display: verificationEngagement.nfc
order: 2
enabled: true
verificationProtocol:
ISO_MDL:
type: "ISO_MDL"
display: "exchange.isoMdl"
order: 7
params:
holder:
sessionEncryptionAlgorithm: ECDSA
bleMode: BOTH

You reference the keys of these entries (QR_CODE, NFC, and ISO_MDL) when you start the flow in step 1.

The holder parameters apply when your app acts as the wallet:

ParameterValuesDescription
sessionEncryptionAlgorithmECDSA, EDDSAKey algorithm for the ephemeral session key. ECDSA uses the P-256 curve and EDDSA uses X25519. Specify one value only. Required, with no default.
bleModePERIPHERAL, CENTRAL, BOTHWhich BLE modes the wallet supports. Required, with no default.

BOTH gives the widest compatibility, since the verifier can then use whichever mode it supports. The SDK's verifier side supports both modes and needs no BLE configuration of its own.

note

NFC engagement on iOS is implemented, but is not currently available for production use.

Platform setup​

The SDK does not request permissions on its own. If the required permissions have not been granted when the flow starts, calls will fail. Declare the permissions below, and request the runtime permissions in your app before starting device engagement.

Android​

Add the following to your AndroidManifest.xml:

<!-- Android 12 and later -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />

<!-- Android 11 and earlier -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />

<!-- NFC engagement -->
<uses-permission android:name="android.permission.NFC" />

<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
<uses-feature
android:name="android.hardware.nfc"
android:required="false" />
<uses-feature
android:name="android.hardware.nfc.hce"
android:required="false" />

On Android 12 and later, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT, and BLUETOOTH_SCAN are runtime permissions, so your app must also request all three from the user, whichever bleMode you use.

The neverForLocation flag declares that your app does not use Bluetooth scanning to determine location, so no location permission is needed on Android 12 and later.

On Android 11 and earlier, Bluetooth scanning requires location access instead, so request ACCESS_FINE_LOCATION at runtime on these versions.

NFC engagement service​

For NFC engagement, the wallet acts as an NFC tag that the verifier reads. On Android, this uses host card emulation, which requires declaring the SDK's engagement service inside the <application> element of your manifest:

<service
android:name="ch.procivis.one.core.nfc.EngagementService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/engagement_aid_list" />
</service>

The android:permission attribute ensures that only the system's NFC service can bind to the engagement service.

The service also needs an AID list, which tells Android which NFC requests to route to it. Create it at android/app/src/main/res/xml/engagement_aid_list.xml:

<host-apdu-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:requireDeviceUnlock="true">
<aid-group
android:category="other"
android:description="@string/app_name">
<aid-filter android:name="D2760000850101" />
</aid-group>
</host-apdu-service>

D2760000850101 identifies the NDEF Tag Application, which is how the verifier finds the wallet's device engagement data over NFC. With requireDeviceUnlock set to true, the holder must unlock their device before the wallet responds to an NFC tap.

If you do not use NFC engagement, you can leave out both the service and the AID list.

iOS​

Add a Bluetooth usage description to your Info.plist. iOS shows this text when it asks the user for Bluetooth access:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used to connect to verifiers and share credentials with them.</string>

NFC engagement is not currently available for production use on iOS, so no NFC setup is needed for the wallet.

Flow diagram​

1. Start device engagement​

const { proofId, interactionId, url } = await core.proposeProof({
protocol: 'ISO_MDL',
organisationId: organisationId,
engagement: ['QR_CODE'], // or ['NFC']
});

protocol and engagement reference keys from your configuration. engagement takes an array, but pass exactly one value: the SDK supports one engagement method per flow.

This call creates a proof in the PENDING state and starts the BLE side of the connection according to your bleMode. Keep both IDs it returns. You use proofId to check state and read the request, and interactionId to submit or reject.

Two optional fields are also available:

  • uiMessage: text shown on the iOS system overlay during NFC engagement. It has no effect for QR code engagement or on Android.
  • ecosystem: use this to enforce the constraints of a regulatory ecosystem. This gateway behaves the same as handle-invitation as described in Enforcing an Ecosystem as a Wallet.

Display the QR code​

For QR code engagement, url contains the device engagement data. Render it as a QR code for the verifier to scan, using any QR code library. For NFC engagement, url is not returned, and the holder taps their device against the verifier's instead.

2. Wait for the verifier's request​

The SDK does not notify your app when the verifier connects. Poll getProof until the proof leaves the PENDING state:

import { ProofState } from '@procivis/react-native-one-core';

const waitForRequest = async (proofId) => {
while (true) {
const proof = await core.getProof(proofId);

if (proof.state === ProofState.REQUESTED) {
return proof;
}
if (proof.state === ProofState.ERROR) {
throw new Error('Connection to the verifier failed');
}

await new Promise((resolve) => setTimeout(resolve, 1000));
}
};
  • REQUESTED: the verifier connected and sent its request. Continue to step 3. The BLE connection stays open while the holder decides.
  • ERROR: the connection failed, or the verifier cancelled or sent invalid data, or the request failed to meet the constraints of the eocsystem applied in step 1.

There is no timeout on PENDING. A proof waits for a verifier until the holder cancels, so give the holder a way to cancel and call deleteProof when they do.

For more on ecosystem logic, see Enforcing Ecosystems as a Wallet. The enforcement logic described there for handle-invitation applies to this step in the ISO proximity flow.

3. Show the request to the holder​

const definition = await core.getPresentationDefinitionV2(proofId);

The response describes what the verifier is asking for:

  • credentialQueries: one entry per requested credential, keyed by credential query ID. Each entry either lists the wallet's credentials that match (applicableCredentials) or explains why none do (failureHint, with a reason of NO_CREDENTIAL, VALIDITY, or CONSTRAINT).
  • credentialSets: which combinations of credential queries satisfy the request.

Each claim of a matching credential includes userSelection, which marks claims the holder can choose whether to share. Keep in mind that the verifier may still reject a presentation that leaves out claims it requires (see Mandatory and optional claims).

For a full walkthrough of handling the presentation definition, see Wallet Workflow.

Check who is asking​

The verifier is authenticated to the wallet only when it signs its request with a certificate (reader authentication). The proof details show whether it did:

const proof = await core.getProof(proofId);
const verifier = proof.verifier;

When the request is signed, verifier holds the identifier created from the verifier's certificate. When it is not, verifier is empty, and the wallet has no verified information about who is making the request. Your app should make that clear to the holder.

If the verifier is registered in a trust ecosystem, more detail is available:

const trust = await core.getProofTrustInformation(proofId);
  • verifier: the verifier as registered in the ecosystem, including its name, country, contact details, service description, and supervisory authority, plus any intermediary acting on its behalf. EUDI is currently the only supported ecosystem type.
  • ecosystemErrors: one entry per ecosystem provider that rejected the verifier, keyed by provider ID.

4. Submit or reject​

To share the selected credentials:

await core.holderSubmitProofV2(interactionId, {
[credentialQueryId]: [
{
credentialId: credentialId,
userSelections: ['path/to/optional/claim'],
},
],
});

Both submit and reject take the interactionId from step 1, not the proofId.

The submission maps each credential query ID from step 3 to the credentials chosen for it. userSelections lists the paths of any optional claims (userSelection: true) the holder chose to share. Omit it, or pass an empty array, to withhold all optional claims.

The SDK sends the presentation, closes the BLE session, and moves the proof to ACCEPTED. On the wallet side, ACCEPTED means the presentation was sent. The verifier does not report its validation result back to the wallet.

To decline the request instead:

await core.holderRejectProof(interactionId);

The SDK sends an empty response to the verifier, closes the BLE session, and moves the proof to REJECTED.

Either way, the session is over. To present again, start a new flow with step 1.

5. Cancel the flow​

await core.deleteProof(proofId);

What this does depends on how far the flow has progressed:

  • In PENDING, before the verifier has sent a request, the proof is deleted completely with no history retained, and the BLE session closes.
  • In REQUESTED, the proof moves to RETRACTED. The BLE session closes and the history of the interaction is retained.

Proof states​

A proof presented with this flow moves through these states:

StateMeaning
PENDINGDevice engagement started. Waiting for the verifier to connect and send a request.
REQUESTEDThe verifier's request was received. Waiting for the holder to respond.
ACCEPTEDThe holder submitted the presentation.
REJECTEDThe holder rejected the request.
RETRACTEDThe holder cancelled after the request was received.
ERRORA technical error occurred, or the connection was lost.

For all proof state transitions, see Proof request states as a holder.

Limitations​

  • One request per session. After the wallet submits or rejects, the BLE connection closes. Any further request requires a new device engagement.
  • Age over checks. ISO/IEC 18013-5 lets a verifier ask whether the holder is over a given age, and the wallet answers true or false without revealing the exact age. The wallet can currently answer an age_over_NN request only if the credential contains that exact data element.
  • NFC engagement on iOS is not currently available for production use.

Next steps​