An Android implementation of the ISO 18013-5 standard for mobile driving licenses and digital identity documents.
This project implements the ISO 18013-5 standard for mobile driving licenses (mDL) and other identity documents on Android devices.
- Android SDK 26+
- Kotlin 1.8+
- Android device with Bluetooth support
- Android Studio Arctic Fox or later
-
Clone this repository:
git clone https://github.com/pagopa/iso18013-android.git cd iso18013-android
-
Open the project in Android Studio.
-
Build the project:
./gradlew build
To run unit tests for the CBOR module:
./gradlew :cbor:test
To run unit tests for the Proximity module:
./gradlew :proximity:test
To run all unit tests:
./gradlew test
This section describes the steps required to publish a new version of the library to Maven.
Open your Gradle file, proximity/build.gradle.kts
and/or cbor/build.gradle.kts
and update the version in the mavenPublishing
block:
mavenPublishing {
- coordinates("it.pagopa.io.wallet.proximity", "proximity", "x.y.z")
+ coordinates("it.pagopa.io.wallet.proximity", "proximity", "x.y.z+1")
}
Replace proximity
with cbor
(and the respective coordinates) if you are releasing that library instead.
Commit the changes to the remote repository with a commit message like chore: Release proximity/cbor vx.y.z
.
Create a git tag corresponding to your version, then push it to your remote repository. For example:
git tag proximity-v1.1.1
git push origin proximity-v1.1.1
(Adjust the tag name according to your library and version.)
Once the tag is pushed, the release process on Maven will be triggered automatically through GitHub Actions.
You can enable or disable logging for both libraries via ProximityLogger
and CborLogger
. For example, in MainActivity
:
ProximityLogger.enabled = BuildConfig.DEBUG
CborLogger.enabled = BuildConfig.DEBUG
It consists of three main modules:
The CBOR module handles Concise Binary Object Representation (CBOR) encoding and decoding, which is the data format specified by ISO 18013-5 for identity documents. It includes:
- CBOR parsing and encoding
- COSE (CBOR Object Signing and Encryption) implementation
- Document data structures
The public classes in this library are:
- COSEManager
- MDoc
The COSEManager
is designed for signing arbitrary byte arrays using COSE (CBOR Object Signing and Encryption) and for verifying COSE signatures.
Examples:
Sign data:
val data = //your byte array data
when (val result = coseManager.signWithCOSE(
data = data,
alias = "pagoPA"
)) {
is SignWithCOSEResult.Failure -> failureAppDialog(result.msg)
is SignWithCOSEResult.Success -> {
// handle result
signature = result.signature//bytes[]
publicKey = result.publicKey//bytes[]
}
}
Verify Sign1Message:
// it returns a Boolean
coseManager.verifySign1(
dataSigned = what,
publicKey = pubKey
)
Examples of how to use COSEManager
can be found in the SignAndVerifyViewViewModel
class.
MDoc:
The MDoc
class is a polymorphic class in Kotlin that returns an object of type ModelMDoc
. The ModelMDoc
is a
parser for a CBOR format, which includes a method to convert it to JSON.
The MDoc
class has three constructors:
-
Primary Constructor: This is private and is used internally by the other constructors.
private constructor(source: Any, isByteArray: Boolean = false)
-
String Constructor: This takes a
Base64String
source and setsisByteArray
tofalse
.constructor(source: String) : this(source, false)
-
ByteArray Constructor: This takes a
bytes[]
source and setsisByteArray
totrue
.constructor(source: ByteArray) : this(source, true)
The Proximity module manages secure communication between devices for document exchange, including:
- QR code engagement
- Bluetooth communication
- Device connection management
- Request and response protocols
- Certificate validation
The public classes here are:
- QrEngagement
- ResponseGenerator
and a data class:
/**
* BLE Retrieval Method
* @property peripheralServerMode set if the peripheral server mode is enabled
* @property centralClientMode set if the central client mode is enabled
* @property clearBleCache set if the BLE cache should be cleared
*/
data class BleRetrievalMethod(
val peripheralServerMode: Boolean,
val centralClientMode: Boolean,
val clearBleCache: Boolean
) : DeviceRetrievalMethod
QrEngagement
is used to generate and handle QR-based connections for an mdoc session.
companion object {
/**
* Create an instance and configures the QR engagement.
* First of all you must call [configure] to build QrEngagementHelper.
* To accept just some certificates use [withReaderTrustStore] method.
* To create a QrCode use [getQrCodeString] method.
* To observe all events call [withListener] method.
* To close the connection call [close] method.
*/
fun build(context: Context, retrievalMethods: List<DeviceRetrievalMethod>): QrEngagement {
return QrEngagement(context).apply {
this.retrievalMethods = retrievalMethods
qrEngagementBuilder = QrEngagementHelper.Builder(
context,
eDevicePrivateKey.publicKey,
retrievalMethods.transportOptions,
qrEngagementListener,
context.mainExecutor()
).setConnectionMethods(retrievalMethods.connectionMethods)
}
}
}
This is thew way this class is intended to be instantiated. Examples can be found into MasterViewViewModel class.
-
configure: builds QrEngagementHelper by com.android.identity package and returns QrEngagement instance created via QrEngagement.build static method.
fun configure() = apply { qrEngagement = qrEngagementBuilder.build() }
-
withReaderTrustStore: Method to inject certificates to be verified sent by mdoc verifier app.
/** * Use this if you have certificates into your **Raw Resource** folder. * *You have still other two methods with [List] of [ByteArray] for raw certificates and [List] of [String] for pem* * @param certificates a [List] of [Int] representing your raw resource * @return [QrEngagement] */ fun withReaderTrustStore(certificates: List<Int>) = apply { certificates.setReaderTrustStore() } /** * Use this if you have certificates **As [ByteArray]**. * *You have still other two methods with [List] of [Int] for raw resources and [List] of [String] for pem* * @param certificates a [List] of [ByteArray] representing your raw certificates * @return [QrEngagement] */ @JvmName("withReaderTrustStore1") fun withReaderTrustStore(certificates: List<ByteArray>) = apply { certificates.setReaderTrustStore() } /** * Use this if you have certificates **As [String]**. * *You have still other two methods with [List] of [Int] for raw resources and [List] of [ByteArray] for raw certificates* * @param certificates a [List] of [String] representing your pem certificates * @return [QrEngagement] */ @JvmName("withReaderTrustStore2") fun withReaderTrustStore(certificates: List<String>) = apply { certificates.setReaderTrustStore() }
-
getQrCodeString: Gives back QR code string for engagement
fun getQrCodeString() = apply { if (!checkQrEngagementInit()) return "" return qrEngagement.deviceEngagementUriEncoded }
-
withListener: Starts the listener for qrCodeEngagement
fun withListener(callback: QrEngagementListener) = apply { this.listener = callback }
-
close: Closes the connection with the mdoc verifier
fun close() { if (!checkQrEngagementInit()) return try { if (deviceRetrievalHelper != null) deviceRetrievalHelper!!.disconnect() qrEngagement.close() } catch (exception: RuntimeException) { ProximityLogger.e(this.javaClass.name, "Error closing QR engagement $exception") } }
The listener:
interface QrEngagementListener {
fun onDeviceConnecting()
fun onDeviceConnected(deviceRetrievalHelper: DeviceRetrievalHelperWrapper)
fun onError(msg: String)
fun onDocumentRequestReceived(request: String?, sessionsTranscript: ByteArray)
fun onDeviceDisconnected(transportSpecificTermination: Boolean)
}
ResponseGenerator
is used to create a response in ByteArray
format for the connected mdoc verifier.
interface Response {
/**@param [response] [ByteArray] generated for response*/
fun onResponseGenerated(response: ByteArray)
/**@param [message] [String] for error reached*/
fun onError(message: String)
}
/**
* It creates a mdoc response in ByteArray format respect documents requested and disclosed
* @param fieldRequestedAndAccepted a JSON with values to share (I.E.:{
"org.iso.18013.5.1.mDL": {
"org.iso.18013.5.1": {
"portrait": true,
"birth_date": true,
"given_name": true,
"issue_date": true,
"expiry_date": true,
"family_name": true,
"document_number": true,
"issuing_country": true,
"issuing_authority": true,
"driving_privileges": true,
"un_distinguishing_sign": true
}
}
})
* @return[Response.onResponseGenerated] if ByteArray is created without Exceptions, else
* [Response.onError] if disclosedDocumentsArray is Empty with "no doc found" message or if an
* [Exception] was reached with [Throwable.message].
*/
@JvmName("createResponseWithCallback")
fun createResponse(
documents: Array<DocRequested>,
fieldRequestedAndAccepted: String,
response: Response
) {
val (responseToSend, message) = this.createResponse(
documents, fieldRequestedAndAccepted
)
responseToSend?.let {
response.onResponseGenerated(it)
} ?: run {
response.onError(message)
ProximityLogger.e(
"Sending resp",
"found doc but fail to generate raw response: $message"
)
}
}
where DocRequested
is:
@Parcelize
data class DocRequested(
val issuerSignedContent: String,
val alias: String,
val docType: String
) : Parcelable
See in app MasterViewViewModel.shareInfo
method to understand how to retrieve documents from JSON request and correctly
send to response.
The OpenID4VP
class can be used to generate a sessionTranscript
to pass to ResponseGenerator
to use standard ISO 18013-7.
Class parameters can be retrieved by calling backend an getting parameters themselves.
Example:
val sessionTranscript = OpenID4VP(
clientId,
responseUri,
authorizationRequestNonce,
mdocGeneratedNonce
).createSessionTranscript()
Then you can create a device response doing:
val responseGenerator = ResponseGenerator(sessionTranscript)
responseGenerator.createResponse(
documents,
fieldRequestedAndAccepted,
object : ResponseGenerator.Response {
override fun onResponseGenerated(response: ByteArray) {
//do what you want with response
}
override fun onError(message: String) {
//ERROR!!
}
}
)
To use the Proximity module, you need to declare the following permissions in your AndroidManifest.xml
as it uses Bluetooth according to the official Android documentation:
<!-- Required for Bluetooth on Android >=12 or SDK >=31 and must be required at runtime -->
<!-- We defined the neverForLocation flag as we do not derive it from the Bluetooth -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- Required for Bluetooth on Android <=11 SDK <= 30 ACCESS_FINE_LOCATION must be requested at runtime -->
<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"/>