This SDK provides an ISO 18013-5 compliant framework for the proceeding roles:
- Holder (credential sharing).
- Verifier (credential requesting).
Consuming applications adopt the role relevant to their use case. As an example, an identity wallet adopts the Holder role, and a relying party app adopts the Verifier role.
The current implementation includes a demo app and implements ISO 18013-5 for in-person Bluetooth presentation and verification.
Internal team members can find the team ways of working on Confluence.
The SDK implements the ISO 18013-5 specification:
- Device Engagement: Generates and scans QR codes, broadcasts and connects over Bluetooth Low Energy (BLE).
- Session Management: Establishes secure channels (mdoc session encryption).
- Message Passing: Creates, transmits, and parses
DeviceRequestsandDeviceResponses.
This repository contains packages for:
- Bluetooth: sharing data over Bluetooth
- Core features: Common capabilities across the code base
- Holder: securely share a credential with a verifier
- Models: representing data models in Concise Binary Object Representation (CBOR) format
- CryptoService: encryption and decryption of data for transit
- Verifier: securely receive and verify a credential from a holder
The user doesn't pre-select a credential prior to session initialisation. The SDK determines the Verifier's attribute requirements after establishing a secure connection. Data exchange proceeds as follows:
- The SDK receives the
DeviceRequestand queries the consumer via theCredentialProvider. - The SDK (or consumer) presents the consent UI based on the requested attributes.
- Following consent, the consumer provides the requested data and cryptographic signatures.
classDiagram
namespace Holder {
class CredentialPresentationSession
}
namespace Verifier {
class CredentialVerificationSession
}
namespace Models {
class DeviceEngagement
class SessionEstablishment
class DeviceRequest
class DeviceResponse
}
namespace CryptoService {
class EncryptionSession
class DecryptionSession
}
namespace BluetoothTransmission {
class BluetoothCommunicationSession{
<<interface>>
sendMessage(Data data)
}
class BluetoothCentralSession
class BluetoothPeripheralSession
}
BluetoothCommunicationSession<|--BluetoothCentralSession
BluetoothCommunicationSession <|-- BluetoothPeripheralSession
- The Documentation relating to project configuration and developer set up.
We recommend that you start by reading the GOV.UK Wallet Technical Documentation
The consumer adopting the Holder role provisions and stores credentials securely. It acts as the secure vault, supplying both issuer-signed data and device signatures when a Verifier initiates a request.
To maintain cryptographic boundaries, the consumer provides the exact CBOR IssuerSignedItem bytes
originally signed by the Issuer. The SDK doesn't sign these attributes. The SDK constructs
DeviceAuthentication payloads to prove credentials as part of binding to the current BLE session.
The Android Keystore's private key signs credentials. Finally, the SDK handles all mdoc session
encryption for the transport tunnel.
Import the module into Wallet Core using GitHub Packages / Gradle Modules.
Wallet Core implements the CredentialProvider interface to provide the Sharing SDK with access to credentials and signing capabilities:
interface CredentialProvider {
suspend fun getCredentials(
request: CredentialRequest
): List<Credential>
suspend fun sign(
payload: ByteArray,
documentId: String
): ByteArray
}The CredentialRequest contains an array of document types that the verifier requests:
data class CredentialRequest(
val documentTypes: List<String>
)
data class Credential(
val id: String,
val rawCredential: ByteArray
)Initially getCredentials always returns an array of exactly one element: the decrypted raw CBOR data for the user's mDL credential.
The consumer initialises the SDK with the app context and a logger, then creates a
CredentialPresenter by passing the CredentialProvider implementation.
val sdk = CredentialSharingSdkImpl(
applicationContext = context,
logger = logger
)
val credentialProvider = MyCredentialProvider()
val presenter = sdk.presentCredentialSdk.presenter(credentialProvider)The consumer adds the presenter's flow to its view hierarchy, which triggers the sharing journey to start:
ShareCredential(
component = presenter,
modifier = Modifier.fillMaxWidth()
)The consumer adopting the Verifier role requests attributes and consumes the verified response. It acts as the trust anchor, supplying the SDK with the Root Certificates of trusted issuers.
To maintain cryptographic boundaries, the SDK handles the complete transaction lifecycle:
- Manages the camera scanner
- Establishes the secure BLE tunnel
- Decrypts the
DeviceResponse - Cryptographically validates the Issuer's signature and data integrity.
The consumer defines the request and receives the validated data.
The consumer initialises the Verifier module, injecting the Root Certificates used to validate the
Issuer's signature on the credential. The SDK utilises an internal PrerequisiteGate to resolve
transport availability at runtime.
import com.credentialsharing.sdk.*
// Provide the Root CAs for the issuing authorities you trust
val trustedRoots = listOf(myGovernmentRootCA, myOtherTrustedCA)
val verifier = CredentialVerifier(trustedCertificates = trustedRoots)The consumer defines the CredentialRequest up front. This specifies the document type and the
required attributes.
val request = CredentialRequest(
documentType = "org.iso.18013.5.1.mDL",
requestedElements = listOf("family_name", "given_name", "age_over_18")
)The SDK takes control of the flow:
- Launches the camera
- Scans the engagement QR code
- Establishes the BLE connection
- Transmits the request
- Validates the response.
- Then, the consumer awaits the final, cryptographically verified data.
lifecycleScope.launch {
try {
// The SDK handles the entire scanning, connection, and validation lifecycle
val verifiedData = verifier.requestDocument(
request = request,
activity = this@MyActivity
)
// The SDK has already validated the MSO signature and hash integrity.
// The consumer can safely proceed with the verified flow.
val ageOver18 = verifiedData.getValue("age_over_18") as? Boolean
val familyName = verifiedData.getValue("family_name") as? String
} catch (e: Exception) {
// Handle errors (e.g., user cancelled, invalid signature, connection dropped)
}
}