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

CBOR

The IDK includes a CBOR (RFC 8949) library used throughout the mDoc, COSE, and credential modules. It provides type-safe encoding and decoding, a Kotlin DSL for building CBOR structures, tagged value support, and diagnostic output.

Modules

ModuleDescription
lib-cbor-publicTypes, interfaces, and the builder DSL
lib-cbor-implEncoder, decoder, and diagnostics

Core Types

All CBOR values are represented by the CborItem<Type> sealed hierarchy. This gives you compile-time type safety when constructing and inspecting CBOR data.

Primitives

TypeDescription
CborUIntUnsigned integer (major type 0)
CborNIntNegative integer (major type 1)
CborStringUTF-8 text string
CborByteStringRaw byte string
CborBoolBoolean, with subtypes CborTrue and CborFalse
CborNullCBOR null value
CborFloat16/32-bit floating point
CborDouble64-bit floating point

Collections

TypeDescription
CborArray<V>Ordered sequence of CBOR items; supports indefinite-length encoding
CborMap<K, V>Key-value map of CBOR items; supports indefinite-length encoding

Tagged Values

TypeDescription
CborTagged<T>Wraps a value with an RFC 8949 tag number
CborEncodedItem<T>Tag 24 embedded CBOR with lazy decoding; the inner value is not decoded until data() is called

COSE Labels

TypeDescription
NumberLabelInteger-based COSE map key
StringLabelString-based COSE map key

Builder DSL

The library provides a Kotlin DSL for building CBOR maps and arrays without manual type wrapping.

Building Maps

val engagement = cborMap {
"version" to "1.0"
1 to algorithmId
"security" array {
add(cipherSuite)
add(ephemeralKey)
}
optional("info", optionalValue) // skipped when null
}

Building Arrays

val items = cborArray {
+"hello"
add(42)
+true
map {
"nested" to "value"
}
}

DSL Features

  • optional(key, value): Adds the entry only when value is non-null. Eliminates boilerplate null checks.
  • nonEmptyArray(key) { ... }: Adds an array entry only when at least one element is present.
  • String and Int keys: Both "key" to value and 1 to value are supported.
  • @DslMarker annotation: Prevents accidental access to outer builder scopes, catching mistakes at compile time.

Encoding and Decoding

There are three ways to encode and decode CBOR, depending on the context.

DI-injected services

The lib-cbor-impl module contributes three singleton services into AppScope via Metro. If your class is part of the DI graph, inject them as constructor parameters:

@Inject
class MyCredentialProcessor(
private val cborEncoder: CborEncoder,
private val cborParser: CborParser,
private val cborDiagnostics: CborDiagnostics,
) {
fun processCredential(issuerSignedBytes: ByteArray) {
// Decode
val result = cborParser.parse(issuerSignedBytes)
if (result.isOk) {
val item = result.value
// ...
} else {
// handle error
}

// Encode
val response = cborMap {
"status" to 0
"version" to "1.0"
}
val bytes: ByteArray = cborEncoder.encode(response)
}
}

The interfaces and their implementations:

InterfaceImplementationScopeDescription
CborEncoderCborEncoderImplAppScopeEncodes a CborItem to ByteArray
CborParserCborParserImplAppScopeParses ByteArray into CborItem, returns IdkResult
CborDiagnosticsCborDiagnosticsImplAppScopeRenders CBOR as human-readable diagnostic notation

CborParser methods

CborParser has two methods:

interface CborParser {
// Parse from the start of the byte array
fun parse(
bytes: ByteArray,
config: CborDecoderConfig = CborDecoderConfig.DEFAULT,
): IdkResult<CborItem<*>, IdkError>

// Parse from a specific offset, returns (newOffset, item) pair
fun parseWithOffset(
bytes: ByteArray,
offset: Int,
config: CborDecoderConfig = CborDecoderConfig.DEFAULT,
): IdkResult<Pair<Int, CborItem<*>>, IdkError>
}

parseWithOffset is useful when reading multiple CBOR values concatenated in a single byte array. It returns the byte offset after the decoded item so you can continue reading from there.

