doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements

This commit is contained in:
master
2026-01-19 09:02:59 +02:00
parent 8c4bf54aed
commit 17419ba7c4
809 changed files with 170738 additions and 12244 deletions

View File

@@ -0,0 +1,399 @@
// -----------------------------------------------------------------------------
// VerdictRekorPublisher.cs
// Sprint: SPRINT_20260118_016_Attestor_rekor_publishing_path
// Task: RP-003 - Create VerdictRekorPublisher service
// Description: Orchestrates verdict publishing to Rekor transparency log
// -----------------------------------------------------------------------------
using System.Threading.Channels;
namespace StellaOps.Attestor.Rekor;
/// <summary>
/// Orchestrates verdict publishing to Rekor transparency log.
/// Handles signing, submission, and proof verification.
/// </summary>
public sealed class VerdictRekorPublisher : IVerdictRekorPublisher
{
private readonly IRekorClient _rekorClient;
private readonly ISignerClient? _signerClient;
private readonly IVerdictLedgerService? _ledgerService;
private readonly VerdictRekorPublisherOptions _options;
private readonly Channel<VerdictPublishRequest> _publishQueue;
/// <summary>
/// Creates a new verdict Rekor publisher.
/// </summary>
public VerdictRekorPublisher(
IRekorClient rekorClient,
ISignerClient? signerClient = null,
IVerdictLedgerService? ledgerService = null,
VerdictRekorPublisherOptions? options = null)
{
_rekorClient = rekorClient ?? throw new ArgumentNullException(nameof(rekorClient));
_signerClient = signerClient;
_ledgerService = ledgerService;
_options = options ?? new VerdictRekorPublisherOptions();
_publishQueue = Channel.CreateBounded<VerdictPublishRequest>(
new BoundedChannelOptions(_options.QueueCapacity)
{
FullMode = BoundedChannelFullMode.Wait
});
}
/// <inheritdoc />
public async Task<VerdictPublishResult> PublishAsync(
VerdictPublishRequest request,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(request);
try
{
// 1. Build DSSE envelope
var envelope = await BuildEnvelopeAsync(request, ct);
// 2. Submit to Rekor
var submission = await _rekorClient.SubmitAsync(envelope, ct);
// 3. Verify inclusion proof
if (submission.InclusionProof != null && _options.VerifyImmediately)
{
var verified = await _rekorClient.VerifyInclusionAsync(
submission.LogIndex,
submission.InclusionProof,
ct);
if (!verified)
{
return VerdictPublishResult.Failed(
"Inclusion proof verification failed",
submission.Uuid);
}
}
// 4. Update verdict ledger with Rekor UUID
if (_ledgerService != null && !string.IsNullOrEmpty(request.VerdictLedgerId))
{
await UpdateLedgerWithRekorUuidAsync(
Guid.Parse(request.VerdictLedgerId),
submission.Uuid,
ct);
}
return VerdictPublishResult.Success(
submission.Uuid,
submission.LogIndex,
submission.IntegratedTime);
}
catch (RekorCircuitOpenException ex)
{
// Queue for retry
if (_options.QueueOnCircuitOpen)
{
await _publishQueue.Writer.WriteAsync(request, ct);
return VerdictPublishResult.Queued(ex.Message);
}
return VerdictPublishResult.Failed(ex.Message);
}
catch (Exception ex)
{
return VerdictPublishResult.Failed(ex.Message);
}
}
/// <inheritdoc />
public async Task<VerdictPublishResult> PublishDeferredAsync(
VerdictPublishRequest request,
CancellationToken ct = default)
{
await _publishQueue.Writer.WriteAsync(request, ct);
return VerdictPublishResult.Queued("Deferred for background processing");
}
/// <inheritdoc />
public IAsyncEnumerable<VerdictPublishRequest> GetPendingAsync(CancellationToken ct = default)
{
return _publishQueue.Reader.ReadAllAsync(ct);
}
private async Task<DsseEnvelope> BuildEnvelopeAsync(
VerdictPublishRequest request,
CancellationToken ct)
{
// Build the verdict payload
var payload = new VerdictPayload
{
VerdictHash = request.VerdictHash,
Decision = request.Decision,
BomRef = request.BomRef,
PolicyBundleHash = request.PolicyBundleHash,
Timestamp = request.Timestamp ?? DateTimeOffset.UtcNow
};
var payloadBytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(payload);
var payloadBase64 = Convert.ToBase64String(payloadBytes);
// Sign if signer is available
byte[]? signature = null;
string? keyId = null;
if (_signerClient != null)
{
var signResult = await _signerClient.SignAsync(payloadBytes, ct);
signature = signResult.Signature;
keyId = signResult.KeyId;
}
return new DsseEnvelope
{
PayloadType = "application/vnd.stellaops.verdict+json",
Payload = payloadBase64,
Signatures = signature != null
? [new DsseSignature { KeyId = keyId, Sig = Convert.ToBase64String(signature) }]
: []
};
}
private async Task UpdateLedgerWithRekorUuidAsync(
Guid ledgerId,
string rekorUuid,
CancellationToken ct)
{
// This would update the ledger entry with the Rekor UUID
// Implementation depends on ledger service interface
await Task.CompletedTask;
}
}
/// <summary>
/// Interface for verdict Rekor publishing.
/// </summary>
public interface IVerdictRekorPublisher
{
/// <summary>
/// Publishes a verdict to Rekor immediately.
/// </summary>
Task<VerdictPublishResult> PublishAsync(VerdictPublishRequest request, CancellationToken ct = default);
/// <summary>
/// Queues a verdict for deferred publishing.
/// </summary>
Task<VerdictPublishResult> PublishDeferredAsync(VerdictPublishRequest request, CancellationToken ct = default);
/// <summary>
/// Gets pending publish requests.
/// </summary>
IAsyncEnumerable<VerdictPublishRequest> GetPendingAsync(CancellationToken ct = default);
}
/// <summary>
/// Request to publish a verdict to Rekor.
/// </summary>
public sealed record VerdictPublishRequest
{
/// <summary>Verdict ledger ID.</summary>
public string? VerdictLedgerId { get; init; }
/// <summary>Verdict hash.</summary>
public required string VerdictHash { get; init; }
/// <summary>Decision.</summary>
public required string Decision { get; init; }
/// <summary>BOM reference.</summary>
public required string BomRef { get; init; }
/// <summary>Policy bundle hash.</summary>
public required string PolicyBundleHash { get; init; }
/// <summary>Timestamp.</summary>
public DateTimeOffset? Timestamp { get; init; }
}
/// <summary>
/// Result of verdict publishing.
/// </summary>
public sealed record VerdictPublishResult
{
/// <summary>Publish status.</summary>
public required VerdictPublishStatus Status { get; init; }
/// <summary>Rekor UUID (if published).</summary>
public string? RekorUuid { get; init; }
/// <summary>Rekor log index (if published).</summary>
public long? LogIndex { get; init; }
/// <summary>Integrated time (if published).</summary>
public DateTimeOffset? IntegratedTime { get; init; }
/// <summary>Error message (if failed).</summary>
public string? ErrorMessage { get; init; }
/// <summary>Creates a success result.</summary>
public static VerdictPublishResult Success(string rekorUuid, long logIndex, DateTimeOffset integratedTime)
{
return new VerdictPublishResult
{
Status = VerdictPublishStatus.Published,
RekorUuid = rekorUuid,
LogIndex = logIndex,
IntegratedTime = integratedTime
};
}
/// <summary>Creates a queued result.</summary>
public static VerdictPublishResult Queued(string message)
{
return new VerdictPublishResult
{
Status = VerdictPublishStatus.Queued,
ErrorMessage = message
};
}
/// <summary>Creates a failed result.</summary>
public static VerdictPublishResult Failed(string message, string? rekorUuid = null)
{
return new VerdictPublishResult
{
Status = VerdictPublishStatus.Failed,
RekorUuid = rekorUuid,
ErrorMessage = message
};
}
}
/// <summary>
/// Publish status.
/// </summary>
public enum VerdictPublishStatus
{
/// <summary>Successfully published to Rekor.</summary>
Published,
/// <summary>Queued for later publishing.</summary>
Queued,
/// <summary>Publishing failed.</summary>
Failed
}
/// <summary>
/// Options for verdict Rekor publisher.
/// </summary>
public sealed record VerdictRekorPublisherOptions
{
/// <summary>Queue capacity for deferred submissions.</summary>
public int QueueCapacity { get; init; } = 1000;
/// <summary>Whether to verify inclusion immediately after submission.</summary>
public bool VerifyImmediately { get; init; } = true;
/// <summary>Whether to queue submissions when circuit is open.</summary>
public bool QueueOnCircuitOpen { get; init; } = true;
}
/// <summary>
/// Exception when Rekor circuit breaker is open.
/// </summary>
public sealed class RekorCircuitOpenException : Exception
{
/// <summary>Creates a new exception.</summary>
public RekorCircuitOpenException(string message) : base(message) { }
}
// Supporting types
/// <summary>DSSE envelope.</summary>
public sealed record DsseEnvelope
{
/// <summary>Payload type.</summary>
public required string PayloadType { get; init; }
/// <summary>Base64-encoded payload.</summary>
public required string Payload { get; init; }
/// <summary>Signatures.</summary>
public IReadOnlyList<DsseSignature> Signatures { get; init; } = [];
}
/// <summary>DSSE signature.</summary>
public sealed record DsseSignature
{
/// <summary>Key ID.</summary>
public string? KeyId { get; init; }
/// <summary>Base64-encoded signature.</summary>
public required string Sig { get; init; }
}
/// <summary>Verdict payload for Rekor.</summary>
public sealed record VerdictPayload
{
/// <summary>Verdict hash.</summary>
public required string VerdictHash { get; init; }
/// <summary>Decision.</summary>
public required string Decision { get; init; }
/// <summary>BOM reference.</summary>
public required string BomRef { get; init; }
/// <summary>Policy bundle hash.</summary>
public required string PolicyBundleHash { get; init; }
/// <summary>Timestamp.</summary>
public required DateTimeOffset Timestamp { get; init; }
}
/// <summary>Rekor client interface.</summary>
public interface IRekorClient
{
/// <summary>Submits an envelope to Rekor.</summary>
Task<RekorSubmissionResult> SubmitAsync(DsseEnvelope envelope, CancellationToken ct = default);
/// <summary>Verifies an inclusion proof.</summary>
Task<bool> VerifyInclusionAsync(long logIndex, object proof, CancellationToken ct = default);
}
/// <summary>Rekor submission result.</summary>
public sealed record RekorSubmissionResult
{
/// <summary>UUID.</summary>
public required string Uuid { get; init; }
/// <summary>Log index.</summary>
public required long LogIndex { get; init; }
/// <summary>Integrated time.</summary>
public required DateTimeOffset IntegratedTime { get; init; }
/// <summary>Inclusion proof.</summary>
public object? InclusionProof { get; init; }
}
/// <summary>Signer client interface.</summary>
public interface ISignerClient
{
/// <summary>Signs data.</summary>
Task<SignResult> SignAsync(byte[] data, CancellationToken ct = default);
}
/// <summary>Sign result.</summary>
public sealed record SignResult
{
/// <summary>Signature bytes.</summary>
public required byte[] Signature { get; init; }
/// <summary>Key ID.</summary>
public required string KeyId { get; init; }
}
/// <summary>Verdict ledger service interface.</summary>
public interface IVerdictLedgerService
{
// Interface defined in VerdictLedger module
}

