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.
Algorithm
Section titled “Algorithm”canonical_json(value) serializes a JSON value with these rules:
- Objects: keys sorted lexicographically (code-unit order), recursively.
Output is
{"k1":v1,"k2":v2}— no whitespace anywhere. - Arrays: order preserved exactly as given.
[v1,v2]. - 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. - 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).
- Booleans / null:
true,false,null. - 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.
Reference implementation
Section titled “Reference implementation”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.)
Test vector
Section titled “Test vector”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:
c348dc7edace101542c7f772e6f4c9b3f9f2a219d5cbdcb77bf98722b7bbd651A 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.
Relationship to RFC 8785 (JCS)
Section titled “Relationship to RFC 8785 (JCS)”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.