Skip to main content
Version: v0.13 (Latest)

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 uses the underlying Mdoc library for JSON conversion, presentations, and validations.

Overview

The SDK provides several ways to work with e-license data:

FeatureDescription
Simple DisplayHuman-readable JSON format for UI display
Full JSONComplete ISO 18013-5 structure in JSON
VerificationCryptographic validation of license authenticity
PresentationShare licenses with verifiers via NFC/BLE

Mdoc Format

When you issue a license, the SDK returns license documents that conform to ISO 18013-5 section 8.3.2.1.2.2. Each document contains:

  • Document Type (docType): Identifies the license type
  • Namespaces: Data elements organized by namespace
  • Issuer Signed Data: Cryptographically signed payload
  • Validity Information: Signed/validFrom/validUntil timestamps

Display Formats

Simple JSON Display

The toSimpleDisplay() method converts complex CBOR structures into a clean, human-readable format:

import com.sphereon.kiwa.elicense.sdk.display.ElicenseDocumentSimpleDisplay

// After issuing a license
val issueResult = holderService.commands.issue.execute(request)

issueResult.onSuccess { result ->
result.documents.mobileeIDdocuments.forEach { doc ->
// Convert to simple display format
val display: ElicenseDocumentSimpleDisplay = doc.toSimpleDisplay()

// Convert to JSON string
val jsonString: String = display.toJsonString()
println(jsonString)

// Access specific namespace data
val namespaceData = display.getNamespace(display.docType)
namespaceData?.forEach { (key, value) ->
println("$key: $value")
}

// Access validity information
println("Valid from: ${display.validityInfo.validFrom}")
println("Valid until: ${display.validityInfo.validUntil}")
}
}

Example Output:

{
"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"
}
},
"validityInfo": {
"signed": "2025-06-23T13:47:31.0962123Z",
"validFrom": "2025-06-23T13:47:31.0962123Z",
"validUntil": "2027-12-22T00:00:00Z"
}
}

ElicenseDocumentSimpleDisplay Interface

PropertyTypeDescription
docTypeStringDocument type identifier
nameSpacesMap<String, JsonObject>Data organized by namespace
validityInfoValidityInfoCryptographic validity timestamps
MethodReturnsDescription
getNamespace(nameSpace)Map<String, JsonElement>?Get data for a specific namespace
toJsonString()StringSerialize to JSON string

Validity Information

The validityInfo object contains cryptographic timestamps that determine the license's validity:

FieldDescription
signedWhen the license was cryptographically signed
validFromEarliest date the license is valid
validUntilExpiration date of the license
warning

A license is only valid when the current date falls between validFrom and validUntil. Relying parties performing verification will reject licenses outside this validity window.

Full JSON Format

For advanced use cases requiring the complete ISO 18013-5 structure, use toJson():

// Get complete Mdoc structure as JSON
val fullJson = licenseDocument.toJson()
println(fullJson)

This returns the full CBOR-to-JSON conversion including:

  • Digest IDs and random values
  • Protected and unprotected headers
  • X.509 certificate chains
  • Signature data

Example Structure:

{
"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"
}
}
]
},
"issuerAuth": {
"protectedHeader": { "alg": -7 },
"unprotectedHeader": {
"x5chain": ["MIIB..."]
},
"payload": "...",
"signature": "..."
}
}
}
note

The full JSON format closely mirrors the ISO 18013-5 specification. Familiarity with the specification is helpful when working with this format.

License Verification

Automatic Verification

When you issue licenses via the SDK, verification is performed automatically. The SDK validates:

  1. Certificate chain validity
  2. Digital signatures
  3. Digest values
  4. Document type consistency
  5. Validity timestamps

Invalid licenses are rejected with appropriate error messages.

Manual Verification

For scenarios where you need to manually verify a license (e.g., as a Relying Party), use the verification service:

import com.sphereon.idk.mdoc.verification.VerifyResultsType
import com.sphereon.idk.mdoc.verification.VerifyResultType

// Verify from raw bytes
val results: VerifyResultsType<*> = verifierService.verifyLicenseBytes(licenseByteArray)

// Or verify from decoded document
// val results = verifierService.verifyLicense(licenseDocument)

// Check overall result
if (results.error) {
println("Verification failed")
} else {
println("Verification successful")
}

// Examine individual verification steps
results.verifications.forEach { step: VerifyResultType ->
println("${step.name}: ${if (step.error) "FAILED" else "PASSED"}")
step.message?.let { println(" Message: $it") }
if (step.critical && step.error) {
println(" CRITICAL FAILURE")
}
}

Verification Steps

The verification process follows ISO 18013-5 section 9:

StepDescriptionISO Reference
1Validate MSO certificate chain9.3.3
2Verify IssuerAuth digital signature9.1.2.4
3Validate digest values for all IssuerSignedItems9.1.2.5
4Verify DocType consistency-
5Validate temporal validity (signed, validFrom, validUntil)-

Verification Result Interface

interface VerifyResultsType<out KeyType : KeyType> {
// True if any critical verification failed
val error: Boolean

// Individual verification step results
val verifications: Array<out VerifyResultType>

// Key information extracted during verification
val keyInfo: KeyInfoType<KeyType>?
}

interface VerifyResultType {
// Name of the verification step
val name: String

// Whether this step failed
val error: Boolean

// Human-readable message
val message: String?

// Detailed technical message
val detailMessage: String?

// Whether failure of this step is critical
val critical: Boolean
}

Decoding Raw License Data

If you have raw CBOR bytes (e.g., from storage or external source), decode them:

// Decode raw CBOR bytes
val decodeResult = holderService.commands.decode.execute(rawCborBytes)

decodeResult.onSuccess { documents ->
// documents is ElicenseIssueDocuments containing:
// - version: Schema version
// - removedDocuments: Revoked/expired licenses
// - mobileeIDdocuments: Active licenses

documents.mobileeIDdocuments.forEach { doc ->
val display = doc.toSimpleDisplay()
println(display.toJsonString())
}
}.onFailure { error ->
println("Decode failed: ${error.message}")
}

ElicenseIssueDocuments Structure

PropertyTypeDescription
versionCborStringSchema version
removedDocumentsArray<Document>Revoked or expired licenses
mobileeIDdocumentsArray<Document>Active license documents

Presenting Licenses to Verifiers

Once licenses are stored on a device, holders can present them to Relying Parties (verifiers) for validation. This is typically done via:

  • QR Code: One party displays, the other scans
  • NFC: Tap-to-share between devices
  • Bluetooth Low Energy (BLE): Wireless transfer after initial connection

The engagement process is handled by the Identity Development Kit (IDK). The workflow:

  1. Engagement Initialization: Create or scan a QR code / tap via NFC
  2. Connection Setup: BLE or NFC connection established automatically
  3. Data Transfer: License data transferred securely
  4. Verification: Relying Party validates the received license

For detailed engagement documentation, see:

info

No internet connection is required during the presentation. Both holder and verifier can operate offline once the initial engagement is established.

Best Practices

Display Considerations

  1. Check validity dates before displaying licenses to users
  2. Show expiration warnings when validUntil is approaching
  3. Use the simple display format for UI rendering
  4. Access specific namespaces using the docType as the key

Verification Best Practices

  1. Always verify licenses received from external sources
  2. Check the critical flag on verification results
  3. Log verification details for audit purposes
  4. Handle expired licenses gracefully in your UI

Storage Recommendations

  1. Store raw CBOR bytes for maximum fidelity
  2. Cache simple display data for quick UI rendering
  3. Re-verify after storage to ensure data integrity
  4. Implement secure storage using platform-specific secure enclaves

Next Steps