Skip to content
 
 

Repository files navigation

EUDI ISO 18013-5 Wallet Transfer library for Android

This repository has been discontinued. ❗ The Transfer Manager library has been merged into the eudi-lib-android-wallet-core monorepo. All future development, issues, and releases will happen there. The Maven artifact eu.europa.ec.eudi:eudi-lib-android-iso18013-data-transfer continues to be published from the monorepo under the same group and artifact ID.

Important! Before you proceed, please read the EUDI Wallet Reference Implementation project description

Overview

This library provides a set of classes to manage the transfer of documents in an EUDI ISO 18013-5 Android Wallet.

It defines the interfaces for TransferManager and Transfer classes and provides a standard implementation of the TransferManager.

Currently, the library supports the following devices engagement methods:

  • QR
  • NFC tag

and the transfer of the documents is done using Bluetooth Low Energy (BLE).

Additionally, Transfer Manager incorporates reader authentication, ensuring the trustworthiness of the verifier (reader) device.

Reader authentication is accomplished by verifying the following:

  • Certificate path validation: Verifying the certificate path from the device's certificate to a trusted root certificate
  • Certificate's profile validation: Examines the attributes and constraints defined in the certificate profile to ensure that they meet the predefined criteria for a trusted certificate
  • Revocation checking: Optional certificate revocation checking via CRL, configurable through RevocationPolicy (disabled by default for backwards compatibility).

The library is written in Kotlin and is available for Android.

❗ Disclaimer

The released software is an initial development release version:

  • The initial development release is an early endeavor reflecting the efforts of a short timeboxed period, and by no means can be considered as the final product.
  • The initial development release may be changed substantially over time, might introduce new features but also may change or remove existing ones, potentially breaking compatibility with your existing code.
  • The initial development release is limited in functional scope.
  • The initial development release may contain errors or design flaws and other problems that could cause system or other failures and data loss.
  • The initial development release has reduced security, privacy, availability, and reliability standards relative to future releases. This could make the software slower, less reliable, or more vulnerable to attacks than mature software.
  • The initial development release is not yet comprehensively documented.
  • Users of the software must perform sufficient engineering and additional testing in order to properly evaluate their application and determine whether any of the open-sourced components is suitable for use in that application.
  • We strongly recommend not putting this version of the software into production use.
  • Only the latest version of the software will be supported

Requirements

  • Android 8 (API level 26) or higher

Dependencies

To use snapshot versions, add the following to your project's settings.gradle file:

dependencyResolutionManagement {
    repositories {
        // ...
        maven {
            url = uri("https://central.sonatype.com/repository/maven-snapshots/")
            mavenContent { snapshotsOnly() }
        }
        // ...
    }
}

To include the library in your project, add the following dependencies to your app's build.gradle file.

dependencies {
    implementation "eu.europa.ec.eudi:eudi-lib-android-iso18013-data-transfer:0.14.1-SNAPSHOT"
}

How to Use

Below is a quick overview of how to use the library.

For source code documentation, see in the docs directory.

Instantiating the TransferManager

The library provides a TransferManager implementation that can be used to present documents using the ISO 18013-5 for proximity presentation.

To create a new instance of the TransferManager, you can use the TransferManager.getDefault method.

The following example demonstrates how to create a new instance of the TransferManager

val documentManager: DocumentManager =
    TODO("The document manager to retrieve the requested documents")

val readerTrustStore = ReaderTrustStore.getDefault(
    trustedCertificates = listOf(
        // trustedReaderCertificate1,
        // trustedReaderCertificate2
    )
)

val transferManager = TransferManager.getDefault(
    context = context,
    documentManager = documentManager,
    retrievalMethods = listOf(
        BleRetrievalMethod(
            peripheralServerMode = true,
            centralClientMode = false,
            clearBleCache = true,
        )
    ),
    readerTrustStore = readerTrustStore,
    readerAuthPolicy = ReaderAuthPolicy.EnforceIfPresent, // default
)

Attaching a TransferEvent.Listener

The transfer process is event-driven. To receive events, you need to attach a TransferEvent.Listener to the TransferManager.

The available events are:

  1. TransferEvent.QrEngagementReady: The QR code is ready to be displayed. Get the QR code from TransferEvent.QrEngagementReady.qrCode.
  2. TransferEvent.Connecting: The devices are connecting. Use this event to display a progress indicator.
  3. TransferEvent.Connected: the devices are connected.
  4. TransferEvent.RequestReceived: A request is received. Get the processed request with the TransferEvent.RequestReceived.processedRequest and the initial raw request as received TransferEvent.RequestReceived.request.
  5. TransferEvent.ResponseSent: A response is sent.
  6. TransferEvent.Redirect: This event prompts to redirect the user to the given Redirect URI. Get the Redirect URI with TransferEvent.Redirect.redirectUri.
  7. TransferEvent.IntentToSend: This event indicates that an intent is ready to be sent. Get the intent with TransferEvent.IntentToSend.intent. This is used mainly for Digital Credential API implementation.
  8. TransferEvent.Disconnected: The devices are disconnected.
  9. TransferEvent.Error: An error occurred. Get the Throwable error with TransferEvent.Error.error.

