Learn how to build an iOS application that can present an mDoc via a proximity workflow
Introduction
In this tutorial we will use the iOS native mDoc Holder SDK to build an iOS application that can present a claimed mDoc to a verifier that supports proximity verification as per ISO/IEC 18013-5.
- The user launches the wallet application and generates a QR code.
- The verifier scans the QR code, connects with the wallet and requests an mDoc for verification.
- The wallet displays matching credentials to the user and asks for consent to share them with the verifier.
- The verifier receives the walletβs response and verifies the provided credential.
The result will look something like this:
Prerequisites
Before we get started, letβs make sure you have everything you need.
Prior knowledge
-
The verification workflow described in this tutorial is based on the ISO/IEC 18013-5 standard. If you are unfamiliar with this standard, refer to our Docs section for more information:
- What are mDocs?
- What is credential verification?
- Breakdown of the proximity presentation workflow.
-
We assume you have experience developing iOS native apps in Swift.
If you need to get a holding solution up and running quickly with minimal development resources and in-house domain expertise, talk to us about our white-label MATTR GO Hold app which might be a good fit for you.
Assets
As part of your onboarding process you should have been provided with access to the following assets:
- ZIP file which includes the required framework:
(
MobileCredentialHolderSDK-*version*.xcframework.zip
). - Sample Wallet app: You can use this app for reference as we work through this tutorial.
This tutorial is only meant to be used with the most recent version of the iOS mDocs Holder SDK.
Development environment
- Xcode setup with either:
- Local build settings if you are developing locally.
- iOS developer account if you intend to publish your app.
Prerequisite tutorial
- You must complete the Claim a credential tutorial and claim the mDoc provided in the tutorial.
- This application is used as the base for the current tutorial.
Testing devices
As this tutorial implements a proximity presentation workflow, you will need two separate physical iOS devices to test the end-to-end result:
- Holder device:
- Supported iOS device to run the built application on, setup with:
- Biometric authentication.
- Bluetooth access.
- Available internet connection.
- Supported iOS device to run the built application on, setup with:
- Verifier device:
- Android/iOS device with an installed verifier application. We recommend downloading and using the MATTR GO Verify example app.
- Setup with Bluetooth access.
Got everything? Letβs get going!
Tutorial steps
To enable a user to present a stored mDoc to a verifier via a proximity presentation workflow, you will build the following capabilities into your wallet application:
- Create a QR code for the verifier to scan and establish a secure connection.
- Receive and handle a presentation request from the verifier.
- Send a matching mDoc presentation to the verifier.
Create a QR code for the verifier to scan
The first capability we need to build is to establish a secure communication channel between the verifier and holder devices. As defined in ISO/IEC 18130-5:2021, a proximity presentation workflow is always initiated by the holder (wallet user), who must create a QR code for the verifier to scan in order to initiate the device engagement phase.
To achieve this, your wallet application needs a UI element for the user to interact with and
trigger device engagement by calling the SDKβs
createProximityPresentationSession
method.
-
Open the project that you built as part of the Claim a credential tutorial.
-
Open the
ContentView
file and add the following code under the// Proximity Presentation - Step 1.2: Create deviceEngagementString and proximityPresentationSession variables
comment to create new variables to hold the device engagement string and the proximity presentation session.ContentView@Published var deviceEngagementString: String? @Published var proximityPresentationSession: ProximityPresentationSession?
-
Replace the
print
statement under the// Proximity Presentation - Step 1.3: Create function to create a proximity presentation session and generate QR code
comment with the following code to call the SDKβscreateProximityPresentationSession
method when the user selects the Create QR Code button:ContentViewTask { @MainActor in do { proximityPresentationSession = try await mobileCredentialHolder.createProximityPresentationSession( onRequestReceived: onRequestReceived(_:error:) ) deviceEngagementString = proximityPresentationSession?.deviceEngagement } catch { print(error) } }
At this stage the project wonβt compile because we need to update the signature of the func onRequestReceived
.
-
Replace the
func
statement below the// Proximity Presentation - Step 1.4: Update function signature
comment with the following code to update the functionβs signature (donβt change the function body for now):ContentViewfunc onRequestReceived(_ mobileCredentialRequests: [(request: MobileCredentialRequest, matchedMobileCredentials: [MobileCredentialMetadata])]?, error: Error?) {
-
Replace the
EmptyView()
statement under the// Proximity Presentation - Step 1.5: Add button to generate QR code
comment and add the following code to create a button that will generate the QR code when the user selects it:ContentViewButton { viewModel.createDeviceEngagementString() // Navigates user to presentCredentialsView, once the string has been created. viewModel.navigationPath.append(NavigationState.presentCredentials) } label: { Text("Present Credentials") }
Now, when the user selects the Create QR Code button, the application will call the SDKβs
createProximityPresentationSession
method, which returns a
ProximityPresentationSession
instance that includes a deviceEngagement
string in base64
format:
"mdoc:owBjMS4wAYIB2BhYS6QBAiABIVgghaBYJe7KSqcEolhmnIJaYJ2AIevkKbEy5xP7tkwlqAwiWCAMGCGe6uFI2hKeghb59h_K4hPV-Ldq6vnaxsRiySMH9gKBgwIBowD0AfULUKRoj0ZH60Qco-m0k97qRSQ"
The deviceEngagement
string is always prefixed with mdoc:
and contains the information required
to establish a secure connection between the two devices, including:
- Wireless communication protocols supported by the holder.
- How the verifier can connect with the holder.
- Ephemeral public key which is unique to the current session.
- Additional feature negotiations.
Your app needs to convert this deviceEngagement
string into a QR code and display it in the wallet
UI for the verifier to scan.
-
Replace the
return nil
statement under the// Proximity Presentation - Step 1.6: Generate QR code
comment with the following code to retrieve data from thedeviceEngagement
string and convert it into a QR code:ContentViewguard let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil } filter.setValue(data, forKey: "inputMessage") guard let ciimage = filter.outputImage else { return nil } let transform = CGAffineTransform(scaleX: 10, y: 10) let scaledCIImage = ciimage.transformed(by: transform) let uiimage = UIImage(ciImage: scaledCIImage) return uiimage.pngData()
-
Replace the
EmptyView()
statement under the// Proximity Presentation - Step 1.7: Create QR code view
comment with the following code to generate a QR Code and display it to the user:ContentViewVStack { Text("Scan to establish device engagement session") .font(.title3) Spacer() if let imageData = generateQRCode(data: viewModel.deviceEngagementString?.data(using: .utf8) ?? Data()), let image = UIImage(data: imageData) { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fit) } Spacer() }
-
Run the app and select the Present Credentials button. You should see a result similar to the following:
As the user selects the Present Credentials button, they are navigated to the new view. The application generates and displays a QR code that can be scanned by a verifier device to establish a secure proximity communication channel over Bluetooth.
Once the QR code is displayed, the
ProximityPresentationSession
enters a listening state, ready to establish a Bluetooth connection with a verifier app that scans
this QR Code.
Once the verifier scans the QR code, the devices will automatically exchange public keys to establish a secure communication channel, enabling the verifier to send a presentation request, which details what information is required and for what purpose.
Handle the presentation request
The
createProximityPresentationSession
function can handle three types of events:
onConnected
: When a secure connection is established.onSessionTerminated
: When a secure connection is terminated for whatever reason.onRequestReceived
: When a presentation request is received from the verifier.
onConnected
and onSessionTerminated
are optional events and will not be implemented in this
tutorial. Check out our SDK
Docs
for a complete description of these events and how you can handle them.
When the SDK receives a presentation request from a verifier, an onRequestReceived
event is
triggered. The sdk then checks its credential storage for any credentials that match the information
defined in this request.
The application then needs to present these matching credentials to the user, and provide a UI element to provide consent to sharing this information with the verifier.
The following step is also included in the Online presentation tutorial. If you had already completed this tutorial you may skip to step 2.
-
Add the following code under the
// Proximity and Online Presentation: Create variables for credential presentations
comment to create the following variables:ContentView@Published var matchedCredentials: [MobileCredential] = [] @Published var matchedMetadata: [MobileCredentialMetadata] = [] @Published var credentialRequest: [MobileCredentialRequest] = []
matchedCredentials
: Holds stored credentials that match the credential request.matchedMetadata
: Holds metadata of credentials that match the credential request.credentialRequest
: Holds the credentials that were requested for verification.
Once the application receives a credential request from a verifier (onRequestReceived
event), the
createProximityPresentationSession
function will return a mobileCredentialRequests
array that includes pairs of credentials requested
by the verifier
(MobileCredentialRequest)
and the metadata of stored credentials that match the requested information
(MobileCredentialMetadata).
- Replace the
print
statements under the// Proximity Presentation - Step 2.1: Store credential requests and matched credentials
comment with the following code to store the values from themobileCredentialRequests
array in thematchedMetadata
andcredentialRequest
variables:
Task { @MainActor in
matchedMetadata = mobileCredentialRequests?
.flatMap { $0.matchedMobileCredentials }
.compactMap { $0 } ?? []
credentialRequest = mobileCredentialRequests?
.compactMap { $0.request }
.compactMap { $0 } ?? []
// Navigate to presentation view if there are no errors
if error == nil {
navigationPath.append(NavigationState.proximityPresentation)
} else {
print(error!)
}
}
The following two steps are also included in the Online presentation tutorial. If you had already completed this tutorial you may skip to step 5.
-
Replace the
print
statement under the// Proximity and Online Presentation: Retrieve a credential from storage
comment with the following code create a function that uses the SDKβs getCredential method to retrieve a credential from the application storage:ContentViewTask { do { let credential = try await mobileCredentialHolder.getCredential(credentialId: id) matchedCredentials.append(credential) } catch { print(error) } }
The MobileCredentialMetadata object does not include the values of claims included in the credential. To display these values, the above function calls the SDKβs getCredential method with the
id
property of the MobileCredentialMetadata. -
Create a new file called
PresentCredentialsView.swift
and paste the following code to create a view to display credential requests and matching credentials stored in the application:PresentCredentialsViewimport MobileCredentialHolderSDK import SwiftUI struct PresentCredentialsView: View { @ObservedObject var viewModel: PresentCredentialsViewModel @State var selectedID: String? init(viewModel: PresentCredentialsViewModel) { self.viewModel = viewModel } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { Text("Requested Documents") .font(.headline) .padding(.leading) ForEach(viewModel.requestedDocuments, id: \.docType) { requestedDocument in DocumentView(viewModel: DocumentViewModel(from: requestedDocument)) } Text("Matched Credentials") .font(.headline) .padding(.leading) ForEach(viewModel.matchedMetadata, id: \.id) { matchedMetadata in VStack(alignment: .leading, spacing: 10) { if let matchedCredential = viewModel.matchedMobileCredential(id: matchedMetadata.id) { DocumentView(viewModel: DocumentViewModel(from: matchedCredential)) .padding(.vertical) .background(selectedID == matchedMetadata.id ? Color.blue.opacity(0.2) : Color.clear) .onTapGesture { guard selectedID != matchedMetadata.id else { selectedID = nil return } selectedID = matchedMetadata.id } Button("Hide claim values") { viewModel.matchedCredentials.removeAll(where: { $0.id == matchedMetadata.id }) } .frame(maxWidth: .infinity, alignment: .center) } else { DocumentView(viewModel: DocumentViewModel(from: matchedMetadata)) .padding(.vertical) .background(selectedID == matchedMetadata.id ? Color.blue.opacity(0.2) : Color.clear) .onTapGesture { guard selectedID != matchedMetadata.id else { selectedID = nil return } selectedID = matchedMetadata.id } Button("Show claim values") { viewModel.getCredentialAction(matchedMetadata.id) } .frame(maxWidth: .infinity, alignment: .center) } } } } } if selectedID != nil { Button("Send Response") { viewModel.sendCredentialAction(selectedID!) } .buttonStyle(.borderedProminent) .clipShape(Capsule()) .frame(maxWidth: .infinity, alignment: .center) } } } // MARK: PresentCredentialsViewModel class PresentCredentialsViewModel: ObservableObject { @Binding var requestedDocuments: [MobileCredentialRequest] @Binding var matchedCredentials: [MobileCredential] @Binding var matchedMetadata: [MobileCredentialMetadata] var getCredentialAction: (String) -> Void var sendCredentialAction: (String) -> Void init( requestedDocuments: Binding<[MobileCredentialRequest]>, matchedCredentials: Binding<[MobileCredential]>, matchedMetadata: Binding<[MobileCredentialMetadata]>, sendCredentialAction: @escaping (String) -> Void, getCredentialAction: @escaping (String) -> Void ) { self._requestedDocuments = requestedDocuments self._matchedCredentials = matchedCredentials self._matchedMetadata = matchedMetadata self.sendCredentialAction = sendCredentialAction self.getCredentialAction = getCredentialAction } func matchedMobileCredential(id: String) -> MobileCredential? { matchedCredentials.first(where: { $0.id == id }) } }
The
PresentCredentialsView
view is used to:- Display requested information.
- Display stored credentials that include the requested information.
- Enable the user to provide consent to sharing the requested information with the verifier.
The
PresentCredentialsViewModel
object is used to reference values from a credential request. It takes two closures in its initializer:getCredentialAction: (String) -> Void
is used to display claim values.sendCredentialAction: (String) -> Void
is used to send a credential response to the verifier once the user selected a credential and provided consent by selecting the Send Response button.
-
Return to the
ContentView
file and replace theEmptyView()
statement under the// Proximity Presentation - Step 2.5: Display proximity presentation view
comment with the new view that we created:ContentViewPresentCredentialsView( viewModel: PresentCredentialsViewModel( requestedDocuments: $viewModel.credentialRequest, matchedCredentials: $viewModel.matchedCredentials, matchedMetadata: $viewModel.matchedMetadata, sendCredentialAction: viewModel.sendProximityPresentationResponse(id:), getCredentialAction: viewModel.getCredential(id:) ) )
-
Run the app, select the Present Credentials button and then select the Create QR code button. Next, use your testing verifier app to scan the presented QR code and send a presentation request. You should see a result similar to the following:
As the user selects the Present Credentials button, the wallet generates and displays a QR code.
When a compliant verifier app scans the QR code, a secure communication channel is established via Bluetooth. The verifier then sends a presentation request, which is displayed to the user on their digital wallet, alongside any credentials they have stored in their wallet that match the request.
When the user selects the Show claim values button, the SDK retrieves the corresponding credential and the application displays its claim values.
When the user selects a credential, it is highlighted and the Send Response button appears at the bottom of the screen.
Send a presentation response
The next (and final!) capability we need to build is for the wallet application to send a presentation response upon receiving consent from the user to share information with the verifier.
Once the user provides this consent by selecting the Send Response button, the wallet
application should call the SDKβs
sendResponse
method to share the selected credentials with the verifier as a presentation response.
-
In the
ContentView
file replace the printstatement
under the// Proximity Presentation - Step 3.1: Send a credential response
comment with the following code to call thesendResponse
method when the user selects the Send Response button:ContentViewTask { do { let _ = try await proximityPresentationSession?.sendResponse(credentialIds: [id]) // set presentation session to nil after sending a response onlinePresentationSession = nil // Return to root view after the response is sent navigationPath = NavigationPath() } catch { print(error) } }
The
sendResponse
function signs the presentation response with the userβs device private key (to prove Device authentication) and shares it as an encoded CBOR file. -
Run the app and perform the following:
- Select the Present Credentials button.
- Use your testing verifier app to scan the present QR code and send a presentation request.
- Back on the holder device, select the matching credential to share and select the Send Response button.
You should see a result similar to the following:
As the user selects the credential to share, the verifier app will receive the presentation response, verify any incoming credentials and display the verification results.
Congratulations, you have now completed this tutorial, and should have a working wallet application that can claim an mDoc using an OID4VCI workflow, and present it to a verifier for proximity verification via Bluetooth.
Summary
You have just used the iOS mDoc holder SDK to built a wallet application that can present a claimed mDoc to a verifier that supports proximity verification as per ISO/IEC 18013-5.
This was achieved by building the following capabilities into the application:
- Generating a QR code for the verifier to scan and establish a secure communication channel.
- Receive and handle a presentation request from the verifier.
- Display matching credentials to the user and ask for consent to share them with the verifier.
- Send matching credentials to the verifier as a presentation response.
Whatβs next?
- You can build additional capabilities into your new application:
- Present a claimed mDoc for verification via an online presentation workflow into your new application.
- You can check out the iOS mDoc holder SDK Docs to learn more about available functions and classes.