View File

@@ -0,0 +1,233 @@
// -----------------------------------------------------------------------------
// RekorCircuitBreakerPolicy.cs
// Sprint: SPRINT_20260118_016_Attestor_rekor_publishing_path
// Task: RP-004 - Add circuit breaker for Rekor availability
// Description: Polly-based circuit breaker for Rekor API calls
// -----------------------------------------------------------------------------
namespace StellaOps.Attestor.Infrastructure.Resilience;
/// <summary>
/// Circuit breaker policy for Rekor API calls.
/// Uses Polly patterns for HTTP resilience.
/// </summary>
public sealed class RekorCircuitBreakerPolicy
{
private readonly RekorCircuitBreakerOptions _options;
private CircuitState _state = CircuitState.Closed;
private int _failureCount;
private DateTimeOffset _lastFailure;
private DateTimeOffset _circuitOpenedAt;
private readonly object _lock = new();
/// <summary>
/// Current circuit state.
/// </summary>
public CircuitState State
{
get
{
lock (_lock)
{
UpdateState();
return _state;
}
}
}
/// <summary>
/// Creates a new circuit breaker policy.
/// </summary>
public RekorCircuitBreakerPolicy(RekorCircuitBreakerOptions? options = null)
{
_options = options ?? new RekorCircuitBreakerOptions();
}
/// <summary>
/// Executes an action with circuit breaker protection.
/// </summary>
public async Task<T> ExecuteAsync<T>(
Func<CancellationToken, Task<T>> action,
CancellationToken ct = default)
{
lock (_lock)
{
UpdateState();
if (_state == CircuitState.Open)
{
throw new RekorCircuitOpenException(
$"Circuit is open. Retry after {_circuitOpenedAt.Add(_options.BreakDuration) - DateTimeOffset.UtcNow}");
}
}
try
{
var result = await action(ct);
lock (_lock)
{
OnSuccess();
}
return result;
}
catch (Exception ex) when (IsTransientException(ex))
{
lock (_lock)
{
OnFailure();
}
throw;
}
}
/// <summary>
/// Records a successful call.
/// </summary>
public void OnSuccess()
{
lock (_lock)
{
_failureCount = 0;
if (_state == CircuitState.HalfOpen)
{
_state = CircuitState.Closed;
}
}
}
/// <summary>
/// Records a failed call.
/// </summary>
public void OnFailure()
{
lock (_lock)
{
_failureCount++;
_lastFailure = DateTimeOffset.UtcNow;
if (_failureCount >= _options.FailureThreshold)
{
_state = CircuitState.Open;
_circuitOpenedAt = DateTimeOffset.UtcNow;
}
}
}
/// <summary>
/// Manually resets the circuit breaker.
/// </summary>
public void Reset()
{
lock (_lock)
{
_state = CircuitState.Closed;
_failureCount = 0;
}
}
private void UpdateState()
{
if (_state == CircuitState.Open)
{
var elapsed = DateTimeOffset.UtcNow - _circuitOpenedAt;
if (elapsed >= _options.BreakDuration)
{
_state = CircuitState.HalfOpen;
}
}
}
private static bool IsTransientException(Exception ex)
{
return ex is HttpRequestException ||
ex is TaskCanceledException ||
ex is TimeoutException ||
(ex is AggregateException ae && ae.InnerExceptions.Any(IsTransientException));
}
}
/// <summary>
/// Circuit breaker state.
/// </summary>
public enum CircuitState
{
/// <summary>Circuit is closed, requests flow normally.</summary>
Closed,
/// <summary>Circuit is open, requests are rejected.</summary>
Open,
/// <summary>Circuit is half-open, allowing test requests.</summary>
HalfOpen
}
/// <summary>
/// Options for Rekor circuit breaker.
/// </summary>
public sealed record RekorCircuitBreakerOptions
{
/// <summary>
/// Number of consecutive failures before opening the circuit.
/// Default: 5.
/// </summary>
public int FailureThreshold { get; init; } = 5;
/// <summary>
/// Duration the circuit stays open before transitioning to half-open.
/// Default: 30 seconds.
/// </summary>
public TimeSpan BreakDuration { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Number of successful calls in half-open state before closing.
/// Default: 2.
/// </summary>
public int SuccessThreshold { get; init; } = 2;
/// <summary>
/// Timeout for individual requests.
/// Default: 10 seconds.
/// </summary>
public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(10);
}
/// <summary>
/// Polly-compatible circuit breaker handler for HttpClient.
/// </summary>
public sealed class RekorCircuitBreakerHandler : DelegatingHandler
{
private readonly RekorCircuitBreakerPolicy _policy;
/// <summary>
/// Creates a new circuit breaker handler.
/// </summary>
public RekorCircuitBreakerHandler(RekorCircuitBreakerPolicy policy)
{
_policy = policy ?? throw new ArgumentNullException(nameof(policy));
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken ct)
{
return await _policy.ExecuteAsync(async token =>
{
var response = await base.SendAsync(request, token);
if (!response.IsSuccessStatusCode &&
(int)response.StatusCode >= 500)
{
throw new HttpRequestException(
$"Server error: {response.StatusCode}");
}
return response;
}, ct);
}
}

View File

@@ -0,0 +1,446 @@
// -----------------------------------------------------------------------------
// TsaMultiProvider.cs
// Sprint: SPRINT_20260118_028_Attestor_rfc3161_tsa_client
// Tasks: TASK-028-001, TASK-028-002
// Description: Multi-provider RFC 3161 TSA client with fallback chain
// -----------------------------------------------------------------------------
namespace StellaOps.Attestor.Infrastructure.Timestamping;
/// <summary>
/// Multi-provider RFC 3161 Timestamp Authority client with fallback chain support.
/// </summary>
public interface IMultiProviderTsaClient
{
/// <summary>
/// Requests a timestamp token using the configured provider chain.
/// </summary>
Task<TsaTimestampResult> TimestampAsync(byte[] data, TsaTimestampOptions? options = null, CancellationToken ct = default);
/// <summary>
/// Requests a timestamp token for a specific provider.
/// </summary>
Task<TsaTimestampResult> TimestampWithProviderAsync(string providerName, byte[] data, CancellationToken ct = default);
/// <summary>
/// Gets available provider names.
/// </summary>
IReadOnlyList<string> GetProviderNames();
}
/// <summary>
/// Default implementation of multi-provider TSA client.
/// </summary>
public sealed class MultiProviderTsaClient : IMultiProviderTsaClient
{
private readonly TsaMultiProviderOptions _options;
private readonly HttpClient _httpClient;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Creates a new multi-provider TSA client.
/// </summary>
public MultiProviderTsaClient(
TsaMultiProviderOptions options,
HttpClient httpClient,
TimeProvider? timeProvider = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
public async Task<TsaTimestampResult> TimestampAsync(
byte[] data,
TsaTimestampOptions? options = null,
CancellationToken ct = default)
{
if (!_options.Enabled)
{
return TsaTimestampResult.Disabled();
}
var providerOrder = GetProviderOrder();
var errors = new List<TsaProviderError>();
foreach (var providerName in providerOrder)
{
ct.ThrowIfCancellationRequested();
if (!_options.Providers.TryGetValue(providerName, out var config))
{
continue;
}
var result = await TryTimestampAsync(providerName, config, data, ct);
if (result.Success)
{
return result;
}
errors.Add(new TsaProviderError
{
ProviderName = providerName,
Error = result.ErrorMessage ?? "Unknown error"
});
}
if (_options.RequireTimestamp)
{
return TsaTimestampResult.Failed(
"All TSA providers failed",
errors);
}
return TsaTimestampResult.Skipped("Timestamp not required and all providers failed", errors);
}
/// <inheritdoc />
public async Task<TsaTimestampResult> TimestampWithProviderAsync(
string providerName,
byte[] data,
CancellationToken ct = default)
{
if (!_options.Providers.TryGetValue(providerName, out var config))
{
return TsaTimestampResult.Failed($"Provider '{providerName}' not configured");
}
return await TryTimestampAsync(providerName, config, data, ct);
}
/// <inheritdoc />
public IReadOnlyList<string> GetProviderNames()
{
return _options.Providers.Keys.ToList();
}
private string[] GetProviderOrder()
{
if (_options.FallbackOrder.Length > 0)
{
return _options.FallbackOrder;
}
// Default: start with default provider, then others
var order = new List<string>();
if (!string.IsNullOrEmpty(_options.DefaultProvider) &&
_options.Providers.ContainsKey(_options.DefaultProvider))
{
order.Add(_options.DefaultProvider);
}
order.AddRange(_options.Providers.Keys.Where(k => k != _options.DefaultProvider));
return order.ToArray();
}
private async Task<TsaTimestampResult> TryTimestampAsync(
string providerName,
TsaProviderConfig config,
byte[] data,
CancellationToken ct)
{
try
{
// Generate timestamp request
var request = BuildTimestampRequest(data, config);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(config.TimeoutSeconds));
// Send request
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, config.Url)
{
Content = new ByteArrayContent(request)
};
httpRequest.Content.Headers.ContentType = new("application/timestamp-query");
using var response = await _httpClient.SendAsync(httpRequest, cts.Token);
if (!response.IsSuccessStatusCode)
{
return TsaTimestampResult.Failed(
$"TSA returned {response.StatusCode}",
providerName: providerName);
}
var responseBytes = await response.Content.ReadAsByteArrayAsync(cts.Token);
// Parse and validate response
var parsedResponse = ParseTimestampResponse(responseBytes);
if (!parsedResponse.Success)
{
return TsaTimestampResult.Failed(
parsedResponse.ErrorMessage ?? "Invalid response",
providerName: providerName);
}
return TsaTimestampResult.Succeeded(
providerName,
responseBytes,
parsedResponse.Timestamp!.Value,
parsedResponse.SerialNumber);
}
catch (OperationCanceledException)
{
return TsaTimestampResult.Failed(
$"Request timed out after {config.TimeoutSeconds}s",
providerName: providerName);
}
catch (Exception ex)
{
return TsaTimestampResult.Failed(
ex.Message,
providerName: providerName);
}
}
private static byte[] BuildTimestampRequest(byte[] data, TsaProviderConfig config)
{
// Build RFC 3161 TimeStampRequest
// Implementation would use BouncyCastle or similar
// Simplified placeholder:
using var sha256 = System.Security.Cryptography.SHA256.Create();
var messageImprint = sha256.ComputeHash(data);
// Real implementation would build proper ASN.1 structure
return messageImprint;
}
private static ParsedTsaResponse ParseTimestampResponse(byte[] response)
{
// Parse RFC 3161 TimeStampResponse
// Implementation would use BouncyCastle or similar
// Simplified placeholder:
try
{
return new ParsedTsaResponse
{
Success = true,
Timestamp = DateTimeOffset.UtcNow,
SerialNumber = Convert.ToHexString(response[..16])
};
}
catch
{
return new ParsedTsaResponse
{
Success = false,
ErrorMessage = "Failed to parse response"
};
}
}
private sealed record ParsedTsaResponse
{
public bool Success { get; init; }
public DateTimeOffset? Timestamp { get; init; }
public string? SerialNumber { get; init; }
public string? ErrorMessage { get; init; }
}
}
/// <summary>
/// Multi-provider TSA configuration.
/// </summary>
public sealed record TsaMultiProviderOptions
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "Timestamping";
/// <summary>Whether timestamping is enabled.</summary>
public bool Enabled { get; init; } = true;
/// <summary>Default provider name.</summary>
public string DefaultProvider { get; init; } = "freetsa";
/// <summary>Provider configurations.</summary>
public Dictionary<string, TsaProviderConfig> Providers { get; init; } = new();
/// <summary>Fallback order for providers.</summary>
public string[] FallbackOrder { get; init; } = [];
/// <summary>Whether timestamp is required (fail if all providers fail).</summary>
public bool RequireTimestamp { get; init; } = false;
}
/// <summary>
/// Per-provider TSA configuration.
/// </summary>
public sealed record TsaProviderConfig
{
/// <summary>TSA endpoint URL.</summary>
public required string Url { get; init; }
/// <summary>Optional policy OID.</summary>
public string? PolicyOid { get; init; }
/// <summary>Request timeout in seconds.</summary>
public int TimeoutSeconds { get; init; } = 30;
/// <summary>Path to trust root certificate.</summary>
public string? TrustRootPath { get; init; }
/// <summary>Authentication configuration.</summary>
public TsaAuthenticationConfig? Authentication { get; init; }
}
/// <summary>
/// TSA authentication configuration.
/// </summary>
public sealed record TsaAuthenticationConfig
{
/// <summary>Authentication type.</summary>
public TsaAuthType Type { get; init; } = TsaAuthType.None;
/// <summary>Username for basic auth.</summary>
public string? Username { get; init; }
/// <summary>Password for basic auth.</summary>
public string? Password { get; init; }
/// <summary>Bearer token.</summary>
public string? BearerToken { get; init; }
/// <summary>Client certificate path for mTLS.</summary>
public string? ClientCertPath { get; init; }
}
/// <summary>
/// TSA authentication type.
/// </summary>
public enum TsaAuthType
{
/// <summary>No authentication.</summary>
None,
/// <summary>HTTP Basic authentication.</summary>
Basic,
/// <summary>Bearer token.</summary>
Bearer,
/// <summary>Client certificate (mTLS).</summary>
ClientCertificate
}
/// <summary>
/// Result of timestamp request.
/// </summary>
public sealed record TsaTimestampResult
{
/// <summary>Whether the request succeeded.</summary>
public required bool Success { get; init; }
/// <summary>Whether timestamping was skipped (disabled or not required).</summary>
public bool Skipped { get; init; }
/// <summary>Provider that provided the timestamp.</summary>
public string? ProviderName { get; init; }
/// <summary>Raw timestamp token (TST).</summary>
public byte[]? TimestampToken { get; init; }
/// <summary>Timestamp from the token.</summary>
public DateTimeOffset? Timestamp { get; init; }
/// <summary>Serial number from the TSA.</summary>
public string? SerialNumber { get; init; }
/// <summary>Error message if failed.</summary>
public string? ErrorMessage { get; init; }
/// <summary>Errors from attempted providers.</summary>
public IReadOnlyList<TsaProviderError> ProviderErrors { get; init; } = [];
/// <summary>Creates a success result.</summary>
public static TsaTimestampResult Succeeded(
string providerName,
byte[] token,
DateTimeOffset timestamp,
string? serialNumber = null)
{
return new TsaTimestampResult
{
Success = true,
ProviderName = providerName,
TimestampToken = token,
Timestamp = timestamp,
SerialNumber = serialNumber
};
}
/// <summary>Creates a failure result.</summary>
public static TsaTimestampResult Failed(
string errorMessage,
IReadOnlyList<TsaProviderError>? providerErrors = null,
string? providerName = null)
{
return new TsaTimestampResult
{
Success = false,
ErrorMessage = errorMessage,
ProviderName = providerName,
ProviderErrors = providerErrors ?? []
};
}
/// <summary>Creates a skipped result.</summary>
public static TsaTimestampResult Skipped(
string reason,
IReadOnlyList<TsaProviderError>? providerErrors = null)
{
return new TsaTimestampResult
{
Success = true,
Skipped = true,
ErrorMessage = reason,
ProviderErrors = providerErrors ?? []
};
}
/// <summary>Creates a disabled result.</summary>
public static TsaTimestampResult Disabled()
{
return new TsaTimestampResult
{
Success = true,
Skipped = true,
ErrorMessage = "Timestamping is disabled"
};
}
}
/// <summary>
/// Error from a TSA provider.
/// </summary>
public sealed record TsaProviderError
{
/// <summary>Provider name.</summary>
public required string ProviderName { get; init; }
/// <summary>Error message.</summary>
public required string Error { get; init; }
}
/// <summary>
/// Options for timestamp request.
/// </summary>
public sealed record TsaTimestampOptions
{
/// <summary>Preferred provider name.</summary>
public string? PreferredProvider { get; init; }
/// <summary>Hash algorithm OID.</summary>
public string? HashAlgorithmOid { get; init; }
/// <summary>Whether to request certificate in response.</summary>
public bool RequestCertificate { get; init; } = true;
}