HomeJournalThis post

CBOR vs JSON for Signed Tool Envelopes

Use a mutation corpus to compare canonical bytes, numeric constraints, duplicate keys, size, debugging, and signature verification.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

CBOR vs JSON for a signed tool envelope is a decision about canonical bytes and interoperable meaning, not merely compact binary versus readable text. This guide attacks both encodings with numeric, key-order, duplicate-key, mutation, size, debugging, and signature-verification fixtures before choosing a profile.

CBOR vs JSON starts with the signed meaning

Define the envelope before choosing bytes: version, operation, resource, subject or actor reference, issued-at time, expiry, nonce, idempotency key, arguments, and protected algorithm and key identifiers. Specify which fields are signed, which are transport metadata, and which verifier policy assigns authority. CBOR vs JSON cannot repair an ambiguous tool contract; it can only serialize the meaning you already constrained.

Keep the signing input independent from object insertion order, locale, whitespace, and runtime-specific number printing. Archive one annotated golden envelope with its decoded value, canonical bytes, digest, signature, and expected denial mutations. A signed tool payload becomes interoperable only when every producer and verifier can derive the same bytes from the same allowed data model.

Assign a media type and profile identifier so parsers select the intended rules before decoding. Content sniffing or a generic application/octet-stream label invites peers to apply incompatible assumptions. The CBOR vs JSON decision belongs in that explicit profile.

MutationJSON profileCBOR profileExpected
Reorder keysSame bytesSame bytesVerify
Duplicate keyRejectRejectDeny
Non-finiteRejectReject/profileDeny
Flip valueNew digestNew digestDeny
Unknown fieldPolicyPolicyDeny/profile
Figure 1: The failure table tests meaning and byte stability independently.

Determinism is a profile, not a default

RFC 8949 defines CBOR and deterministic encoding considerations. A generic CBOR encoder may select different valid integer widths, map orders, or floating representations unless configured to a deterministic profile. Therefore “we use CBOR” does not imply stable signature bytes. Name the deterministic CBOR rules, supported tags, map-key types, number domain, and duplicate-key behavior.

JSON has the same trap in a familiar costume. Ordinary JSON permits insignificant whitespace and arbitrary object member order, so serializing a parsed object with a default function is not a signature specification. CBOR vs JSON should compare explicitly profiled encoders. Reject inputs outside the profile before canonicalization rather than guessing how to normalize values a peer may interpret differently.

Use official vectors plus local envelopes. Standards vectors prove the encoder's general behavior, while product vectors prove the narrow field set, protected metadata, and policy your tools actually execute. Archive both sets beside the implementation without reinterpretation.

Constrain JSON numbers before canonicalization

RFC 8785 specifies the JSON Canonicalization Scheme, including deterministic property sorting and number serialization grounded in the I-JSON model. Tool domains should still avoid unconstrained numbers. Use integer minor units for money, bounded integers for counts, and strings for identifiers that might otherwise exceed interoperable numeric precision. Reject non-finite values, negative zero where meaning is unclear, and values outside the declared safe range.

JSON canonicalization makes a supported JSON value byte-stable; it does not decide whether 100 means cents, dollars, milliseconds, or a dangerous maximum. Validate the envelope with the same contract described in JSON Schema vs Zod, then canonicalize only the validated representation. This order prevents a signature from blessing values the executor later coerces into different meaning.

Require producers to reject unsafe integers at construction time, not only verifiers at receipt. Early failure keeps invalid signed objects out of queues, caches, and audit systems that may interpret them differently.

Equivalent values become profile-specific canonical bytesA shared tool value branches through JCS text bytes and deterministic CBOR bytes, each producing a digest verified against protected metadata.tool valueJCS UTF-8deterministic CBORverify
  • Value: validated envelope model
  • JCS: canonical UTF-8 JSON
  • dCBOR: declared deterministic profile
  • Verify: algorithm, key, bytes, and policy
Figure 2: Different encodings can be safe when each has one exact byte contract.

Reject duplicate keys before object construction

A parser that keeps the first duplicate and one that keeps the last can verify and execute different values from the same input. Detect duplicate map or object keys at the byte-parser boundary before converting into a host-language object that has already discarded evidence. Apply the rule recursively. Include visually confusable Unicode keys and normalization cases, but do not normalize identifiers unless the profile explicitly requires it.

CBOR vs JSON must also define unknown-field behavior. Versioned extension points can be safe when signatures cover them and old executors reject operations they do not understand. Silently dropping an unknown maxAmount before verification is unsafe because the signer and executor may disagree about its effect. Prefer a closed envelope plus a namespaced, policy-reviewed extension map when forward evolution is necessary.

Fuzz the byte parser under memory and nesting limits. Canonicalization occurs only after safe parsing, so a tiny signed-request endpoint must still reject deeply nested or oversized hostile inputs cheaply.

Keep signature containers and payload profiles distinct

RFC 9052 specifies COSE structures for CBOR-based signing and encryption. A COSE envelope carries protected headers and a payload, but applications still define the payload schema, deterministic requirements, key policy, and external associated data. Likewise, a JSON deployment needs an explicit signature construction rather than signing an arbitrary pretty-printed body.

Compare with HTTP Message Signatures when the protected meaning includes method, target URI, or headers beyond the tool object. CBOR vs JSON may be the wrong boundary if transport components are part of the authorization decision. Sign the smallest complete semantic unit, cover algorithm and key identifiers, and prevent an attacker from moving a valid payload to a different operation or resource.

