sprints work

This commit is contained in:
StellaOps Bot
2025-12-24 21:46:08 +02:00
parent 43e2af88f6
commit b9f71fc7e9
161 changed files with 29566 additions and 527 deletions

View File

@@ -0,0 +1,349 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.GraphRoot.Models;
using StellaOps.Canonical.Json;
namespace StellaOps.Attestor.GraphRoot;
/// <summary>
/// Implementation of graph root attestation service.
/// Creates and verifies DSSE-signed in-toto statements for graph roots.
/// </summary>
public sealed class GraphRootAttestor : IGraphRootAttestor
{
private const string ToolName = "stellaops/attestor/graph-root";
private const string PayloadType = "application/vnd.in-toto+json";
private static readonly string _toolVersion = GetToolVersion();
private readonly IMerkleRootComputer _merkleComputer;
private readonly EnvelopeSignatureService _signatureService;
private readonly Func<string?, EnvelopeKey?> _keyResolver;
private readonly ILogger<GraphRootAttestor> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="GraphRootAttestor"/> class.
/// </summary>
/// <param name="merkleComputer">Service for computing Merkle roots.</param>
/// <param name="signatureService">Service for signing envelopes.</param>
/// <param name="keyResolver">Function to resolve signing keys by ID.</param>
/// <param name="logger">Logger instance.</param>
public GraphRootAttestor(
IMerkleRootComputer merkleComputer,
EnvelopeSignatureService signatureService,
Func<string?, EnvelopeKey?> keyResolver,
ILogger<GraphRootAttestor> logger)
{
_merkleComputer = merkleComputer ?? throw new ArgumentNullException(nameof(merkleComputer));
_signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService));
_keyResolver = keyResolver ?? throw new ArgumentNullException(nameof(keyResolver));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<GraphRootAttestationResult> AttestAsync(
GraphRootAttestationRequest request,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(request);
ct.ThrowIfCancellationRequested();
_logger.LogDebug(
"Creating graph root attestation for {GraphType} with {NodeCount} nodes and {EdgeCount} edges",
request.GraphType,
request.NodeIds.Count,
request.EdgeIds.Count);
// 1. Sort node and edge IDs lexicographically for determinism
var sortedNodeIds = request.NodeIds
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
var sortedEdgeIds = request.EdgeIds
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
var sortedEvidenceIds = request.EvidenceIds
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
// 2. Build leaf data for Merkle tree
var leaves = BuildLeaves(
sortedNodeIds,
sortedEdgeIds,
request.PolicyDigest,
request.FeedsDigest,
request.ToolchainDigest,
request.ParamsDigest);
// 3. Compute Merkle root
var rootBytes = _merkleComputer.ComputeRoot(leaves);
var rootHex = Convert.ToHexStringLower(rootBytes);
var rootHash = $"{_merkleComputer.Algorithm}:{rootHex}";
_logger.LogDebug("Computed Merkle root: {RootHash}", rootHash);
// 4. Build in-toto statement
var computedAt = DateTimeOffset.UtcNow;
var attestation = BuildAttestation(
request,
sortedNodeIds,
sortedEdgeIds,
sortedEvidenceIds,
rootHash,
rootHex,
computedAt);
// 5. Canonicalize the attestation
var payload = CanonJson.CanonicalizeVersioned(attestation);
// 6. Sign the payload
var key = _keyResolver(request.SigningKeyId);
if (key is null)
{
throw new InvalidOperationException(
$"Unable to resolve signing key: {request.SigningKeyId ?? "(default)"}");
}
var signResult = _signatureService.Sign(payload, key, ct);
if (!signResult.IsSuccess)
{
throw new InvalidOperationException(
$"Signing failed: {signResult.Error?.Message}");
}
var dsseSignature = DsseSignature.FromBytes(signResult.Value!.Value.Span, signResult.Value.KeyId);
var envelope = new DsseEnvelope(PayloadType, payload, [dsseSignature]);
_logger.LogInformation(
"Created graph root attestation with root {RootHash} for {GraphType}",
rootHash,
request.GraphType);
// Note: Rekor publishing would be handled by a separate service
// that accepts the envelope after creation
return new GraphRootAttestationResult
{
RootHash = rootHash,
Envelope = envelope,
RekorLogIndex = null, // Would be set by Rekor service
NodeCount = sortedNodeIds.Count,
EdgeCount = sortedEdgeIds.Count
};
}
/// <inheritdoc />
public async Task<GraphRootVerificationResult> VerifyAsync(
DsseEnvelope envelope,
IReadOnlyList<GraphNodeData> nodes,
IReadOnlyList<GraphEdgeData> edges,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(envelope);
ArgumentNullException.ThrowIfNull(nodes);
ArgumentNullException.ThrowIfNull(edges);
ct.ThrowIfCancellationRequested();
_logger.LogDebug(
"Verifying graph root attestation with {NodeCount} nodes and {EdgeCount} edges",
nodes.Count,
edges.Count);
// 1. Deserialize attestation from envelope payload
GraphRootAttestation? attestation;
try
{
attestation = JsonSerializer.Deserialize<GraphRootAttestation>(envelope.Payload.Span);
}
catch (JsonException ex)
{
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = $"Failed to deserialize attestation: {ex.Message}"
};
}
if (attestation?.Predicate is null)
{
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = "Attestation or predicate is null"
};
}
// 2. Sort and recompute
var recomputedNodeIds = nodes
.Select(n => n.NodeId)
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
var recomputedEdgeIds = edges
.Select(e => e.EdgeId)
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
// 3. Build leaves using the same inputs from the attestation
var leaves = BuildLeaves(
recomputedNodeIds,
recomputedEdgeIds,
attestation.Predicate.Inputs.PolicyDigest,
attestation.Predicate.Inputs.FeedsDigest,
attestation.Predicate.Inputs.ToolchainDigest,
attestation.Predicate.Inputs.ParamsDigest);
// 4. Compute Merkle root
var recomputedRootBytes = _merkleComputer.ComputeRoot(leaves);
var recomputedRootHex = Convert.ToHexStringLower(recomputedRootBytes);
var recomputedRootHash = $"{_merkleComputer.Algorithm}:{recomputedRootHex}";
// 5. Compare roots
if (!string.Equals(recomputedRootHash, attestation.Predicate.RootHash, StringComparison.Ordinal))
{
_logger.LogWarning(
"Graph root mismatch: expected {Expected}, computed {Computed}",
attestation.Predicate.RootHash,
recomputedRootHash);
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = $"Root mismatch: expected {attestation.Predicate.RootHash}, got {recomputedRootHash}",
ExpectedRoot = attestation.Predicate.RootHash,
ComputedRoot = recomputedRootHash,
NodeCount = recomputedNodeIds.Count,
EdgeCount = recomputedEdgeIds.Count
};
}
_logger.LogDebug("Graph root verification succeeded: {RootHash}", recomputedRootHash);
return new GraphRootVerificationResult
{
IsValid = true,
ExpectedRoot = attestation.Predicate.RootHash,
ComputedRoot = recomputedRootHash,
NodeCount = recomputedNodeIds.Count,
EdgeCount = recomputedEdgeIds.Count
};
}
private static List<ReadOnlyMemory<byte>> BuildLeaves(
IReadOnlyList<string> sortedNodeIds,
IReadOnlyList<string> sortedEdgeIds,
string policyDigest,
string feedsDigest,
string toolchainDigest,
string paramsDigest)
{
var leaves = new List<ReadOnlyMemory<byte>>(
sortedNodeIds.Count + sortedEdgeIds.Count + 4);
// Add node IDs
foreach (var nodeId in sortedNodeIds)
{
leaves.Add(Encoding.UTF8.GetBytes(nodeId));
}
// Add edge IDs
foreach (var edgeId in sortedEdgeIds)
{
leaves.Add(Encoding.UTF8.GetBytes(edgeId));
}
// Add input digests (deterministic order)
leaves.Add(Encoding.UTF8.GetBytes(policyDigest));
leaves.Add(Encoding.UTF8.GetBytes(feedsDigest));
leaves.Add(Encoding.UTF8.GetBytes(toolchainDigest));
leaves.Add(Encoding.UTF8.GetBytes(paramsDigest));
return leaves;
}
private static GraphRootAttestation BuildAttestation(
GraphRootAttestationRequest request,
IReadOnlyList<string> sortedNodeIds,
IReadOnlyList<string> sortedEdgeIds,
IReadOnlyList<string> sortedEvidenceIds,
string rootHash,
string rootHex,
DateTimeOffset computedAt)
{
var subjects = new List<GraphRootSubject>
{
// Primary subject: the graph root itself
new GraphRootSubject
{
Name = rootHash,
Digest = new Dictionary<string, string> { ["sha256"] = rootHex }
}
};
// Add artifact subject if provided
if (!string.IsNullOrEmpty(request.ArtifactDigest))
{
subjects.Add(new GraphRootSubject
{
Name = request.ArtifactDigest,
Digest = ParseDigest(request.ArtifactDigest)
});
}
return new GraphRootAttestation
{
Subject = subjects,
Predicate = new GraphRootPredicate
{
GraphType = request.GraphType.ToString(),
RootHash = rootHash,
RootAlgorithm = "sha256",
NodeCount = sortedNodeIds.Count,
EdgeCount = sortedEdgeIds.Count,
NodeIds = sortedNodeIds,
EdgeIds = sortedEdgeIds,
Inputs = new GraphInputDigests
{
PolicyDigest = request.PolicyDigest,
FeedsDigest = request.FeedsDigest,
ToolchainDigest = request.ToolchainDigest,
ParamsDigest = request.ParamsDigest
},
EvidenceIds = sortedEvidenceIds,
CanonVersion = CanonVersion.Current,
ComputedAt = computedAt,
ComputedBy = ToolName,
ComputedByVersion = _toolVersion
}
};
}
private static Dictionary<string, string> ParseDigest(string digest)
{
var colonIndex = digest.IndexOf(':');
if (colonIndex > 0 && colonIndex < digest.Length - 1)
{
var algorithm = digest[..colonIndex];
var value = digest[(colonIndex + 1)..];
return new Dictionary<string, string> { [algorithm] = value };
}
// Assume sha256 if no algorithm prefix
return new Dictionary<string, string> { ["sha256"] = digest };
}
private static string GetToolVersion()
{
var assembly = typeof(GraphRootAttestor).Assembly;
var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString()
?? "1.0.0";
return version;
}
}

