save progress
This commit is contained in:
@@ -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()}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user