The following example demonstrates how to attach a TransferEvent.Listener to the TransferManager. It also demonstrates how to handle the different events.

transferManager.addTransferEventListener { event ->
    when (event) {
        is TransferEvent.QrEngagementReady -> {
            // Qr code is ready to be displayed
            val qrCodeBitmap = event.qrCode.asBitmap(size = 800)
            // or
            val qrCodeView = event.qrCode.asView(context, size = 800)
        }

        TransferEvent.Connecting -> {
            // Informational event that devices are connecting
        }

        TransferEvent.Connected -> {
            // Informational event that the transfer has been connected
        }

        is TransferEvent.RequestReceived -> try {
            // assuming DeviceRequest is the request type
            val processedRequest = event.processedRequest.getOrThrow() as ProcessedDeviceRequest
            // the request has been received and processed

            // the request processing was successful
            // requested documents can be shown in the application
            val requestedDocuments = processedRequest.requestedDocuments
            // ...
            // application must create the DisclosedDocuments object
            val disclosedDocuments = DisclosedDocuments(
                // DisclosedDocument(),
                // DisclosedDocument(),
            )
            // generate the response
            val response = processedRequest.generateResponse(
                disclosedDocuments = disclosedDocuments,
                signatureAlgorithm = Algorithm.ESP256
            ).getOrThrow() as DeviceResponse

            transferManager.sendResponse(response)

        } catch (e: Throwable) {
            // An error occurred
            // handle the error
        }

        TransferEvent.ResponseSent -> {
            // Informational event that the response has been sent
        }
        is TransferEvent.Redirect -> {
            // A redirect is needed. Used mainly for the OpenId4VP implementation
            val redirectUri = event.redirectUri // the redirect URI
        }
        is TransferEvent.IntentToSend -> {
            // An intent is ready to be sent
            // Used mainly for the DCAPI implementation
            val intent = event.intent // the intent to send.
        }
        TransferEvent.Disconnected -> {
            // Informational event that device has been disconnected
        }
        is TransferEvent.Error -> {
            // An error occurred
            val cause = event.error
        }
    }
}

Initiating transfer

Transfer Manager provides the following methods for enabling device engagement and initiating the transfer:

  1. Scanning QR code
  2. Using NFC

These engagement methods offer seamless device pairing and data transfer initiation.

Using QR Code

With the TransferManager instance created, you can initiate the transfer with QR code by calling the TransferManager.startQrEngagement() method. The method initiates the transfer process and triggers the TransferEvent.QrEngagementReady event when the QR code is ready to be displayed.

Using NFC

To use NFC, you must create a service that extends the abstract NfcEngagementService and override the NfcEngagementService.transferManager property.

For example:

package com.example.myapp

import eu.europa.ec.eudi.iso18013.transfer.engagement.NfcEngagementService

class NfcEngagementServiceImpl : NfcEngagementService() {
    override val transferManager: TransferManager
        get() = TODO("Return the transfer manager here")
}

Then add the service to your application's manifest file, like shown below:

<application>
    <!-- rest of manifest -->
    <service android:exported="true" android:label="@string/nfc_engagement_service_desc"
        android:name="com.example.myapp.NfcEngagementServiceImpl"
        android:permission="android.permission.BIND_NFC_SERVICE">
        <intent-filter>
            <action android:name="android.nfc.action.NDEF_DISCOVERED" />
            <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
        </intent-filter>

        <!-- the following "@xml/nfc_engagement_apdu_service" in meta-data is provided by the library -->
        <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
            android:resource="@xml/nfc_engagement_apdu_service" />
    </service>

</application>

You can enable or disable the NFC device engagement in your app by calling the NfcEngagementService.enable() and NfcEngagementService.disable() methods.

In the example below, the NFC device engagement is enabled when activity is resumed and disabled when the activity is paused.

import androidx.appcompat.app.AppCompatActivity
import eu.europa.ec.eudi.iso18013.transfer.engagement.NfcEngagementService

class MainActivity : AppCompatActivity() {

    override fun onResume() {
        super.onResume()
        NfcEngagementService.enable(this)
    }

    override fun onPause() {
        super.onPause()
        NfcEngagementService.disable(this)
    }
}

Optionally, in the NfcEngagementService.enable() method you can define your class that implements the abstract NfcEngagementService.

For example:

 NfcEngagementService.enable(this, NfcEngagementServiceImpl::class.java)

This way, you can define the NfcEngagementServiceImpl service to be preferred while this activity is in the foreground.

