using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Excititor.Attestation.Dsse; using StellaOps.Excititor.Attestation.Models; using StellaOps.Excititor.Attestation.Signing; using StellaOps.Excititor.Attestation.Transparency; using StellaOps.Excititor.Core; namespace StellaOps.Excititor.Attestation; public sealed class VexAttestationClientOptions { public IReadOnlyDictionary DefaultMetadata { get; set; } = ImmutableDictionary.Empty; } public sealed class VexAttestationClient : IVexAttestationClient { private readonly VexDsseBuilder _builder; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; private readonly IReadOnlyDictionary _defaultMetadata; private readonly ITransparencyLogClient? _transparencyLogClient; public VexAttestationClient( VexDsseBuilder builder, IOptions options, ILogger logger, TimeProvider? timeProvider = null, ITransparencyLogClient? transparencyLogClient = null) { _builder = builder ?? throw new ArgumentNullException(nameof(builder)); ArgumentNullException.ThrowIfNull(options); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timeProvider = timeProvider ?? TimeProvider.System; _defaultMetadata = options.Value.DefaultMetadata; _transparencyLogClient = transparencyLogClient; } public async ValueTask SignAsync(VexAttestationRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); var mergedMetadata = MergeMetadata(request.Metadata, _defaultMetadata); var envelope = await _builder.CreateEnvelopeAsync(request, mergedMetadata, cancellationToken).ConfigureAwait(false); var envelopeDigest = VexDsseBuilder.ComputeEnvelopeDigest(envelope); var signedAt = _timeProvider.GetUtcNow(); var diagnosticsBuilder = ImmutableDictionary.Empty .Add("envelope", JsonSerializer.Serialize(envelope)) .Add("predicateType", "https://stella-ops.org/attestations/vex-export"); VexRekorReference? rekorReference = null; if (_transparencyLogClient is not null) { try { var entry = await _transparencyLogClient.SubmitAsync(envelope, cancellationToken).ConfigureAwait(false); rekorReference = new VexRekorReference("0.2", entry.Location, entry.LogIndex, entry.InclusionProofUrl is not null ? new Uri(entry.InclusionProofUrl) : null); diagnosticsBuilder = diagnosticsBuilder.Add("rekorLocation", entry.Location); } catch (Exception ex) { _logger.LogError(ex, "Failed to submit attestation to Rekor transparency log"); throw; } } var metadata = new VexAttestationMetadata( predicateType: "https://stella-ops.org/attestations/vex-export", rekor: rekorReference, envelopeDigest: envelopeDigest, signedAt: signedAt); _logger.LogInformation("Generated DSSE envelope for export {ExportId} ({Digest})", request.ExportId, envelopeDigest); return new VexAttestationResponse(metadata, diagnosticsBuilder); } public ValueTask VerifyAsync(VexAttestationRequest request, CancellationToken cancellationToken) { // Placeholder until verification flow is implemented in EXCITITOR-ATTEST-01-003. return ValueTask.FromResult(new VexAttestationVerification(true, ImmutableDictionary.Empty)); } private static IReadOnlyDictionary MergeMetadata( IReadOnlyDictionary requestMetadata, IReadOnlyDictionary defaults) { if (defaults.Count == 0) { return requestMetadata; } var merged = new Dictionary(defaults, StringComparer.Ordinal); foreach (var kvp in requestMetadata) { merged[kvp.Key] = kvp.Value; } return merged.ToImmutableDictionary(StringComparer.Ordinal); } }