CBOR: Concise Binary Object Representation for IoT and Embedded Systems
What Is CBOR?
CBOR — Concise Binary Object Representation — is a binary data serialization format standardized as RFC 7049 (2013) and updated by RFC 8949 (2020) by the Internet Engineering Task Force (IETF). Designed by Carsten Bormann and Paul Hoffman, CBOR was created to be a compact, self-describing binary format that maps directly to JSON's data model (numbers, strings, arrays, maps, booleans, null) while being:
- Smaller: typically 20–40% smaller than JSON for the same data
- Faster to parse: no text-to-number conversion, no string escaping/unescaping
- Richer: native support for binary data (byte strings), indefinite-length encoding, numeric tags for semantic types (dates, URIs, UUIDs, bignum, etc.)
- Streamable: can encode data of unknown length without a length prefix
CBOR is the serialization format of choice for:
- IoT and constrained devices (IETF COSE, OSCORE, LwM2M, SUIT firmware updates)
- CBOR-based protocols (CoAP, CDDL schema language, COSE signing/encryption)
- WebAuthn/FIDO2: credential responses are CBOR-encoded
- Apple PassKit and Android Credential Manager: attestation documents in CBOR
- IETF token formats: CWT (CBOR Web Token, the binary JWT equivalent)
Data Model
CBOR's type system maps precisely to JSON, plus additional types:
| CBOR Major Type | Values |
|---|---|
| 0 — Unsigned Integer | 0 to 2^64−1 |
| 1 — Negative Integer | −1 to −2^64 |
| 2 — Byte String | arbitrary binary data |
| 3 — Text String | UTF-8 text |
| 4 — Array | ordered list of data items |
| 5 — Map | key-value pairs (keys can be any type) |
| 6 — Tag | semantic annotation for a data item |
| 7 — Floating Point | float16, float32, float64, simple values (true, false, null, undefined) |
The tag type (major type 6) is CBOR's extensibility mechanism — predefined tags assign semantic meaning:
| Tag | Meaning |
|---|---|
| 0 | Standard date/time string (ISO 8601) |
| 1 | Numeric date/time (Unix timestamp, int or float) |
| 2 | Unsigned bignum (byte string → arbitrary precision int) |
| 3 | Negative bignum |
| 4 | Decimal fraction (mantissa + exponent as array) |
| 21 | Base64url-encoded data |
| 22 | Base64-encoded data |
| 23 | Base16/hex-encoded data |
| 32 | URI |
| 33 | Base64url text |
| 37 | UUID (16-byte fixed-length byte string) |
| 55799 | Self-described CBOR (magic number for file identification) |
Encoding Mechanics
CBOR encodes each data item as a 1-byte initial byte followed by optional additional bytes:
- Bits 7-5 of the initial byte: major type (3 bits, values 0–7)
- Bits 4-0: additional info (5 bits)
Additional info values 0–23 carry the value directly in the initial byte (no additional bytes). Values 24–27 indicate that 1, 2, 4, or 8 additional bytes carry the value.
Example Encoding
JSON {"temp": 23.5, "unit": "C", "ok": true} → CBOR:
A3 # map(3) — 3 key-value pairs
64 # text(4) — 4-char string
74656D70 # "temp"
F9 41BC # float16(23.5) — 3 bytes vs JSON's 4 bytes for "23.5"
64 # text(4)
756E6974 # "unit"
61 # text(1)
43 # "C"
62 # text(2)
6F6B # "ok"
F5 # true (simple value)
Total: 19 bytes vs. JSON's 38 bytes ({"temp":23.5,"unit":"C","ok":true}) — about 50% smaller, and float16 preserves the value exactly (no rounding from decimal string).
Deterministic Encoding (dCBOR and RFC 8949 Core Det)
CBOR allows multiple valid encodings for the same logical value (e.g., an integer 5 could be encoded in 1, 2, 3, or 5 bytes). For cryptographic applications (digital signatures, hashing), deterministic encoding is essential:
RFC 8949 §4.2 "Core Deterministic Encoding Requirements":
- Integers: smallest possible encoding
- Maps: keys sorted by encoded byte length, then lexicographically
- No indefinite-length encoding
- Floating-point: prefer integer encoding when value is an integer (e.g., 1.0 → integer 1)
dCBOR (Deterministic CBOR, draft-mcnally-deterministic-cbor) adds stricter rules for application-level determinism.
CBOR in Python: cbor2
import cbor2
# Basic encode/decode
data = {
'temperature': 23.5,
'unit': 'Celsius',
'active': True,
'readings': [22.1, 23.5, 24.0, 23.8],
'raw_bytes': b'\x01\x02\x03\xff'
}
# Encode to bytes
encoded = cbor2.dumps(data)
print(len(encoded)) # much smaller than json.dumps
# Decode
decoded = cbor2.loads(encoded)
print(decoded['temperature']) # 23.5
# File I/O
with open('telemetry.cbor', 'wb') as f:
cbor2.dump(data, f)
with open('telemetry.cbor', 'rb') as f:
loaded = cbor2.load(f)
# Compare sizes
import json
json_size = len(json.dumps(data).encode())
cbor_size = len(cbor2.dumps(data))
print(f"JSON: {json_size} bytes, CBOR: {cbor_size} bytes, ratio: {cbor_size/json_size:.2f}")
# Tagged values
from cbor2 import CBORTag
import datetime
# Encode a datetime with CBOR tag 1 (numeric timestamp)
ts = CBORTag(1, 1700000000.5)
encoded_ts = cbor2.dumps(ts)
# CBOR encoder handles datetime natively
dt = datetime.datetime(2024, 1, 15, 12, 30, 0, tzinfo=datetime.timezone.utc)
encoded_dt = cbor2.dumps(dt) # automatically uses tag 1 (numeric) or 0 (string)
CBOR vs. JSON vs. MessagePack
| Feature | JSON | CBOR | MessagePack |
|---|---|---|---|
| Format | Text (UTF-8) | Binary | Binary |
| Self-describing | Yes | Yes | Partial |
| Binary data | Base64 (overhead) | Native byte string | Native byte string |
| Integer range | Limited (float64) | 64-bit + bignum | 64-bit |
| Floating point | 64-bit only | 16/32/64-bit | 32/64-bit |
| Semantic tags | No | Yes (extensible) | No |
| Deterministic encoding | No | Yes (Core Det) | No |
| Streaming / indefinite length | No | Yes | No |
| IETF standard | RFC 8259 | RFC 8949 | No RFC |
| Map key types | String only | Any type | Any type |
| Canonical form | No | RFC 8949 §4.2 | No |
| Typical size vs JSON | 100% | 60–80% | 70–85% |
COSE: CBOR Object Signing and Encryption
COSE (RFC 9052) is the cryptographic layer built on CBOR, equivalent to what JOSE (JSON Web Signature / JSON Web Encryption) is to JSON:
COSE_Sign1: single-signature signed CBOR objectCOSE_Encrypt0: encrypted CBOR object (AES-GCM, ChaCha20-Poly1305)COSE_Mac0: CBOR object with message authentication code (HMAC)
WebAuthn authenticator responses are COSE-encoded. Apple's attestation certificates embed CBOR+COSE. IETF SUIT (firmware update manifest) uses COSE-signed CBOR.
from cose.messages import Sign1Message
from cose.keys import CoseKey
from cose.algorithms import Es256
import cbor2
# Create a COSE_Sign1 message
payload = cbor2.dumps({'cmd': 'update', 'version': '2.1.0', 'size': 49152})
msg = Sign1Message(phdr={'alg': Es256}, payload=payload)
msg.key = private_key # EC P-256 key
encoded = msg.encode() # CBOR-encoded signed message
CWT: CBOR Web Token (Binary JWT)
CWT (RFC 8392) is the CBOR equivalent of JWT (JSON Web Token):
JWT: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6ImFsaWNlIn0.signature
CWT: D28443A10126A104524173796D6D6574726963454344... (much shorter binary)
CWT claim keys are integers rather than strings (iss=1, sub=2, exp=4, iat=6…), saving bytes on the wire. Essential for constrained IoT devices where JWT overhead is significant.
Diagnostic Notation
CBOR has a human-readable diagnostic notation (not a serialization format, just for documentation):
{
"temp": 23.5_1, / _1 = float16 /
"unit": "C",
"ts": 1(1700000000), / tag(1, timestamp) /
"data": h'DEADBEEF', / byte string in hex /
"tags": ["a", "b", "c"]
}
Tools like cbor.me or the cbor-diag tool render CBOR bytes as diagnostic notation.
Practical Tips
- Use CBOR where JSON is too verbose: sensor telemetry, firmware manifests, WebAuthn credentials, network protocol payloads
- Use tag 55799 to self-describe CBOR files (
D9D9F7magic bytes) — allows automatic detection without file extension - For cryptographic use: always enforce deterministic encoding (RFC 8949 §4.2 or dCBOR) before signing or hashing
- Float16 is lossy: only use it when precision is not critical (sensor readings, percentages) — float32 or float64 for financial/scientific data
- Map key type: prefer integer keys over string keys in CBOR maps when the schema is fixed (saves bytes) — this is what CWT and COSE do
- Streaming encoder: use indefinite-length arrays/maps when encoding data of unknown size without buffering
CBOR's compact binary encoding, native byte string support, semantic tags, deterministic encoding, and IETF standardization make it the format of choice for any application where JSON's text overhead is unacceptable.
Related conversions
Frequent conversions across the catalogue: