save progress

This commit is contained in:
StellaOps Bot
2026-01-04 19:08:47 +02:00
parent f7d27c6fda
commit 75611a505f
97 changed files with 4531 additions and 293 deletions

View File

@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Determinism;
using StellaOps.Signer.Core;
namespace StellaOps.Signer.Infrastructure.Auditing;
@@ -11,11 +12,16 @@ public sealed class InMemorySignerAuditSink : ISignerAuditSink
{
private readonly ConcurrentDictionary<string, SignerAuditEntry> _entries = new(StringComparer.Ordinal);
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ILogger<InMemorySignerAuditSink> _logger;
public InMemorySignerAuditSink(TimeProvider timeProvider, ILogger<InMemorySignerAuditSink> logger)
public InMemorySignerAuditSink(
TimeProvider timeProvider,
ILogger<InMemorySignerAuditSink> logger,
IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -30,7 +36,7 @@ public sealed class InMemorySignerAuditSink : ISignerAuditSink
ArgumentNullException.ThrowIfNull(entitlement);
ArgumentNullException.ThrowIfNull(caller);
var auditId = Guid.NewGuid().ToString("d");
var auditId = _guidProvider.NewGuid().ToString("d");
var entry = new SignerAuditEntry(
auditId,
_timeProvider.GetUtcNow(),

View File

@@ -3,6 +3,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Determinism;
using StellaOps.Signer.Core;
namespace StellaOps.Signer.Infrastructure.Signing;
@@ -17,15 +18,18 @@ public sealed class DefaultSigningKeyResolver : ISigningKeyResolver
private readonly DsseSignerOptions _options;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ILogger<DefaultSigningKeyResolver> _logger;
public DefaultSigningKeyResolver(
IOptions<DsseSignerOptions> options,
TimeProvider timeProvider,
ILogger<DefaultSigningKeyResolver> logger)
ILogger<DefaultSigningKeyResolver> logger,
IGuidProvider? guidProvider = null)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -56,7 +60,7 @@ public sealed class DefaultSigningKeyResolver : ISigningKeyResolver
{
// Generate ephemeral key identifier using timestamp for uniqueness
var now = _timeProvider.GetUtcNow();
var keyId = $"{KeylessKeyIdPrefix}{tenant}:{now:yyyyMMddHHmmss}:{Guid.NewGuid():N}";
var keyId = $"{KeylessKeyIdPrefix}{tenant}:{now:yyyyMMddHHmmss}:{_guidProvider.NewGuid():N}";
var expiresAt = now.AddMinutes(KeylessExpiryMinutes);
return new SigningKeyResolution(

View File

@@ -18,17 +18,20 @@ public sealed class SigstoreSigningService : ISigstoreSigningService
private readonly IFulcioClient _fulcioClient;
private readonly IRekorClient _rekorClient;
private readonly SigstoreOptions _options;
private readonly TimeProvider _timeProvider;
private readonly ILogger<SigstoreSigningService> _logger;
public SigstoreSigningService(
IFulcioClient fulcioClient,
IRekorClient rekorClient,
IOptions<SigstoreOptions> options,
ILogger<SigstoreSigningService> logger)
ILogger<SigstoreSigningService> logger,
TimeProvider? timeProvider = null)
{
_fulcioClient = fulcioClient ?? throw new ArgumentNullException(nameof(fulcioClient));
_rekorClient = rekorClient ?? throw new ArgumentNullException(nameof(rekorClient));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -133,7 +136,7 @@ public sealed class SigstoreSigningService : ISigstoreSigningService
}
// 3. Check certificate validity
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
if (now < cert.NotBefore || now > cert.NotAfter)
{
_logger.LogWarning(

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />

View File

@@ -145,6 +145,7 @@ public static class KeyRotationEndpoints
[FromBody] RevokeKeyRequestDto request,
IKeyRotationService rotationService,
ILoggerFactory loggerFactory,
TimeProvider timeProvider,
CancellationToken ct)
{
var logger = loggerFactory.CreateLogger("KeyRotationEndpoints.RevokeKey");
@@ -183,7 +184,7 @@ public static class KeyRotationEndpoints
{
KeyId = keyId,
AnchorId = anchorId,
RevokedAt = request.EffectiveAt ?? DateTimeOffset.UtcNow,
RevokedAt = request.EffectiveAt ?? timeProvider.GetUtcNow(),
Reason = request.Reason,
AllowedKeyIds = result.AllowedKeyIds.ToList(),
RevokedKeyIds = result.RevokedKeyIds.ToList(),
@@ -217,9 +218,10 @@ public static class KeyRotationEndpoints
[FromRoute] string keyId,
[FromQuery] DateTimeOffset? signedAt,
IKeyRotationService rotationService,
TimeProvider timeProvider,
CancellationToken ct)
{
var checkTime = signedAt ?? DateTimeOffset.UtcNow;
var checkTime = signedAt ?? timeProvider.GetUtcNow();
try
{

View File

@@ -17,8 +17,14 @@ builder.Services.AddAuthentication(StubBearerAuthenticationDefaults.Authenticati
builder.Services.AddAuthorization();
builder.Services.AddSignerPipeline();
// Configure TimeProvider for deterministic testing support
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.Configure<SignerEntitlementOptions>(options =>
{
// Note: Using 1-hour expiry for demo/test tokens.
// Actual expiry is calculated at runtime relative to TimeProvider.
options.Tokens["valid-poe"] = new SignerEntitlementDefinition(
LicenseId: "LIC-TEST",
CustomerId: "CUST-TEST",

View File

@@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Determinism;
using StellaOps.Signer.KeyManagement.Entities;
namespace StellaOps.Signer.KeyManagement;
@@ -22,17 +23,20 @@ public sealed class KeyRotationService : IKeyRotationService
private readonly ILogger<KeyRotationService> _logger;
private readonly KeyRotationOptions _options;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public KeyRotationService(
KeyManagementDbContext dbContext,
ILogger<KeyRotationService> logger,
IOptions<KeyRotationOptions> options,
TimeProvider? timeProvider = null)
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? new KeyRotationOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
/// <inheritdoc />
@@ -85,7 +89,7 @@ public sealed class KeyRotationService : IKeyRotationService
// Create key history entry
var keyEntry = new KeyHistoryEntity
{
HistoryId = Guid.NewGuid(),
HistoryId = _guidProvider.NewGuid(),
AnchorId = anchorId,
KeyId = request.KeyId,
PublicKey = request.PublicKey,
@@ -106,7 +110,7 @@ public sealed class KeyRotationService : IKeyRotationService
// Create audit log entry
var auditEntry = new KeyAuditLogEntity
{
LogId = Guid.NewGuid(),
LogId = _guidProvider.NewGuid(),
AnchorId = anchorId,
KeyId = request.KeyId,
Operation = KeyOperation.Add,
@@ -209,7 +213,7 @@ public sealed class KeyRotationService : IKeyRotationService
// Create audit log entry
var auditEntry = new KeyAuditLogEntity
{
LogId = Guid.NewGuid(),
LogId = _guidProvider.NewGuid(),
AnchorId = anchorId,
KeyId = keyId,
Operation = KeyOperation.Revoke,

View File

@@ -15,6 +15,10 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Migrations\*.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

View File

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using StellaOps.Determinism;
using StellaOps.Signer.KeyManagement.Entities;
namespace StellaOps.Signer.KeyManagement;
@@ -22,17 +23,20 @@ public sealed class TrustAnchorManager : ITrustAnchorManager
private readonly IKeyRotationService _keyRotationService;
private readonly ILogger<TrustAnchorManager> _logger;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public TrustAnchorManager(
KeyManagementDbContext dbContext,
IKeyRotationService keyRotationService,
ILogger<TrustAnchorManager> logger,
TimeProvider? timeProvider = null)
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_keyRotationService = keyRotationService ?? throw new ArgumentNullException(nameof(keyRotationService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
/// <inheritdoc />
@@ -115,7 +119,7 @@ public sealed class TrustAnchorManager : ITrustAnchorManager
var entity = new TrustAnchorEntity
{
AnchorId = Guid.NewGuid(),
AnchorId = _guidProvider.NewGuid(),
PurlPattern = request.PurlPattern,
AllowedKeyIds = request.AllowedKeyIds?.ToList() ?? [],
AllowedPredicateTypes = request.AllowedPredicateTypes?.ToList(),

View File

@@ -21,13 +21,15 @@ public sealed class AmbientOidcTokenProvider : IOidcTokenProvider, IDisposable
private readonly JwtSecurityTokenHandler _tokenHandler;
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly FileSystemWatcher? _watcher;
private readonly TimeProvider _timeProvider;
private OidcTokenResult? _cachedToken;
private bool _disposed;
public AmbientOidcTokenProvider(
OidcAmbientConfig config,
ILogger<AmbientOidcTokenProvider> logger)
ILogger<AmbientOidcTokenProvider> logger,
TimeProvider? timeProvider = null)
{
ArgumentNullException.ThrowIfNull(config);
ArgumentNullException.ThrowIfNull(logger);
@@ -35,6 +37,7 @@ public sealed class AmbientOidcTokenProvider : IOidcTokenProvider, IDisposable
_config = config;
_logger = logger;
_tokenHandler = new JwtSecurityTokenHandler();
_timeProvider = timeProvider ?? TimeProvider.System;
if (_config.WatchForChanges && File.Exists(_config.TokenPath))
{
@@ -65,7 +68,8 @@ public sealed class AmbientOidcTokenProvider : IOidcTokenProvider, IDisposable
try
{
// Check cache first
if (_cachedToken is not null && !_cachedToken.WillExpireSoon(TimeSpan.FromSeconds(30)))
var now = _timeProvider.GetUtcNow();
if (_cachedToken is not null && !_cachedToken.WillExpireSoon(now, TimeSpan.FromSeconds(30)))
{
return _cachedToken;
}
@@ -111,7 +115,7 @@ public sealed class AmbientOidcTokenProvider : IOidcTokenProvider, IDisposable
public OidcTokenResult? GetCachedToken()
{
var cached = _cachedToken;
if (cached is null || cached.IsExpired)
if (cached is null || cached.IsExpiredAt(_timeProvider.GetUtcNow()))
{
return null;
}
@@ -132,7 +136,7 @@ public sealed class AmbientOidcTokenProvider : IOidcTokenProvider, IDisposable
var expiresAt = jwt.ValidTo != DateTime.MinValue
? new DateTimeOffset(jwt.ValidTo, TimeSpan.Zero)
: DateTimeOffset.UtcNow.AddHours(1); // Default if no exp claim
: _timeProvider.GetUtcNow().AddHours(1); // Default if no exp claim
var subject = jwt.Subject;
var email = jwt.Claims.FirstOrDefault(c => c.Type == "email")?.Value;

View File

@@ -47,7 +47,8 @@ public sealed class EphemeralKeyPair : IDisposable
/// <param name="publicKey">The public key bytes.</param>
/// <param name="privateKey">The private key bytes (will be copied).</param>
/// <param name="algorithm">The algorithm identifier.</param>
public EphemeralKeyPair(byte[] publicKey, byte[] privateKey, string algorithm)
/// <param name="timeProvider">Optional time provider for deterministic timestamp.</param>
public EphemeralKeyPair(byte[] publicKey, byte[] privateKey, string algorithm, TimeProvider? timeProvider = null)
{
ArgumentNullException.ThrowIfNull(publicKey);
ArgumentNullException.ThrowIfNull(privateKey);
@@ -56,7 +57,7 @@ public sealed class EphemeralKeyPair : IDisposable
_publicKey = (byte[])publicKey.Clone();
_privateKey = (byte[])privateKey.Clone();
Algorithm = algorithm;
CreatedAt = DateTimeOffset.UtcNow;
CreatedAt = (timeProvider ?? TimeProvider.System).GetUtcNow();
}
/// <summary>

View File

@@ -75,9 +75,11 @@ public sealed record FulcioCertificateResult(
public TimeSpan Validity => NotAfter - NotBefore;
/// <summary>
/// Checks if the certificate is currently valid.
/// Checks if the certificate is valid at the specified time.
/// </summary>
public bool IsValid => DateTimeOffset.UtcNow >= NotBefore && DateTimeOffset.UtcNow <= NotAfter;
/// <param name="at">The time to check validity against.</param>
/// <returns>True if the certificate is valid at the specified time.</returns>
public bool IsValidAt(DateTimeOffset at) => at >= NotBefore && at <= NotAfter;
/// <summary>
/// Gets the full certificate chain including the leaf certificate.

View File

@@ -62,15 +62,20 @@ public sealed record OidcTokenResult
public string? Email { get; init; }
/// <summary>
/// Whether the token is expired.
/// Checks whether the token is expired at the specified time.
/// </summary>
public bool IsExpired => DateTimeOffset.UtcNow >= ExpiresAt;
/// <param name="now">The time to check against.</param>
/// <returns>True if the token is expired.</returns>
public bool IsExpiredAt(DateTimeOffset now) => now >= ExpiresAt;
/// <summary>
/// Whether the token will expire within the specified buffer time.
/// Checks whether the token will expire within the specified buffer time.
/// </summary>
public bool WillExpireSoon(TimeSpan buffer) =>
DateTimeOffset.UtcNow.Add(buffer) >= ExpiresAt;
/// <param name="now">The current time.</param>
/// <param name="buffer">The time buffer before expiration.</param>
/// <returns>True if the token will expire soon.</returns>
public bool WillExpireSoon(DateTimeOffset now, TimeSpan buffer) =>
now.Add(buffer) >= ExpiresAt;
}
/// <summary>