View File

@@ -0,0 +1,52 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Attestor.Envelope;
namespace StellaOps.Attestor.GraphRoot;
/// <summary>
/// Extension methods for registering graph root attestation services.
/// </summary>
public static class GraphRootServiceCollectionExtensions
{
/// <summary>
/// Adds graph root attestation services to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddGraphRootAttestation(this IServiceCollection services)
{
services.TryAddSingleton<IMerkleRootComputer, Sha256MerkleRootComputer>();
services.TryAddSingleton<EnvelopeSignatureService>();
services.TryAddSingleton<IGraphRootAttestor, GraphRootAttestor>();
return services;
}
/// <summary>
/// Adds graph root attestation services with a custom key resolver.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="keyResolver">Function to resolve signing keys by ID.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddGraphRootAttestation(
this IServiceCollection services,
Func<IServiceProvider, Func<string?, EnvelopeKey?>> keyResolver)
{
ArgumentNullException.ThrowIfNull(keyResolver);
services.TryAddSingleton<IMerkleRootComputer, Sha256MerkleRootComputer>();
services.TryAddSingleton<EnvelopeSignatureService>();
services.AddSingleton<IGraphRootAttestor>(sp =>
{
var merkleComputer = sp.GetRequiredService<IMerkleRootComputer>();
var signatureService = sp.GetRequiredService<EnvelopeSignatureService>();
var logger = sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<GraphRootAttestor>>();
var resolver = keyResolver(sp);
return new GraphRootAttestor(merkleComputer, signatureService, resolver, logger);
});
return services;
}
}

