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.
Versions and references
Section titled “Versions and references”| 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.
The container
Section titled “The container”An mdoc is a Document:
docType: the document type string, e.g.org.iso.18013.5.1.mDL.IssuerSigned: the data, asIssuerSignedItems grouped by namespace, each item carrying a random salt, a digest id, the element identifier and its CBOR value; plusissuerAuth, 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.
Issuance and provisioning
Section titled “Issuance and provisioning”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.
Issuer authentication and trust
Section titled “Issuer authentication and trust”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.
Validity and revocation
Section titled “Validity and revocation”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.
Requests
Section titled “Requests”A request names the doctype and the elements per namespace:
- native:
PresentationRequest(docType, namespaces = mapOf(ns to setOf(elements))), wrapped in aRequestPresetfor 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.
Where in the code
Section titled “Where in the code”| 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* |
Explanatory JSON
Section titled “Explanatory JSON”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.