ISO/IEC 18013-5 standardizes the Mobile Driver’s License (mDL), which is purportedly a digital credential that represents a person’s driving license information. Here I dissect an example of two important messages specified in this format.
Understanding how these bytes are verified was the first step to developing a zero-knowledge protocol for proving “knowledge of an mDL that verifies a given attribute” as described in our recent paper, Anonymous credentials from ECDSA
The MSO
Lets begin with the inner-most message, the mobile signed object or MSO. The MSO is the byte string that is signed by the issuer. Here is an example:
a66776657273696f6e63312e306f646967657374416c676f726974686d675348412d32353667646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c756544696765737473a1716f72672e69736f2e31383031332e352e31a2005820ff753bf6c11963fd644ccce43556cde1857acdfd8e7e607c01dff5dc3e1afbfc015820c5702c252638a4d0e97744cd384f0c0426079484bea1a2c94be58fb5e8c228616d6465766963654b6579496e666fa1696465766963654b6579a401022001215820027a5a1fa9151f67cdd2c57faf84ccfdb05bcf59078765fa0b7e1a8223790dd22258203e170811becf184d36ba38dbc229217d4faca0f162d554e5f1408d59c4baaa0b6c76616c6964697479496e666fa3667369676e6564c074323032352d30342d32385432313a30303a30305a6976616c696446726f6dc074323032352d30342d32385432313a30303a30305a6a76616c6964556e74696cc074323032362d30342d32385432313a30303a30305a
Parsing these CBOR-encoded bytes yields:
{ "version": "1.0",
"digestAlgorithm": "SHA-256",
"docType": "org.iso.18013.5.1.mDL",
"valueDigests": {
"org.iso.18013.5.1": {
0: h'ff753bf6c11963fd644ccce43556cde1857acdfd8e7e607c01dff5dc3e1afbfc',
1: h'c5702c252638a4d0e97744cd384f0c0426079484bea1a2c94be58fb5e8c22861'}
},
"deviceKeyInfo": {
"deviceKey": {
1: 2,
-1: 1,
-2: h'027a5a1fa9151f67cdd2c57faf84ccfdb05bcf59078765fa0b7e1a8223790dd2',
-3: h'3e170811becf184d36ba38dbc229217d4faca0f162d554e5f1408d59c4baaa0b'
}
},
"validityInfo": {
"signed": 0("2025-04-28T21:00:00Z"),
"validFrom": 0("2025-04-28T21:00:00Z"),
"validUntil": 0("2026-04-28T21:00:00Z")
}
}
The valueDigests
component holds the salted hashes of the attributes. In order to disclose age_over_18
, for example, one needs to provide a pre-image to one of the hashes in that list, as we discuss below.
The deviceKeyInfo
component contains a public key whose corresponding secret key is stored in a secure element on the holder’s device. Specifically, the deviceKey
field is encoded in cbor as a COSE_Key The field 1:2
indicates that the key type is “Elliptic Curve Keys w/ x- and y-coordinate pair.” The -2
and -3
fields encode the x and y coordinate of the public key, and in this case, the algorithm is P256.
The DeviceResponse object
The MSO is contained in a DeviceResponse
object, which contains the additional information needed for credential verification, such as the issuer’s public key, the signature, and the pre-images to the salted hashes. Here is one example:
a36776657273696f6e63312e3069646f63756d656e747381a367646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c6973737565725369676e6564a26a6e616d65537061636573a1716f72672e69736f2e31383031332e352e3182d8185860a4686469676573744944006672616e646f6d58202231183c7d9f4d2e2e146d248458b4cce948798fda8240d360a8f40f6d7548d371656c656d656e744964656e7469666965726b6167655f6f7665725f31386c656c656d656e7456616c7565f5d818585fa4686469676573744944016672616e646f6d5820cd6b77638d8f2618b72f9025b88c3d632abba8de96e3932da3566e491f19cb9171656c656d656e744964656e746966696572636e796d6c656c656d656e7456616c756567313233313234346a697373756572417574688443a10126a118215901833082017f30820126a003020102020101300a06082a8648ce3d04030230293111300f060355040a130861626876696f7573311430120603550403130b54657374696e67206b6579301e170d3235303432383231313031345a170d3235303530353231313031345a30293111300f060355040a130861626876696f7573311430120603550403130b54657374696e67206b65793059301306072a8648ce3d020106082a8648ce3d03010703420004b4682ec20e06e8df840b5dd32959798ab20c544d4da50109ff4684d06fd261fcf6f8e9a811911329a5f653fcec5990092c91a65bc1695d291cd51de9c94e7db7a33f303d300e0603551d0f0101ff0404030205a0301d0603551d250416301406082b0601050507030106082b06010505070302300c0603551d130101ff04023000300a06082a8648ce3d04030203470030440220310f9ac756cffa493d2f7577ab380e9d4da691eb5a8f19ebb2d939a167491fbe02204c7dacde12f698b1f96ea1c9bca13ed79cd567f404473da06605a8f149ef670159017fd81859017aa66776657273696f6e63312e306f646967657374416c676f726974686d675348412d32353667646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c756544696765737473a1716f72672e69736f2e31383031332e352e31a2005820ff753bf6c11963fd644ccce43556cde1857acdfd8e7e607c01dff5dc3e1afbfc015820c5702c252638a4d0e97744cd384f0c0426079484bea1a2c94be58fb5e8c228616d6465766963654b6579496e666fa1696465766963654b6579a401022001215820027a5a1fa9151f67cdd2c57faf84ccfdb05bcf59078765fa0b7e1a8223790dd22258203e170811becf184d36ba38dbc229217d4faca0f162d554e5f1408d59c4baaa0b6c76616c6964697479496e666fa3667369676e6564c074323032352d30342d32385432313a30303a30305a6976616c696446726f6dc074323032352d30342d32385432313a30303a30305a6a76616c6964556e74696cc074323032362d30342d32385432313a30303a30305a584029229fa5da137c73fae1e5cb875ba0d2213d0dbedc9d0896a74599088372d898834505b86d3ade7ead4fc93aa28cd5c4f3ff2dd489fe899a71e72113ae74fa8f6c6465766963655369676e6564a26a6e616d65537061636573d81841a06a64657669636541757468a16f6465766963655369676e61747572658443a10126a0f65840520b17b8bd7eae5d4e7dcdb88cddb24e7b2bf65afe420a1c48812d97657842bab50574f5c829c32fa6d7a228db9664dc754e2f6c901c7b9a974959fc9b35cb4c6673746174757300
Parsing these cbor bytes yields:
{ "version": "1.0",
"documents": [
{ "docType": "org.iso.18013.5.1.mDL",
"issuerSigned": {
"nameSpaces": {
"org.iso.18013.5.1": [
24(h'a4686469676573744944006672616e646f6d58202231183c7d9f4d2e2e146d248458b4cce948798fda8240d360a8f40f6d7548d371656c656d656e744964656e7469666965726b6167655f6f7665725f31386c656c656d656e7456616c7565f5'),
24(h'a4686469676573744944016672616e646f6d5820cd6b77638d8f2618b72f9025b88c3d632abba8de96e3932da3566e491f19cb9171656c656d656e744964656e746966696572636e796d6c656c656d656e7456616c75656731323331323434')
]
},
"issuerAuth": [
h'a10126',
{33: h'3082017f30820126a003020102020101300a06082a8648ce3d04030230293111300f060355040a130861626876696f7573311430120603550403130b54657374696e67206b6579301e170d3235303432383231313031345a170d3235303530353231313031345a30293111300f060355040a130861626876696f7573311430120603550403130b54657374696e67206b65793059301306072a8648ce3d020106082a8648ce3d03010703420004b4682ec20e06e8df840b5dd32959798ab20c544d4da50109ff4684d06fd261fcf6f8e9a811911329a5f653fcec5990092c91a65bc1695d291cd51de9c94e7db7a33f303d300e0603551d0f0101ff0404030205a0301d0603551d250416301406082b0601050507030106082b06010505070302300c0603551d130101ff04023000300a06082a8648ce3d04030203470030440220310f9ac756cffa493d2f7577ab380e9d4da691eb5a8f19ebb2d939a167491fbe02204c7dacde12f698b1f96ea1c9bca13ed79cd567f404473da06605a8f149ef6701'},
h'd81859017aa66776657273696f6e63312e306f646967657374416c676f726974686d675348412d32353667646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c756544696765737473a1716f72672e69736f2e31383031332e352e31a2005820ff753bf6c11963fd644ccce43556cde1857acdfd8e7e607c01dff5dc3e1afbfc015820c5702c252638a4d0e97744cd384f0c0426079484bea1a2c94be58fb5e8c228616d6465766963654b6579496e666fa1696465766963654b6579a401022001215820027a5a1fa9151f67cdd2c57faf84ccfdb05bcf59078765fa0b7e1a8223790dd22258203e170811becf184d36ba38dbc229217d4faca0f162d554e5f1408d59c4baaa0b6c76616c6964697479496e666fa3667369676e6564c074323032352d30342d32385432313a30303a30305a6976616c696446726f6dc074323032352d30342d32385432313a30303a30305a6a76616c6964556e74696cc074323032362d30342d32385432313a30303a30305a',
h'29229fa5da137c73fae1e5cb875ba0d2213d0dbedc9d0896a74599088372d898834505b86d3ade7ead4fc93aa28cd5c4f3ff2dd489fe899a71e72113ae74fa8f'
]
},
"deviceSigned": {
"nameSpaces": 24(h'a0'),
"deviceAuth": {
"deviceSignature": [
h'a10126',
{},
null, h'520b17b8bd7eae5d4e7dcdb88cddb24e7b2bf65afe420a1c48812d97657842bab50574f5c829c32fa6d7a228db9664dc754e2f6c901c7b9a974959fc9b35cb4c'
]}
}
}
],
"status": 0
}
The MSO is stored in the 3rd component of the issuerAuth
field that starts as d81859019d...
and then includes the a667...
bytes of the MSO.
Interpreting the salted hash
The strings that are stored in documents[issuerSigned][namespaces][org.iso...1]
are preimages to the salted hashes that have been encoded in cbor format.
Decoding the first one listed produces:
{"digestID": 0,
"random": h'2231183C7D9F4D2E2E146D248458B4CCE948798FDA8240D360A8F40F6D7548D3',
"elementIdentifier": "age_over_18",
"elementValue": true
}
which indicates that index 0 in the salted hash portion of the MSO consists of the hash of these four elements that together mean that the holder’s age_over_18
attribute is true.
Indeed, by placing this object into a “cbor tag”, you can verify that
sha256(hex.decode(D8 1858 60 a468646...c7565f5)) = ff753bf6...
where the ff753bf6..
corresponds to the first salted hash in the signed MSO above.
Thus, verifying the age_over_18
property requires first identifying the portion of the DeviceResponse for this attribute, decoding it in CBOR, verifying that the attribute identifier is equal to age_over_18
and its value is true
, and then computing the hash of the four-tuple, and verifying its equality with one of the salted hashes that is included in the MSO. Keep in mind that the boolean true
is encoded as 0xf5
in cbor.
Verifying the issuer signature
The MSO was signed by an issuer; in this example, that issuer is my self-signed certificate whose ECDSA P256 public key (x,y) coordinates are:
Issuer_pk = (b4682ec20e06e8df840b5dd32959798ab20c544d4da50109ff4684d06fd261fc,
f6f8e9a811911329a5f653fcec5990092c91a65bc1695d291cd51de9c94e7db7)
It now remains to compute the exact bytes of the msg
that is signed. Unfortunately, these bytes are not simply the MSO bytes, but undergo a few “COSE” transformations. RFC8152 explains how COSE_Sign1 messages are formatted. In our case, these are the signed bytes:
846a5369676e61747572653143a101264059017fd81859017aa66776657273696f6e63312e306f646967657374416c676f726974686d675348412d32353667646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c756544696765737473a1716f72672e69736f2e31383031332e352e31a2005820ff753bf6c11963fd644ccce43556cde1857acdfd8e7e607c01dff5dc3e1afbfc015820c5702c252638a4d0e97744cd384f0c0426079484bea1a2c94be58fb5e8c228616d6465766963654b6579496e666fa1696465766963654b6579a401022001215820027a5a1fa9151f67cdd2c57faf84ccfdb05bcf59078765fa0b7e1a8223790dd22258203e170811becf184d36ba38dbc229217d4faca0f162d554e5f1408d59c4baaa0b6c76616c6964697479496e666fa3667369676e6564c074323032352d30342d32385432313a30303a30305a6976616c696446726f6dc074323032352d30342d32385432313a30303a30305a6a76616c6964556e74696cc074323032362d30342d32385432313a30303a30305a
These bytes encoded a Sig_signature
structure that represents the array:
["Signature1",
h'A10126',
h'',
h'D81859017AA66776657273696F6E63312E306F646967657374416C676F726974686D675348412D32353667646F6354797065756F72672E69736F2E31383031332E352E312E6D444C6C76616C756544696765737473A1716F72672E69736F2E31383031332E352E31A2005820FF753BF6C11963FD644CCCE43556CDE1857ACDFD8E7E607C01DFF5DC3E1AFBFC015820C5702C252638A4D0E97744CD384F0C0426079484BEA1A2C94BE58FB5E8C228616D6465766963654B6579496E666FA1696465766963654B6579A401022001215820027A5A1FA9151F67CDD2C57FAF84CCFDB05BCF59078765FA0B7E1A8223790DD22258203E170811BECF184D36BA38DBC229217D4FACA0F162D554E5F1408D59C4BAAA0B6C76616C6964697479496E666FA3667369676E6564C074323032352D30342D32385432313A30303A30305A6976616C696446726F6DC074323032352D30342D32385432313A30303A30305A6A76616C6964556E74696CC074323032362D30342D32385432313A30303A30305A']
The main D818...
portion of this message is a cbor tagged byte string, the 59017a
indicate 378 bytes follow, and the final A676...
bytes are the MSO that we expect.
The A10126
represents a map with code -7
, which corresponds to the ES256 signing algorithm as per Table 5 of RFC8152.
The signature of this message can be found in the fourth component of the issuerAuth
field in the DeviceResponse, i.e., the one that begins 29229fa...
. This string is simply the concatenation of the \((r,s)\) components of the ECDSA signature.
Here is a small golang program that verifies this signature:
1package main
2
3import (
4 "crypto/ecdsa"
5 "crypto/elliptic"
6 "crypto/sha256"
7 "encoding/hex"
8 "fmt"
9 "math/big"
10)
11
12func main() {
13 msg := "846a5369676e61747572653143a101264059017fd81859017aa66776657273696f6e63312e306f646967657374416c676f726974686d675348412d32353667646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c756544696765737473a1716f72672e69736f2e31383031332e352e31a2005820ff753bf6c11963fd644ccce43556cde1857acdfd8e7e607c01dff5dc3e1afbfc015820c5702c252638a4d0e97744cd384f0c0426079484bea1a2c94be58fb5e8c228616d6465766963654b6579496e666fa1696465766963654b6579a401022001215820027a5a1fa9151f67cdd2c57faf84ccfdb05bcf59078765fa0b7e1a8223790dd22258203e170811becf184d36ba38dbc229217d4faca0f162d554e5f1408d59c4baaa0b6c76616c6964697479496e666fa3667369676e6564c074323032352d30342d32385432313a30303a30305a6976616c696446726f6dc074323032352d30342d32385432313a30303a30305a6a76616c6964556e74696cc074323032362d30342d32385432313a30303a30305a"
14 msgb, _ := hex.DecodeString(msg)
15 e := sha256.Sum256(msgb)
16
17 ipkx, _ := hex.DecodeString("b4682ec20e06e8df840b5dd32959798ab20c544d4da50109ff4684d06fd261fc")
18 ipky, _ := hex.DecodeString("f6f8e9a811911329a5f653fcec5990092c91a65bc1695d291cd51de9c94e7db7")
19 ipk := &ecdsa.PublicKey{
20 Curve: elliptic.P256(),
21 X: new(big.Int).SetBytes(ipkx),
22 Y: new(big.Int).SetBytes(ipky),
23 }
24
25 sigb, _ := hex.DecodeString("29229fa5da137c73fae1e5cb875ba0d2213d0dbedc9d0896a74599088372d898834505b86d3ade7ead4fc93aa28cd5c4f3ff2dd489fe899a71e72113ae74fa8f")
26 r := new(big.Int).SetBytes(sigb[:32])
27 s := new(big.Int).SetBytes(sigb[32:])
28
29 fmt.Printf("ver: %v", ecdsa.Verify(ipk, e[:], r, s))
30
31}
Verifying the device-binding signature
The MSO contains a device public key in the deviceAuth
section of the MSO. This key should be stored in the device’s secure element, and must not be easily extractable from the holder’s phone. When the credential is issued, the issuer typically verifies a key assertion given by the secure element on this public key before signing the credential.
During the presentation of a credential, the relying party offers a challenge that must be incorporated into a message that is signed by the device-bound key. If the secure element hasn’t been compromised, the implication is that the only way to produce this challenge signature is on the device that was used during the issuance; this process prevents basic attacks in which the credential is copied to many other devices.
The challenge message that is signed is called the DeviceAuthenticationBytes
, and it contains a SessionTranscript
object that is defined in 9.1.5.1 of the spec. In my example, the signed bytes are
846a5369676e61747572653143a101264058c5d81858c1847444657669636541757468656e7469636174696f6e83f6f6847142726f7773657248616e646f766572763158200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f205835a363636174016474797065016764657461696c73a1676261736555726c77687474703a2f2f72656c79696e6770617274792e636f6d58202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40756f72672e69736f2e31383031332e352e312e6d444cd81841a0
which again consists of the COSE1 headers, and tagged byte streams, but ultimately, a SessionTranscript
object of the form:
["DeviceAuthentication",
[null, null,
["BrowserHandoverv1",
h'0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20',
h'A363636174016474797065016764657461696C73A1676261736555726C77687474703A2F2F72656C79696E6770617274792E636F6D',
h'2122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F40']],
"org.iso.18013.5.1.mDL", 24(h'A0')
]
The components of this array are taken from the end-to-end key exchange protocol that is used to negotiate an encryption key between the holder device and the relying party. The DeviceResponse[deviceSigned][deviceAuth][deviceSignature]
component contains its signature, in this case, the \((r,s)\) values are 520b1...
Here is another small golang program that verifies this signature.
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/big"
)
func main() {
msg := "846a5369676e61747572653143a101264058c5d81858c1847444657669636541757468656e7469636174696f6e83f6f6847142726f7773657248616e646f766572763158200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f205835a363636174016474797065016764657461696c73a1676261736555726c77687474703a2f2f72656c79696e6770617274792e636f6d58202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40756f72672e69736f2e31383031332e352e312e6d444cd81841a0"
msgb, _ := hex.DecodeString(msg)
e := sha256.Sum256(msgb)
dpkx, _ := hex.DecodeString("027a5a1fa9151f67cdd2c57faf84ccfdb05bcf59078765fa0b7e1a8223790dd2")
dpky, _ := hex.DecodeString("3e170811becf184d36ba38dbc229217d4faca0f162d554e5f1408d59c4baaa0b")
dpk := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: new(big.Int).SetBytes(dpkx),
Y: new(big.Int).SetBytes(dpky),
}
sigb, _ := hex.DecodeString("520b17b8bd7eae5d4e7dcdb88cddb24e7b2bf65afe420a1c48812d97657842bab50574f5c829c32fa6d7a228db9664dc754e2f6c901c7b9a974959fc9b35cb4c")
r := new(big.Int).SetBytes(sigb[:32])
s := new(big.Int).SetBytes(sigb[32:])
fmt.Printf("ver: %v", ecdsa.Verify(dpk, e[:], r, s))
}
The issuer’s certificate
The issuer’s signing key is often provided via a certificate chain. This cert chain is included in 2nd component of the issuerAuth
portion of the DeviceResponse
object, i.e., the portion that begins {33: h'308201...
. In this example, it is self-signed by me.
Other verification steps
This short recap elides a few other checks. For example, one must verify that the time at which the presentment is verified falls between validFrom
and validUntil
.