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
| Module | Description |
|---|---|
lib-cbor-public | Types, interfaces, and the builder DSL |
lib-cbor-impl | Encoder, 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
| Type | Description |
|---|---|
CborUInt | Unsigned integer (major type 0) |
CborNInt | Negative integer (major type 1) |
CborString | UTF-8 text string |
CborByteString | Raw byte string |
CborBool | Boolean, with subtypes CborTrue and CborFalse |
CborNull | CBOR null value |
CborFloat | 16/32-bit floating point |
CborDouble | 64-bit floating point |
Collections
| Type | Description |
|---|---|
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
| Type | Description |
|---|---|
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
| Type | Description |
|---|---|
NumberLabel | Integer-based COSE map key |
StringLabel | String-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 whenvalueis 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 valueand1 to valueare supported. @DslMarkerannotation: 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:
| Interface | Implementation | Scope | Description |
|---|---|---|---|
CborEncoder | CborEncoderImpl | AppScope | Encodes a CborItem to ByteArray |
CborParser | CborParserImpl | AppScope | Parses ByteArray into CborItem, returns IdkResult |
CborDiagnostics | CborDiagnosticsImpl | AppScope | Renders 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().
| Setting | Default | Strict | Permissive | Purpose |
|---|---|---|---|---|
maxDepth | 64 | 32 | unlimited | Stack overflow from deep nesting |
maxItems | 1,000,000 | 100,000 | unlimited | Memory exhaustion from large collections |
maxStringLength | 10,000,000 | 1,000,000 | unlimited | Memory 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
| Tag | Type | Description |
|---|---|---|
| 0 | CborDate | RFC 3339 datetime string |
| 1 | CborTime | Epoch-based timestamp |
| 24 | CborEncodedItem | Embedded 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
| Option | Effect |
|---|---|
PRETTY_PRINT | Adds indentation and line breaks for readability |
EMBEDDED_CBOR | Automatically decodes and expands Tag 24 content inline |
BSTR_PRINT_LENGTH | Shows 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.