more audit work
This commit is contained in:
@@ -6,7 +6,8 @@ namespace StellaOps.Attestor.Core.Rekor;
|
||||
public sealed class RekorInclusionVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// True if inclusion proof was successfully verified.
|
||||
/// True if inclusion proof was successfully verified (Merkle path only).
|
||||
/// Check <see cref="CheckpointSignatureValid"/> for checkpoint signature status.
|
||||
/// </summary>
|
||||
public required bool Verified { get; init; }
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using NpgsqlTypes;
|
||||
using StellaOps.Attestor.Core.Observability;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Core.Queue;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Queue;
|
||||
|
||||
@@ -29,6 +30,7 @@ public sealed class PostgresRekorSubmissionQueue : IRekorSubmissionQueue
|
||||
private readonly AttestorMetrics _metrics;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<PostgresRekorSubmissionQueue> _logger;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
private const int DefaultCommandTimeoutSeconds = 30;
|
||||
|
||||
@@ -37,12 +39,14 @@ public sealed class PostgresRekorSubmissionQueue : IRekorSubmissionQueue
|
||||
IOptions<RekorQueueOptions> options,
|
||||
AttestorMetrics metrics,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
ILogger<PostgresRekorSubmissionQueue> logger)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -55,7 +59,7 @@ public sealed class PostgresRekorSubmissionQueue : IRekorSubmissionQueue
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var id = Guid.NewGuid();
|
||||
var id = _guidProvider.NewGuid();
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO attestor.rekor_submission_queue (
|
||||
@@ -138,7 +142,7 @@ public sealed class PostgresRekorSubmissionQueue : IRekorSubmissionQueue
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
var queuedAt = reader.GetDateTime(reader.GetOrdinal("created_at"));
|
||||
var queuedAt = reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at"));
|
||||
var waitTime = (now - queuedAt).TotalSeconds;
|
||||
_metrics.RekorQueueWaitTime.Record(waitTime);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@@ -131,7 +132,15 @@ internal sealed class HttpRekorClient : IRekorClient
|
||||
Origin = checkpointElement.TryGetProperty("origin", out var origin) ? origin.GetString() : null,
|
||||
Size = checkpointElement.TryGetProperty("size", out var size) && size.TryGetInt64(out var sizeValue) ? sizeValue : 0,
|
||||
RootHash = checkpointElement.TryGetProperty("rootHash", out var rootHash) ? rootHash.GetString() : null,
|
||||
Timestamp = checkpointElement.TryGetProperty("timestamp", out var ts) && ts.ValueKind == JsonValueKind.String && DateTimeOffset.TryParse(ts.GetString(), out var dto) ? dto : null
|
||||
Timestamp = checkpointElement.TryGetProperty("timestamp", out var ts)
|
||||
&& ts.ValueKind == JsonValueKind.String
|
||||
&& DateTimeOffset.TryParse(
|
||||
ts.GetString(),
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out var dto)
|
||||
? dto
|
||||
: null
|
||||
}
|
||||
: null,
|
||||
Inclusion = inclusionElement.ValueKind == JsonValueKind.Object
|
||||
@@ -269,6 +278,10 @@ internal sealed class HttpRekorClient : IRekorClient
|
||||
"Successfully verified Rekor inclusion for UUID {Uuid} at index {Index}",
|
||||
rekorUuid, logIndex);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Checkpoint signature verification is unavailable for UUID {Uuid}; treating checkpoint as unverified",
|
||||
rekorUuid);
|
||||
|
||||
return RekorInclusionVerificationResult.Success(
|
||||
logIndex.Value,
|
||||
computedRootHex,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
internal static class RekorBackendResolver
|
||||
{
|
||||
public static RekorBackend ResolveBackend(
|
||||
AttestorOptions options,
|
||||
string? backendName,
|
||||
bool allowFallbackToPrimary)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var normalized = string.IsNullOrWhiteSpace(backendName)
|
||||
? "primary"
|
||||
: backendName.Trim();
|
||||
|
||||
if (string.Equals(normalized, "primary", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BuildBackend("primary", options.Rekor.Primary);
|
||||
}
|
||||
|
||||
if (string.Equals(normalized, "mirror", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BuildBackend("mirror", options.Rekor.Mirror);
|
||||
}
|
||||
|
||||
if (allowFallbackToPrimary)
|
||||
{
|
||||
return BuildBackend(normalized, options.Rekor.Primary);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unknown Rekor backend: {backendName}");
|
||||
}
|
||||
|
||||
public static RekorBackend BuildBackend(string name, AttestorOptions.RekorBackendOptions options)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.Url))
|
||||
{
|
||||
throw new InvalidOperationException($"Rekor backend '{name}' is not configured.");
|
||||
}
|
||||
|
||||
return new RekorBackend
|
||||
{
|
||||
Name = name,
|
||||
Url = new Uri(options.Url, UriKind.Absolute),
|
||||
ProofTimeout = TimeSpan.FromMilliseconds(options.ProofTimeoutMs),
|
||||
PollInterval = TimeSpan.FromMilliseconds(options.PollIntervalMs),
|
||||
MaxAttempts = options.MaxAttempts
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ using StellaOps.Attestor.Core.InToto;
|
||||
using StellaOps.Attestor.Core.InToto.Layout;
|
||||
using StellaOps.Attestor.Infrastructure.InToto;
|
||||
using StellaOps.Attestor.Verify;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure;
|
||||
|
||||
@@ -39,6 +40,7 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
services.AddMemoryCache();
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
services.AddSystemGuidProvider();
|
||||
|
||||
services.AddSingleton<IDsseCanonicalizer, DefaultDsseCanonicalizer>();
|
||||
services.AddSingleton(sp =>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Plugin.BouncyCastle\StellaOps.Cryptography.Plugin.BouncyCastle.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Kms\StellaOps.Cryptography.Kms.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\Router/__Libraries/StellaOps.Messaging\StellaOps.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -15,6 +15,7 @@ using StellaOps.Attestor.Core.Storage;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Core.Transparency;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Submission;
|
||||
|
||||
@@ -384,7 +385,7 @@ internal sealed class AttestorSubmissionService : IAttestorSubmissionService
|
||||
AttestorOptions.RekorBackendOptions backendOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var backend = BuildBackend(backendName, backendOptions);
|
||||
var backend = RekorBackendResolver.BuildBackend(backendName, backendOptions);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
@@ -782,20 +783,4 @@ internal sealed class AttestorSubmissionService : IAttestorSubmissionService
|
||||
new SubmissionOutcome(backend, url ?? string.Empty, null, null, null, latency, error);
|
||||
}
|
||||
|
||||
private static RekorBackend BuildBackend(string name, AttestorOptions.RekorBackendOptions options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.Url))
|
||||
{
|
||||
throw new InvalidOperationException($"Rekor backend '{name}' is not configured.");
|
||||
}
|
||||
|
||||
return new RekorBackend
|
||||
{
|
||||
Name = name,
|
||||
Url = new Uri(options.Url, UriKind.Absolute),
|
||||
ProofTimeout = TimeSpan.FromMilliseconds(options.ProofTimeoutMs),
|
||||
PollInterval = TimeSpan.FromMilliseconds(options.PollIntervalMs),
|
||||
MaxAttempts = options.MaxAttempts
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0055-M | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0055-T | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0055-A | TODO | Reopened after revalidation 2026-01-06. |
|
||||
| AUDIT-0055-A | DONE | Applied determinism, backend resolver, and Rekor client fixes 2026-01-08. |
|
||||
| VAL-SMOKE-001 | DONE | Fixed continuation token behavior; unit tests pass. |
|
||||
|
||||
@@ -14,6 +14,7 @@ using StellaOps.Attestor.Core.Storage;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Core.Transparency;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
using StellaOps.Attestor.Verify;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Verification;
|
||||
@@ -238,7 +239,7 @@ internal sealed class AttestorVerificationService : IAttestorVerificationService
|
||||
var backendOptions = string.Equals(backendName, "mirror", StringComparison.OrdinalIgnoreCase)
|
||||
? _options.Rekor.Mirror
|
||||
: _options.Rekor.Primary;
|
||||
var backend = BuildBackend(backendName ?? "primary", backendOptions);
|
||||
var backend = RekorBackendResolver.ResolveBackend(_options, backendName, allowFallbackToPrimary: true);
|
||||
|
||||
using var activity = _activitySource.StartProofRefresh(backend.Name, _options.Verification.PolicyId);
|
||||
|
||||
@@ -354,23 +355,6 @@ internal sealed class AttestorVerificationService : IAttestorVerificationService
|
||||
};
|
||||
}
|
||||
|
||||
private static RekorBackend BuildBackend(string name, AttestorOptions.RekorBackendOptions options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.Url))
|
||||
{
|
||||
throw new InvalidOperationException($"Rekor backend '{name}' is not configured.");
|
||||
}
|
||||
|
||||
return new RekorBackend
|
||||
{
|
||||
Name = name,
|
||||
Url = new Uri(options.Url, UriKind.Absolute),
|
||||
ProofTimeout = TimeSpan.FromMilliseconds(options.ProofTimeoutMs),
|
||||
PollInterval = TimeSpan.FromMilliseconds(options.PollIntervalMs),
|
||||
MaxAttempts = options.MaxAttempts
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeTag(string? value) => string.IsNullOrWhiteSpace(value) ? "unknown" : value;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using StellaOps.Attestor.Core.Queue;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Workers;
|
||||
|
||||
@@ -153,7 +154,7 @@ public sealed class RekorRetryWorker : BackgroundService
|
||||
|
||||
try
|
||||
{
|
||||
var backend = ResolveBackend(item.Backend);
|
||||
var backend = RekorBackendResolver.ResolveBackend(_attestorOptions, item.Backend, allowFallbackToPrimary: false);
|
||||
var request = BuildSubmissionRequest(item);
|
||||
|
||||
var response = await _rekorClient.SubmitAsync(request, backend, ct);
|
||||
@@ -188,16 +189,6 @@ public sealed class RekorRetryWorker : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private RekorBackend ResolveBackend(string backend)
|
||||
{
|
||||
return backend.ToLowerInvariant() switch
|
||||
{
|
||||
"primary" => BuildBackend("primary", _attestorOptions.Rekor.Primary),
|
||||
"mirror" => BuildBackend("mirror", _attestorOptions.Rekor.Mirror),
|
||||
_ => throw new InvalidOperationException($"Unknown Rekor backend: {backend}")
|
||||
};
|
||||
}
|
||||
|
||||
private static AttestorSubmissionRequest BuildSubmissionRequest(RekorQueueItem item)
|
||||
{
|
||||
var dsseEnvelope = ParseDsseEnvelope(item.DssePayload);
|
||||
@@ -260,22 +251,6 @@ public sealed class RekorRetryWorker : BackgroundService
|
||||
};
|
||||
}
|
||||
|
||||
private static RekorBackend BuildBackend(string name, AttestorOptions.RekorBackendOptions options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.Url))
|
||||
{
|
||||
throw new InvalidOperationException($"Rekor backend '{name}' is not configured.");
|
||||
}
|
||||
|
||||
return new RekorBackend
|
||||
{
|
||||
Name = name,
|
||||
Url = new Uri(options.Url, UriKind.Absolute),
|
||||
ProofTimeout = TimeSpan.FromMilliseconds(options.ProofTimeoutMs),
|
||||
PollInterval = TimeSpan.FromMilliseconds(options.PollIntervalMs),
|
||||
MaxAttempts = options.MaxAttempts
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,6 +15,7 @@ using StellaOps.Attestor.Core.Observability;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Core.Queue;
|
||||
using StellaOps.Attestor.Infrastructure.Queue;
|
||||
using StellaOps.Determinism;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Xunit;
|
||||
|
||||
@@ -63,6 +64,7 @@ public class PostgresRekorSubmissionQueueIntegrationTests : IAsyncLifetime
|
||||
}),
|
||||
_metrics,
|
||||
_timeProvider,
|
||||
SystemGuidProvider.Instance,
|
||||
NullLogger<PostgresRekorSubmissionQueue>.Instance);
|
||||
}
|
||||
|
||||
@@ -261,6 +263,7 @@ public class PostgresRekorSubmissionQueueIntegrationTests : IAsyncLifetime
|
||||
Options.Create(new RekorQueueOptions { MaxAttempts = 2 }),
|
||||
_metrics,
|
||||
_timeProvider,
|
||||
SystemGuidProvider.Instance,
|
||||
NullLogger<PostgresRekorSubmissionQueue>.Instance);
|
||||
|
||||
var id = await queue.EnqueueAsync("tenant-1", "sha256:deadletter", new byte[] { 0x01 }, "primary");
|
||||
@@ -307,6 +310,7 @@ public class PostgresRekorSubmissionQueueIntegrationTests : IAsyncLifetime
|
||||
Options.Create(new RekorQueueOptions { MaxAttempts = 1 }),
|
||||
_metrics,
|
||||
_timeProvider,
|
||||
SystemGuidProvider.Instance,
|
||||
NullLogger<PostgresRekorSubmissionQueue>.Instance);
|
||||
|
||||
var id = await queue.EnqueueAsync("tenant-dlq", "sha256:dlq", new byte[] { 0x01 }, "primary");
|
||||
|
||||
Reference in New Issue
Block a user