Skip to main content
Version: v0.25.0 (Latest)

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:

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
}
}
}

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).

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
}

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 element
  • deviceKeyInfo: the public key of the device the credential is bound to
  • validityInfo: 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.

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
}
}

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:

val limitedIssuerSigned = issuerSigned.limitDisclosures(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.

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)
)

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

CodeNameWhen it's used
0OKThe request was processed and documents are included
10General errorSomething went wrong, no further detail
11CBOR decoding errorThe request contained invalid CBOR
12CBOR errorCBOR processing failed during response creation
20Session terminationThe holder declined, or the session is ending normally

On the verifier side, check the status before accessing documents:

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}")
}
}

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:

ElementTypeDescription
family_namestringFamily name
given_namestringGiven name(s)
birth_datefull-dateDate of birth
issue_datefull-dateDocument issue date
expiry_datefull-dateDocument expiry date
issuing_countrystringISO 3166-1 alpha-2 country code
issuing_authoritystringIssuing authority name
document_numberstringLicense number
portraitbytesJPEG photo of the holder
driving_privilegesarrayLicense categories and restrictions
un_distinguishing_signstringUN country distinguishing sign
sexintegerISO/IEC 5218 code (0=unknown, 1=male, 2=female, 9=not applicable)
heightintegerHeight in centimeters
weightintegerWeight in kilograms
eye_colourstringEye color
hair_colourstringHair color
resident_addressstringResidential address
age_over_18booleanAge attestation (true if 18 or older)
age_over_21booleanAge attestation (true if 21 or older)
nationalitystringNationality

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.