using System.Security.Cryptography; using System.Text.Json; namespace StellaOps.Canonical.Json; /// /// Canonical JSON serialization with deterministic hashing. /// Produces bit-identical output across environments for proof replay. /// /// /// Key guarantees: /// /// Object keys are sorted alphabetically (Ordinal comparison) /// No whitespace or formatting variations /// Consistent number formatting /// UTF-8 encoding without BOM /// /// public static class CanonJson { /// /// Canonicalizes an object to a deterministic byte array. /// Object keys are recursively sorted using Ordinal comparison. /// /// The type to serialize. /// The object to canonicalize. /// UTF-8 encoded canonical JSON bytes. public static byte[] Canonicalize(T obj) { var json = JsonSerializer.SerializeToUtf8Bytes(obj, new JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); using var doc = JsonDocument.Parse(json); using var ms = new MemoryStream(); using var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = false }); WriteElementSorted(doc.RootElement, writer); writer.Flush(); return ms.ToArray(); } /// /// Canonicalizes an object using custom serializer options. /// Object keys are recursively sorted using Ordinal comparison. /// /// The type to serialize. /// The object to canonicalize. /// JSON serializer options to use for initial serialization. /// UTF-8 encoded canonical JSON bytes. public static byte[] Canonicalize(T obj, JsonSerializerOptions options) { var json = JsonSerializer.SerializeToUtf8Bytes(obj, options); using var doc = JsonDocument.Parse(json); using var ms = new MemoryStream(); using var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = false }); WriteElementSorted(doc.RootElement, writer); writer.Flush(); return ms.ToArray(); } /// /// Canonicalizes raw JSON bytes by parsing and re-sorting keys. /// Use this when you have existing JSON that needs to be canonicalized. /// /// UTF-8 encoded JSON bytes. /// UTF-8 encoded canonical JSON bytes. public static byte[] CanonicalizeParsedJson(ReadOnlySpan jsonBytes) { using var doc = JsonDocument.Parse(jsonBytes.ToArray()); using var ms = new MemoryStream(); using var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = false }); WriteElementSorted(doc.RootElement, writer); writer.Flush(); return ms.ToArray(); } private static void WriteElementSorted(JsonElement el, Utf8JsonWriter w) { switch (el.ValueKind) { case JsonValueKind.Object: w.WriteStartObject(); foreach (var prop in el.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal)) { w.WritePropertyName(prop.Name); WriteElementSorted(prop.Value, w); } w.WriteEndObject(); break; case JsonValueKind.Array: w.WriteStartArray(); foreach (var item in el.EnumerateArray()) { WriteElementSorted(item, w); } w.WriteEndArray(); break; default: el.WriteTo(w); break; } } /// /// Computes SHA-256 hash of bytes, returns lowercase hex string. /// /// The bytes to hash. /// 64-character lowercase hex string. public static string Sha256Hex(ReadOnlySpan bytes) => Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); /// /// Computes SHA-256 hash of bytes, returns prefixed hash string. /// /// The bytes to hash. /// Hash string with "sha256:" prefix. public static string Sha256Prefixed(ReadOnlySpan bytes) => "sha256:" + Sha256Hex(bytes); /// /// Canonicalizes an object and computes its SHA-256 hash. /// /// The type to serialize. /// The object to hash. /// 64-character lowercase hex string. public static string Hash(T obj) { var canonical = Canonicalize(obj); return Sha256Hex(canonical); } /// /// Canonicalizes an object and computes its prefixed SHA-256 hash. /// /// The type to serialize. /// The object to hash. /// Hash string with "sha256:" prefix. public static string HashPrefixed(T obj) { var canonical = Canonicalize(obj); return Sha256Prefixed(canonical); } }