save progress

This commit is contained in:
StellaOps Bot
2026-01-02 21:06:27 +02:00
parent f46bde5575
commit 3f197814c5
441 changed files with 21545 additions and 4306 deletions

View File

@@ -39,6 +39,7 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
private readonly Func<string?, EnvelopeKey?> _keyResolver;
private readonly IRekorClient? _rekorClient;
private readonly GraphRootAttestorOptions _options;
private readonly TimeProvider _timeProvider;
private readonly ILogger<GraphRootAttestor> _logger;
/// <summary>
@@ -56,7 +57,8 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
Func<string?, EnvelopeKey?> keyResolver,
ILogger<GraphRootAttestor> logger,
IRekorClient? rekorClient = null,
IOptions<GraphRootAttestorOptions>? options = null)
IOptions<GraphRootAttestorOptions>? options = null,
TimeProvider? timeProvider = null)
{
_merkleComputer = merkleComputer ?? throw new ArgumentNullException(nameof(merkleComputer));
_signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService));
@@ -64,6 +66,7 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_rekorClient = rekorClient;
_options = options?.Value ?? new GraphRootAttestorOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -91,14 +94,20 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
var normalizedPolicyDigest = NormalizeDigest(request.PolicyDigest);
var normalizedFeedsDigest = NormalizeDigest(request.FeedsDigest);
var normalizedToolchainDigest = NormalizeDigest(request.ToolchainDigest);
var normalizedParamsDigest = NormalizeDigest(request.ParamsDigest);
// 2. Build leaf data for Merkle tree
var leaves = BuildLeaves(
sortedNodeIds,
sortedEdgeIds,
request.PolicyDigest,
request.FeedsDigest,
request.ToolchainDigest,
request.ParamsDigest);
sortedEvidenceIds,
normalizedPolicyDigest,
normalizedFeedsDigest,
normalizedToolchainDigest,
normalizedParamsDigest);
// 3. Compute Merkle root
var rootBytes = _merkleComputer.ComputeRoot(leaves);
@@ -108,7 +117,7 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
_logger.LogDebug("Computed Merkle root: {RootHash}", rootHash);
// 4. Build in-toto statement
var computedAt = DateTimeOffset.UtcNow;
var computedAt = _timeProvider.GetUtcNow();
var attestation = BuildAttestation(
request,
sortedNodeIds,
@@ -116,6 +125,10 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
sortedEvidenceIds,
rootHash,
rootHex,
normalizedPolicyDigest,
normalizedFeedsDigest,
normalizedToolchainDigest,
normalizedParamsDigest,
computedAt);
// 5. Canonicalize the attestation
@@ -129,7 +142,7 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
$"Unable to resolve signing key: {request.SigningKeyId ?? "(default)"}");
}
var signResult = _signatureService.Sign(payload, key, ct);
var signResult = _signatureService.SignDsse(PayloadType, payload, key, ct);
if (!signResult.IsSuccess)
{
throw new InvalidOperationException(
@@ -260,8 +273,8 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
};
// Compute bundle hash
var bundleJson = JsonSerializer.Serialize(EnvDsseEnvelope);
var bundleHash = SHA256.HashData(Encoding.UTF8.GetBytes(bundleJson));
var bundleJson = CanonJson.Canonicalize(EnvDsseEnvelope);
var bundleHash = SHA256.HashData(bundleJson);
return new AttestorSubmissionRequest
{
@@ -303,6 +316,24 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
nodes.Count,
edges.Count);
if (!string.Equals(envelope.PayloadType, PayloadType, StringComparison.Ordinal))
{
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = $"Unexpected payloadType '{envelope.PayloadType}'."
};
}
if (!TryVerifyEnvelopeSignatures(envelope, ct, out var signatureFailure))
{
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = signatureFailure ?? "No valid DSSE signatures found."
};
}
// 1. Deserialize attestation from envelope payload
GraphRootAttestation? attestation;
try
@@ -336,15 +367,69 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
.Select(e => e.EdgeId)
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
var predicateNodeIds = attestation.Predicate.NodeIds?.ToList() ?? [];
var predicateEdgeIds = attestation.Predicate.EdgeIds?.ToList() ?? [];
var predicateEvidenceIds = attestation.Predicate.EvidenceIds?.ToList() ?? [];
if (!SequenceEqual(predicateNodeIds, recomputedNodeIds))
{
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = "Predicate node IDs do not match provided graph data."
};
}
if (!SequenceEqual(predicateEdgeIds, recomputedEdgeIds))
{
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = "Predicate edge IDs do not match provided graph data."
};
}
var sortedPredicateEvidenceIds = predicateEvidenceIds
.OrderBy(x => x, StringComparer.Ordinal)
.ToList();
if (!SequenceEqual(predicateEvidenceIds, sortedPredicateEvidenceIds))
{
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = "Predicate evidence IDs are not in deterministic order."
};
}
string normalizedPolicyDigest;
string normalizedFeedsDigest;
string normalizedToolchainDigest;
string normalizedParamsDigest;
try
{
normalizedPolicyDigest = NormalizeDigest(attestation.Predicate.Inputs.PolicyDigest);
normalizedFeedsDigest = NormalizeDigest(attestation.Predicate.Inputs.FeedsDigest);
normalizedToolchainDigest = NormalizeDigest(attestation.Predicate.Inputs.ToolchainDigest);
normalizedParamsDigest = NormalizeDigest(attestation.Predicate.Inputs.ParamsDigest);
}
catch (ArgumentException ex)
{
return new GraphRootVerificationResult
{
IsValid = false,
FailureReason = $"Invalid predicate digest: {ex.Message}"
};
}
// 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);
sortedPredicateEvidenceIds,
normalizedPolicyDigest,
normalizedFeedsDigest,
normalizedToolchainDigest,
normalizedParamsDigest);
// 4. Compute Merkle root
var recomputedRootBytes = _merkleComputer.ComputeRoot(leaves);
@@ -385,13 +470,14 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
private static List<ReadOnlyMemory<byte>> BuildLeaves(
IReadOnlyList<string> sortedNodeIds,
IReadOnlyList<string> sortedEdgeIds,
IReadOnlyList<string> sortedEvidenceIds,
string policyDigest,
string feedsDigest,
string toolchainDigest,
string paramsDigest)
{
var leaves = new List<ReadOnlyMemory<byte>>(
sortedNodeIds.Count + sortedEdgeIds.Count + 4);
sortedNodeIds.Count + sortedEdgeIds.Count + sortedEvidenceIds.Count + 4);
// Add node IDs
foreach (var nodeId in sortedNodeIds)
@@ -405,6 +491,12 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
leaves.Add(Encoding.UTF8.GetBytes(edgeId));
}
// Add evidence IDs
foreach (var evidenceId in sortedEvidenceIds)
{
leaves.Add(Encoding.UTF8.GetBytes(evidenceId));
}
// Add input digests (deterministic order)
leaves.Add(Encoding.UTF8.GetBytes(policyDigest));
leaves.Add(Encoding.UTF8.GetBytes(feedsDigest));
@@ -421,6 +513,10 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
IReadOnlyList<string> sortedEvidenceIds,
string rootHash,
string rootHex,
string policyDigest,
string feedsDigest,
string toolchainDigest,
string paramsDigest,
DateTimeOffset computedAt)
{
var subjects = new List<GraphRootSubject>
@@ -457,10 +553,10 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
EdgeIds = sortedEdgeIds,
Inputs = new GraphInputDigests
{
PolicyDigest = request.PolicyDigest,
FeedsDigest = request.FeedsDigest,
ToolchainDigest = request.ToolchainDigest,
ParamsDigest = request.ParamsDigest
PolicyDigest = policyDigest,
FeedsDigest = feedsDigest,
ToolchainDigest = toolchainDigest,
ParamsDigest = paramsDigest
},
EvidenceIds = sortedEvidenceIds,
CanonVersion = CanonVersion.Current,
@@ -476,13 +572,13 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
var colonIndex = digest.IndexOf(':');
if (colonIndex > 0 && colonIndex < digest.Length - 1)
{
var algorithm = digest[..colonIndex];
var value = digest[(colonIndex + 1)..];
var algorithm = digest[..colonIndex].ToLowerInvariant();
var value = digest[(colonIndex + 1)..].ToLowerInvariant();
return new Dictionary<string, string> { [algorithm] = value };
}
// Assume sha256 if no algorithm prefix
return new Dictionary<string, string> { ["sha256"] = digest };
return new Dictionary<string, string> { ["sha256"] = digest.ToLowerInvariant() };
}
private static string GetToolVersion()
@@ -493,4 +589,104 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
?? "1.0.0";
return version;
}
private bool TryVerifyEnvelopeSignatures(
EnvDsseEnvelope envelope,
CancellationToken ct,
out string? failureReason)
{
if (envelope.Signatures.Count == 0)
{
failureReason = "Envelope does not contain signatures.";
return false;
}
foreach (var signature in envelope.Signatures)
{
ct.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(signature.KeyId))
{
continue;
}
var key = _keyResolver(signature.KeyId);
if (key is null)
{
continue;
}
if (!string.Equals(signature.KeyId, key.KeyId, StringComparison.Ordinal))
{
continue;
}
if (!TryDecodeSignature(signature.Signature, out var signatureBytes))
{
continue;
}
var envelopeSignature = new EnvelopeSignature(signature.KeyId, key.AlgorithmId, signatureBytes);
var verified = _signatureService.VerifyDsse(envelope.PayloadType, envelope.Payload.Span, envelopeSignature, key, ct);
if (verified.IsSuccess)
{
failureReason = null;
return true;
}
}
failureReason = "DSSE signature verification failed.";
return false;
}
private static bool TryDecodeSignature(string signature, out byte[] signatureBytes)
{
try
{
signatureBytes = Convert.FromBase64String(signature);
return signatureBytes.Length > 0;
}
catch (FormatException)
{
signatureBytes = [];
return false;
}
}
private static bool SequenceEqual(IReadOnlyList<string> left, IReadOnlyList<string> right)
{
if (left.Count != right.Count)
{
return false;
}
for (var i = 0; i < left.Count; i++)
{
if (!string.Equals(left[i], right[i], StringComparison.Ordinal))
{
return false;
}
}
return true;
}
private static string NormalizeDigest(string digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
throw new ArgumentException("Digest must be provided.", nameof(digest));
}
var trimmed = digest.Trim();
var colonIndex = trimmed.IndexOf(':');
if (colonIndex > 0 && colonIndex < trimmed.Length - 1)
{
var algorithm = trimmed[..colonIndex].ToLowerInvariant();
var value = trimmed[(colonIndex + 1)..].ToLowerInvariant();
return $"{algorithm}:{value}";
}
return $"sha256:{trimmed.ToLowerInvariant()}";
}
}

