mdoc debugger
Paste an mdoc below to decode it: the issuerAuth COSE_Sign1, the MobileSecurityObject it carries,
every data element with its digest checked against the one the issuer signed, the certificate chain
and the device authentication. It is free, and it runs entirely in your browser.
An mdoc is harder to read than a JSON credential, and not only because it is CBOR. The claims sit
in byte strings inside byte strings: each data element is wrapped in CBOR tag 24, the
MobileSecurityObject is wrapped in tag 24 as well, and the thing the issuer actually signed is a
hash of those wrapper bytes rather than of the values you can see. Open an mdoc in a generic CBOR
viewer and you get a tree of opaque bstr nodes.
This tool takes the whole thing apart, and then does the one check that matters: for every data
element it recomputes the digest and compares it with valueDigests in the MobileSecurityObject,
so you can see which elements really are covered by the issuer's signature and which have been
altered.
Everything here runs in your browser. The mdoc you paste is not uploaded, not stored, not logged and not put in the URL — there is no server side to this page at all. Even so, a real credential out of a wallet is personal data about its holder, so prefer a test credential when you can, and clear the box when you are done.
Paste an mdoc
What you can paste
The encoding is detected for you — hex, base64 or base64url, with any line breaks and spaces removed first — and so is the structure. All four of these work:
- a
DeviceResponse, the shape an OpenID4VPvp_tokencarries for themso_mdocformat: a version, an array of documents and a status code; - a bare
IssuerSigned, the shape an OpenID4VCI credential response carries:nameSpacesandissuerAuthand nothing else; - a bare
COSE_Sign1, which isissuerAuthon its own; - a bare MobileSecurityObject, which lists the digests the issuer signed.
A DeviceResponse and an IssuerSigned both give you the full digest check. The other two have no
data elements to hash, so they show the MobileSecurityObject and say so.
What this tool checks
issuerAuth. The COSE_Sign1 under CBOR tag 18: the protected header, where label 1 is the signature algorithm —-7is ES256, which is what the Business Wallet issues — the unprotected header, where label 33 is thex5chaincertificate chain, and the sizes of the payload and the signature. An algorithm the tool does not recognise is named as unrecognised rather than passed over in silence.- The MobileSecurityObject. The
issuerAuthpayload is a tag-24 byte string holding embedded CBOR, so it is unwrapped before being read. You getversion,digestAlgorithm,docType, thevalueDigestsper namespace, the holder'sdeviceKeyas a COSE_Key, and thevalidityInfotimestamps with a verdict on the validity window. - Every data element.
nameSpacesmaps each namespace to an array of tag-24 byte strings, each holding anIssuerSignedItemwith adigestID, arandomsalt, anelementIdentifierand anelementValue. Every one gets a row: the namespace, the digest id, the identifier, the decoded value and the length of the salt. - The digests. For each element the digest is computed with the algorithm the
MobileSecurityObject names — SHA-256, SHA-384 or SHA-512 — over the tag-24 byte string exactly
as it arrived on the wire, and compared with
valueDigests. This is the one place a decoder is easy to get wrong: hashing the item after re-encoding it produces different bytes, and then every element on a perfectly valid credential is reported as altered. - Withheld digests. A digest in
valueDigeststhat no presented element accounts for is listed separately. That is the normal state of a selectively disclosed mdoc — the holder kept those elements back — and a verifier must not read it as tampering. - Typed values. CBOR tag 1004
full-dateis whatbirth_date,issue_dateandexpiry_dateuse, tag 0 and tag 1 carry the validity timestamps,driving_privilegesis an array of maps, andportraitis raw image bytes. They are rendered as CBOR diagnostic notation, so a tagged value stays visible as1004("1979-04-12")instead of collapsing into a bare string. - Device authentication. On a
DeviceResponse,deviceSigned.nameSpacesis unwrapped and the tool states whetherdeviceAuthcarries adeviceSignature(a COSE_Sign1) or adeviceMac(a COSE_Mac0). - Status and errors. The
statusof aDeviceResponse—0is OK — plusdocumentErrorsand any per-elementerrors.
What this tool cannot tell you
It does not verify any signature, and it is not a trust decision. An mdoc can decode perfectly, with every digest matching, and still be signed by someone you have no reason to trust.
- The issuer's signature over the MobileSecurityObject is not checked. Checking it would mean
taking a key from the credential itself — the
x5chain, or a URL inside it — and a browser tool that fetches whatever untrusted input points at is a server-side request forgery primitive. So this page never makes a network request. The certificate chain is parsed and shown locally, and no authority-information-access, CRL or OCSP URL in it is ever dereferenced. - The certificate chain is not validated. No path is built, no trust anchor is consulted, no revocation is checked.
- Device authentication is not verified, and this is a property of the format rather than a
shortcut.
DeviceAuthis computed over aSessionTranscriptbuilt from the verifier's own request — the client identifier, the nonce, and the response URI or the origin. A page you paste into has none of that, so the tool reports which form of device authentication is present and stops there. You will not see a misleading red cross for it. - A matching digest proves integrity, not authenticity. It tells you the element is the one the MobileSecurityObject was built over. Whether that MobileSecurityObject was signed by a real issuing authority is exactly the part this tool declines to answer.
For the real checks, use the Verify a Credential API in the Business Wallet, which validates the signature, the trust chain and the status list.
How an mdoc is put together
An IssuerSigned has two members. nameSpaces holds the data elements, grouped by namespace, and
issuerAuth holds the signature over them:
IssuerSigned = {
"nameSpaces": { "org.iso.18013.5.1": [ IssuerSignedItemBytes, … ] },
"issuerAuth": COSE_Sign1
}
IssuerSignedItemBytes = #6.24(bstr .cbor IssuerSignedItem)
IssuerSignedItem = {
"digestID": uint,
"random": bstr, ; at least 16 bytes of salt
"elementIdentifier": tstr, ; "given_name"
"elementValue": any ; "Joris"
}
The signature does not cover those items directly. issuerAuth is a COSE_Sign1 whose payload is a
MobileSecurityObject, and the MobileSecurityObject carries a digest of each item:
MobileSecurityObject = {
"version": "1.0",
"digestAlgorithm": "SHA-256",
"valueDigests": { "org.iso.18013.5.1": { 1: bstr, 2: bstr, … } },
"deviceKeyInfo": { "deviceKey": COSE_Key },
"docType": "org.iso.18013.5.1.mDL",
"validityInfo": { "signed": tdate, "validFrom": tdate, "validUntil": tdate }
}
Two consequences follow, and they are what makes mdoc selective disclosure work.
The first is that the holder can leave elements out. The issuer signed a digest for every element,
but only the elements actually presented appear in nameSpaces. A digest left over is not evidence
of anything: a verifier cannot tell a withheld element from a decoy.
The second is that the digest is taken over the encoded tag-24 item, not over the values inside
it. random is in there precisely so that the digest of age_over_18: true cannot be guessed
and confirmed by anyone who did not receive the element. It also means a verifier has to hash the
bytes it received rather than re-serialising what it parsed, because CBOR allows more than one
encoding of the same data and a re-encode changes the hash.
For the wider picture, see OpenID4VP presentation explained and the Digital Credentials API explained.
Frequently asked questions
Does anything I paste get sent to Credenco?
No. The page makes no network request while decoding, there is no backend for it, and nothing is written to local storage, cookies or the URL. Reloading the page loses whatever you pasted, by design.
Every element says the digest differs. What is wrong?
Usually the credential was re-encoded somewhere on its way to you. A CBOR value can be encoded in more than one valid way, so a pipeline that decodes an mdoc and re-serialises it keeps the meaning and changes the bytes — and the digests are over the bytes. Paste the credential exactly as it came off the wire. If it still differs, something really has altered the element.
Why does it not check the issuer's signature?
Verifying means taking a public key from a place the credential itself names. A browser tool that resolves arbitrary URLs out of untrusted input is a security hole, so this page decodes only, and it parses the certificate chain without fetching anything the chain points at. Use the Business Wallet's verification API, or your own verifier library, for the cryptographic checks.
What is the difference between a withheld digest and an altered element?
A withheld digest is one the MobileSecurityObject lists but no element in the response matches — normal selective disclosure. An altered element is one that is in the response but whose bytes hash to something other than the digest the issuer signed. The first is expected; the second means the credential has been tampered with.
Can it read SD-JWT VC credentials?
No — but the SD-JWT VC debugger can. SD-JWT VC is a tilde-separated sequence of base64url JSON segments, which is a different format entirely.
Which document types does it understand?
All of them. Nothing here is specific to the mobile driving licence: docType, the namespaces and
the element identifiers are read out of the credential, so an EUDI PID, a company registration mdoc
or any other ISO 18013-5 document type decodes the same way. The element names shown are the ones
the issuer used.