using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using FluentAssertions; using StellaOps.Cryptography; using StellaOps.Provenance.Attestation; using StellaOps.TestKit; using Xunit; namespace StellaOps.Provenance.Attestation.Tests; public class SampleStatementDigestTests { private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests(); private static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNamingPolicy = null, WriteIndented = false }; private static string RepoRoot { get { var dir = AppContext.BaseDirectory; while (!string.IsNullOrEmpty(dir)) { var candidate = Path.Combine(dir, "samples", "provenance"); if (Directory.Exists(candidate)) { return dir; } var parent = Directory.GetParent(dir); dir = parent?.FullName ?? string.Empty; } throw new DirectoryNotFoundException("Could not locate repository root containing samples/provenance."); } } private static IEnumerable<(string Name, BuildStatement Statement)> LoadSamples() { var samplesDir = Path.Combine(RepoRoot, "samples", "provenance"); foreach (var path in Directory.EnumerateFiles(samplesDir, "*.json").OrderBy(p => p, StringComparer.Ordinal)) { var json = File.ReadAllText(path); var statement = JsonSerializer.Deserialize(json, SerializerOptions); if (statement is null) { continue; } yield return (Path.GetFileName(path), statement); } } [Trait("Category", TestCategories.Unit)] [Fact] public void Hashes_match_expected_samples() { // Expected hashes using FIPS profile (SHA-256 for attestation purpose) var expectations = new Dictionary(StringComparer.Ordinal) { ["build-statement-sample.json"] = "3d9f673803f711940f47c85b33ad9776dc90bdfaf58922903cc9bd401b9f56b0", ["export-service-statement.json"] = "fa73e8664566d45497d4c18d439b42ff38b1ed6e3e25ca8e29001d1201f1d41b", ["job-runner-statement.json"] = "27a5b433c320fed2984166641390953d02b9204ed1d75076ec9c000e04f3a82a", ["orchestrator-statement.json"] = "d79467d03da33d0b8f848d7a340c8cde845802bad7dadcb553125e8553615b28" }; foreach (var (name, statement) in LoadSamples()) { BuildStatementDigest.ComputeHashHex(_cryptoHash, statement) .Should() .Be(expectations[name], because: $"{name} hash must be deterministic"); } } [Trait("Category", TestCategories.Unit)] [Fact] public void Merkle_root_is_stable_across_sample_set() { var statements = LoadSamples().Select(pair => pair.Statement).ToArray(); BuildStatementDigest.ComputeMerkleRootHex(_cryptoHash, statements) .Should() .Be("958465d432c9c8497f9ea5c1476cc7f2bea2a87d3ca37d8293586bf73922dd73"); } }