CBOR: Concise Binary Object Representation Explained
CBOR (Concise Binary Object Representation) is a binary data serialization format standardized by the IETF as RFC 7049 (2013) and updated to RFC 8949 (2020). Designed explicitly as a binary superset of JSON, CBOR can represent all JSON data types plus binary data, typed numeric arrays, and semantic tags — all in a compact binary encoding that requires no schema. CBOR is the required encoding for CTAP2 (FIDO2 authenticator protocol), WebAuthn, COSE (CBOR Object Signing and Encryption), and numerous IoT standards including CoAP (Constrained Application Protocol).
The design philosophy prioritizes four goals: (1) extremely compact encoding, (2) no schema required for encoding or decoding, (3) a formal IETF standard with long-term stability, and (4) extensibility through semantic tags without breaking decoders.
CBOR vs JSON vs MessagePack
Before diving into the encoding, it helps to understand where CBOR sits in the binary serialization landscape:
| Aspect | CBOR | JSON | MessagePack |
|---|---|---|---|
| Standard | ✅ IETF RFC 8949 | ✅ ECMA-404/RFC 8259 | ❌ Not IETF/ISO |
| Schema required | ❌ No | ❌ No | ❌ No |
| Binary data | ✅ Native byte string | ❌ Base64 only | ✅ Native bin type |
| Typed numbers | ✅ Half/float/double | ❌ Floating point only | ✅ int/float types |
| Semantic tags | ✅ Yes (tags 0-999) | ❌ No | ⚠️ Ext types |
| Indefinite length | ✅ Yes | ❌ No | ❌ No |
| Self-describing | ✅ Tag 55799 | ✅ Yes | ❌ No magic |
| IoT protocol use | ✅ CoAP, CTAP2 | ❌ Too large | ❌ Not standardized |
| Size vs JSON | ~50-70% | 100% | ~60-80% |
CBOR's key advantage over MessagePack is the IETF standardization — for security-critical systems (FIDO2, WebAuthn, COSE), an IETF RFC is a hard requirement. CBOR and MessagePack have similar encoding efficiency, but CBOR's semantic tag system and indefinite-length encoding give it additional flexibility.
CBOR Encoding: Major Types
Every CBOR data item begins with an initial byte that encodes the major type (3 bits) and additional information (5 bits):
Initial byte: MMMAAAAA
MMM = Major type (0-7)
AAAAA = Additional info (0-31)
Major Type 0: Unsigned Integer
Value 0-23: Single byte (0x00-0x17)
Value 24-255: 1-byte header (0x18) + 1 byte value
Value 256-65535: 1-byte header (0x19) + 2 byte value (big-endian)
Value > 65535: 0x1A (4 bytes) or 0x1B (8 bytes)
Examples:
0 → 0x00
23 → 0x17 (max single-byte unsigned)
24 → 0x18 0x18
255 → 0x18 0xFF
1000 → 0x19 0x03 0xE8
Major Type 1: Negative Integer
Negative integers are encoded as -(1 + n) where n is the unsigned value:
-1 → 0x20 (-(1 + 0))
-10 → 0x29 (-(1 + 9))
-100 → 0x38 0x63 (-(1 + 99))
Major Type 2: Byte String
h'0102030405' (5 bytes) → 0x45 0x01 0x02 0x03 0x04 0x05
^^
Major type 2 (010) + additional 5 (00101)
Major Type 3: Text String (UTF-8)
"hello" → 0x65 0x68 0x65 0x6C 0x6C 0x6F
^^
Major type 3 (011) + length 5 (00101)
Major Type 4: Array
[1, 2, 3] → 0x83 0x01 0x02 0x03
^^
Major type 4 (100) + count 3 (00011)
Major Type 5: Map (Key-Value Pairs)
{"a": 1} → 0xA1 0x61 0x61 0x01
^^ ^^^^^^^^^^ ^^
Map Text "a" Int 1
1 pair
Major Type 6: Tagged Item
Tags add semantic meaning to the following data item:
Tag 0 (datetime string): 0xC0 0x74 "2024-03-15T10:30:00Z"
Tag 1 (epoch timestamp): 0xC1 0x1A 0x65 0xF4 0x68 0x70 (uint32 Unix timestamp)
Tag 2 (bignum): 0xC2 0x48 <8 bytes>
Tag 37 (UUID): 0xD8 0x25 0x50 <16 UUID bytes>
Tag numbers 0-999 are standardized by IANA. Common tags:
| Tag | Semantics |
|---|---|
| 0 | Date/time string (RFC 3339) |
| 1 | Epoch-based date/time (numeric) |
| 2 | Positive bignum (arbitrary precision) |
| 3 | Negative bignum |
| 4 | Decimal fraction |
| 5 | Bigfloat |
| 16 | COSE_Encrypt0 |
| 18 | COSE_Sign1 |
| 24 | Embedded CBOR |
| 32 | URI |
| 33 | Base64url |
| 37 | UUID |
| 55799 | Self-described CBOR (magic number, enables detection) |
Major Type 7: Floats and Special Values
false → 0xF4
true → 0xF5
null → 0xF6
undefined → 0xF7
float16 (half-precision): 0xF9 + 2 bytes
float32 (single): 0xFA + 4 bytes
float64 (double): 0xFB + 8 bytes
break code (indefinite end): 0xFF
CBOR's half-precision float (16-bit, float16) is particularly useful for sensor data and ML inference — many IoT measurements and neural network activations fit in float16 without loss of relevant precision, cutting bandwidth in half vs float32.
Indefinite-Length Encoding
CBOR's indefinite-length encoding allows streaming serialization when the total length is not known in advance:
Indefinite array:
0x9F ← start indefinite array
0x01 ← item 1
0x02 ← item 2
0x82 0x03 0x04 ← item 3 (a nested [3, 4])
0xFF ← break (end of indefinite array)
Indefinite map:
0xBF ← start indefinite map
0x61 0x61 0x01 ← "a": 1
0x61 0x62 0x02 ← "b": 2
0xFF ← break
Indefinite byte string (chunked):
0x5F ← start chunked bytes
0x44 0xDE 0xAD 0xBE 0xEF ← chunk 1 (4 bytes)
0x43 0xCA 0xFE 0xBA ← chunk 2 (3 bytes)
0xFF ← break
Total: deadbeef cafeba
This is essential for devices with limited RAM that cannot buffer the entire output before sending — a microcontroller can begin streaming a CBOR-encoded sensor reading before all measurements are collected.
CBOR Diagnostic Notation
CBOR has a human-readable diagnostic notation for debugging:
# JSON equivalent
{
"id": 42,
"name": "Sensor-7",
"readings": [23.5, 24.1, 22.8],
"ts": "2024-03-15T10:00:00Z",
"data": h'0102030405',
"uuid": 37(h'550e8400e29b41d4a716446655440000')
}
# CBOR diagnostic notation
{
"id": 42,
"name": "Sensor-7",
"readings": [23.5_2, 24.1_2, 22.8_2], # _2 = float16
"ts": 0("2024-03-15T10:00:00Z"), # tag 0
"data": h'0102030405', # byte string
"uuid": 37(h'550e8400e29b41d4a716446655440000') # tagged UUID
}
Python: cbor2
import cbor2
import uuid
from datetime import datetime, timezone
# Basic encoding/decoding (mirrors json.dumps/json.loads API)
data = {
"id": 42,
"name": "Alice",
"scores": [99, 87, 94],
"active": True,
"balance": None,
"tags": {"admin", "editor"}, # set → CBOR array
}
encoded = cbor2.dumps(data)
print(f"CBOR: {len(encoded)} bytes")
print(f"Hex: {encoded.hex()}")
decoded = cbor2.loads(encoded)
assert decoded["id"] == 42
# Tagged types
sensor_data = {
"timestamp": datetime(2024, 3, 15, 10, 0, tzinfo=timezone.utc), # tag 1
"device_id": uuid.uuid4(), # tag 37
"readings": cbor2.CBORTag(40, [23.5, 24.1, 22.8]), # custom tag 40 = float16 array
}
encoded = cbor2.dumps(sensor_data)
# Binary data
image_header = bytes([0x89, 0x50, 0x4E, 0x47]) # PNG magic bytes
msg = {"type": "image", "header": image_header, "width": 1920, "height": 1080}
cbor_msg = cbor2.dumps(msg)
back = cbor2.loads(cbor_msg)
assert back["header"] == image_header # bytes, not base64
# Indefinite-length streaming
import io
buf = io.BytesIO()
with cbor2.CBOREncoder(buf) as encoder:
encoder.encode_array_start(None) # indefinite array
for i in range(1000):
encoder.encode({"seq": i, "val": i * 1.5})
encoder.encode_break()
# File I/O
with open('sensor_log.cbor', 'wb') as f:
cbor2.dump(data, f)
with open('sensor_log.cbor', 'rb') as f:
recovered = cbor2.load(f)
CBOR in Security: COSE
CBOR Object Signing and Encryption (COSE, RFC 8152) uses CBOR as its data model for all security structures:
# COSE_Sign1 structure (simplified)
# [protected_header, unprotected_header, payload, signature]
import cbor2
protected = cbor2.dumps({1: -7}) # alg: ES256
unprotected = {4: b'key-id-01'} # kid
payload = b'Hello, COSE!'
signature = b'\x00' * 64 # placeholder
cose_sign1 = cbor2.CBORTag(18, [
protected,
unprotected,
payload,
signature,
])
encoded = cbor2.dumps(cose_sign1)
CBOR in WebAuthn and CTAP2
WebAuthn and CTAP2 (the FIDO2 authenticator protocol) use CBOR for all authenticator data structures. When you use a hardware security key or Touch ID for passwordless authentication, the authenticator sends CBOR-encoded responses:
authenticatorMakeCredential response (CTAP2):
{
01: aaguid_bytes, # authenticator AAGUID
02: credentialId_bytes, # new credential ID
03: credentialPublicKey, # COSE-encoded public key
04: attestationStatement, # attestation proof
05: extensions, # optional extensions
}
CBOR in IoT: CoAP
The Constrained Application Protocol (CoAP) is HTTP for constrained devices (microcontrollers with < 100 KB RAM). CoAP uses CBOR for its payloads, enabling rich structured data exchange on devices that cannot run a full HTTP+JSON stack:
# Simulated CoAP sensor response body
import cbor2
sensor_response = cbor2.dumps({
"temp": cbor2.CBORTag(4, [-2, 2350]), # decimal 23.50°C
"hum": cbor2.CBORTag(4, [-1, 645]), # decimal 64.5%
"bat": 87, # 87%
"ts": 1710500000, # Unix timestamp
})
# 28 bytes vs ~90 bytes for equivalent JSON
print(f"CoAP payload: {len(sensor_response)} bytes")
Conclusion
CBOR's combination of IETF standardization, JSON-compatible type system, native binary data, semantic tags, and indefinite-length encoding makes it uniquely suited for security and IoT applications where JSON is too large and MessagePack lacks the formal standards backing. If you are building anything that touches FIDO2/WebAuthn, COSE-based security tokens, CoAP IoT sensors, or any protocol that requires an IETF-standardized binary encoding with rich extensibility, CBOR is the format designed for exactly that purpose.
Related conversions
Frequent conversions across the catalogue: