eLicense Mdoc display and verification
The e-licenses returned from the Kiwa APIs are Mobile Documents (Mdocs) in CBOR encoding according to the ISO 18013-5 specification. The Kiwa eLicense SDK internally thus also uses the Mdoc library for JSON conversion, presentations, and validations (see next chapter).
Mdoc formatβ
The Issuing a license service returns an array of issued licenses. Each of these licenses is a Mdoc.
That means they contain a document according to 8.3.2.1.2.2 of the ISO 18013-5 specification, which in turn contains the cryptographic material for validations.
You can use the lower level Mdoc SDK to manually interact with the Mdocs, but the Kiwa eLicense SDK has convenience methods, ensuring you do not have to interact at that level.
The main exposed methods in the SDK are:
- Verification and validation of the e-license (see next chapter)
- Displaying the Mdoc CBOR data in simplified or full JSON format. The simplified version removes items not needed for display purposes (this chapter)
- Presenting licenses to 3rd party Relying Parties (mdoc readers)
In Issuing a license we saw how the Kiwa API returned one or more licenses. The response contains a result called decode in which a property
mobileeIDdocuments is found. This is an array of ELicenseDocument objects, which in turn are the regular Mdoc CBOR objects. These can be represented as a bytearray, or decoded
into the ELicenseDocument objects, which the response already has done for you.
You can use the decode command/service when you have the bytearray of a single ELicense:
val decodeResult = holderService.cmd.decode(licenseByteArray)
decodeResult.onSuccess { documentsCbor ->
...
}.onFailure { error ->
println("Failed to decode license: ${error.message.defaultMessage}")
}
Simple Json displayβ
The above document contains all kinds of sub objects, mainly wrapping the CBOR structures. As these provide low-level access, it might be useful in advanced cases, but for regular display, the SDK provides a convenient method.
// Convert the Mdoc CBOR object into a simple JSON display object. Not to be confused with the .toJson() method (see below)
val licenseSimple: ElicenseDocumentSimpleDisplay = licenseDocument.toSimpleDisplay()
// Convert the JSON data object to a JSON string
val asString: String = licenseSimple.toJsonString()
The result should look something like:
{
"docType": "org.iso.23220.1.nl.kiwa.sampcert",
"nameSpaces": {
"org.iso.23220.1.nl.kiwa.sampcert": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1998-06-11",
"issue_date": "2024-10-24",
"issue_place": "Nieuwegein",
"starting_date": "2024-09-25",
"expiry_date": "2024-09-26"
// SNIP FOR READABILITY
}
},
"validityInfo": {
"signed": "2025-06-23T13:47:31.0962123Z",
"validFrom": "2025-06-23T13:47:31.0962123Z",
"validUntil": "2027-12-22T00:00:00Z"
}
}
The validityInfo object contains the cryptographic dates about the validity of the license/Mdoc. Be aware that an e-license is only valid as long as the current date is between
validFrom and validUntil. Relying parties doing validations will throw errors if the current date is not within the validity window!
It makes sense that any holder app also takes this information into account when displaying information.
The docType value will match the key used in the nameSpaces map. For Kiwa licenses, there will only be one nameSpaces object for now. The easiest solution is to first look at
the docType
and then use that value as the lookup key under the nameSpaces key
If you really have a need to look into more properties than the simple display provides you, without all the CBOR complexities, then there is also a generic
toJson()object available you can use. That translates all the CBOR properties into JSON properties, but be aware that it still contains the same structure as an ISO 18013-5 Mdoc, meaning it will be harder to interact with, unless you know the 18013-5 spec well.
Example of CBOR to JSON:
{
"docType": "org.iso.23220.1.nl.kiwa.sampcert",
"issuerSigned": {
"nameSpaces": {
"org.iso.23220.1.nl.kiwa.sampcert": [
{
"digestID": 0,
"random": "7w3AjRPhPxNiNzo5FlyoBA",
"key": "family_name",
"value": {
"cddl": "tstr",
"value": "Doe"
},
"cddl": "tstr"
},
{
"digestID": 1,
"random": "_rcKB4PnjOTpCB5-1FGmug",
"key": "given_name",
"value": {
"cddl": "tstr",
"value": "John"
},
"cddl": "tstr"
},
{
"digestID": 2,
"random": "Iw9Fg5OsE3YxJL5DEef-ZA",
"key": "birth_date",
"value": {
"cddl": "tstr",
"value": "1998-06-11"
},
"cddl": "tstr"
}
// SNIP FOR READABILITY
// Be Aware that Mdocs can have sub elements as well here
]
},
"issuerAuth": {
"protectedHeader": {
"alg": -7
},
"unprotectedHeader": {
"x5chain": [
"MIIBujCCAV+gAwIBAgIQCTaKSPbXo+IrZFXgcdNGpTAKBggqhkjOPQQDAjBAMT4wPAYDVQQDEzVBY2MgS2l3YSBEaWdpdGFsIENlcnRpZmljYXRpb24gU2lnbmVyIEludGVybWVkaWF0ZSBDQTAeFw0yNTAzMjYwOTQ3MzJaFw0yNzEyMjIwMDAwMDBaMCgxJjAkBgNVBAMTHUFjYyBTcGhlcmVvbiBJYURvY3VtZW50U2lnbmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEK+9UZCeDyzToPg2Nr8gvrs4rpOkpZI/R4y6Yp+GcltGetTLO58fmFjNAObcLgM/+QMBPy6kmlkYfj56DJOX4YKNTMFEwCQYDVR0TBAIwADAdBgNVHQ4EFgQUIa6Dtg25fjqcYy9Xv/z1upQ7OY0wDgYDVR0PAQH/BAQDAgeAMBUGA1UdJQEB/wQLMAkGByiBjF0FAQMwCgYIKoZIzj0EAwIDSQAwRgIhAPsuK9aH0BCYnlBTuPNA4gAogkxr3cB+UCs6b1xb03sfAiEAuwKZY2XNrlGMQBE+ib0iSfIcY8lJQtFUxA/XDhbJ0JQ"
]
},
"payload": "2BhZBTumZ2RvY1R5cGV4IG9yZy5pc28uMjMyMjAuMS5ubC5raXdhLnNhbXBjZXJ0Z3ZlcnNpb25jMS4wbHZhbGlkaXR5SW5mb6Nmc2lnbmVkwHgcMjAyNS0wNi0yM1QxMzo0NzozMS4wOTYyMTIzWml2YWxpZEZyb23AeBwyMDI1LTA2LTIzVDEzOjQ3OjMxLjA5NjIxMjNaanZhbGlkVW50aWzAdDIwMjctMTItMjJUMDA6MDA6MDBabHZhbHVlRGlnZXN0c6F4IG9yZy5pc28uMjMyMjAuMS5ubC5raXdhLnNhbXBjZXJ0uBwAWCDVxzGarB2o9wRH4wRLVhTTiwlwZMtUaA0BQzBH38QzggFYIF9WqSpm047yPHFfDdsts_Q7asiyqrowz-vJeWjbfwVqAlggDwFm804y_lTPll_LDWI9Hhg3nEu01UCAs-P86dpGdpUDWCC4sRrfCVcUpS2mF1sZUvzBXAn2Z7XRt2isFcRT-M1wJARYIMRrKDsz135pSEU60XqUEX4WMHTQF0MZROqBBTednSH2BVggW9Rlr07L4wH2rIPi9QZw0gshkFmsGhDvIt5MjxArX5sGWCDhSywYAXwvnqVTxSvi62K-qkv3xFPjP3cI3K3gYmcSKgdYIKw4vZ2cOC5SdBU9sS98MlB21t7xryhqJVK7k2J6tk9tCFggkcZrkprBQ0sbrN3y4SInywawSorQcyu1weVfFAIyEbkJWCC32OUGRfibVYal1r0YUUPiZNLmrukPab4Kxa-JY_N04ApYII4-6JwgphhnzazMSgzA-X39_VGDYucsTt7WY9JDYZ_DC1gg1r0oMgucx-tW0v3LHqzqrUsQ6WUx0U5AeWIi1dcCOtYMWCD5whlv6l6Y1bR8ZS9Z2YblKQ5tBPb-9AjEKzG6eY_9Gg1YIHMGPwSolOM5JOf85gzwoVxpue9h2AKn_pUdTTvzE-d9Dlggsraz_yYQ0xuLm5xbnoAsRVWiWV2MZZ0aHiVOboMmz10PWCCUmXoRKrw67W1lQWdOEZK2Y8kLFWBgN4EYm5_ogK5BchBYIGk0s5We2nKSrYfRV2DDNQ7HF0x9FpH7c6illYJ0SYZ6EVggVX5norPWI_Yvw9DOPm64A1v2mXnyALlAnLwR_XS_BRQSWCBzxH19yp6viBEybQYt6SIy2ojVGB7XdhAP4IHfGytGfhNYIJ-P1-Aur_tIkdA7qxlZXVt1CtyXMoU5jDMpJh1tm70UFFggfDGJnDBYGhBx5rMLY8jBCgbUHAv-LBak4L4LKhHXiC8VWCD5e0u3DnAimMCIsdWmlak4LrQzGWSZM2zVHEkK-I9UXBZYIEHtYln3azFih-T-i_S__j2L22MqOaJP-J37Mj-nXfk1F1ggr_3uOoNSTITinggiQmYvpKaJEd9j2sOh3Ubm3ixAt0wYGFggs4I-1tiEH0HW6io6ueDYhQ0VlP5axHzGqsU9TJwb93EYGVggUdxGJk0cYFll2Lv3RvTd4OpMjGOK3Bq8JNVbySw3vrQYGlggvNLAto-xMMHK7uk6My57O0NqYZB5L81W6QsR_Ev1MwAYG1ggtIesqsTY6njFu2QIkv64nWqUD8TT9VHQExH-jh7eeY5tZGV2aWNlS2V5SW5mb6FpZGV2aWNlS2V5pAECIAEhWCCHkukVQVqtqcPLQCatsIj_s0GuLqUB7u_8FZsERwhjKSJYIHsplWP5I1JsXy_9MztpLEoJXdOvttfk_f1SwJ-tOsMxb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2",
"signature": "_2j8GQLWCB_AfoM1nJhF-L2g5f9HPh6FhcDrT5y1N11LWdYv7cQvAKVxU-j9fLC-AWZXNkYnAvti5vOih7NhWQ"
}
}
}
Manual holder or Relying Party validationβ
When acting as a holder, the code to get the licenses automatically performs checks to validate the correctness of the licenses.
However, as a holder, you can check individual licenses as well. Or when acting as a verifier (also called Relying Party), you want to make sure licenses are being validated upon receipt. The service delegates to the low level Mdoc library internally. If you need more control for whatever reason, you could use that library directly as well.
The verification performs the following steps:
- Validate the certificate included in the Mdoc License Mobile Security Object (MSO) header according to ISO18013-5
9.3.3. - Verify the digital signature of the IssuerAuth structure (ISO1813-5
9.1.2.4) using the public_key, public_key_parameters, and public_key_algorithm from the certificate validation procedure of step 1. - Calculate the digest value for every IssuerSignedItem returned in the DeviceResponse structure
according to
9.1.2.5and verify that these calculated digests equal the corresponding digest values in the MSO. - Verify that the DocType in the MSO matches the relevant DocType in the Documents structure.
- Validate the elements in the ValidityInfo structure, i.e., verify that:
- the 'signed' date is within the validity period of the certificate in the MSO header,
- the current timestamp shall be equal or later than the βvalidFromβ element,
- the 'validUntil' element shall be equal or later than the current timestamp.
In the below code, we will validate a single e-license/Mdoc document. Be aware that this validation also automatically happens when you get the license(s) from the Kiwa REST API, so there is no need to manually perform the below verification at that point.
val results: VerifyResultsType = verifierService.verifyLicenseBytes(/*byte array of e-license*/)
// Or when you already have the license as mdoc/ElicenseIssueDocumentCbor decoded object:
// val results: VerifyResultsType = verifierService.verifyLicense(licenseDocument)
The results object consists of multiple sub objects, which contain information about the above steps being successful or not. It contains an overall boolean value called
error, which means that at least one of the validations contained an error and that error was deemed critical for the overall validation.
It also contains a verifications array of type VerifyResultType containing individual verification step results.
/**
* Interface representing the results of a verification process.
*
* Provides information about the overall success or failure of the verification,
* the individual verification results, and associated key information.
*
* @param KeyType The specific type of key implementing the KeyType interface.
*/
interface VerifyResultsType<out KeyType : KeyType> {
/**
* Indicates whether an error has occurred.
*
* This variable can be used to determine if an operation resulted in an error state.
* A value of `true` means an error is present, while `false` means no error has occurred.
*/
val error: Boolean
/**
* Holds an array of verification results.
*
* Each element of the array represents a specific verification result,
* detailing aspects such as the name of the verification, whether it resulted
* in an error, an optional message describing the result, and whether the
* result is critical.
*/
val verifications: Array<out VerifyResultType>
/**
* Represents information about a specific key.
*
* This variable holds an instance of the [KeyInfoType] interface which is parameterized with [KeyType].
* It encapsulates various details related to a key, such as its type and other metadata.
*
* @property keyInfo an instance of [KeyInfoType] for the specified [KeyType], or null if no key information is present.
*/
val keyInfo: KeyInfoType<KeyType>?
}
/**
* Interface representing the result of a verification process.
*/
interface VerifyResultType {
/**
* Stores the name of a verification.
*/
val name: String
/**
* Indicates whether an error has occurred.
*
* This boolean variable is used to represent the presence of an error condition.
* When set to `true`, it means an error has been detected; when set to `false`,
* it indicates that no error is currently present.
*/
val error: Boolean
/**
* An optional message that provides additional information about the verification result.
*/
val message: String?
val detailMessage: String?
/**
* Indicates whether the condition is critical.
*
* This boolean variable is used to flag a critical state or condition within the application. When set to true,
* it denotes that the error condition was unsuccessful and requires immediate attention or handling.
*/
val critical: Boolean
}

Engaging as a holder/mdoc with a verifier (Relying Party)β
Once one or more eLicenses in mdoc format are in the holders possession there is a large chance that the holder needs to prove the license(s) to a Verifier, called a Relying Party. The Kiwa SDK leverages the Mdoc libraries for the lower level support in these cases. Although the entire lower level functionalities would be available the SDK is tailored towards the license holder, also called the Mdoc as opposed to the Relying Party, typically called verifier or Mdoc reader.
When initiating the verification process as a license holder, it means you will have to start the so-called engagement. The engagement acts like the initialization of typically an attended "offline" peer-to-peer connection. It is called offline because there is no necessity for an active internet connection for either the Relying Party or the license holder when sharing the license. The engagement typically involves a QR or NFC tag. So either the Relying Party (mdoc reader) or the license holder (mdoc) will show a QR code, containing information on how to establish a connection. Then the connection can be established using NFC (slower) or Bluetooth Low Energy. That connection setup is being handled in the background, fully opaque to the users. All they do is show or scan a QR code, or tap 2 devices via NFC. Then the transfer/connection happens in the background
The Idententy Development Kit (IDK) on which the Kiwa eLicense Holder SDK is based, is capable of handling the different engagement scenarios. More information on the IDK can be found in: