Device Request and Response
The mDoc protocol exchanges two top-level CBOR structures: a DeviceRequest from the verifier and a DeviceResponse from the holder. This page covers what these structures contain, how to read and build them, and how the cryptographic proofs work.
DeviceRequest
A DeviceRequest is what the verifier sends to say "here's what I want." It contains one or more DocRequest entries, each targeting a specific document type (e.g., an mDL, a PID, or a custom credential).
DeviceRequest = {
version: "1.0",
docRequests: [DocRequest, ...]
}
DocRequest = {
itemsRequest: DeviceItemsRequest,
readerAuth: COSE_Sign1 (optional)
}
DeviceItemsRequest = {
docType: "org.iso.18013.5.1.mDL",
nameSpaces: {
"org.iso.18013.5.1": {
"family_name": true, // intentToRetain
"given_name": false,
"portrait": false,
...
}
}
}
The structure is nested three levels deep: the request contains doc requests, each doc request contains an items request, and each items request contains namespaces with the individual data elements. The boolean next to each element is the intentToRetain flag (covered below).
OID4VP requests
When the request comes through an OID4VP flow, the DeviceRequest may have an oid4vpRequest field instead of (or in addition to) docRequests. The effectiveDocRequests() method normalizes both cases, converting OID4VP presentation definitions into standard DocRequest objects so you can process them the same way.
Reading a request
On the holder side, you receive the request from the TransferManager and iterate over it to build a consent screen:
- Android/Kotlin
- iOS/Swift
val deviceRequest = transferManager.receiveDeviceRequest()
for (docRequest in deviceRequest.docRequests) {
val docType = docRequest.docType
for ((namespace, elements) in docRequest.itemsRequest.nameSpaces) {
for ((elementId, intentToRetain) in elements) {
// Show each requested element to the user
// intentToRetain tells you if the verifier plans to store it
}
}
}
let deviceRequest = try await transferManager.receiveDeviceRequest()
for docRequest in deviceRequest.docRequests {
let docType = docRequest.docType
for (namespace, elements) in docRequest.itemsRequest.nameSpaces {
for (elementId, intentToRetain) in elements {
// Show each requested element to the user
}
}
}
Intent to retain
Each requested element has an intentToRetain flag. When true, the verifier is declaring that it may store this data element beyond the current session (e.g., in a database for records). When false, the verifier says it will only use it for this verification and discard it after.
This distinction matters for user consent. A wallet UI might show retained elements with a warning icon, or a policy engine might block sharing of certain elements (like portrait) when the verifier intends to retain them. The flag is a declaration, not an enforcement mechanism, but it has legal significance under regulations like eIDAS and GDPR.
Reader authentication
A DocRequest can optionally include readerAuth, a COSE_Sign1 signature over the items request. This lets the holder verify the verifier's identity before sharing any data. Not all verifiers provide this. Use transferManager.validateReaderAuthentication() to check it (see the Transfer Manager page).
DeviceResponse
The DeviceResponse is what the holder sends back. It contains signed documents and a status code.
DeviceResponse = {
version: "1.0",
documents: [Document, ...],
documentErrors: [...] (optional),
status: 0
}
A response with status 0 and documents means success. A response with status 20 and no documents means the user declined or the session is ending. Errors per document type are reported in documentErrors.
Document structure
Each Document in the response has two parts: data the issuer signed at issuance time, and data the holder's device signs at presentation time.
Document = {
docType: "org.iso.18013.5.1.mDL",
issuerSigned: IssuerSigned,
deviceSigned: DeviceSigned
}
This dual-signature design is what makes mDoc secure. The issuer signature proves the data is genuine. The device signature proves the holder presenting it is the legitimate owner of the credential.
IssuerSigned
IssuerSigned contains everything the credential issuer put into the document at issuance time. It has two parts: the actual data elements (organized by namespace) and a COSE_Sign1 signature over the Mobile Security Object (MSO).
- Android/Kotlin
- iOS/Swift
val document = deviceResponse.documents?.first()!!
val issuerSigned = document.issuerSigned
// The MSO contains digests, validity info, and the device public key
val mso: MobileSecurityObject = issuerSigned.MSO
val deviceKeyInfo = issuerSigned.deviceKeyInfo
// Which namespaces have data in this credential
val namespaces: Set<NameSpace> = issuerSigned.getNameSpaces()
// Get the individual signed items for a namespace
val items = issuerSigned.getIssuerSignedItems("org.iso.18013.5.1")
items?.forEach { item ->
val name = item.elementIdentifier // e.g., "family_name"
val value = item.elementValue // the actual value
val digest = item.digestID // references the MSO digest
}
let document = deviceResponse.documents?.first!
let issuerSigned = document.issuerSigned
let mso = issuerSigned.MSO
let deviceKeyInfo = issuerSigned.deviceKeyInfo
let namespaces = issuerSigned.getNameSpaces()
let items = issuerSigned.getIssuerSignedItems(ns: "org.iso.18013.5.1")
items?.forEach { item in
let name = item.elementIdentifier
let value = item.elementValue
}
How IssuerSigned works under the hood
Each data element is wrapped in an IssuerSignedItem that includes a random salt and a digest ID. The MSO contains a hash of each item (computed over the salt + identifier + value). When a verifier receives the response, it recomputes the hashes and checks them against the MSO. If any element was tampered with, the hash won't match.
This design enables selective disclosure: the holder can omit elements from the response, and the verifier can still verify that the elements it did receive are authentic by checking their hashes against the MSO. The MSO itself is signed by the issuer via COSE_Sign1, so the verifier can trust it by validating the issuer's certificate chain.
Mobile Security Object (MSO)
The MSO is the heart of the issuer's cryptographic proof:
digestAlgorithm: which hash algorithm was used (typically SHA-256)valueDigests: a map of namespace to digest ID to hash, one per data elementdeviceKeyInfo: the public key of the device the credential is bound tovalidityInfo: when the credential was signed, and its validity window (validFrom,validUntil,expectedUpdate)docType: the document type this MSO covers
ValidityInfo
val validity = mso.validityInfo
val signed = validity.signed // when the issuer signed this
val validFrom = validity.validFrom // earliest valid moment
val validUntil = validity.validUntil // expiration
val update = validity.expectedUpdate // when the issuer expects to refresh (optional)
A verifier should check that the current time falls within the validFrom..validUntil window.
DeviceSigned
DeviceSigned proves that the person presenting the credential is the legitimate holder. It contains device authentication (a COSE_Sign1 or COSE_Mac0 over the session transcript) and optionally additional device-provided claims.
- Android/Kotlin
- iOS/Swift
val deviceSigned = document.deviceSigned!!
// Device-provided claims (usually empty for standard mDL)
val deviceNameSpaces: DeviceNameSpaces = deviceSigned.nameSpaces
// The authentication proof
val deviceAuth: DeviceAuth = deviceSigned.deviceAuth
// Check which authentication method was used
when (deviceAuth.getAuthType()) {
DeviceAuthType.SIGNATURE -> {
// COSE_Sign1 with the device's private key
val signature = deviceAuth.deviceSignature
}
DeviceAuthType.MAC -> {
// COSE_Mac0 with a shared secret (less common)
val mac = deviceAuth.deviceMac
}
}
let deviceSigned = document.deviceSigned!
let deviceNameSpaces = deviceSigned.nameSpaces
let deviceAuth = deviceSigned.deviceAuth
How device authentication works
The device signs (or MACs) a DeviceAuthentication structure that includes the session transcript, the document type, and any device namespaces. Because the session transcript is unique to this session (it includes ephemeral keys from both sides), the signature cannot be replayed in a different session.
The verifier checks this signature against the device public key from the MSO's deviceKeyInfo. If the signature is valid, it proves that whoever is presenting the credential has access to the private key that was registered at issuance time.
Selective Disclosure
The holder doesn't have to share every element in the credential. limitDisclosures filters an IssuerSigned to include only the elements the verifier actually requested:
- Android/Kotlin
- iOS/Swift
val limitedIssuerSigned = issuerSigned.limitDisclosures(docRequest)
let limitedIssuerSigned = issuerSigned.limitDisclosures(docRequest: docRequest)
This returns a new IssuerSigned with only the matching elements. The MSO and issuer signature stay the same (they cover all elements by hash, so omitting elements doesn't break anything). The verifier can verify the elements it received and knows nothing about the ones that were omitted.
In practice, transferManager.createResponse() calls limitDisclosures internally, so you only need it when building responses manually.
Building Requests (Verifier Side)
When you're building a verifier, you construct a DeviceRequest using the builder API. The IDK uses inline value classes (DocType, NameSpace, DataElementIdentifier, IntentToRetain) to prevent mixing up string parameters.
- Android/Kotlin
- iOS/Swift
val docRequest = DocRequest.Builder()
.withItemsRequest(
DeviceItemsRequest.Builder()
.withDocType(DocType("org.iso.18013.5.1.mDL"))
.nameSpace(NameSpace("org.iso.18013.5.1"))
.add(DataElementIdentifier("family_name"), IntentToRetain(false))
.add(DataElementIdentifier("given_name"), IntentToRetain(false))
.add(DataElementIdentifier("birth_date"), IntentToRetain(false))
.add(DataElementIdentifier("portrait"), IntentToRetain(false))
.add(DataElementIdentifier("age_over_18"), IntentToRetain(false))
.build()
)
.build()
val deviceRequest = DeviceRequest(
version = DeviceRequestVersion("1.0"),
docRequests = arrayOf(docRequest)
)
let docRequest = DocRequest.Builder()
.withItemsRequest(
DeviceItemsRequest.Builder()
.withDocType(DocType("org.iso.18013.5.1.mDL"))
.nameSpace(NameSpace("org.iso.18013.5.1"))
.add(DataElementIdentifier("family_name"), IntentToRetain(false))
.add(DataElementIdentifier("given_name"), IntentToRetain(false))
.add(DataElementIdentifier("birth_date"), IntentToRetain(false))
.add(DataElementIdentifier("portrait"), IntentToRetain(false))
.add(DataElementIdentifier("age_over_18"), IntentToRetain(false))
.build()
)
.build()
let deviceRequest = DeviceRequest(
version: DeviceRequestVersion("1.0"),
docRequests: [docRequest]
)
The builder calls chain: set the doc type, pick a namespace, add elements with their retain flags. Call nameSpace() again to switch namespaces. You can also use addUsingElements() to add from a list of DataElement objects, or addUsingDefinitions() to add from predefined element definitions.
Requesting multiple document types
A single DeviceRequest can ask for multiple document types. For example, a verifier might request both an mDL and a PID in one request:
val deviceRequest = DeviceRequest(
version = DeviceRequestVersion("1.0"),
docRequests = arrayOf(mdlDocRequest, pidDocRequest)
)
The holder's wallet will match each doc request against available credentials and show the user what's being asked.
Status Codes
| Code | Name | When it's used |
|---|---|---|
| 0 | OK | The request was processed and documents are included |
| 10 | General error | Something went wrong, no further detail |
| 11 | CBOR decoding error | The request contained invalid CBOR |
| 12 | CBOR error | CBOR processing failed during response creation |
| 20 | Session termination | The holder declined, or the session is ending normally |
On the verifier side, check the status before accessing documents:
- Android/Kotlin
- iOS/Swift
when (deviceResponse.status?.value?.toInt()) {
0 -> {
// Process documents
val documents = deviceResponse.documents ?: emptyList()
for (doc in documents) {
verifyAndProcess(doc)
}
}
20 -> {
// Holder declined or session ended
handleDeclined()
}
else -> {
// Error
handleError("Status: ${deviceResponse.status}")
}
}
switch deviceResponse.status?.value {
case 0:
let documents = deviceResponse.documents ?? []
for doc in documents {
verifyAndProcess(document: doc)
}
case 20:
handleDeclined()
default:
handleError(message: "Status: \(deviceResponse.status)")
}
Also check documentErrors if present. Individual documents can fail (e.g., the holder has an mDL but not the requested PID), and those per-document errors show up there rather than in the top-level status.
Common mDL Data Elements
Standard elements defined in the org.iso.18013.5.1 namespace for mobile driving licenses:
| Element | Type | Description |
|---|---|---|
family_name | string | Family name |
given_name | string | Given name(s) |
birth_date | full-date | Date of birth |
issue_date | full-date | Document issue date |
expiry_date | full-date | Document expiry date |
issuing_country | string | ISO 3166-1 alpha-2 country code |
issuing_authority | string | Issuing authority name |
document_number | string | License number |
portrait | bytes | JPEG photo of the holder |
driving_privileges | array | License categories and restrictions |
un_distinguishing_sign | string | UN country distinguishing sign |
sex | integer | ISO/IEC 5218 code (0=unknown, 1=male, 2=female, 9=not applicable) |
height | integer | Height in centimeters |
weight | integer | Weight in kilograms |
eye_colour | string | Eye color |
hair_colour | string | Hair color |
resident_address | string | Residential address |
age_over_18 | boolean | Age attestation (true if 18 or older) |
age_over_21 | boolean | Age attestation (true if 21 or older) |
nationality | string | Nationality |
The age_over_* elements are particularly useful for age verification scenarios where you don't need to know the holder's actual date of birth. A verifier can request just age_over_18 and get a yes/no answer without learning anything else about the holder.
Other document types (EU PID, national ID cards) define their own namespaces and elements following the same pattern.