How to Issue Mobile Driver’s Licenses (ISO/IEC 18013-5 mDLs) via OID4VCI with walt.id
This guide provides a comprehensive walkthrough for issuing an mDL credential based on the ISO/IEC 18103-5 standard using the walt.id Issuer API. The issuance process will utilize the OID4VCI protocol.
Setup
See how to access to the issuer API below.
- Deployed (Testing Only) - Use our deployed version for testing.
- Local - Run the API in your environment with our open-source setup.
Preparing for Issuance: Key Components
The mobile driver's license (mDL) ecosystem builds on established PKI principles that employ X.509 digital certificates, while introducing domain-specific roles and data models. Its two main components are:
- IACA — Issuing Authority Certification Authority: This is an authority that serves as the root of trust vouching for authorized credential issuers and is represented by a self-signed X.509 digital certificate that is published to verifiers.
- DS — Document Signer (Credential Issuer): This is the entity that issues and cryptographically signs mDL credential(s). It holds a private key and an X.509 digital certificate issued by an IACA. Typically, DS certificates are short-lived and are rotated periodically.
- The mDL Claims: The mDL is a verifiable credential that contains claims about the subject (holder), such as
name, birthdate and driving privileges. The ISO/IEC 18103-5 standard document
defines a set of mandatory fields (e.g.,
family_name
,birth_date
) that must be included in every mDL, as well as a set of optional fields (e.g.,age_over_NN
attestations,height
,weight
,sex
).
Example mDL Credential in JSON format:
{
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22",
"issue_date": "2019-10-20",
"expiry_date": "2024-10-20",
"issuing_country": "AT",
"issuing_authority": "AT DMV",
"document_number": "123456789",
"portrait": [ 141, 12 ],
"driving_privileges": [
{
"vehicle_category_code": "A",
"issue_date": "2018-08-09",
"expiry_date": "2024-10-20"
},
{
"vehicle_category_code": "B",
"issue_date": "2017-02-23",
"expiry_date": "2024-10-20"
}
],
"un_distinguishing_sign": "AT"
}
Note: mDLs are encoded in CBOR, which is a binary format and is shown in JSON representation above for readability
purposes. Claims values that are encoded as JSON integer arrays, e.g., the portrait
claim's value, represent byte
arrays.
Issuing an mDL
In this section, we'll walk through the steps required to successfully issue an mDL by generating signing keys (for the IACA & the DS) which we store ourselves. For production environments, we recommend using an external KMS provider for key management due to the enhanced security. Learn more about the different types of keys and the storage options here.
Note: At the moment, you can only use secp256r1
keys to onboard IACAs and DSs and to issue mDLs.
Step 1: Onboard an Issuing Authority Certification Authority (IACA)
Every mobile driver’s license ecosystem starts with a trusted root certificate of an IACA. The IACA serves as the cryptographic authority that issues DS certificates that are deemed trustworthy.
🔧 What this step does
- Create a self-signed X.509 root certificate (CA=true).
- Encode in it identity info (e.g., country, organization) for the IACA.
- Include optional extensions (e.g., IACA alternative names, CRL distribution URIs).
IACA Onboarding Request
Endpoint: /onboard/iso-mdl/iacas
| API Reference
Example Request
curl -X 'POST' \
'http://0.0.0.0:7002/onboard/iso-mdl/iacas' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
}'
Body
{
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
}
Body Parameters
certificateData
: JSON - Data that will be encoded in the generated certificate of the IACA.country
: String - Two-letter ISO 3166-1 alpha-2 country code (e.g.,US
,DE
).commonName
: String - Human-readable name of the issuing authority (e.g.,Ministry of Transport
).issuerAlternativeNameConf
: JSON - Metadata for the IACA; can include uri and/or email.uri
: String - Uniform resource identifier regarding the IACA's contact information.email
: String - RFC822 name for contacting the IACA.
Example Response
The IACA onboarding endpoint will return an object containing the generated signing key of the IACA in JWK format, the PEM-encoded, JSON stringified self-signed X.509 certificate of the IACA and an object that contains the data that is encoded in the generated certificate (for convenience - useful for the DS onboadrding endpoint below).
{
"iacaKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "u-UvsghdzpSXv5HmG5ngvm4Dv8yyRYw9fKA6mdp1KWs",
"crv": "P-256",
"kid": "R_E_QZ-Ea6etoAdWfUHSjjexRYz447ffnnfIO9kxn_Y",
"x": "n_b1GmZTSEhioK3z8MGqcb7nxXqyjFaLR-OfKOnspwU",
"y": "nGRVvuHTtEAZ1HjgdLaLZnYxrkiRV_e4V2Wz0qVWa-M"
}
},
"certificatePEM": "-----BEGIN CERTIFICATE-----\nMIIBtTCCAVqgAwIBAgIUNlgkpoam39UxORhMNRkwuFzD9pQwCgYIKoZIzj0EAwIwJDELMAkGA1UEBhMCVVMxFTATBgNVBAMMDEV4YW1wbGUgSUFDQTAeFw0yNTA1MjgxMjIzMDFaFw00MDA1MjQxMjIzMDFaMCQxCzAJBgNVBAYTAlVTMRUwEwYDVQQDDAxFeGFtcGxlIElBQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASf9vUaZlNISGKgrfPwwapxvufFerKMVotH458o6eynBZxkVb7h07RAGdR44HS2i2Z2Ma5IkVf3uFdls9KlVmvjo2owaDAdBgNVHQ4EFgQUjCMRsfolTeK5Ds6MqOWj5Nx01BQwEgYDVR0TAQH/BAgwBgEB/wIBADAjBgNVHRIEHDAahhhodHRwczovL2lhY2EuZXhhbXBsZS5jb20wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMCA0kAMEYCIQCnUfp3OyxcaPCT34SQ4dTNyNN0qgxKWpWIDeUXkrs7HwIhALFYrMrINeAats4ZWRxZMK6bykb9dcOwkmBCv96MoZVi\n-----END CERTIFICATE-----\n",
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
}
Defaults:
- A local (
jwk
) secp256r1 key is automatically created. - Validity defaults to 15 years, unless otherwise specified.
As we've used the local (jwk
) key type, it's important to note that we need to save the returned values ourselves
for future reference. The API doesn't save any information about created keys.
Remember to save the value of the IACA's PEM-encoded X.509 certificate (certificatePEM
field in the response) as
it is required for verification of
mDLs.
Step 2: Onboard a Document Signer
Document signers are the entities responsible for signing mDL credentials. These entities are identified by an X.509 digital certificate that is signed by an IACA and contains, among others, a special purpose extension that marks it as valid for mDL issuance.
🔧 What this step does
- Issue a DS certificate signed by the input IACA private key.
- Embed an Extended Key Usage (EKU) value marking it as suitable for mDL signing.
- Include CRL distribution info and various other optional fields.
DS Onboarding Request
Endpoint: /onboard/iso-mdl/document-signers
| API Reference
Example Request
curl -X 'POST' \
'http://0.0.0.0:7002/onboard/iso-mdl/document-signers' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"iacaSigner": {
"iacaKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "R_E_QZ-Ea6etoAdWfUHSjjexRYz447ffnnfIO9kxn_Y",
"x": "n_b1GmZTSEhioK3z8MGqcb7nxXqyjFaLR-OfKOnspwU",
"y": "nGRVvuHTtEAZ1HjgdLaLZnYxrkiRV_e4V2Wz0qVWa-M",
"d": "u-UvsghdzpSXv5HmG5ngvm4Dv8yyRYw9fKA6mdp1KWs"
}
},
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
},
"certificateData": {
"country": "US",
"commonName": "Example DS",
"crlDistributionPointUri": "https://iaca.example.com/crl"
}
}'
Body
{
"iacaSigner": {
"iacaKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "R_E_QZ-Ea6etoAdWfUHSjjexRYz447ffnnfIO9kxn_Y",
"x": "n_b1GmZTSEhioK3z8MGqcb7nxXqyjFaLR-OfKOnspwU",
"y": "nGRVvuHTtEAZ1HjgdLaLZnYxrkiRV_e4V2Wz0qVWa-M",
"d": "u-UvsghdzpSXv5HmG5ngvm4Dv8yyRYw9fKA6mdp1KWs"
}
},
"certificateData": {
"country": "US",
"commonName": "Example IACA",
"issuerAlternativeNameConf": {
"uri": "https://iaca.example.com"
}
}
},
"certificateData": {
"country": "US",
"commonName": "Example DS",
"crlDistributionPointUri": "https://iaca.example.com/crl"
}
}
Body Parameters
iacaSigner
: JSON - Object containing necessary data related to the signing IACA.iacaKey
: JSON - The JSON serialized signing key of the issuing IACA - used as is from the its respective onboarding endpoint.certificateData
: JSON - The data that is encoded in the IACA's X.509 certificate - used as is from its respective onboarding endpoint.
certificateData
: JSON - Data that will be encoded in the generated certificate of the DS.country
: String - Two-letter ISO 3166-1 alpha-2 country code (e.g.,US
,DE
).commonName
: String - Human-readable name of the issuing authority (e.g.,Ministry of Transport
).crlDistributionPointUri
: String - URL where the relevant certificate revocation list (CRL) is published.
Requirements
- Must provide the IACA's signing key and certificate data (as obtained from the IACA onboarding endpoint).
- Countries and state/province fields must match between IACA and DS.
- CRL URI is mandatory for DS onboarding.
Example Response
The DS onboarding endpoint will return an object containing the generated signing key of the DS in JWK format, the PEM-encoded, JSON stringified X.509 certificate of the DS that is signed by the IACA signing key in the request and an object that contains the data that is encoded in the generated certificate of the DS (for convenience).
{
"documentSignerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "ZSHgIcRvbwV9s224kHUaFqkEPShCAdwXocGl_w3M42Q",
"crv": "P-256",
"kid": "pX99OZjL2iNqM7OMkE1r1rYyuAObvPntewcDHdc2bMM",
"x": "GWKpdL3jPoPJ5wKgSA-jxS2jgp-ZUDE6sIQbeB86vF0",
"y": "F3xAwH96_xVciV7mFQslU_eRQgP-5pSZiNf8bjMoGfo"
}
},
"certificatePEM": "-----BEGIN CERTIFICATE-----\nMIICCDCCAa2gAwIBAgIUDo8kr194t6sttt6KL3YcnMtcaYYwCgYIKoZIzj0EAwIwJDELMAkGA1UEBhMCVVMxFTATBgNVBAMMDEV4YW1wbGUgSUFDQTAeFw0yNTA1MjkwNzE4MzlaFw0yNjA4MjkwNzE4MzlaMCIxCzAJBgNVBAYTAlVTMRMwEQYDVQQDDApFeGFtcGxlIERTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGWKpdL3jPoPJ5wKgSA+jxS2jgp+ZUDE6sIQbeB86vF0XfEDAf3r/FVyJXuYVCyVT95FCA/7mlJmI1/xuMygZ+qOBvjCBuzAfBgNVHSMEGDAWgBSMIxGx+iVN4rkOzoyo5aPk3HTUFDAdBgNVHQ4EFgQU7S49LSeg/e0onfT44FVbL/rSKnswDgYDVR0PAQH/BAQDAgeAMCMGA1UdEgQcMBqGGGh0dHBzOi8vaWFjYS5leGFtcGxlLmNvbTAVBgNVHSUBAf8ECzAJBgcogYxdBQECMC0GA1UdHwQmMCQwIqAgoB6GHGh0dHBzOi8vaWFjYS5leGFtcGxlLmNvbS9jcmwwCgYIKoZIzj0EAwIDSQAwRgIhAMuSq75BPBXXBWGtIMd57fhRqpKf3Yzl3ldDdoQsK2xEAiEA/dmWLMLiJPV3UzmQS5MUHtn611z0VlL/k3YAdaVJ51c=\n-----END CERTIFICATE-----\n",
"certificateData": {
"country": "US",
"commonName": "Example DS",
"crlDistributionPointUri": "https://iaca.example.com/crl"
}
}
Defaults:
- A local (
jwk
) secp256r1 key is automatically created. - Validity defaults to 457 days, unless otherwise specified.
As we've used the local (jwk
) key type, it's important to note that we need to save the returned values ourselves
for future reference. The API doesn't save any information about created keys.
Remember to save the value of the DS PEM-encoded X.509 certificate (certificatePEM
field in the response) as
it is required for the next (issuance) step.
Step 3: Issue the mDL
To issue mDLs, we will use the document signers key (obtained previously).
To facilitate the issuance of the mDL from us (the document signer, or issuer) to the holder, we will utilize the OID4VCI protocol. In particular, we will be generating a credential offer URL that can be accepted by any compliant wallet to receive the credential.
The credential offer URL specifies the credentials to be issued. This includes details such as the URL of the issuer and information about the credential's format and type.
mDL Issuance Request
Endpoint:/openid4vc/mdoc/issue
| API Reference
Example Request
curl -X 'POST' \
'https://issuer.portal.test.waltid.cloud/openid4vc/mdoc/issue' \
-H 'accept: */*' \
-H 'statusCallbackUri: https://example.com/$id' \
-H 'Content-Type: application/json' \
-d '{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "-wSIL_tMH7-mO2NAfHn03I8ZWUHNXVzckTTb96Wsc1s",
"crv": "P-256",
"kid": "sW5yv0UmZ3S0dQuUrwlR9I3foREBHHFwXhGJGqGEVf0",
"x": "Pzp6eVSAdXERqAp8q8OuDEhl2ILGAaoaQXTJ2sD2g5U",
"y": "6dwhUAzKzKUf0kNI7f40zqhMZNT0c40O_WiqSLCTNZo"
}
},
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"mdocData": {
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22",
"issue_date": "2019-10-20",
"expiry_date": "2024-10-20",
"issuing_country": "AT",
"issuing_authority": "AT DMV",
"document_number": "123456789",
"portrait": [ 141, 182 ],
"driving_privileges": [
{
"vehicle_category_code": "A",
"issue_date": "2018-08-09",
"expiry_date": "2024-10-20"
},
{
"vehicle_category_code": "B",
"issue_date": "2017-02-23",
"expiry_date": "2024-10-20"
}
],
"un_distinguishing_sign": "AT"
}
},
"x5Chain": [
"-----BEGIN CERTIFICATE-----\nMIICCTCCAbCgAwIBAgIUfqyiArJZoX7M61/473UAVi2/UpgwCgYIKoZIzj0EAwIwKDELMAkGA1UEBhMCQVQxGTAXBgNVBAMMEFdhbHRpZCBUZXN0IElBQ0EwHhcNMjUwNjAyMDY0MTEzWhcNMjYwOTAyMDY0MTEzWjAzMQswCQYDVQQGEwJBVDEkMCIGA1UEAwwbV2FsdGlkIFRlc3QgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPzp6eVSAdXERqAp8q8OuDEhl2ILGAaoaQXTJ2sD2g5Xp3CFQDMrMpR/SQ0jt/jTOqExk1PRzjQ79aKpIsJM1mqOBrDCBqTAfBgNVHSMEGDAWgBTxCn2nWMrE70qXb614U14BweY2azAdBgNVHQ4EFgQUx5qkOLC4lpl1xpYZGmF9HLxtp0gwDgYDVR0PAQH/BAQDAgeAMBoGA1UdEgQTMBGGD2h0dHBzOi8vd2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMCQGA1UdHwQdMBswGaAXoBWGE2h0dHBzOi8vd2FsdC5pZC9jcmwwCgYIKoZIzj0EAwIDRwAwRAIgHTap3c6yCUNhDVfZWBPMKj9dCWZbrME03kh9NJTbw1ECIAvVvuGll9O21eR16SkJHHAA1pPcovhcTvF9fz9cc66M\n-----END CERTIFICATE-----\n"
]
}'
Header Parameters
statusCallbackUri
: URL - Receive updates on the created issuance process, e.g. when a credential was successfully claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a$id
, which will be replaced by the issuance session id. For example:https://myurl.com/$id
,https://myurl.com
orhttps://myurl.com/test/$id
Expand To Learn More
Body
The data send to the provided URL will contain a JSON body:
id
: String - the issuance session idtype
: String - the event typedata
: JsonObject - the data for the event
Event Types
Possible events (event types) and their data are:
resolved_credential_offer
with the credential offer as JSON (in our Web Wallet: called when the issuance offer is entered into the wallet, but not processing / accepted yet)requested_token
with the issuance request for the token as json object (called for the token required to receive the credentials)
Credential issuance (called for every credential that's issued (= requested from wallet))
jwt_issue
withjwt
being the issued jwtsdjwt_issue
withsdjwt
being the issued sdjwtbatch_jwt_issue
withjwt
being the issued jwtbatch_sdjwt_issue
withsdjwt
being the issued sdjwtgenerated_mdoc
withmdoc
being the CBOR (HEX) of the signed mdoc
To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.
Body
{
"issuerKey": {
"type": "jwk",
"jwk": {
"kty": "EC",
"d": "-wSIL_tMH7-mO2NAfHn03I8ZWUHNXVzckTTb96Wsc1s",
"crv": "P-256",
"kid": "sW5yv0UmZ3S0dQuUrwlR9I3foREBHHFwXhGJGqGEVf0",
"x": "Pzp6eVSAdXERqAp8q8OuDEhl2ILGAaoaQXTJ2sD2g5U",
"y": "6dwhUAzKzKUf0kNI7f40zqhMZNT0c40O_WiqSLCTNZo"
}
},
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"mdocData": {
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1986-03-22",
"issue_date": "2019-10-20",
"expiry_date": "2024-10-20",
"issuing_country": "AT",
"issuing_authority": "AT DMV",
"document_number": "123456789",
"portrait": [ 141, 182 ],
"driving_privileges": [
{
"vehicle_category_code": "A",
"issue_date": "2018-08-09",
"expiry_date": "2024-10-20"
},
{
"vehicle_category_code": "B",
"issue_date": "2017-02-23",
"expiry_date": "2024-10-20"
}
],
"un_distinguishing_sign": "AT"
}
},
"x5Chain": [
"-----BEGIN CERTIFICATE-----\nMIICCTCCAbCgAwIBAgIUfqyiArJZoX7M61/473UAVi2/UpgwCgYIKoZIzj0EAwIwKDELMAkGA1UEBhMCQVQxGTAXBgNVBAMMEFdhbHRpZCBUZXN0IElBQ0EwHhcNMjUwNjAyMDY0MTEzWhcNMjYwOTAyMDY0MTEzWjAzMQswCQYDVQQGEwJBVDEkMCIGA1UEAwwbV2FsdGlkIFRlc3QgRG9jdW1lbnQgU2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPzp6eVSAdXERqAp8q8OuDEhl2ILGAaoaQXTJ2sD2g5Xp3CFQDMrMpR/SQ0jt/jTOqExk1PRzjQ79aKpIsJM1mqOBrDCBqTAfBgNVHSMEGDAWgBTxCn2nWMrE70qXb614U14BweY2azAdBgNVHQ4EFgQUx5qkOLC4lpl1xpYZGmF9HLxtp0gwDgYDVR0PAQH/BAQDAgeAMBoGA1UdEgQTMBGGD2h0dHBzOi8vd2FsdC5pZDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMCQGA1UdHwQdMBswGaAXoBWGE2h0dHBzOi8vd2FsdC5pZC9jcmwwCgYIKoZIzj0EAwIDRwAwRAIgHTap3c6yCUNhDVfZWBPMKj9dCWZbrME03kh9NJTbw1ECIAvVvuGll9O21eR16SkJHHAA1pPcovhcTvF9fz9cc66M\n-----END CERTIFICATE-----\n"
]
}
Body Parameters
issuerKey
: JSON - A JWK or reference object to a key stored in an external KMS to sign the mDL with. Supported algorithms: secp256r1.
JWK Format:{"type": "jwk", "jwk": Here JSON Web Key Object}
. Can be provided as String Or JWK object to " issuerKey".
KMS Key: Please refer to the Key Management Section and the KMS you want to use for more details on the structure of the reference object.credentialConfigurationId
: String - org.iso.18013.5.1.mDLmdocData
: JSON - Claims to be added to the mDL. In the provided example above, all mandatory claims are included.x5Chain
: JSON Array - Must contain a single entry that contains the PEM-encoded, JSON stringified X.509 certificate of the DS (as output by the respective onboarding endpoint).
Example Response
The issuer endpoint will respond with Credential Offer URL.
Plain Response
openid-credential-offer://issuer.portal.test.waltid.cloud/draft13/?credential_offer_uri=https%3A%2F%2Fissuer.portal.test.waltid.cloud%2Fdraft13%2FcredentialOffer%3Fid%3D52b19ff5-5b42-423a-ad16-63099760baea
Decoded
{
"credential_issuer": "https://issuer.portal.test.waltid.cloud/draft13",
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJ0eXBlIjoiand0IiwiYWxnIjoiRVMyNTYiLCJraWQiOiJ0R3pBbUthdE0tbzlicDM1Y291aDZUUXhxZHpVeFgzQWwtcU9iVXhZelh3In0.eyJzdWIiOiI1MmIxOWZmNS01YjQyLTQyM2EtYWQxNi02MzA5OTc2MGJhZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwudGVzdC53YWx0aWQuY2xvdWQvZHJhZnQxMyIsImF1ZCI6IlRPS0VOIn0.aAgMH0XTq2YVTpsSGtwPIkinTai6Hr0iJyBttDOljPD2WV2kJolZR35K9hiqpz4ZBwGqUbHjmpYaywQqxb8Wfg"
}
},
"credential_configuration_ids": [
"org.iso.18013.5.1.mDL"
]
}
Step 4: Receive the Credential Offer
The created credential offer can now be embedded into a QR code for users to scan with their mobile wallet or pasted manually into the credential offer field of our web wallet.
Try It Out: Use our web wallet for a practical demonstration. After logging in, click the 'request credential' button and paste the received Offer URL into the text field below the camera.
🎉 Congratulations, you've issued a mDL using OID4VCI! 🎉