Static Cbor object

For code outside the DI graph (tests, utilities, standalone scripts), the Cbor object provides static access to the same operations:

// Encode
val bytes = Cbor.encode(cborMap { "key" to "value" })

// Decode (returns IdkResult)
val result = Cbor.tryDecode(bytes)

// Decode with offset
val (newOffset, item) = Cbor.tryDecodeWithOffset(bytes, offset = 0)

// Diagnostics
val text = Cbor.toDiagnostics(item, setOf(DiagnosticOption.PRETTY_PRINT))
val textFromBytes = Cbor.toDiagnosticsEncoded(bytes)

Direct encoding on CborItem

Every CborItem has an encodeCbor() method that encodes itself to bytes without needing an injected encoder:

val map = cborMap { "name" to "Alice" }
val bytes: ByteArray = map.encodeCbor()

This is convenient for one-off encoding when you already have a CborItem in hand.

Decoder security limits

CborDecoderConfig protects against malicious or malformed input. Pass it to parse() or tryDecode().

SettingDefaultStrictPermissivePurpose
maxDepth6432unlimitedStack overflow from deep nesting
maxItems1,000,000100,000unlimitedMemory exhaustion from large collections
maxStringLength10,000,0001,000,000unlimitedMemory exhaustion from huge strings
// Use strict limits for untrusted external input
val result = cborParser.parse(untrustedBytes, CborDecoderConfig.STRICT)

// Use permissive limits only for input you control
val result = cborParser.parse(trustedBytes, CborDecoderConfig.PERMISSIVE)

Tagged CBOR

RFC 8949 defines semantic tags that annotate CBOR values with additional meaning. The IDK supports these through CborTagged<T>.

Standard Tags

TagTypeDescription
0CborDateRFC 3339 datetime string
1CborTimeEpoch-based timestamp
24CborEncodedItemEmbedded CBOR (byte string containing a CBOR-encoded value)
1004-Full-date string (RFC 8943)

Tag 24 is used heavily in mDoc for wrapping structures like DeviceEngagement and IssuerSignedItem. The wrapped content is not decoded until explicitly requested, which improves performance when processing large credential payloads.

// Wrap a value in Tag 24
val encoded = CborEncodedItem.fromValue(deviceEngagement, encoder)

// Lazy decode: the inner bytes are only parsed when data() is called
val decoded = encoded.data { bytes -> decoder.decode(bytes) }

Diagnostic Notation

The CborDiagnostics service renders CBOR structures in human-readable diagnostic notation, which is useful for debugging and logging.

val diagnostics: CborDiagnostics = ...

// Render a CborItem as diagnostic text
val text = diagnostics.render(item, setOf(DiagnosticOption.PRETTY_PRINT))

// Render directly from raw bytes
val text = diagnostics.renderEncoded(bytes, setOf(
DiagnosticOption.PRETTY_PRINT,
DiagnosticOption.EMBEDDED_CBOR
))

Diagnostic Options

OptionEffect
PRETTY_PRINTAdds indentation and line breaks for readability
EMBEDDED_CBORAutomatically decodes and expands Tag 24 content inline
BSTR_PRINT_LENGTHShows byte count instead of raw byte content

CDDL Type System

The CDDL sealed class hierarchy maps CBOR types to CDDL (Concise Data Definition Language) notation. This is used internally for JSON interop and type metadata.

Common CDDL types include CDDL.bstr, CDDL.tstr, CDDL.uint, CDDL.int, CDDL.bool, and CDDL.float. Each type provides factory methods for creating typed CborItem instances, ensuring that values conform to the expected CDDL schema.

JSON Interop

CborItem instances support conversion to JSON for interoperability with JSON-based systems:

  • toJson(): Converts the CBOR item to a standard JSON representation.
  • toJsonSimple(): Produces a simplified JSON form, omitting type metadata.
  • toJsonWithCDDL(): Outputs JSON annotated with CDDL type information, useful for debugging or schema validation.