View File

@@ -1,3 +1,4 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Attestor.Envelope;
@@ -18,6 +19,7 @@ public static class GraphRootServiceCollectionExtensions
{
services.TryAddSingleton<IMerkleRootComputer, Sha256MerkleRootComputer>();
services.TryAddSingleton<EnvelopeSignatureService>();
services.TryAddSingleton(TimeProvider.System);
services.TryAddSingleton<IGraphRootAttestor, GraphRootAttestor>();
return services;
@@ -37,14 +39,16 @@ public static class GraphRootServiceCollectionExtensions
services.TryAddSingleton<IMerkleRootComputer, Sha256MerkleRootComputer>();
services.TryAddSingleton<EnvelopeSignatureService>();
services.TryAddSingleton(TimeProvider.System);
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);
var timeProvider = sp.GetService<TimeProvider>();
return new GraphRootAttestor(merkleComputer, signatureService, resolver, logger);
return new GraphRootAttestor(merkleComputer, signatureService, resolver, logger, timeProvider: timeProvider);
});
return services;

View File

@@ -6,6 +6,7 @@
<Nullable>enable</Nullable>
<RootNamespace>StellaOps.Attestor.GraphRoot</RootNamespace>
<Description>Graph root attestation service for creating and verifying DSSE attestations of Merkle graph roots.</Description>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,27 +0,0 @@
<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.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
</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" />
<ProjectReference Include="..\..\StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0053-M | DONE | Maintainability audit for StellaOps.Attestor.GraphRoot. |
| AUDIT-0053-T | DONE | Test coverage audit for StellaOps.Attestor.GraphRoot. |
| AUDIT-0053-A | TODO | Pending approval for changes. |
| AUDIT-0053-A | DONE | Applied audit remediation for graph root attestation. |