Files
git.stella-ops.org/docs/api/reference/canon-json.md
2025-12-24 21:45:46 +02:00

7.9 KiB

CanonJson API Reference

Namespace: StellaOps.Canonical.Json
Assembly: StellaOps.Canonical.Json
Version: 1.0.0


Overview

The CanonJson class provides RFC 8785-compliant JSON canonicalization and cryptographic hashing utilities for content-addressed identifiers. It ensures deterministic, reproducible JSON serialization across all environments.


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.

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}

CanonicalizeVersioned

public static byte[] CanonicalizeVersioned<T>(T obj, string version = CanonVersion.Current)

Canonicalizes an object with a version marker for content-addressed hashing.

Parameters:

  • obj: The object to canonicalize
  • version: Canonicalization version (default: CanonVersion.Current)

Returns: UTF-8 encoded canonical JSON bytes with version marker

Exceptions:

  • ArgumentNullException: When version is null
  • ArgumentException: When version is empty

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}

Hashing Methods

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 hash
  • version: 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 hash
  • version: 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)

IJsonCanonicalizer Interface

Interface for JSON canonicalization implementations.

Methods

Canonicalize

byte[] Canonicalize(ReadOnlySpan<byte> json)

Canonicalizes UTF-8 JSON bytes per RFC 8785.

Parameters:

  • json: Raw UTF-8 JSON bytes to canonicalize

Returns: Canonical UTF-8 JSON bytes


CanonicalizeWithVersion

byte[] CanonicalizeWithVersion(ReadOnlySpan<byte> json, string version)

Canonicalizes UTF-8 JSON bytes with version marker prepended.

Parameters:

  • json: Raw UTF-8 JSON bytes to canonicalize
  • version: Version string to embed

Returns: Canonical UTF-8 JSON bytes with _canonVersion field


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)
{
    // Detect format and verify accordingly
    if (CanonVersion.IsVersioned(payload))
    {
        var version = CanonVersion.ExtractVersion(payload);
        // Re-canonicalize with same version and compare
        var computed = CanonJson.HashVersioned(payload, version!);
        return computed == expectedHash;
    }
    
    // Legacy format
    var legacyHash = CanonJson.Hash(payload);
    return legacyHash == expectedHash;
}

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 Ordinal string comparison (case-sensitive, ASCII)
Number format IEEE 754, shortest representation
String escaping Minimal (only ", \, control characters)
Whitespace None (compact output)
Encoding UTF-8 without BOM

Version Marker Position

The _canonVersion field is always first in the output due to:

  1. Underscore (_) sorts before all letters in ASCII
  2. After injecting version, remaining keys are sorted normally
{"_canonVersion":"stella:canon:v1","aaa":1,"bbb":2,"zzz":3}