sprints work

This commit is contained in:
StellaOps Bot
2025-12-24 16:28:46 +02:00
parent 8197588e74
commit 4231305fec
43 changed files with 7190 additions and 36 deletions

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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)