View File

@@ -0,0 +1,62 @@
// <copyright file="GraphType.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// </copyright>
namespace StellaOps.Attestor.GraphRoot;
/// <summary>
/// Types of graphs that can have their roots attested.
/// </summary>
public enum GraphType
{
/// <summary>
/// Unknown or unspecified graph type.
/// </summary>
Unknown = 0,
/// <summary>
/// Call graph showing function/method invocation relationships.
/// Used for reachability analysis.
/// </summary>
CallGraph = 1,
/// <summary>
/// Dependency graph showing package/library dependencies.
/// </summary>
DependencyGraph = 2,
/// <summary>
/// SBOM component graph with artifact relationships.
/// </summary>
SbomGraph = 3,
/// <summary>
/// Evidence graph linking vulnerabilities to evidence records.
/// </summary>
EvidenceGraph = 4,
/// <summary>
/// Policy evaluation graph showing rule evaluation paths.
/// </summary>
PolicyGraph = 5,
/// <summary>
/// Proof spine graph representing the chain of evidence segments.
/// </summary>
ProofSpine = 6,
/// <summary>
/// Combined reachability graph (call graph + dependency graph).
/// </summary>
ReachabilityGraph = 7,
/// <summary>
/// VEX observation linkage graph.
/// </summary>
VexLinkageGraph = 8,
/// <summary>
/// Custom/user-defined graph type.
/// </summary>
Custom = 100
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.GraphRoot.Models;
namespace StellaOps.Attestor.GraphRoot;
/// <summary>
/// Service for creating and verifying graph root attestations.
/// Graph root attestations bind a Merkle root computed from sorted node/edge IDs
/// and input digests to a signed DSSE envelope with an in-toto statement.
/// </summary>
public interface IGraphRootAttestor
{
/// <summary>
/// Create a graph root attestation.
/// </summary>
/// <param name="request">The attestation request containing graph data and signing options.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The attestation result containing the root hash and signed envelope.</returns>
Task<GraphRootAttestationResult> AttestAsync(
GraphRootAttestationRequest request,
CancellationToken ct = default);
/// <summary>
/// Verify a graph root attestation against provided graph data.
/// </summary>
/// <param name="envelope">The DSSE envelope to verify.</param>
/// <param name="nodes">The graph nodes to verify against.</param>
/// <param name="edges">The graph edges to verify against.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The verification result.</returns>
Task<GraphRootVerificationResult> VerifyAsync(
DsseEnvelope envelope,
IReadOnlyList<GraphNodeData> nodes,
IReadOnlyList<GraphEdgeData> edges,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Attestor.GraphRoot;
/// <summary>
/// Service for computing Merkle tree roots from leaf data.
/// </summary>
public interface IMerkleRootComputer
{
/// <summary>
/// Compute a Merkle root from the given leaves.
/// </summary>
/// <param name="leaves">The leaf data in order.</param>
/// <returns>The computed root hash bytes.</returns>
byte[] ComputeRoot(IReadOnlyList<ReadOnlyMemory<byte>> leaves);
/// <summary>
/// The hash algorithm used for Merkle computation.
/// </summary>
string Algorithm { get; }
}

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.GraphRoot.Models;
/// <summary>
/// In-toto statement for graph root attestation.
/// PredicateType: "https://stella-ops.org/attestation/graph-root/v1"
/// </summary>
public sealed record GraphRootAttestation
{
/// <summary>
/// In-toto statement type URI.
/// </summary>
[JsonPropertyName("_type")]
public string Type { get; init; } = "https://in-toto.io/Statement/v1";
/// <summary>
/// Subjects: the graph root hash and artifact it describes.
/// </summary>
[JsonPropertyName("subject")]
public required IReadOnlyList<GraphRootSubject> Subject { get; init; }
/// <summary>
/// Predicate type for graph root attestations.
/// </summary>
[JsonPropertyName("predicateType")]
public string PredicateType { get; init; } = GraphRootPredicateTypes.GraphRootV1;
/// <summary>
/// Graph root predicate payload.
/// </summary>
[JsonPropertyName("predicate")]
public required GraphRootPredicate Predicate { get; init; }
}
/// <summary>
/// Subject in an in-toto statement, representing an artifact or root hash.
/// </summary>
public sealed record GraphRootSubject
{
/// <summary>
/// The name or identifier of the subject.
/// For graph roots, this is typically the root hash.
/// For artifacts, this is the artifact reference.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Digests of the subject in algorithm:hex format.
/// </summary>
[JsonPropertyName("digest")]
public required IReadOnlyDictionary<string, string> Digest { get; init; }
}
/// <summary>
/// Well-known predicate type URIs for graph root attestations.
/// </summary>
public static class GraphRootPredicateTypes
{
/// <summary>
/// Graph root attestation predicate type v1.
/// </summary>
public const string GraphRootV1 = "https://stella-ops.org/attestation/graph-root/v1";
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Attestor.GraphRoot.Models;
/// <summary>
/// Request to create a graph root attestation.
/// The attestation binds a Merkle root computed from sorted node/edge IDs
/// and input digests to a DSSE envelope with in-toto statement.
/// </summary>
public sealed record GraphRootAttestationRequest
{
/// <summary>
/// Type of graph being attested.
/// </summary>
public required GraphType GraphType { get; init; }
/// <summary>
/// Node IDs to include in the root computation.
/// Will be sorted lexicographically for deterministic ordering.
/// </summary>
public required IReadOnlyList<string> NodeIds { get; init; }
/// <summary>
/// Edge IDs to include in the root computation.
/// Will be sorted lexicographically for deterministic ordering.
/// </summary>
public required IReadOnlyList<string> EdgeIds { get; init; }
/// <summary>
/// Policy bundle digest used during graph computation.
/// </summary>
public required string PolicyDigest { get; init; }
/// <summary>
/// Feed snapshot digest used during graph computation.
/// </summary>
public required string FeedsDigest { get; init; }
/// <summary>
/// Toolchain digest (scanner versions, analyzers, etc.).
/// </summary>
public required string ToolchainDigest { get; init; }
/// <summary>
/// Evaluation parameters digest (config, thresholds, etc.).
/// </summary>
public required string ParamsDigest { get; init; }
/// <summary>
/// Artifact digest this graph describes (container image, SBOM, etc.).
/// </summary>
public required string ArtifactDigest { get; init; }
/// <summary>
/// Linked evidence IDs referenced by this graph.
/// </summary>
public IReadOnlyList<string> EvidenceIds { get; init; } = [];
/// <summary>
/// Whether to publish the attestation to a Rekor transparency log.
/// </summary>
public bool PublishToRekor { get; init; } = false;
/// <summary>
/// Signing key ID to use for the DSSE envelope.
/// If null, the default signing key will be used.
/// </summary>
public string? SigningKeyId { get; init; }
}

View File

@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.GraphRoot.Models;
/// <summary>
/// Predicate for graph root attestations.
/// Contains the computed Merkle root and all inputs needed for reproducibility.
/// </summary>
public sealed record GraphRootPredicate
{
/// <summary>
/// Type of graph that was attested.
/// </summary>
[JsonPropertyName("graphType")]
public required string GraphType { get; init; }
/// <summary>
/// Merkle root hash in algorithm:hex format.
/// </summary>
[JsonPropertyName("rootHash")]
public required string RootHash { get; init; }
/// <summary>
/// Hash algorithm used (e.g., "sha256").
/// </summary>
[JsonPropertyName("rootAlgorithm")]
public string RootAlgorithm { get; init; } = "sha256";
/// <summary>
/// Number of nodes included in the root computation.
/// </summary>
[JsonPropertyName("nodeCount")]
public required int NodeCount { get; init; }
/// <summary>
/// Number of edges included in the root computation.
/// </summary>
[JsonPropertyName("edgeCount")]
public required int EdgeCount { get; init; }
/// <summary>
/// Sorted node IDs for deterministic verification.
/// </summary>
[JsonPropertyName("nodeIds")]
public required IReadOnlyList<string> NodeIds { get; init; }
/// <summary>
/// Sorted edge IDs for deterministic verification.
/// </summary>
[JsonPropertyName("edgeIds")]
public required IReadOnlyList<string> EdgeIds { get; init; }
/// <summary>
/// Input digests for reproducibility verification.
/// </summary>
[JsonPropertyName("inputs")]
public required GraphInputDigests Inputs { get; init; }
/// <summary>
/// Linked evidence IDs referenced by this graph.
/// </summary>
[JsonPropertyName("evidenceIds")]
public IReadOnlyList<string> EvidenceIds { get; init; } = [];
/// <summary>
/// Canonicalizer version used for serialization.
/// </summary>
[JsonPropertyName("canonVersion")]
public required string CanonVersion { get; init; }
/// <summary>
/// When the root was computed (UTC ISO-8601).
/// </summary>
[JsonPropertyName("computedAt")]
public required DateTimeOffset ComputedAt { get; init; }
/// <summary>
/// Tool that computed the root.
/// </summary>
[JsonPropertyName("computedBy")]
public required string ComputedBy { get; init; }
/// <summary>
/// Tool version.
/// </summary>
[JsonPropertyName("computedByVersion")]
public required string ComputedByVersion { get; init; }
}
/// <summary>
/// Input digests for graph computation, enabling reproducibility verification.
/// </summary>
public sealed record GraphInputDigests
{
/// <summary>
/// Policy bundle digest used during graph computation.
/// </summary>
[JsonPropertyName("policyDigest")]
public required string PolicyDigest { get; init; }
/// <summary>
/// Feed snapshot digest used during graph computation.
/// </summary>
[JsonPropertyName("feedsDigest")]
public required string FeedsDigest { get; init; }
/// <summary>
/// Toolchain digest (scanner versions, analyzers, etc.).
/// </summary>
[JsonPropertyName("toolchainDigest")]
public required string ToolchainDigest { get; init; }
/// <summary>
/// Evaluation parameters digest (config, thresholds, etc.).
/// </summary>
[JsonPropertyName("paramsDigest")]
public required string ParamsDigest { get; init; }
}

View File

@@ -0,0 +1,107 @@
using StellaOps.Attestor.Envelope;
namespace StellaOps.Attestor.GraphRoot.Models;
/// <summary>
/// Result of creating a graph root attestation.
/// </summary>
public sealed record GraphRootAttestationResult
{
/// <summary>
/// Computed Merkle root hash in algorithm:hex format.
/// </summary>
public required string RootHash { get; init; }
/// <summary>
/// Signed DSSE envelope containing the in-toto statement.
/// </summary>
public required DsseEnvelope Envelope { get; init; }
/// <summary>
/// Rekor log index if the attestation was published to transparency log.
/// </summary>
public string? RekorLogIndex { get; init; }
/// <summary>
/// Number of nodes included in the root computation.
/// </summary>
public required int NodeCount { get; init; }
/// <summary>
/// Number of edges included in the root computation.
/// </summary>
public required int EdgeCount { get; init; }
}
/// <summary>
/// Result of verifying a graph root attestation.
/// </summary>
public sealed record GraphRootVerificationResult
{
/// <summary>
/// Whether the verification passed.
/// </summary>
public required bool IsValid { get; init; }
/// <summary>
/// Failure reason if verification failed.
/// </summary>
public string? FailureReason { get; init; }
/// <summary>
/// Expected root hash from the attestation.
/// </summary>
public string? ExpectedRoot { get; init; }
/// <summary>
/// Recomputed root hash from the provided graph data.
/// </summary>
public string? ComputedRoot { get; init; }
/// <summary>
/// Number of nodes verified.
/// </summary>
public int? NodeCount { get; init; }
/// <summary>
/// Number of edges verified.
/// </summary>
public int? EdgeCount { get; init; }
}
/// <summary>
/// Node data for verification.
/// </summary>
public sealed record GraphNodeData
{
/// <summary>
/// Node identifier.
/// </summary>
public required string NodeId { get; init; }
/// <summary>
/// Optional node content for extended verification.
/// </summary>
public string? Content { get; init; }
}
/// <summary>
/// Edge data for verification.
/// </summary>
public sealed record GraphEdgeData
{
/// <summary>
/// Edge identifier.
/// </summary>
public required string EdgeId { get; init; }
/// <summary>
/// Source node identifier.
/// </summary>
public string? SourceNodeId { get; init; }
/// <summary>
/// Target node identifier.
/// </summary>
public string? TargetNodeId { get; init; }
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace StellaOps.Attestor.GraphRoot;
/// <summary>
/// Default SHA-256 Merkle root computer using binary tree construction.
/// </summary>
public sealed class Sha256MerkleRootComputer : IMerkleRootComputer
{
/// <inheritdoc />
public string Algorithm => "sha256";
/// <inheritdoc />
public byte[] ComputeRoot(IReadOnlyList<ReadOnlyMemory<byte>> leaves)
{
ArgumentNullException.ThrowIfNull(leaves);
if (leaves.Count == 0)
{
throw new ArgumentException("At least one leaf is required to compute a Merkle root.", nameof(leaves));
}
// Hash each leaf to create the initial level
var currentLevel = new List<byte[]>(leaves.Count);
foreach (var leaf in leaves)
{
currentLevel.Add(SHA256.HashData(leaf.Span));
}
// Build tree bottom-up
while (currentLevel.Count > 1)
{
var nextLevel = new List<byte[]>((currentLevel.Count + 1) / 2);
for (var i = 0; i < currentLevel.Count; i += 2)
{
var left = currentLevel[i];
// If odd number of nodes, duplicate the last one
var right = i + 1 < currentLevel.Count ? currentLevel[i + 1] : left;
// Combine and hash
var combined = new byte[left.Length + right.Length];
Buffer.BlockCopy(left, 0, combined, 0, left.Length);
Buffer.BlockCopy(right, 0, combined, left.Length, right.Length);
nextLevel.Add(SHA256.HashData(combined));
}
currentLevel = nextLevel;
}
return currentLevel[0];
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>StellaOps.Attestor.GraphRoot</RootNamespace>
<Description>Graph root attestation service for creating and verifying DSSE attestations of Merkle graph roots.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Evidence.Core\StellaOps.Evidence.Core.csproj" />
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
</ItemGroup>
</Project>