Skip to content

mdoc architecture

All three credential profiles (Emirates ID, mDL, mCL) use the same container and the same security mechanisms. This page describes those once; each profile page only adds its own document type, namespaces and attributes.

Standard vs. ours. Everything in this page is standard ISO/IEC 18013-5 behaviour. The profile pages say clearly which parts are standardised schemas (mDL, Photo ID) and which are project-defined (mCL, and the Emirates ID data choices on top of Photo ID). Nothing here is certified or approved by any authority.

Container, proximity, trust lists ISO/IEC 18013-5:2021 (mDL and the mso_mdoc format); RICAL from the 2nd-edition draft
Online presentation OpenID4VP (Draft 24 openid4vp and Draft 29 openid4vp-v1-*); ISO/IEC 18013-7 for the DC-API transcript
Browser transport W3C Digital Credentials API (org-iso-mdoc on iOS; openid4vp on Android/Chrome)
Issuance OpenID4VCI (pre-authorised code flow)
Photo ID data model ISO/IEC 23220-2 (namespace org.iso.23220.1) and the Photo ID application profile (org.iso.23220.photoid.1)
Signing / encoding COSE (RFC 9052/9053), CBOR (RFC 8949)
Revocation IETF Token Status List (draft)

Assumptions: single-issuer documents; EC P-256 keys (ES256 / ESP256); SHA-256 digests.

An mdoc is a Document:

  • docType: the document type string, e.g. org.iso.18013.5.1.mDL.
  • IssuerSigned: the data, as IssuerSignedItems grouped by namespace, each item carrying a random salt, a digest id, the element identifier and its CBOR value; plus issuerAuth, a COSE_Sign1 over the Mobile Security Object (MSO).
  • The MSO holds the digest of every item (so a verifier can check integrity of whatever subset is disclosed), the document’s validity window (validFrom / validUntil), the device key the document is bound to, and optionally a status reference for revocation.
  • DeviceSigned: at presentation time, the holder signs (or MACs) the session with the device key.

Selective disclosure follows: the holder sends only the requested IssuerSignedItems; the verifier recomputes their digests and matches them against the signed MSO.

Real issuance is OpenID4VCI: the wallet receives a credential offer (openid-credential-offer://), fetches issuer metadata, obtains an access token (DPoP-bound; a Bearer fallback exists for issuers that do not support DPoP), proves possession of a fresh device key, and receives the signed mdoc. WalletClient.issueFromOffer drives it. The EMSO mCL is issued this way by Accredify Nexus.

The demo mDL and Emirates ID are self-issued in the demo apps (DemoIssuer): the same MSO construction, signed by a fixed demo Document Signer under a fixed demo IACA. This exists only so the demos work without an issuer; the SDK ships no issuance keys.

issuerAuth carries the Document Signer (DS) certificate in x5chain. The verifier builds the chain to a trusted IACA (Issuer Authority CA). Trust comes from TrustConfig: raw IACA PEMs, or a signed VICAL (list of IACAs, each with the doctypes it may sign; a document of another doctype is rejected even if the chain is valid). On the web the Verify API holds the issuer list per tenant.

Reader authentication runs the other way: a verifier signs its request (proximity DeviceRequest, or the OpenID4VP request JWT with x5c), and the wallet checks it against TrustConfig.readerRical / readerRootsPem to name the requester on the consent screen.

Device binding and presentation verification

Section titled “Device binding and presentation verification”

Every presentation is bound to a session transcript so a captured response cannot be replayed:

Channel Transcript Where
Proximity (QR engagement + BLE) SessionTranscript(DeviceEngagement, EReaderKey, Handover=null) MdocProximityPresenter / MdocReaderSession
OpenID4VP direct_post OpenID4VP handover over client_id, nonce, response_uri Openid4VpTranscript; web SessionTranscript.forOid4Vp
Digital Credentials API DC-API handover over browser origin and nonce web SessionTranscript.forOid4VpDcApi

The verifier checks, in order: issuer chain to a trusted IACA (for this doctype), the MSO signature, each disclosed item’s digest, the validity window, the device signature over the transcript with the MSO’s device key, then revocation. Native code gets an AcceptanceResult; the web server gets the same signals as JSON. A single failed check rejects the presentation.

validFrom / validUntil come from the MSO (not from the printed expiry date, which is data). When the MSO carries a status reference, RevocationResolver fetches and caches the issuer’s status list; offline, the last answer is used and flagged stale (revocationDataStale, revocationCheckedAt). Documents without a status reference report revocationPublished = false.

A request names the doctype and the elements per namespace:

  • native: PresentationRequest(docType, namespaces = mapOf(ns to setOf(elements))), wrapped in a RequestPreset for operators;
  • web: DCQL built by Requests.mdl(...), Requests.photoId(...) or a custom { docType, claims: [{ namespace, element }] }.

Ask for the minimum. Age checks use age_over_NN booleans, never the birth date.

Mechanism Native (sdk-core) Web (Verify API)
Document types / labels CredentialType → DocumentTypes.kt request.ts
Present MdocPresenter, MdocProximityPresenter –
Verify MdocVerifier, VerifierClient.verify verify.ts
Trust IssuerTrustStore, ReaderTrustStore trust.iacas, reader identity
Revocation RevocationResolver (status validation off in this version)
Transcripts ProximityTranscript, Openid4VpTranscript SessionTranscript.forOid4Vp*

Examples in the profile pages show data as JSON for readability. On the wire everything is CBOR: dates are full-date (tag 1004) or tdate strings, portraits are byte strings (bstr), and booleans / integers are native CBOR types.