Receiving a request and sending a response

When a request is received, the TransferManager triggers the TransferEvent.RequestReceived. The event contains the processed request and the initial raw request. The processed request is validated and contains the requested documents.

The requested documents can be retrieved by the ProcessedRequest.Success.requestedDocuments property. Each requested document contains the DocumentId that can be used to get the document from the DocumentManager. This will allow the application to display the requested documents to the user.

The ProcessedRequest.Success.generateResponse(DisclosedDocuments, Algorithm?) method is used to generate the response. Each DisclosedDocument in the DisclosedDocuments object must contain the DocumentId and the list of DocItems, that are to be disclosed. There is a possibility that requested document's keys, that are needed to sign the response. In this case, each DisclosedDocument must contain the keyUnlockData property that defines the key unlocking.

Finally, the response is sent back to the verifier with the TransferManager.sendResponse(Response).

Note: Currently, only a single request-response cycle per session is supported. Sending a response automatically terminates the presentation session. To perform another exchange, a new session must be started.

The following example demonstrates how to handle the request and send the response.

transferManager.addTransferEventListener { event ->
    when (event) {
        is TransferEvent.RequestReceived -> try {
            // DeviceRequest assumed is received
            val processedRequest = event.processedRequest.getOrThrow() as ProcessedDeviceRequest
            // the request has been received and processed

            // the request processing was successful
            // requested documents can be shown in the application
            val requestedDocuments = processedRequest.requestedDocuments
            // display the requested documents so the user can confirm the disclosure

            // ...

            // Disclosed documents are constructed in ui.
            // If document keys to use for signing response is needed and require unlocking,
            // information about unlocking the keys should be passed to the disclosed document
            // for the corresponding document.
            // Here we use a software key unlock data to unlock the key, for the first requested document.
            val disclosedDocuments = DisclosedDocuments(
                DisclosedDocument(
                    requestedDocuments.first(),
                    keyUnlockData = SoftwareKeyUnlockData(passphrase = "passphrase_passed_from_ui")
                ),
            )
            // generate the response
            val response = processedRequest.generateResponse(
                disclosedDocuments = disclosedDocuments,
                signatureAlgorithm = Algorithm.ESP256
            ).getOrThrow() as DeviceResponse

            transferManager.sendResponse(response)

        } catch (e: Throwable) {
            // An error occurred
            // handle the error
        }

        else -> {
            // ... rest of the events
        }
    }
}

Reader authentication

Reader authentication is a critical security component that verifies the authenticity and trustworthiness of the verifier (reader) device before sharing any personal data. It ensures that only legitimate and authorized entities can request and receive sensitive document information.

The library performs three key verification steps:

  1. Certificate Path Validation: Validates the complete certificate chain from the reader's certificate up to a trusted root certificate, ensuring a chain of trust is established.

  2. Certificate Profile Validation: Examines multiple attributes and constraints defined in the certificate to ensure it meets predefined security criteria, including:

    • Key Usage validation
    • Extended Key Usage validation
    • Validity period verification
    • Critical extensions check
    • Mandatory extensions verification
    • Authority Key Identifier validation
    • Subject Key Identifier validation
    • Common Name verification
    • Signature algorithm verification
  3. Certificate Revocation Checking: When enabled via RevocationPolicy, the library checks if certificates in the chain have been revoked using CRL distribution points. Revocation checking uses Java's PKIXRevocationChecker which verifies CRL signatures, rejects expired CRLs, and checks all certificates in the chain (not just the leaf). Revocation checking is disabled by default for backwards compatibility.

Default Implementation

By default, the library provides a standard implementation of the ReaderTrustStore interface through ReaderTrustStoreImpl, which performs certificate path and profile validation. Revocation checking is disabled by default:

// Create a default ReaderTrustStore with your trusted certificates
val readerTrustStore = ReaderTrustStore.getDefault(
    trustedCertificates = listOf(
        // List your trusted root certificates here
        trustedRootCertificate1,
        trustedRootCertificate2
    )
)

// Assign the trust store to the TransferManager
transferManager = TransferManager.getDefault(
    context = context,
    documentManager = documentManager,
    retrievalMethods = retrievalMethods,
    readerTrustStore = readerTrustStore
)

To enable revocation checking, pass a RevocationPolicy:

// HardFail: reject if certificate is revoked OR CRL is unavailable
val readerTrustStore = ReaderTrustStore.getDefault(
    trustedCertificates = trustedCertificates,
    revocationPolicy = RevocationPolicy.HardFail
)

// SoftFail: reject if certificate is revoked, but tolerate CRL unavailability
val readerTrustStore = ReaderTrustStore.getDefault(
    trustedCertificates = trustedCertificates,
    revocationPolicy = RevocationPolicy.SoftFail
)

