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:
| Feature | Description |
|---|---|
| Simple Display | Human-readable JSON format for UI display |
| Full JSON | Complete ISO 18013-5 structure in JSON |
| Verification | Cryptographic validation of license authenticity |
| Presentation | Share 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:
- Android/Kotlin
- iOS/Swift
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}")
}
}
import KiwaSdk
issueResult.onSuccess { result in
for doc in result!.documents.mobileeIDdocuments {
// Convert to simple display format
let display = doc.toSimpleDisplay()
// Convert to JSON string
let jsonString = display.toJsonString()
print(jsonString)
// Access specific namespace data
if let namespaceData = display.getNamespace(nameSpace: display.docType) {
for (key, value) in namespaceData {
print("\(key): \(value)")
}
}
// Access validity information
print("Valid from: \(display.validityInfo.validFrom)")
print("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
| Property | Type | Description |
|---|---|---|
docType | String | Document type identifier |
nameSpaces | Map<String, JsonObject> | Data organized by namespace |
validityInfo | ValidityInfo | Cryptographic validity timestamps |
| Method | Returns | Description |
|---|---|---|
getNamespace(nameSpace) | Map<String, JsonElement>? | Get data for a specific namespace |
toJsonString() | String | Serialize to JSON string |
Validity Information
The validityInfo object contains cryptographic timestamps that determine the license's validity:
| Field | Description |
|---|---|
signed | When the license was cryptographically signed |
validFrom | Earliest date the license is valid |
validUntil | Expiration date of the license |
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": "..."
}
}
}
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:
- Certificate chain validity
- Digital signatures
- Digest values
- Document type consistency
- 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:
- Android/Kotlin
- iOS/Swift
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")
}
}
import KiwaSdk
// Verify from raw bytes
let results = verifierService.verifyLicenseBytes(data: licenseByteArray)
// Check overall result
if results.error {
print("Verification failed")
} else {
print("Verification successful")
}
// Examine individual verification steps
for step in results.verifications {
let status = step.error ? "FAILED" : "PASSED"
print("\(step.name): \(status)")
if let message = step.message {
print(" Message: \(message)")
}
}
Verification Steps
The verification process follows ISO 18013-5 section 9:
| Step | Description | ISO Reference |
|---|---|---|
| 1 | Validate MSO certificate chain | 9.3.3 |
| 2 | Verify IssuerAuth digital signature | 9.1.2.4 |
| 3 | Validate digest values for all IssuerSignedItems | 9.1.2.5 |
| 4 | Verify DocType consistency | - |
| 5 | Validate 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
| Property | Type | Description |
|---|---|---|
version | CborString | Schema version |
removedDocuments | Array<Document> | Revoked or expired licenses |
mobileeIDdocuments | Array<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:
- Engagement Initialization: Create or scan a QR code / tap via NFC
- Connection Setup: BLE or NFC connection established automatically
- Data Transfer: License data transferred securely
- Verification: Relying Party validates the received license
For detailed engagement documentation, see:
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
- Check validity dates before displaying licenses to users
- Show expiration warnings when
validUntilis approaching - Use the simple display format for UI rendering
- Access specific namespaces using the
docTypeas the key
Verification Best Practices
- Always verify licenses received from external sources
- Check the
criticalflag on verification results - Log verification details for audit purposes
- Handle expired licenses gracefully in your UI
Storage Recommendations
- Store raw CBOR bytes for maximum fidelity
- Cache simple display data for quick UI rendering
- Re-verify after storage to ensure data integrity
- Implement secure storage using platform-specific secure enclaves
Next Steps
- Sample App - See a complete implementation
- IDK Engagement - Learn about NFC/BLE presentations
- API Reference - Detailed API documentation