using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Scanner.Surface.FS; /// /// Verifies determinism metadata on a Surface manifest by checking composition recipe, /// layer fragment attestations, and DSSE payload integrity. /// public sealed class SurfaceManifestDeterminismVerifier { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { WriteIndented = false }; public async Task VerifyAsync( SurfaceManifestDocument manifest, Func>> artifactLoader, CancellationToken cancellationToken = default) { if (manifest is null) { throw new ArgumentNullException(nameof(manifest)); } if (artifactLoader is null) { throw new ArgumentNullException(nameof(artifactLoader)); } var errors = new List(); var merkleRoot = (manifest.DeterminismMerkleRoot ?? manifest.Determinism?.MerkleRoot)?.Trim().ToLowerInvariant(); if (string.IsNullOrWhiteSpace(merkleRoot)) { errors.Add("determinism.merkleRoot missing from manifest."); } var artifactsByDigest = manifest.Artifacts.ToDictionary(a => a.Digest, StringComparer.OrdinalIgnoreCase); var artifactsByUri = manifest.Artifacts.Where(a => !string.IsNullOrWhiteSpace(a.Uri)) .ToDictionary(a => a.Uri, StringComparer.OrdinalIgnoreCase); // Validate composition recipe first; it anchors the Merkle root. var recipe = manifest.Artifacts.FirstOrDefault(a => string.Equals(a.Kind, "composition.recipe", StringComparison.Ordinal)); if (recipe is null) { errors.Add("composition.recipe artifact missing."); } else { var recipeBytes = await LoadAndValidateDigestAsync(recipe, artifactLoader, errors, cancellationToken).ConfigureAwait(false); if (recipeBytes.Length > 0) { var computedRoot = ComputeSha256Hex(recipeBytes.Span); if (string.IsNullOrWhiteSpace(merkleRoot)) { merkleRoot = computedRoot; } else if (!string.Equals(merkleRoot, computedRoot, StringComparison.Ordinal)) { errors.Add($"determinism.merkleRoot mismatch: manifest={merkleRoot}, recipe={computedRoot}."); } await VerifyAttestationAsync( recipe, recipeBytes, expectedPayloadType: recipe.MediaType, artifactsByDigest, artifactsByUri, artifactLoader, errors, cancellationToken).ConfigureAwait(false); } } // Validate each layer fragment and its DSSE. foreach (var fragment in manifest.Artifacts.Where(a => string.Equals(a.Kind, "layer.fragments", StringComparison.Ordinal))) { var fragmentBytes = await LoadAndValidateDigestAsync(fragment, artifactLoader, errors, cancellationToken).ConfigureAwait(false); if (fragmentBytes.Length == 0) { continue; } await VerifyAttestationAsync( fragment, fragmentBytes, expectedPayloadType: fragment.MediaType, artifactsByDigest, artifactsByUri, artifactLoader, errors, cancellationToken).ConfigureAwait(false); } return new SurfaceDeterminismVerificationResult(errors.Count == 0, merkleRoot, errors); } private static async Task> LoadAndValidateDigestAsync( SurfaceManifestArtifact artifact, Func>> loader, List errors, CancellationToken cancellationToken) { try { cancellationToken.ThrowIfCancellationRequested(); var bytes = await loader(artifact).ConfigureAwait(false); if (bytes.Length == 0) { errors.Add($"artifact:{artifact.Kind} ({artifact.Digest}) content missing."); return ReadOnlyMemory.Empty; } var computedDigest = $"sha256:{ComputeSha256Hex(bytes.Span)}"; if (!string.Equals(computedDigest, artifact.Digest, StringComparison.OrdinalIgnoreCase)) { errors.Add($"artifact:{artifact.Kind} digest mismatch (manifest={artifact.Digest}, computed={computedDigest})."); } return bytes; } catch (Exception ex) { errors.Add($"artifact:{artifact.Kind} load failed: {ex.Message}"); return ReadOnlyMemory.Empty; } } private static async Task VerifyAttestationAsync( SurfaceManifestArtifact target, ReadOnlyMemory targetContent, string expectedPayloadType, IReadOnlyDictionary artifactsByDigest, IReadOnlyDictionary artifactsByUri, Func>> loader, List errors, CancellationToken cancellationToken) { if (target.Attestations is null || target.Attestations.Count == 0) { errors.Add($"artifact:{target.Kind} missing dsse attestation."); return; } var attestation = target.Attestations.FirstOrDefault(a => string.Equals(a.Kind, "dsse", StringComparison.Ordinal)); if (attestation is null) { errors.Add($"artifact:{target.Kind} missing dsse attestation."); return; } if (!artifactsByDigest.TryGetValue(attestation.Digest, out var dsseArtifact) && (!string.IsNullOrWhiteSpace(attestation.Uri) && !artifactsByUri.TryGetValue(attestation.Uri, out dsseArtifact))) { errors.Add($"artifact:{target.Kind} attestation not found in manifest (digest={attestation.Digest})."); return; } if (dsseArtifact is null) { errors.Add($"artifact:{target.Kind} attestation lookup returned null instance."); return; } var dsseBytes = await LoadAndValidateDigestAsync(dsseArtifact, loader, errors, cancellationToken).ConfigureAwait(false); if (dsseBytes.Length == 0) { return; } try { using var doc = JsonDocument.Parse(dsseBytes.ToArray(), new JsonDocumentOptions { AllowTrailingCommas = false }); var root = doc.RootElement; if (!root.TryGetProperty("payloadType", out var payloadTypeProp)) { errors.Add($"artifact:{target.Kind} attestation payloadType missing."); return; } var payloadType = payloadTypeProp.GetString() ?? string.Empty; if (!string.Equals(payloadType, expectedPayloadType, StringComparison.Ordinal)) { errors.Add($"artifact:{target.Kind} attestation payloadType mismatch (expected={expectedPayloadType}, actual={payloadType})."); } if (!root.TryGetProperty("payload", out var payloadProp)) { errors.Add($"artifact:{target.Kind} attestation payload missing."); return; } var payload = DecodeBase64Url(payloadProp.GetString()); if (!payload.Span.SequenceEqual(targetContent.Span)) { errors.Add($"artifact:{target.Kind} attestation payload does not match artifact content."); } if (root.TryGetProperty("signatures", out var sigArray) && sigArray.ValueKind == JsonValueKind.Array && sigArray.GetArrayLength() > 0) { var sigNode = sigArray[0]; if (sigNode.TryGetProperty("sig", out var sigValue)) { var sigBytes = DecodeBase64Url(sigValue.GetString()); var sigText = Encoding.UTF8.GetString(sigBytes.Span); var expectedSig = ComputeSha256Hex(targetContent.Span); if (!string.Equals(sigText, expectedSig, StringComparison.OrdinalIgnoreCase)) { errors.Add($"artifact:{target.Kind} attestation signature mismatch."); } } } } catch (Exception ex) { errors.Add($"artifact:{target.Kind} attestation parse failed: {ex.Message}"); } } private static string ComputeSha256Hex(ReadOnlySpan bytes) { Span hash = stackalloc byte[32]; SHA256.HashData(bytes, hash); return Convert.ToHexString(hash).ToLowerInvariant(); } private static ReadOnlyMemory DecodeBase64Url(string? value) { if (string.IsNullOrEmpty(value)) { return ReadOnlyMemory.Empty; } var padded = value.Replace('-', '+').Replace('_', '/'); switch (padded.Length % 4) { case 2: padded += "=="; break; case 3: padded += "="; break; } return Convert.FromBase64String(padded); } } public sealed record SurfaceDeterminismVerificationResult( bool Success, string? MerkleRoot, IReadOnlyList Errors) { public bool IsDeterministic => Success; }