Present credentials with your holder app

The Credential Manager Holder API enables your Android holder (also called "wallet") app to manage and present digital credentials to verifiers. This guide describes how to register your holder app's credentials with Credential Manager and how to handle a presentation request when a user selects a credential. The Holder API is supported on Android 6 (API level 23) and higher.

Add dependencies

To use the Credential Manager Holder API, add the following dependencies to your app module's build.gradle script:

dependencies {
    // Used to implement credential registries
    implementation "androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-openid:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-sdjwtvc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha05"
}

Register your credentials

First, create a RegistryManager instance that you will use to register an OpenId4VpRegistry request.

// Create the registry manager
val registryManager = RegistryManager.create(context)

Build an OpenId4VpRegistry request

You need to register an OpenId4VpRegistry to handle OpenID4VP requests from verifiers. Assuming you have local data types loaded with your wallet credentials (e.g., sdJwtsFromStorage and mdocsFromStorage), you must convert them into their Jetpack DigitalCredentialEntry equivalents: SdJwtEntry and MdocEntry.

Add Sd-JWTs into the registry

Map each local SD-JWT credential to an SdJwtEntry for the registry:

fun mapToSdJwtEntries(sdJwtsFromStorage: List<StoredSdJwtEntry>): List<SdJwtEntry> {
    val list = mutableListOf<SdJwtEntry>()

    for (sdJwt in sdJwtsFromStorage) {
        list.add(
            SdJwtEntry(
                verifiableCredentialType = sdJwt.getVCT(),
                claims = sdJwt.getClaimsList(),
                entryDisplayPropertySet = sdJwt.toDisplayProperties(),
                id = sdJwt.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

Add mdocs into the Registry

Map your local mdoc credentials into the Jetpack type MdocEntry:

fun mapToMdocEntries(mdocsFromStorage: List<StoredMdocEntry>): List<MdocEntry> {
    val list = mutableListOf<MdocEntry>()

    for (mdoc in mdocsFromStorage) {
        list.add(
            MdocEntry(
                docType = mdoc.retrieveDocType(),
                fields = mdoc.getFields(),
                entryDisplayPropertySet = mdoc.toDisplayProperties(),
                id = mdoc.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

Key points about the code

  • Encrypted IDs: One method of configuring the ID field is to register an encrypted credential identifier, so only you can decrypt the value.
  • Localization: The UI display fields for both formats should be localized.

Combine and register your request

Combine your converted entries and register the request with the RegistryManager:

val credentialEntries =
    mapToSdJwtEntries(sdJwtsFromStorage) + mapToMdocEntries(mdocsFromStorage)

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    id = "my-wallet-openid-registry-v1" // A stable, unique ID to identify your registry record.
)

Optional: Inline issuance

If there are no existing credentials of a requested type available for a user, you can display an option to add a credential to your holder. To do this, add an inlineIssuanceEntries list containing the credentials supported to your OpenId4VpRegistry:

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    inlineIssuanceEntries = emptyList(),
                listOf(
                MdocInlineIssuanceEntry(
                    id = "Issuance",
                    display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                        subtitle = "Mobile Drivers License, State Id, and Others",
                    ),
                    supportedMdocs = setOf(
                        MdocInlineIssuanceEntry.SupportedMdoc(
                        "eu.europa.ec.eudi.pid.1"
                        ),
                        MdocInlineIssuanceEntry.SupportedMdoc(
                        "org.iso.18013.5.1.mDL1"
                        ),
                    )
                ),
                SdJwtInlineIssuanceEntry(
                    id = "sd-jwt-issuance",
                    display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                        subtitle = "Mobile Drivers License, State Id, and Others",
                    ),
                    supportedSdJwts = setOf(
                        SdJwtInlineIssuanceEntry.SupportedSdJwt("urn:openid:interop:id:1")
                    )
                )
            ),
    id = "my-wallet-openid-registry-v1"
)

Register your credentials with CredentialManager.

try {
    val response = registryManager.registerCredentials(openidRegistryRequest)
} catch (e: Exception) {
    // Handle failure
}

App metadata management

The metadata your holder app registers with CredentialManagerhas the following properties:

  • Persistence: The information is saved locally, persisting across reboots.
  • Siloed Storage: Each app's registry records are stored separately, meaning one app cannot change another app's registry records.
  • Keyed Updates: Each app's registry records are keyed by an id, which allows re-identifying, updating, or deleting records.
  • Updating Metadata: It is good practice to update the persisted metadata whenever your app changes or is first loaded. If a registry is called multiple times under the same id, the latest call overwrites all prior records. To update, re-register without needing to clear the old record first.

Optional: Create a matcher

A matcher is a Wasm binary that Credential Manager runs in a sandbox to filter your registered credentials against an incoming Verifier request.

  • Default matcher: The OpenId4VpRegistry class automatically includes the default OpenId4VP matcher (OpenId4VpDefaults.DEFAULT_MATCHER) when you instantiate it. For all standard OpenID4VP use cases, the library handles matching for you.
  • Custom matcher: You would only implement a custom matcher if you are supporting a non-standard protocol that requires its own matching logic.

Handle a selected credential

When a user selects a credential, your holder app needs to handle the request. You will need to define an Activity that listens to the androidx.credentials.registry.provider.action.GET_CREDENTIAL intent filter. Our sample wallet demonstrates this procedure.

The intent launches your activity with the Verifier request and calling origin, which you extract with the PendingIntentHandler.retrieveProviderGetCredentialRequest function. This returns a ProviderGetCredentialRequest containing all the information associated with the verifier request. There are three key components:

You can process the presentation request with the following code:

request.credentialOptions.forEach { option ->
    if (option is GetDigitalCredentialOption) {
        Log.i(TAG, "Got DC request: ${option.requestJson}")
        processRequest(option.requestJson)
    }
}

An example of this can be seen in the sample wallet.

Check the verifier identity

First, extract the ProviderGetCredentialRequest from the intent:

val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)

Check for privileged apps: Privileged apps (like web browsers) can make calls on behalf of other verifiers by setting the origin parameter. To retrieve this origin, pass a list of privileged and trusted callers (an allowlist in JSON format) to the CallingAppInfo getOrigin API.

val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

If origin is not empty: The origin is returned if the packageName and certificate fingerprints match an app in your allowlist. Consider this a privileged call and set this origin on the OpenID4VP response instead of computing the origin using the calling app's signature.

If origin is empty: The verifier request is from an Android app. The origin mapped to the OpenID4VP response should be calculated as android:apk-key-hash:<encoded SHA 256 fingerprint>.

val appSigningInfo =
    request?.callingAppInfo?.signingInfoCompat?.signingCertificateHistory?.get(0)?.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val certHash =
    Base64.encodeToString(md.digest(appSigningInfo), Base64.NO_WRAP or Base64.NO_PADDING)
val computedOrigin = "android:apk-key-hash:$certHash"

Render the Holder UI

When a credential is selected, the holder app is invoked, guiding the user through the app's UI. There are two standard ways to handle this workflow:

  • If additional user authentication is needed to release the credential, use the BiometricPrompt API. This is demonstrated in the sample.
  • Otherwise, many wallets opt for a silent return by rendering an empty activity that immediately passes the data back to the calling app. This minimizes user clicks and provides a more seamless experience.

Return the credential response

Once your holder app is ready to send the result back, finish the activity with the credential response:

PendingIntentHandler.setGetCredentialResponse(
    resultData,
    GetCredentialResponse(DigitalCredential(response.responseJson))
)
setResult(RESULT_OK, resultData)
finish()

If there is an exception, you can similarly send the credential exception:

PendingIntentHandler.setGetCredentialException(
    resultData,
    GetCredentialUnknownException() // Configure the proper exception
)
setResult(RESULT_OK, resultData)
finish()

Refer to the sample app for a full example of returning the credential response in context.