Bind the payload profile version into protected signature metadata. An attacker must not be able to move identical bytes under a different decoder whose tag, number, or unknown-field semantics grant new meaning. Reject unknown versions before payload use.

  1. 1Parse

    Reject malformed, duplicate, or unsupported values

  2. 2Validate

    Apply the versioned tool-envelope profile

  3. 3Canonicalize

    Derive exact JCS or deterministic CBOR bytes

  4. 4Verify

    Check signature, freshness, replay, and policy

Figure 3: Verification parses once, canonicalizes once, and executes the verified value.

Attack canonical bytes with a mutation corpus

Start from golden values and encode them in at least two independent implementations. Assert identical canonical bytes within each profile, then reorder keys, alter whitespace, change integer widths, swap equivalent floating forms, introduce duplicates, flip one argument, add an unknown field, expire the timestamp, and replay the nonce. Some harmless representation changes should preserve canonical bytes; semantic changes must produce a new digest or fail parsing.

The mutation corpus below models JSON canonicalization for a deliberately tiny integer-and-string profile, not full RFC 8785 or CBOR. Production code should use reviewed implementations and official test vectors. Its bounded purpose is to prove the invariants: key order cannot affect bytes, unsupported values are rejected, and one semantic mutation invalidates the signature. Extend the same corpus to every language that signs or verifies.

Have an incident responder decode a captured envelope using only repository tooling and documentation. The timed exercise exposes missing keys, profile knowledge, and redaction guidance before production pressure arrives.

This teaching fixture intentionally supports only null, booleans, safe integers, strings, arrays, and plain objects; it is not a substitute for a standards-complete JCS or deterministic CBOR library.

Runnable artifact — canonical-byte-mutations.test.mjs

import assert from "node:assert/strict";import{createHmac,timingSafeEqual}from"node:crypto";
const canon=x=>{if(x===null||typeof x==="boolean"||typeof x==="string")return JSON.stringify(x);if(Number.isSafeInteger(x))return String(x);if(Array.isArray(x))return"["+x.map(canon).join(",")+"]";if(x&&Object.getPrototypeOf(x)===Object.prototype)return"{"+Object.keys(x).sort().map(k=>JSON.stringify(k)+":"+canon(x[k])).join(",")+"}";throw Error("unsupported")};
const mac=x=>createHmac("sha256","fixture-key").update(canon(x)).digest();const a={op:"read",args:{limit:7,id:"x"}},b={args:{id:"x",limit:7},op:"read"};assert.equal(canon(a),canon(b));assert.equal(timingSafeEqual(mac(a),mac(b)),true);assert.equal(timingSafeEqual(mac(a),mac({...a,op:"write"})),false);assert.throws(()=>canon({x:1.25}));console.log("PASS: canonical bytes are stable and semantic mutations fail");

Run node canonical-byte-mutations.test.mjs. Expected receipt: PASS: canonical bytes are stable and semantic mutations fail.

Measure size in the complete exchange

Compare canonical payload bytes, signature container, base64 or other transport expansion, headers, compression, and total request size across representative envelopes. Tiny payload benchmarks can exaggerate encoding differences, while large binary tensors may dominate either format. Measure encode, parse, canonicalize, sign, and verify time separately on the actual server and agent runtimes; never infer production latency from byte count alone.

Debugging cost belongs in the same ledger. JSON can be inspected with common tools, while CBOR may need diagnostic notation and specialized capture support. A smaller COSE envelope can still be a poor choice if incident responders cannot safely decode it. Preserve redacted decoding tools and golden fixtures in the repository, and train the operational path before choosing compactness as a headline benefit.

Benchmark batches and single messages separately. A gateway that verifies many tool calls may care about allocation and key lookup more than the few dozen bytes saved by one encoding.

Add freshness, replay, and key policy

Canonical bytes and a valid signature say that a key approved those bytes; they do not say the message is fresh or authorized now. Verify issued-at and expiry against bounded clock skew, consume a nonce or idempotency key atomically, validate issuer and audience, and map the key to an allowed tool operation. Webhook signatures need replay defenses develops that lifecycle in detail.

Log the envelope version, encoding profile, digest, key ID, verification outcome, freshness decision, replay result, and policy decision without storing secrets. Tie artifact signing lessons from AI model weight signing to key rotation and provenance. CBOR vs JSON is one layer of the receipt; authorization lives in the complete verifier path.

Rotate signing keys through overlapping verification windows without changing canonicalization. Encoding migration and key migration are separate risks and should not be combined unless the rollback plan covers every pairing. Name those pairings in the drill. Record the overlap explicitly.

Choose the profile your ecosystem can prove

Publish the allowed data model, canonicalization profile, signature container, test vectors, independent implementations, numeric rules, duplicate and unknown-key policy, total size, operational tooling, and migration plan. A reasonable verdict may choose JCS for small human-debuggable tool requests or deterministic CBOR with COSE for a constrained binary ecosystem. Neither conclusion generalizes beyond the measured clients and servers.

Revisit CBOR vs JSON when a new language, device, number type, or envelope field enters the trust boundary. Require it to pass the golden and hostile corpus before receiving production keys. The winning encoding is not the one with the shortest demo; it is the one for which every participant derives identical bytes, rejects ambiguous meaning, and leaves a responder enough evidence to explain a tool effect.

Keep the old verifier read-only during a staged migration. It can compare decisions and bytes, but only one named profile should authorize effects so disagreement never creates two competing acceptance paths.