This repository contains a library that offers support for ISO 18013-5 mdoc via
CredentialManager present in Android 14 (U). An example app is also included.
The library contains support for both providing and requesting credentials, and the example app does both.
Releases are distributed through Google Maven.
In order for an app to request a mdoc credential, it needs to create a GetCredentialRequest for
use with CredentialManager (via the Jetpack library) which includes the appropriate options for mdoc. This is fairly
straightforward:
val option = GetMdocCredentialOption(
handover = MdocHandover.ANDROID,
nonce = nonce,
publicKey = publicKey,
documentType = MdocCredential.DOCUMENT_TYPE_MDL,
requestedElements = elements,
criticalElements = elements,
retentionInDays = GetMdocCredentialOption.RETENTION_NONE
)
val request = GetCredentialRequest(listOf(option))
val credman = CredentialManager.create(context)
val cred = credman.getCredential(context, request).credentialhandover identifies whether this request is coming from an app or a Web Browser. This affects how
the SessionTranscript (18013-5#9.1.5.1) is formed.
nonce is any non-empty ByteArray
publicKey is an ECPublicKey using secp256r1
requestedElements is a Set of MdocElement containing the elements you want to be included
in the resulting credential.
criticalElements is a similar Set of MdocElement, but these are only used to narrow the
selections in the CredentialManager picker UI. It may be useful to include a more minimal set
of elements here to ensure that a credential request is successful. Of course, it's also fine to
use the same set of elements for both.
A successful request returns a MdocCredential, which itself contains the HPKE-encrypted
DeviceResponse CBOR and the encapsulated sender key. The example app contains some code to do HPKE via
Tink and uses cbor-java
to parse the DeviceResponse. This code or variants of it may find their way into the
identity-credential project eventually.
An unsuccessful request will throw GetCredentialException.
For mdoc, we want to use the registry feature of CredentialManager. This allows us to provide
credentials without the user needing to enable the service in the Android Settings, and also
avoids some potential privacy-related badness that can occur with other providers seeing
unrelated requests. This library has a helper, MdocRegistry, to make it easy to register credentials:
val SUPPORTED_ELEMENTS = setOf(
MdocCredentialElement("family_name", MdocCredentialElement.NAMESPACE_MDL),
MdocCredentialElement("issue_date", MdocCredentialElement.NAMESPACE_MDL),
MdocCredentialElement("expiry_date", MdocCredentialElement.NAMESPACE_MDL),
MdocCredentialElement("document_number", MdocCredentialElement.NAMESPACE_MDL),
MdocCredentialElement("driving_privileges", MdocCredentialElement.NAMESPACE_MDL),
)
val registry = MdocRegistry(context)
val pendingIntent = PendingIntent.getActivity(
context,
0,
Intent(context, GetCredentialActivity::class.java),
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
)
registry.registerCredential(
title = "Jane Doe's Driver's License",
icon = Icon.createWithResource(context, android.R.drawable.ic_secure),
documentType = MdocCredential.DOCUMENT_TYPE_MDL,
supportedElements = SUPPORTED_ELEMENTS,
pendingIntent = pendingIntent
)The pendingIntent is used to launch the consent UI for your provider, so in the above example
you would replace GetCredentialActivity with your own.
The supportedElements argument is the list of elements your provider is capable of returning
for a given credential. If all supported elements are not present here, a request may fail to match
and will not be presented to the user for selection.
As mentioned above, the PendingIntent references your own Activity that will be launched to
service incoming requests. This Activity will typically show the user what is being requested and
may also allow some modification of the response. Once it's determined that the user does want to
allow this credential to fulfill the request, the provider needs to encrypt the mdoc with the
public key present in the request, set this as the result of the Activity, and finally finish()
the Activity. Some example code to form a response is below:
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)!!
val option =
GetMdocCredentialOption.createFrom(request.credentialOptions[0] as GetCustomCredentialOption)
val hpke = MdocHpke(option.publicKey)
val handoverBytes = when (option.handover) {
MdocHandover.ANDROID -> hpke.generateAndroidSessionTranscript(
nonce = option.nonce,
publicKey = option.publicKey,
packageName = request.callingAppInfo.packageName
)
MdocHandover.BROWSER -> hpke.generateBrowserSessionTranscript(
nonce = option.nonce,
publicKey = option.publicKey,
origin = request.callingAppInfo.origin!!
)
}
val credential =
MdocCredential(hpke.encrypt(cborResponseBytes, handoverBytes))
val response = GetCredentialResponse(credential)
val result = Intent()
PendingIntentHandler.setGetCredentialResponse(result, response)
setResult(Activity.RESULT_OK, result)
finish()If the provider wants to abort the request, because the user has declined to consent to sharing the
credential or any other reason, it can return an error via PendingIntentHandler.setGetCredentialException()
and finish() the Activity.