Skip to content

Canonical JSON

Every hash in SP/1 — fingerprints, audit rows, certificate subject hashes — is computed over canonical JSON. Two implementations that disagree by one byte produce useless evidence, so this algorithm is normative.

canonical_json(value) serializes a JSON value with these rules:

  1. Objects: keys sorted lexicographically (code-unit order), recursively. Output is {"k1":v1,"k2":v2} — no whitespace anywhere.
  2. Arrays: order preserved exactly as given. [v1,v2].
  3. Strings: standard JSON string encoding (as produced by ECMA-404 / JSON.stringify): " and \ escaped, control characters escaped; non-ASCII characters are emitted literally as UTF-8, NOT \u-escaped.
  4. Numbers: MUST be finite. Serialized in ECMAScript number-to-string form. Implementations SHOULD avoid non-integer numbers in hashed structures entirely (SP/1 core structures use only strings and string arrays).
  5. Booleans / null: true, false, null.
  6. Rejected: undefined, functions, non-finite numbers, and cyclic references MUST cause an error — never a silent substitution.

The hash input is the UTF-8 encoding of the resulting string.

function canonicalJson(value: unknown): string {
if (value === null) return 'null';
switch (typeof value) {
case 'string': return JSON.stringify(value);
case 'number':
if (!Number.isFinite(value)) throw new Error('non-finite number');
return JSON.stringify(value);
case 'boolean': return value ? 'true' : 'false';
case 'object': break;
default: throw new Error(`unsupported type: ${typeof value}`);
}
if (Array.isArray(value)) {
return `[${value.map(canonicalJson).join(',')}]`;
}
const keys = Object.keys(value as object).sort();
return `{${keys
.map((k) => `${JSON.stringify(k)}:${canonicalJson((value as never)[k])}`)
.join(',')}}`;
}

(Cycle detection omitted for brevity; conforming implementations MUST detect cycles.)

Input (any key order):

{ "b": 1, "a": { "z": true, "y": [3, 2, 1] }, "c": "héllo \"quoted\"" }

Canonical form (exact bytes, UTF-8):

{"a":{"y":[3,2,1],"z":true},"b":1,"c":"héllo \"quoted\""}

SHA-256 of the canonical form:

c348dc7edace101542c7f772e6f4c9b3f9f2a219d5cbdcb77bf98722b7bbd651

A conforming implementation MUST reproduce this digest exactly. Note the array [3,2,1] is not sorted, é is a literal two-byte UTF-8 sequence, and the embedded quotes are backslash-escaped.

SP/1 canonical JSON is deliberately a strict subset in spirit of RFC 8785: for structures containing only strings, string arrays, and objects (all SP/1 core structures), the two produce identical output. SP/1 does not require full JCS number canonicalization because hashed structures avoid non-integer numbers by construction.