Holder
Holderβ
This guide covers how to get started from the lenses of a holder app. i.e, the responsibilities include provisioning & storage of digital identities.
Initially weβll cover the steps required to create an mDoc & store it in a secure area.
π Storageβ
Before working with identity documents in Multipaz, you need to initialize platform-specific secure storage and cryptographic infrastructure. This setup should happen early in your app lifecycle.
Storageβ
Storage
is responsible for securely holding data items on the device.
Multipaz provides platform-specific implementations through the Platform.getNonBackedUpStorage()
method:
- Android: uses local encrypted storage.
- iOS: wraps native secure storage.
SecureAreaβ
A SecureArea
represents a secure environment for creating and managing key material and other sensitive objects (e.g., for signing identity credentials).
Multipaz offers multiple SecureArea
implementations:
-
AndroidKeystoreSecureArea: Uses the Android Keystore.
-
SecureEnclaveSecureArea: Uses the Apple Secure Enclave for iOS devices.
-
CloudSecureArea: Delegates key management to a secure remote server.
-
SoftwareSecureArea: Pure software-based secure area. Instantiate using
SoftwareSecureArea.create()
SecureAreaRepositoryβ
A SecureAreaRepository
manages a collection of SecureArea
instances. This allows you to define which SecureArea
to use for different keys or operations.
It provides fine-grained control and extensibility when your app needs to support multiple secure environments.
Initializationβ
You must initialize Storage
, SecureArea
, and SecureAreaRepository
before using the DocumentStore
or working with identity documents.
This setup should be done once, early in your appβs lifecycle (e.g., inside App()
):
lateinit var storage: Storage
lateinit var secureArea: SecureArea
lateinit var secureAreaRepository: SecureAreaRepository
//. . .
storage = org.multipaz.util.Platform.getNonBackedUpStorage()
secureArea = org.multipaz.util.Platform.getSecureArea(storage)
secureAreaRepository = SecureAreaRepository.Builder()
.add(secureArea)
.build()
Refer to this code for the implementation of the Storage section of this guide.
π DocumentStoreβ
Before you can create or manage real-world identity documents, you need to set up repositories and storage for document types and documents. This should be done after initializing your secure storage components.
DocumentTypeRepositoryβ
A DocumentTypeRepository
manages the metadata for different document types your app understands and uses.
- Standard Document Types: Multipaz provides a set of standard document types through the
multipaz-knowntypes
package, such as:DrivingLicense
EUCertificateOfResidence
PhotoID
VaccinationDocument
VehicleRegistration
- Custom Document Types: You can define your own document types using the
DocumentType.Builder
factory method.
DocumentStoreβ
A DocumentStore
is responsible for securely holding and managing real-world identity documents, such as Mobile Driving Licenses (mDL), in accordance with the ISO/IEC 18013-5:2021 specification.
- Initialization: Create a
DocumentStore
instance using either thebuildDocumentStore
function or theDocumentStore.Builder
class. - Dependencies: Pass the previously-initialized
storage
andsecureAreaRepository
to theDocumentStore
.
Implementationβ
lateinit var documentTypeRepository: DocumentTypeRepository
lateinit var documentStore: DocumentStore
// . . .
documentTypeRepository = DocumentTypeRepository().apply {
addDocumentType(DrivingLicense.getDocumentType())
}
documentStore = buildDocumentStore(
storage = storage,
secureAreaRepository = secureAreaRepository
) {}
By clearly structuring the setup of DocumentTypeRepository
and DocumentStore
, you ensure your app is ready to manage identity documents securely and efficiently. Always perform this setup early in your app lifecycle, after initializing storage and secure areas.
Refer to this part for the implementation of the DocumentStore section of this guide.
π Creation of an mDocβ
After initializing your DocumentStore
and related components, you can proceed to create an mDoc (mobile Document) credential. This section guides you through creating a Document and generating a standards-compliant mDoc credential.
Creating a Documentβ
A Document
represents an individual item created and managed by the DocumentStore
.
- Method: Use
DocumentStore#createDocument
to create a new document.
val document = documentStore.createDocument(
displayName = "Erika's Driving License",
typeDisplayName = "Utopia Driving License
)
Creating an MdocCredentialβ
An MdocCredential
represents a mobile credential, such as a Mobile Driving License (mDL), following the ISO/IEC 18013-5:2021 standard.
1. Prepare Timestampsβ
Set up the credentialβs validity period and signing time:
val now = Clock.System.now()
val signedAt = now
val validFrom = now
val validUntil = now + 365.days
2. Generate IACA Certificateβ
The IACA (Issuing Authority Certificate Authority) certificate is required for signing the Document Signing (DS) certificate.
val iacaKey = Crypto.createEcPrivateKey(EcCurve.P256)
val iacaCert = MdocUtil.generateIacaCertificate(
iacaKey = iacaKey,
subject = X500Name.fromName(name = "CN=Test IACA Key"),
serial = ASN1Integer.fromRandom(numBits = 128),
validFrom = validFrom,
validUntil = validUntil,
issuerAltNameUrl = "https://issuer.example.com",
crlUrl = "https://issuer.example.com/crl"
)
3. Generate Document Signing (DS) Certificateβ
The DS certificate signs the mDoc credential.
val dsKey = Crypto.createEcPrivateKey(EcCurve.P256)
val dsCert = MdocUtil.generateDsCertificate(
iacaCert = iacaCert,
iacaKey = iacaKey,
dsKey = dsKey.publicKey,
subject = X500Name.fromName(name = "CN=Test DS Key"),
serial = ASN1Integer.fromRandom(numBits = 128),
validFrom = validFrom,
validUntil = validUntil
)
4. Create the mDoc Credentialβ
Finally, use the document and generate certificates to create the mDoc credential.
val mdocCredential =
DrivingLicense.getDocumentType().createMdocCredentialWithSampleData(
document = document,
secureArea = secureArea,
createKeySettings = CreateKeySettings(
algorithm = Algorithm.ESP256,
nonce = "Challenge".encodeToByteString(),
userAuthenticationRequired = true
),
dsKey = dsKey,
dsCertChain = X509CertChain(listOf(dsCert)),
signedAt = signedAt,
validFrom = validFrom,
validUntil = validUntil,
)
By following these steps, you can securely create and provision an mDoc credential, ready to be managed and used within your application.
Refer to this part for the implementation of the Creating an MdocCredential section of this guide.
π Lookup and Manage Documentsβ
Once your DocumentStore
is initialized and populated, you can fetch, list, and manage documents within it.
Listing and Fetching Documentsβ
You can retrieve all documents stored in the DocumentStore
using DocumentStore#listDocuments
. For each document ID retrieved, use DocumentStore#lookupDocument
to get the corresponding document object.
Example: Listing Documents
val documents = mutableStateListOf<Document>()
for (documentId in documentStore.listDocuments()) {
documentStore.lookupDocument(documentId).let { document ->
if (document != null && !documents.contains(document))
documents.add(document)
}
}
Deleting Documentsβ
To remove a document from the DocumentStore
, use the DocumentStore#deleteDocument
method and provide the document's identifier.
Example: Deleting a Document
documentStore.deleteDocument(document.identifier)
By following these steps, you can efficiently list, fetch, and delete documents managed by your DocumentStore
, ensuring your application's document management remains clean and up-to-date.
π Presentationβ
The presentation phase allows a user to present a credential (such as an mDL) to a verifier, typically using BLE, NFC, or QR code. This section covers runtime permissions, setting up presentment flows, and generating engagement QR codes.
Runtime Permissionsβ
Multipaz provides composable functions for requesting runtime permissions in your app. Typical permissions include Bluetooth, Camera, and Notifications.
- Bluetooth Permission: Use
rememberBluetoothPermissionState
- Camera Permission: Use
rememberCameraPermissionState
- Notification Permission: Use
rememberNotificationPermissionState
Example: Requesting BLE Permission
val blePermissionState = rememberBluetoothPermissionState()
if (!blePermissionState.isGranted) {
Button(
onClick = {
coroutineScope.launch {
blePermissionState.launchPermissionRequest()
}
}
) {
Text("Request BLE permissions")
}
AndroidManifest.xml: Required BLE Permissions
<!-- For BLE -->
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Request legacy Bluetooth permissions on older devices. -->
<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_COARSE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
Refer to this part for the implementation of the permissions section of this guide.
π‘οΈ Reader Trustβ
The reader trust mechanism ensures that the holder app can check whether the verifier (reader) apps that request the credentials can be trusted. Multipaz uses the TrustManager
interface to manage and validate trust relationships.
TrustManager Implementationsβ
Multipaz provides several implementations for managing trust:
- LocalTrustManager: Backs trust with local files.
- VicalTrustManager: Uses VICAL, following
ISO/IEC 18013-5
. - CompositeTrustManager: Allows stacking multiple trust managers.
Types of Trustβ
There are two main types of trust in Multipaz:
- Issuer trust: Used by verifier apps to check the authenticity of credentials received from holder devices. See the verifier/issuer trust section (todo: link) for more details.
- Reader trust: Used by holder apps to verify the trustworthiness of verifier (reader) apps requesting credentials. This section focuses on reader trust.
Setting Up Reader Trustβ
To establish reader trust, add trusted verifier app certificates to your trust manager. When a verifier app requests credentials, the associated key is checked against the trusted keys.
Example: Adding a Trusted Reader Certificate
lateinit var readerTrustManager: TrustManager
//. . .
readerTrustManager = TrustManager().apply {
addTrustPoint(
TrustPoint(
certificate = X509Cert.fromPem(
"""
-----BEGIN CERTIFICATE-----
MIICUTCCAdegAwIBAgIQppKZHI1iPN290JKEA79OpzAKBggqhkjOPQQDAzArMSkwJwYDVQQDDCBP
V0YgTXVsdGlwYXogVGVzdEFwcCBSZWFkZXIgUm9vdDAeFw0yNDEyMDEwMDAwMDBaFw0zNDEyMDEw
MDAwMDBaMCsxKTAnBgNVBAMMIE9XRiBNdWx0aXBheiBUZXN0QXBwIFJlYWRlciBSb290MHYwEAYH
KoZIzj0CAQYFK4EEACIDYgAE+QDye70m2O0llPXMjVjxVZz3m5k6agT+wih+L79b7jyqUl99sbeU
npxaLD+cmB3HK3twkA7fmVJSobBc+9CDhkh3mx6n+YoH5RulaSWThWBfMyRjsfVODkosHLCDnbPV
o4G/MIG8MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMFYGA1UdHwRPME0wS6BJ
oEeGRWh0dHBzOi8vZ2l0aHViLmNvbS9vcGVud2FsbGV0LWZvdW5kYXRpb24tbGFicy9pZGVudGl0
eS1jcmVkZW50aWFsL2NybDAdBgNVHQ4EFgQUq2Ub4FbCkFPx3X9s5Ie+aN5gyfUwHwYDVR0jBBgw
FoAUq2Ub4FbCkFPx3X9s5Ie+aN5gyfUwCgYIKoZIzj0EAwMDaAAwZQIxANN9WUvI1xtZQmAKS4/D
ZVwofqLNRZL/co94Owi1XH5LgyiBpS3E8xSxE9SDNlVVhgIwKtXNBEBHNA7FKeAxKAzu4+MUf4gz
8jvyFaE0EUVlS2F5tARYQkU6udFePucVdloi
-----END CERTIFICATE-----
""".trimIndent().trim()
),
displayName = "OWF Multipaz TestApp",
displayIcon = null,
privacyPolicyUrl = "https://apps.multipaz.org"
)
)
}
With this setup, your holder app will trust the official Multipaz TestApp as a valid reader. Add additional trusted readers as needed by importing their certificates. By configuring TrustManager with trusted reader certificates, you ensure that only authorized verifier apps can access user credentials during presentment. Refer to this commit for the implementation of the reader trust in the app.
PresentmentModelβ
PresentmentModel
manages the entire UX/UI flow for credential presentation, providing a state
variable to track the presentation process. Multipaz also offers a Presentment
composable for embedding credential presentment UI.
You can generate QR codes using org.multipaz.compose.qrcode:generateQrCode
.
lateinit var presentmentModel: PresentmentModel
lateinit var presentmentSource: PresentmentSource
// . . .
presentmentModel = PresentmentModel().apply { setPromptModel(promptModel) }
presentmentSource = SimplePresentmentSource(
documentStore = documentStore,
documentTypeRepository = documentTypeRepository,
readerTrustManager = readerTrustManager,
preferSignatureToKeyAgreement = true,
domainMdocSignature = "mdoc",
)
val deviceEngagement = remember { mutableStateOf<ByteString?>(null) }
val state = presentmentModel.state.collectAsState()
when (state.value) {
PresentmentModel.State.IDLE -> {
showQrButton(deviceEngagement)
}
PresentmentModel.State.CONNECTING -> {
showQrCode(deviceEngagement)
}
PresentmentModel.State.WAITING_FOR_SOURCE,
PresentmentModel.State.PROCESSING,
PresentmentModel.State.WAITING_FOR_DOCUMENT_SELECTION,
PresentmentModel.State.WAITING_FOR_CONSENT,
PresentmentModel.State.COMPLETED -> {
Presentment(
appName = "Multipaz Getting Started Sample",
appIconPainter = painterResource(Res.drawable.compose_multiplatform),
presentmentModel = presentmentModel,
presentmentSource = presentmentSource,
documentTypeRepository = documentTypeRepository,
onPresentmentComplete = {
presentmentModel.reset()
},
)
}
}
Refer to this part for the implementation of this section in this guide.
Starting Device Engagementβ
To start engagement for presentment (e.g., via BLE), use a connection method that extends MdocConnectionMethod
(such as MdocConnectionMethodBle
or MdocConnectionMethodNfc
). The following example uses BLE:
Example: BLE Engagement and QR Code
@Composable
private fun showQrButton(showQrCode: MutableState<ByteString?>) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
presentmentModel.reset()
presentmentModel.setConnecting()
presentmentModel.presentmentScope.launch() {
val connectionMethods = listOf(
MdocConnectionMethodBle(
supportsPeripheralServerMode = false,
supportsCentralClientMode = true,
peripheralServerModeUuid = null,
centralClientModeUuid = UUID.randomUUID(),
)
)
val eDeviceKey = Crypto.createEcPrivateKey(EcCurve.P256)
val advertisedTransports = connectionMethods.advertise(
role = MdocRole.MDOC,
transportFactory = MdocTransportFactory.Default,
options = MdocTransportOptions(bleUseL2CAP = true),
)
val engagementGenerator = EngagementGenerator(
eSenderKey = eDeviceKey.publicKey,
version = "1.0"
)
engagementGenerator.addConnectionMethods(advertisedTransports.map {
it.connectionMethod
})
val encodedDeviceEngagement = ByteString(engagementGenerator.generate())
showQrCode.value = encodedDeviceEngagement
val transport = advertisedTransports.waitForConnection(
eSenderKey = eDeviceKey.publicKey,
coroutineScope = presentmentModel.presentmentScope
)
presentmentModel.setMechanism(
MdocPresentmentMechanism(
transport = transport,
eDeviceKey = eDeviceKey,
encodedDeviceEngagement = encodedDeviceEngagement,
handover = Simple.NULL,
engagementDuration = null,
allowMultipleRequests = false
)
)
showQrCode.value = null
}
}) {
Text("Present mDL via QR")
}
)
}
}
Refer to this part for the implementation of this section in this guide.
Displaying the QR Codeβ
Use the following composable to display the QR code generated for presentment.
Example: QR Code Display
@Composable
private fun showQrCode(deviceEngagement: MutableState<ByteString?>) {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (deviceEngagement.value != null) {
val mdocUrl = "mdoc:" + deviceEngagement.value!!.toByteArray().toBase64Url()
val qrCodeBitmap = remember { generateQrCode(mdocUrl) }
Text(text = "Present QR code to mdoc reader")
Image(
modifier = Modifier.fillMaxWidth(),
bitmap = qrCodeBitmap,
contentDescription = null,
contentScale = ContentScale.FillWidth
)
Button(
onClick = {
presentmentModel.reset()
}
) {
Text("Cancel")
}
}
}
}
Refer to this part for the implementation of this section in this guide.
By following these steps, you can request necessary permissions, manage the credential presentment flow, and generate device engagement QR codes for verifiers.