Skip to main content

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.

Your credential never leaves this page

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​

0 of 1,000,000 charactersThe encoding is detected for you. Decoded in your browser — nothing is uploaded, stored or logged.

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 OpenID4VP vp_token carries for the mso_mdoc format: a version, an array of documents and a status code;
  • a bare IssuerSigned, the shape an OpenID4VCI credential response carries: nameSpaces and issuerAuth and nothing else;
  • a bare COSE_Sign1, which is issuerAuth on 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 — -7 is ES256, which is what the Business Wallet issues — the unprotected header, where label 33 is the x5chain certificate 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 issuerAuth payload is a tag-24 byte string holding embedded CBOR, so it is unwrapped before being read. You get version, digestAlgorithm, docType, the valueDigests per namespace, the holder's deviceKey as a COSE_Key, and the validityInfo timestamps with a verdict on the validity window.
  • Every data element. nameSpaces maps each namespace to an array of tag-24 byte strings, each holding an IssuerSignedItem with a digestID, a random salt, an elementIdentifier and an elementValue. 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 valueDigests that 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-date is what birth_date, issue_date and expiry_date use, tag 0 and tag 1 carry the validity timestamps, driving_privileges is an array of maps, and portrait is raw image bytes. They are rendered as CBOR diagnostic notation, so a tagged value stays visible as 1004("1979-04-12") instead of collapsing into a bare string.
  • Device authentication. On a DeviceResponse, deviceSigned.nameSpaces is unwrapped and the tool states whether deviceAuth carries a deviceSignature (a COSE_Sign1) or a deviceMac (a COSE_Mac0).
  • Status and errors. The status of a DeviceResponse — 0 is OK — plus documentErrors and any per-element errors.

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. DeviceAuth is computed over a SessionTranscript built 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.