SearchCtrlK

  • ↑↓navigate
  • ↵open
  • escclose

Can’t find it? Contact us

@sdxc/json-canonical

RFC 8785 JSON Canonicalization Scheme: one byte-exact serialization for hashing and signing JSON

Terminal
npm install @sdxc/json-canonical
Terminal
pnpm add @sdxc/json-canonical
Terminal
yarn add @sdxc/json-canonical
Terminal
bun add @sdxc/json-canonical
Depends on
remix

RFC 8785 JSON Canonicalization Scheme: one byte-exact serialization for hashing and signing JSON.

JSON.stringify writes members in insertion order, so two equal objects can produce different text and different hashes. The JSON Canonicalization Scheme (JCS) fixes every choice: no whitespace, object members sorted by the UTF-16 code units of their names at every depth, and numbers and strings written exactly as ECMAScript writes them. Input must be I-JSON: no NaN, Infinity or lone surrogates.

Installation

Terminal
npm add @sdxc/json-canonical

Fallible calls return @sdxc/result values.

Usage

typescript
import { canonicalize } from "@sdxc/json-canonical";

canonicalize({ b: [4.5, "€"], a: { d: true, c: null } });
// success('{"a":{"c":null,"d":true},"b":[4.5,"€"]}')

canonicalize(JSON.parse('{"n": 1E30, "x": 0.000001}'));
// success('{"n":1e+30,"x":0.000001}')

Refused input

typescript
import { canonicalize } from "@sdxc/json-canonical";
import { isFailure } from "@sdxc/result";

let result = canonicalize({ price: NaN });
if (isFailure(result)) result.error.pointer; // "/price"

API

canonicalize(value: unknown): Result<string, CanonicalJsonError>

The canonical JSON text of value. Encode it with TextEncoder before hashing or signing, as RFC 8785 specifies UTF-8. Plain objects (including Object.create(null)), arrays, strings, finite numbers, booleans and null are accepted; -0 is written 0.

CanonicalJsonError

The failure for NaN, Infinity, a lone surrogate in a string or member name, undefined, a sparse array hole, a cycle, or any value other than the JSON types above. pointer is the RFC 6901 JSON Pointer to the offending value.

Pattern: Hash A JSON Document

typescript
import { canonicalize } from "@sdxc/json-canonical";
import { unwrap } from "@sdxc/result";

async function digest(value: unknown): Promise<string> {
	let bytes = new TextEncoder().encode(unwrap(canonicalize(value)));
	let hash = await crypto.subtle.digest("SHA-256", bytes);
	return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
}

(await digest({ a: 1, b: 2 })) === (await digest({ b: 2, a: 1 })); // true

Pattern: Sign A Payload Both Sides Can Re-Serialize

The sender signs the canonical bytes and sends the JSON in any form; the receiver parses it, canonicalizes again and verifies against the same bytes.

typescript
import { canonicalize } from "@sdxc/json-canonical";
import { isFailure } from "@sdxc/result";

async function verify(body: string, signature: ArrayBuffer, key: CryptoKey): Promise<boolean> {
	let canonical = canonicalize(JSON.parse(body));
	if (isFailure(canonical)) return false;
	let bytes = new TextEncoder().encode(canonical.data);
	return crypto.subtle.verify("Ed25519", key, signature, bytes);
}

Command line

Terminal
bunx @sdxc/json-canonical --help

With @sdxc/kit installed, sdxc pkg json-canonical … runs the same command line, using the version your project installs.

canonicalize prints a JSON file's canonical bytes (or stdin's, when the file is - or absent) with no trailing newline; --newline adds one.

Terminal
bunx @sdxc/json-canonical canonicalize payload.json

hash prints the digest of those bytes in hex, or unpadded base64url with --base64url; --alg takes sha-256 (the default), sha-384 or sha-512.

Terminal
bunx @sdxc/json-canonical hash payload.json --alg sha-512 --base64url

Text that is not JSON exits 2, and JSON outside I-JSON exits 1 naming its JSON Pointer.

API

Can’t find it? Contact us