sprints work
This commit is contained in:
@@ -7,6 +7,7 @@ using System.Text.Json.Serialization;
|
||||
using StellaOps.Attestor.ProofChain.Json;
|
||||
using StellaOps.Attestor.ProofChain.Merkle;
|
||||
using StellaOps.Attestor.ProofChain.Predicates;
|
||||
using StellaOps.Canonical.Json;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Identifiers;
|
||||
|
||||
@@ -31,21 +32,21 @@ public sealed class ContentAddressedIdGenerator : IContentAddressedIdGenerator
|
||||
public EvidenceId ComputeEvidenceId(EvidencePredicate predicate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
var canonical = Canonicalize(predicate with { EvidenceId = null });
|
||||
var canonical = CanonicalizeVersioned(predicate with { EvidenceId = null });
|
||||
return new EvidenceId(HashSha256Hex(canonical));
|
||||
}
|
||||
|
||||
public ReasoningId ComputeReasoningId(ReasoningPredicate predicate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
var canonical = Canonicalize(predicate with { ReasoningId = null });
|
||||
var canonical = CanonicalizeVersioned(predicate with { ReasoningId = null });
|
||||
return new ReasoningId(HashSha256Hex(canonical));
|
||||
}
|
||||
|
||||
public VexVerdictId ComputeVexVerdictId(VexPredicate predicate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
var canonical = Canonicalize(predicate with { VexVerdictId = null });
|
||||
var canonical = CanonicalizeVersioned(predicate with { VexVerdictId = null });
|
||||
return new VexVerdictId(HashSha256Hex(canonical));
|
||||
}
|
||||
|
||||
@@ -143,6 +144,20 @@ public sealed class ContentAddressedIdGenerator : IContentAddressedIdGenerator
|
||||
return new SbomEntryId(sbomDigest, purl, version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes a value with version marker for content-addressed hashing.
|
||||
/// Uses the current canonicalization version (<see cref="CanonVersion.Current"/>).
|
||||
/// </summary>
|
||||
private byte[] CanonicalizeVersioned<T>(T value)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(value, SerializerOptions);
|
||||
return _canonicalizer.CanonicalizeWithVersion(json, CanonVersion.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes a value without version marker.
|
||||
/// Used for SBOM digests which are content-addressed by their raw JSON.
|
||||
/// </summary>
|
||||
private byte[] Canonicalize<T>(T value)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(value, SerializerOptions);
|
||||
|
||||
@@ -4,6 +4,21 @@ namespace StellaOps.Attestor.ProofChain.Json;
|
||||
|
||||
public interface IJsonCanonicalizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonicalizes UTF-8 JSON bytes using RFC 8785 JSON Canonicalization Scheme.
|
||||
/// </summary>
|
||||
/// <param name="utf8Json">UTF-8 encoded JSON bytes.</param>
|
||||
/// <returns>Canonical UTF-8 JSON bytes with sorted keys and no whitespace.</returns>
|
||||
byte[] Canonicalize(ReadOnlySpan<byte> utf8Json);
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalizes UTF-8 JSON bytes with a version marker for content-addressed hashing.
|
||||
/// The version marker is embedded as the first field, ensuring hash stability
|
||||
/// even if canonicalization logic evolves.
|
||||
/// </summary>
|
||||
/// <param name="utf8Json">UTF-8 encoded JSON bytes.</param>
|
||||
/// <param name="version">Canonicalization version (e.g., "stella:canon:v1").</param>
|
||||
/// <returns>Canonical UTF-8 JSON bytes with version marker and sorted keys.</returns>
|
||||
byte[] CanonicalizeWithVersion(ReadOnlySpan<byte> utf8Json, string version);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace StellaOps.Attestor.ProofChain.Json;
|
||||
/// </summary>
|
||||
public sealed class Rfc8785JsonCanonicalizer : IJsonCanonicalizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Field name for version marker. Underscore prefix ensures lexicographic first position.
|
||||
/// </summary>
|
||||
private const string VersionFieldName = "_canonVersion";
|
||||
|
||||
private static readonly JsonWriterOptions CanonicalWriterOptions = new()
|
||||
{
|
||||
Indented = false,
|
||||
@@ -25,6 +30,15 @@ public sealed class Rfc8785JsonCanonicalizer : IJsonCanonicalizer
|
||||
return Canonicalize(document.RootElement);
|
||||
}
|
||||
|
||||
public byte[] CanonicalizeWithVersion(ReadOnlySpan<byte> utf8Json, string version)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(version);
|
||||
|
||||
var reader = new Utf8JsonReader(utf8Json, isFinalBlock: true, state: default);
|
||||
using var document = JsonDocument.ParseValue(ref reader);
|
||||
return CanonicalizeWithVersion(document.RootElement, version);
|
||||
}
|
||||
|
||||
private static byte[] Canonicalize(JsonElement element)
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
@@ -36,6 +50,52 @@ public sealed class Rfc8785JsonCanonicalizer : IJsonCanonicalizer
|
||||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] CanonicalizeWithVersion(JsonElement element, string version)
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
using (var writer = new Utf8JsonWriter(buffer, CanonicalWriterOptions))
|
||||
{
|
||||
WriteCanonicalWithVersion(writer, element, version);
|
||||
}
|
||||
|
||||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteCanonicalWithVersion(Utf8JsonWriter writer, JsonElement element, string version)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
|
||||
// Write version marker first (underscore prefix ensures it stays first after sorting)
|
||||
writer.WriteString(VersionFieldName, version);
|
||||
|
||||
// Write remaining properties sorted
|
||||
var properties = new List<(string Name, JsonElement Value)>();
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
properties.Add((property.Name, property.Value));
|
||||
}
|
||||
properties.Sort(static (x, y) => string.CompareOrdinal(x.Name, y.Name));
|
||||
|
||||
foreach (var (name, value) in properties)
|
||||
{
|
||||
writer.WritePropertyName(name);
|
||||
WriteCanonical(writer, value);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-object root: wrap in versioned object
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString(VersionFieldName, version);
|
||||
writer.WritePropertyName("_value");
|
||||
WriteCanonical(writer, element);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteCanonical(Utf8JsonWriter writer, JsonElement element)
|
||||
{
|
||||
switch (element.ValueKind)
|
||||
|
||||
Reference in New Issue
Block a user