Reader Authentication Enforcement Policy

The ReaderAuthPolicy controls how reader authentication results are enforced when generating a device response via ProcessedDeviceRequest.generateResponse(). It determines whether documents are included in the response based on their reader authentication status.

Three policies are available:

Policy Behavior
ReaderAuthPolicy.DoNotEnforce Documents are always included regardless of reader authentication status.
ReaderAuthPolicy.EnforceIfPresent (Default) Documents with failed reader authentication are excluded. Documents without reader authentication (e.g., when no ReaderTrustStore is configured) are still included.
ReaderAuthPolicy.AlwaysRequire Only documents with verified reader authentication are included. Documents without reader authentication or with failed authentication are excluded.

The policy is configured when creating the TransferManager:

val transferManager = TransferManager.getDefault(
    context = context,
    documentManager = documentManager,
    readerTrustStore = readerTrustStore,
    readerAuthPolicy = ReaderAuthPolicy.AlwaysRequire, // strictest mode
)

Or using the builder:

val transferManager = TransferManagerImpl.Builder(context)
    .documentManager(documentManager)
    .readerTrustStore(readerTrustStore)
    .readerAuthPolicy(ReaderAuthPolicy.AlwaysRequire)
    .build()

Custom Implementation

For advanced use cases, you can create your own custom implementation of the ReaderTrustStore interface:

class CustomReaderTrustStore(
    private val trustedCertificates: List<X509Certificate>
) : ReaderTrustStore {

    override fun createCertificationTrustPath(chain: List<X509Certificate>): List<X509Certificate>? {
        // Your custom implementation for creating trust path
        // Return the certification path or null if no path can be created
    }

    override fun validateCertificationTrustPath(chainToDocumentSigner: List<X509Certificate>): Boolean {
        // Your custom validation logic
        // Return true if the certification path is valid, false otherwise
    }
}

// Use your custom implementation
val customTrustStore = CustomReaderTrustStore(trustedCertificates)
transferManager.readerTrustStore = customTrustStore

You can also implement custom profile validations by implementing the ProfileValidation interface and providing it to the ReaderTrustStoreImpl:

class CustomProfileValidation : ProfileValidation {
    override fun validate(chain: List<X509Certificate>, trustCA: X509Certificate): Boolean {
        // Your custom validation logic
        return true
    }
}

// Create a ReaderTrustStoreImpl with custom profile validation
val customValidation = CustomProfileValidation()
val readerTrustStore = ReaderTrustStoreImpl(
    trustedCertificates = trustedCertificates,
    profileValidation = customValidation
)

The reader authentication results are accessible through the ReaderAuth object received in the RequestReceived event, allowing you to make decisions based on the authentication status:

is TransferEvent.RequestReceived -> {
    val processedRequest = event.processedRequest.getOrThrow() as ProcessedDeviceRequest
    
    // Access reader authentication information
    val readerAuth = processedRequest.readerAuth
    if (readerAuth != null) {
        val isVerified = readerAuth.isVerified
        val readerCommonName = readerAuth.readerCommonName
        val isTrusted = readerAuth.readerCertificatedIsTrusted
        
        // Make decisions based on authentication status
        if (isVerified && isTrusted) {
            // Proceed with the trusted reader
        } else {
            // Handle untrusted reader scenario
        }
    }
}

Zero-Knowledge Proof Support

The library supports Zero-Knowledge Proof (ZKP) systems for selective disclosure. When configured, the wallet can generate ZK proofs instead of disclosing raw document data, providing enhanced privacy.

To enable ZKP support, provide a ZkSystemRepository and optionally configure the ZkResponsePolicy:

val transferManager = TransferManager.getDefault(
    context = context,
    documentManager = documentManager,
    zkSystemRepository = zkSystemRepository,
    zkResponsePolicy = ZkResponsePolicy.Strict,
)

The ZkResponsePolicy determines behavior when ZK proof generation fails:

Policy Behavior
ZkResponsePolicy.Strict Aborts disclosure for the document. Recommended for production.
ZkResponsePolicy.FallbackToFullDisclosure Falls back to sending the full document. Current default for backwards compatibility.

Note: The default policy is FallbackToFullDisclosure for backwards compatibility. This will change to Strict in a future release. We recommend explicitly setting ZkResponsePolicy.Strict in production to prevent unintended full document disclosure when proof generation fails.

When a ZK proof is successfully generated, the credential is not consumed — the credential policy (e.g., one-time-use limits) is not applied, since the actual credential key is never sent to the verifier. Credential consumption only occurs when the full document is disclosed (regular path or fallback).

How to contribute

We welcome contributions to this project. To ensure that the process is smooth for everyone involved, follow the guidelines found in CONTRIBUTING.md.

License

Third-party component licenses

See licenses.md for details.

License details

Copyright (c) 2023 European Commission

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages