343 lines
7.9 KiB
Markdown
343 lines
7.9 KiB
Markdown
# 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
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
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<T>
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
var obj = new { z = 3, a = 1 };
|
|
byte[] canonical = CanonJson.Canonicalize(obj);
|
|
// Result: {"a":1,"z":3}
|
|
```
|
|
|
|
---
|
|
|
|
#### CanonicalizeVersioned<T>
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
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<T>
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
var obj = new { foo = "bar" };
|
|
string hash = CanonJson.Hash(obj);
|
|
// Result: "7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b"
|
|
```
|
|
|
|
---
|
|
|
|
#### HashVersioned<T>
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
var obj = new { foo = "bar" };
|
|
string hash = CanonJson.HashVersioned(obj);
|
|
// Different from legacy hash due to version marker
|
|
```
|
|
|
|
---
|
|
|
|
#### HashPrefixed<T>
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
var obj = new { foo = "bar" };
|
|
string hash = CanonJson.HashPrefixed(obj);
|
|
// Result: "sha256:7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b"
|
|
```
|
|
|
|
---
|
|
|
|
#### HashVersionedPrefixed<T>
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```json
|
|
{"_canonVersion":"stella:canon:v1","aaa":1,"bbb":2,"zzz":3}
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Proof Chain Specification](../modules/attestor/proof-chain-specification.md)
|
|
- [Canonicalization Migration Guide](../operations/canon-version-migration.md)
|
|
- [RFC 8785 - JSON Canonicalization Scheme](https://datatracker.ietf.org/doc/html/rfc8785)
|