save progress

This commit is contained in:
StellaOps Bot
2026-01-06 09:42:02 +02:00
parent 94d68bee8b
commit 37e11918e0
443 changed files with 85863 additions and 897 deletions

View File

@@ -0,0 +1,204 @@
// <copyright file="ReplayProof.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Replay.Core.Models;
/// <summary>
/// Compact proof artifact for audit trails and ticket attachments.
/// Captures the essential evidence that a replay was performed and matched expectations.
/// </summary>
public sealed record ReplayProof
{
/// <summary>
/// Schema version for forward compatibility.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = "1.0.0";
/// <summary>
/// SHA-256 of the replay bundle used.
/// </summary>
[JsonPropertyName("bundleHash")]
public required string BundleHash { get; init; }
/// <summary>
/// Policy version used in the replay.
/// </summary>
[JsonPropertyName("policyVersion")]
public required string PolicyVersion { get; init; }
/// <summary>
/// Merkle root of all verdict outputs.
/// </summary>
[JsonPropertyName("verdictRoot")]
public required string VerdictRoot { get; init; }
/// <summary>
/// Whether the replayed verdict matches the expected verdict.
/// </summary>
[JsonPropertyName("verdictMatches")]
public required bool VerdictMatches { get; init; }
/// <summary>
/// Replay execution duration in milliseconds.
/// </summary>
[JsonPropertyName("durationMs")]
public required long DurationMs { get; init; }
/// <summary>
/// UTC timestamp when replay was performed.
/// </summary>
[JsonPropertyName("replayedAt")]
public required DateTimeOffset ReplayedAt { get; init; }
/// <summary>
/// Version of the replay engine used.
/// </summary>
[JsonPropertyName("engineVersion")]
public required string EngineVersion { get; init; }
/// <summary>
/// Original artifact digest (image or SBOM) that was evaluated.
/// </summary>
[JsonPropertyName("artifactDigest")]
public string? ArtifactDigest { get; init; }
/// <summary>
/// DSSE signature verified status (true/false/null if not present).
/// </summary>
[JsonPropertyName("signatureVerified")]
public bool? SignatureVerified { get; init; }
/// <summary>
/// Key ID used for signature verification.
/// </summary>
[JsonPropertyName("signatureKeyId")]
public string? SignatureKeyId { get; init; }
/// <summary>
/// Additional metadata (e.g., organization, project, tenant).
/// </summary>
[JsonPropertyName("metadata")]
public ImmutableDictionary<string, string>? Metadata { get; init; }
/// <summary>
/// JSON serializer options for canonical serialization (sorted keys, no indentation).
/// </summary>
private static readonly JsonSerializerOptions CanonicalOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
// Note: We manually ensure sorted keys in ToCanonicalJson()
};
/// <summary>
/// Converts the proof to a compact string format: "replay-proof:&lt;sha256&gt;".
/// The hash is computed over the canonical JSON representation.
/// </summary>
/// <returns>Compact proof string suitable for ticket attachments.</returns>
public string ToCompactString()
{
var canonicalJson = ToCanonicalJson();
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(canonicalJson));
var hashHex = Convert.ToHexString(hashBytes).ToLowerInvariant();
return $"replay-proof:{hashHex}";
}
/// <summary>
/// Converts the proof to canonical JSON (RFC 8785 style: sorted keys, minimal whitespace).
/// </summary>
/// <returns>Canonical JSON string.</returns>
public string ToCanonicalJson()
{
// Build ordered dictionary for canonical serialization
var ordered = new SortedDictionary<string, object?>(StringComparer.Ordinal)
{
["artifactDigest"] = ArtifactDigest,
["bundleHash"] = BundleHash,
["durationMs"] = DurationMs,
["engineVersion"] = EngineVersion,
["metadata"] = Metadata is not null && Metadata.Count > 0
? new SortedDictionary<string, string>(Metadata, StringComparer.Ordinal)
: null,
["policyVersion"] = PolicyVersion,
["replayedAt"] = ReplayedAt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ", System.Globalization.CultureInfo.InvariantCulture),
["schemaVersion"] = SchemaVersion,
["signatureKeyId"] = SignatureKeyId,
["signatureVerified"] = SignatureVerified,
["verdictMatches"] = VerdictMatches,
["verdictRoot"] = VerdictRoot,
};
// Remove null values for canonical form
var filtered = ordered.Where(kvp => kvp.Value is not null)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
return JsonSerializer.Serialize(filtered, CanonicalOptions);
}
/// <summary>
/// Parses a compact proof string and validates its hash.
/// </summary>
/// <param name="compactString">The compact proof string (replay-proof:&lt;hash&gt;).</param>
/// <param name="originalJson">The original canonical JSON to verify against.</param>
/// <returns>True if the hash matches, false otherwise.</returns>
public static bool ValidateCompactString(string compactString, string originalJson)
{
if (string.IsNullOrWhiteSpace(compactString) || string.IsNullOrWhiteSpace(originalJson))
{
return false;
}
const string prefix = "replay-proof:";
if (!compactString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var expectedHash = compactString[prefix.Length..];
var actualHashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(originalJson));
var actualHash = Convert.ToHexString(actualHashBytes).ToLowerInvariant();
return string.Equals(expectedHash, actualHash, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Creates a ReplayProof from execution results.
/// </summary>
public static ReplayProof FromExecutionResult(
string bundleHash,
string policyVersion,
string verdictRoot,
bool verdictMatches,
long durationMs,
DateTimeOffset replayedAt,
string engineVersion,
string? artifactDigest = null,
bool? signatureVerified = null,
string? signatureKeyId = null,
ImmutableDictionary<string, string>? metadata = null)
{
return new ReplayProof
{
BundleHash = bundleHash ?? throw new ArgumentNullException(nameof(bundleHash)),
PolicyVersion = policyVersion ?? throw new ArgumentNullException(nameof(policyVersion)),
VerdictRoot = verdictRoot ?? throw new ArgumentNullException(nameof(verdictRoot)),
VerdictMatches = verdictMatches,
DurationMs = durationMs,
ReplayedAt = replayedAt,
EngineVersion = engineVersion ?? throw new ArgumentNullException(nameof(engineVersion)),
ArtifactDigest = artifactDigest,
SignatureVerified = signatureVerified,
SignatureKeyId = signatureKeyId,
Metadata = metadata,
};
}
}