CanonJson API Reference
Audience: .NET developers computing content-addressed identifiers, signing DSSE payloads, or verifying attestations against Stella Ops services.
CanonJsonis the shared canonicalization and hashing primitive behind proof bundles, score proofs, and the attestor proof chain.
Namespace: StellaOps.Canonical.Json
Assembly: StellaOps.Canonical.Json
Source: src/__Libraries/StellaOps.Canonical.Json/ (net10.0)
The package does not declare an explicit
<Version>in its.csproj, so it inherits the build-default assembly version. Treat the version above as unspecified rather than pinned to1.0.0.
Overview
The CanonJson static class provides RFC 8785-compliant JSON canonicalization and cryptographic hashing utilities for content-addressed identifiers. It ensures deterministic, reproducible JSON serialization across all environments.
CanonJson is implemented as a partial class split across several files (CanonJson.cs, CanonJson.Canonicalize.cs, CanonJson.CanonicalizeParsed.cs, CanonJson.Versioned.cs, CanonJson.Hashing.cs, CanonJson.Writer.cs, CanonJson.Options.cs).
Default serialization behavior. The object-typed overloads (
Canonicalize<T>,Serialize<T>,Hash<T>, etc.) first serialize withSystem.Text.Jsonusing these defaults:PropertyNamingPolicy = CamelCase,WriteIndented = false, andEncoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping. This means property names are camelCased before sorting. Pass a customJsonSerializerOptionsto the available overloads to change this. (Source:CanonJson.Options.cs.)
CanonVersion Class
Static class containing canonicalization version constants and utilities.
Constants
| Constant | Type | Value | Description |
|---|---|---|---|
V1 | string | "stella:canon:v1" | Version 1: RFC 8785 JSON canonicalization |
VersionFieldName | string | "_canonVersion" | Field name for version marker (underscore ensures first position) |
Current | string | V1 | Current default version for new hashes |
Methods
IsVersioned
public static bool IsVersioned(ReadOnlySpan<byte> canonicalJson)
Detects if canonical JSON includes a version marker.
Parameters:
canonicalJson: UTF-8 encoded canonical JSON bytes
Returns: true if the JSON starts with {"_canonVersion":, false otherwise
Example:
var json = """{"_canonVersion":"stella:canon:v1","foo":"bar"}"""u8;
bool versioned = CanonVersion.IsVersioned(json); // true
var legacy = """{"foo":"bar"}"""u8;
bool legacyVersioned = CanonVersion.IsVersioned(legacy); // false
ExtractVersion
public static string? ExtractVersion(ReadOnlySpan<byte> canonicalJson)
Extracts the version string from versioned canonical JSON.
Parameters:
canonicalJson: UTF-8 encoded canonical JSON bytes
Returns: The version string (e.g., "stella:canon:v1") or null if not versioned
Example:
var json = """{"_canonVersion":"stella:canon:v1","foo":"bar"}"""u8;
string? version = CanonVersion.ExtractVersion(json); // "stella:canon:v1"
CanonJson Class
Static class providing JSON canonicalization and hashing methods.
Serialization Methods
Serialize
public static string Serialize<T>(T obj)
public static string Serialize<T>(T obj, JsonSerializerOptions options)
Canonicalizes an object and returns the canonical JSON as a UTF-8 string (the string form of Canonicalize<T>). The optional options overload controls the initial serialization. (Source: CanonJson.cs.)
Returns: Canonical JSON string (no version marker).
Example:
var obj = new { z = 3, a = 1 };
string canonical = CanonJson.Serialize(obj);
// Result: {"a":1,"z":3}
Canonicalization Methods
Canonicalize
public static byte[] Canonicalize<T>(T obj)
Canonicalizes an object to RFC 8785 JSON without version marker (legacy format).
Parameters:
obj: The object to canonicalize
Returns: UTF-8 encoded canonical JSON bytes
Example:
var obj = new { z = 3, a = 1 };
byte[] canonical = CanonJson.Canonicalize(obj);
// Result: {"a":1,"z":3}
An overload
Canonicalize<T>(T obj, JsonSerializerOptions options)is also available; it serializes with the supplied options instead of the defaults. (Source:CanonJson.Canonicalize.cs.)
CanonicalizeVersioned
public static byte[] CanonicalizeVersioned<T>(T obj, string version = CanonVersion.Current)
public static byte[] CanonicalizeVersioned<T>(T obj, JsonSerializerOptions options, string version = CanonVersion.Current)
Canonicalizes an object with a version marker for content-addressed hashing.
Parameters:
obj: The object to canonicalizeoptions: (overload) JSON serializer options for the initial serializationversion: Canonicalization version (default:CanonVersion.Current)
Returns: UTF-8 encoded canonical JSON bytes with version marker
Exceptions:
ArgumentNullException: WhenversionisnullArgumentException: Whenversionis empty or whitespace
Validation is performed by
ArgumentException.ThrowIfNullOrWhiteSpace(version), so whitespace-only versions are rejected as well as empty strings.
Behavior with non-object roots: When the root value is not a JSON object (e.g. a primitive, string, or array), the value is wrapped in an object: {"_canonVersion":"<version>","_value":<root>}. (Source: CanonJson.Versioned.cs, WriteElementVersioned.)
Example:
var obj = new { z = 3, a = 1 };
byte[] canonical = CanonJson.CanonicalizeVersioned(obj);
// Result: {"_canonVersion":"stella:canon:v1","a":1,"z":3}
// With explicit version
byte[] v2 = CanonJson.CanonicalizeVersioned(obj, "stella:canon:v2");
// Result: {"_canonVersion":"stella:canon:v2","a":1,"z":3}
CanonicalizeParsedJson
public static byte[] CanonicalizeParsedJson(ReadOnlySpan<byte> jsonBytes)
public static byte[] CanonicalizeParsedJson(ReadOnlySpan<byte> jsonBytes, JavaScriptEncoder encoder)
Canonicalizes already-serialized raw UTF-8 JSON bytes (no object serialization step). Use this when you have JSON on the wire rather than a CLR object. The encoder overload lets the caller control output escaping; it throws ArgumentNullException when encoder is null. (Source: CanonJson.CanonicalizeParsed.cs.)
Returns: UTF-8 encoded canonical JSON bytes (no version marker).
Hashing Methods
Sha256Hex / Sha256Prefixed
public static string Sha256Hex(ReadOnlySpan<byte> bytes)
public static string Sha256Prefixed(ReadOnlySpan<byte> bytes)
Low-level SHA-256 helpers over arbitrary bytes. Sha256Hex returns a 64-character lowercase hex string; Sha256Prefixed returns the same value with a sha256: prefix. The Hash*/Hash*Prefixed methods below are built on these. (Source: CanonJson.Hashing.cs.)
Hash
public static string Hash<T>(T obj)
Computes SHA-256 hash of canonical JSON (legacy format, no version marker).
Parameters:
obj: The object to hash
Returns: Lowercase hex-encoded SHA-256 hash (64 characters)
Example:
var obj = new { foo = "bar" };
string hash = CanonJson.Hash(obj);
// Result: "7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b"
HashVersioned
public static string HashVersioned<T>(T obj, string version = CanonVersion.Current)
Computes SHA-256 hash of versioned canonical JSON.
Parameters:
obj: The object to hashversion: Canonicalization version (default:CanonVersion.Current)
Returns: Lowercase hex-encoded SHA-256 hash (64 characters)
Example:
var obj = new { foo = "bar" };
string hash = CanonJson.HashVersioned(obj);
// Different from legacy hash due to version marker
HashPrefixed
public static string HashPrefixed<T>(T obj)
Computes SHA-256 hash with sha256: prefix (legacy format).
Parameters:
obj: The object to hash
Returns: Hash in format sha256:<64-hex-chars>
Example:
var obj = new { foo = "bar" };
string hash = CanonJson.HashPrefixed(obj);
// Result: "sha256:7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b"
HashVersionedPrefixed
public static string HashVersionedPrefixed<T>(T obj, string version = CanonVersion.Current)
Computes SHA-256 hash with sha256: prefix (versioned format).
Parameters:
obj: The object to hashversion: Canonicalization version (default:CanonVersion.Current)
Returns: Hash in format sha256:<64-hex-chars>
Example:
var obj = new { foo = "bar" };
string hash = CanonJson.HashVersionedPrefixed(obj);
// Result: "sha256:..." (different from HashPrefixed due to version marker)
ICanonicalizable Interface
Namespace: StellaOps.Canonical.Json (source: ICanonicalizable.cs)
Interface for types that opt in to their own canonical JSON serialization and content-addressed digest.
Methods
string GetCanonicalJson(); // deterministic canonical JSON string
string ComputeDigest(); // 64-character lowercase hex SHA-256 of the canonical JSON
CanonJsonExtensions
The same file exposes static helpers that prefer an object’s own ICanonicalizable implementation when present and otherwise fall back to CanonJson.Hash:
public static string GetDigest<T>(T obj) where T : notnull; // ComputeDigest() or CanonJson.Hash(obj)
public static string GetPrefixedDigest<T>(T obj) where T : notnull; // "sha256:" + GetDigest(obj)
IJsonCanonicalizer Interface
Namespace: StellaOps.Attestor.ProofChain.Json — Assembly: StellaOps.Attestor.ProofChain (source: src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Json/IJsonCanonicalizer.cs)
This interface is not part of the
StellaOps.Canonical.Jsonassembly. It is a byte-oriented canonicalization abstraction owned by the Attestor proof-chain library and is documented here for cross-reference.
Interface for byte-in/byte-out JSON canonicalization implementations.
Methods
Canonicalize
byte[] Canonicalize(ReadOnlySpan<byte> utf8Json)
Canonicalizes UTF-8 JSON bytes per RFC 8785.
Parameters:
utf8Json: Raw UTF-8 JSON bytes to canonicalize
Returns: Canonical UTF-8 JSON bytes with sorted keys and no whitespace
CanonicalizeWithVersion
byte[] CanonicalizeWithVersion(ReadOnlySpan<byte> utf8Json, string version)
Canonicalizes UTF-8 JSON bytes with version marker prepended.
Parameters:
utf8Json: Raw UTF-8 JSON bytes to canonicalizeversion: Version string to embed
Returns: Canonical UTF-8 JSON bytes with _canonVersion field
Implementation: Rfc8785JsonCanonicalizer
StellaOps.Attestor.ProofChain.Json.Rfc8785JsonCanonicalizer is the concrete sealed implementation. Its constructor takes bool enableNfcNormalization = true, controlling whether string values are normalized to Unicode NFC for cross-platform stability. It throws ArgumentException (via ThrowIfNullOrWhiteSpace) when version is null/empty/whitespace. (Source: Rfc8785JsonCanonicalizer.cs.)
Usage Examples
Computing Content-Addressed IDs
using StellaOps.Canonical.Json;
// Evidence predicate hashing
var evidence = new EvidencePredicate
{
Source = "scanner/trivy",
SbomEntryId = "sha256:91f2ab3c:pkg:npm/lodash@4.17.21",
VulnerabilityId = "CVE-2021-23337"
};
// Compute versioned hash (recommended)
string evidenceId = CanonJson.HashVersionedPrefixed(evidence);
// Result: "sha256:..."
Verifying Attestations
public bool VerifyAttestation(byte[] payload, string expectedHash)
{
// `payload` is ALREADY-canonical bytes. Hash the bytes directly with
// Sha256Hex — do NOT call Hash<T>/HashVersioned<T>, which serialize a CLR
// object and would treat a byte[] as a JSON array, producing a wrong hash.
var computed = CanonJson.Sha256Hex(payload);
return computed == expectedHash;
// Detect format only when you need the version, e.g. to re-canonicalize
// raw (non-canonical) input via CanonJson.CanonicalizeParsedJson first.
}
The
Hash<T>/HashVersioned<T>family serializes itsobjargument. Use them with CLR objects; useSha256Hex/CanonicalizeParsedJsonwhen you already hold canonical or raw JSON bytes.
Migration from Legacy to Versioned
// Old code (legacy)
var hash = CanonJson.Hash(predicate);
// New code (versioned) - just add "Versioned"
var hash = CanonJson.HashVersioned(predicate);
Algorithm Details
RFC 8785 Compliance
| Requirement | Implementation |
|---|---|
| Key ordering | StringComparer.Ordinal (case-sensitive); keys are NFC-normalized before sorting |
| Number format | Utf8JsonWriter default; negative zero (-0) is normalized to 0 (see below) |
| String escaping | JavaScriptEncoder.UnsafeRelaxedJsonEscaping (relaxed — does not escape <, >, &, +, etc.) |
| Unicode normalization | Both property names and string values are normalized to NFC (Form C) when not already normalized |
| Whitespace | None (compact output) |
| Encoding | UTF-8 without BOM |
Negative-zero normalization.
WriteElementSortedchecks each number viaTryWriteNormalizedNumber: if the value’s IEEE 754 bit pattern is negative zero, it is written as0. (Source:CanonJson.Writer.cs.)
Unicode NFC. Unlike strict RFC 8785, this implementation additionally normalizes property names and string values to Unicode Normalization Form C, so composed/decomposed equivalents produce identical canonical bytes. The
Rfc8785JsonCanonicalizerbyte-level implementation makes this optional viaenableNfcNormalization. (Source:CanonJson.Writer.cs,Rfc8785JsonCanonicalizer.cs.)
Version Marker Position
The _canonVersion field is always first in the output due to:
- Underscore (
_) sorts before all letters in ASCII - After injecting version, remaining keys are sorted normally
{"_canonVersion":"stella:canon:v1","aaa":1,"bbb":2,"zzz":3}
