using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Vexer.Attestation.Dsse; namespace StellaOps.Vexer.Attestation.Transparency; internal sealed class RekorHttpClient : ITransparencyLogClient { private readonly HttpClient _httpClient; private readonly RekorHttpClientOptions _options; private readonly ILogger _logger; public RekorHttpClient(HttpClient httpClient, IOptions options, ILogger logger) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); ArgumentNullException.ThrowIfNull(options); _options = options.Value; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); if (!string.IsNullOrWhiteSpace(_options.BaseAddress)) { _httpClient.BaseAddress = new Uri(_options.BaseAddress, UriKind.Absolute); } if (!string.IsNullOrWhiteSpace(_options.ApiKey)) { _httpClient.DefaultRequestHeaders.Add("Authorization", _options.ApiKey); } } public async ValueTask SubmitAsync(DsseEnvelope envelope, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(envelope); var payload = JsonSerializer.Serialize(envelope); using var content = new StringContent(payload); content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); HttpResponseMessage? response = null; for (var attempt = 0; attempt < _options.RetryCount; attempt++) { response = await _httpClient.PostAsync("/api/v2/log/entries", content, cancellationToken).ConfigureAwait(false); if (response.IsSuccessStatusCode) { break; } _logger.LogWarning("Rekor submission failed with status {Status}; attempt {Attempt}", response.StatusCode, attempt + 1); if (attempt + 1 < _options.RetryCount) { await Task.Delay(_options.RetryDelay, cancellationToken).ConfigureAwait(false); } } if (response is null || !response.IsSuccessStatusCode) { throw new HttpRequestException($"Failed to submit attestation to Rekor ({response?.StatusCode})."); } var entryLocation = response.Headers.Location?.ToString() ?? string.Empty; var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false); var entry = ParseEntryLocation(entryLocation, body); _logger.LogInformation("Rekor entry recorded at {Location}", entry.Location); return entry; } public async ValueTask VerifyAsync(string entryLocation, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(entryLocation)) { return false; } var response = await _httpClient.GetAsync(entryLocation, cancellationToken).ConfigureAwait(false); return response.IsSuccessStatusCode; } private static TransparencyLogEntry ParseEntryLocation(string location, JsonElement body) { var id = body.TryGetProperty("uuid", out var uuid) ? uuid.GetString() ?? string.Empty : Guid.NewGuid().ToString(); var logIndex = body.TryGetProperty("logIndex", out var logIndexElement) ? logIndexElement.GetString() : null; string? inclusionProof = null; if (body.TryGetProperty("verification", out var verification) && verification.TryGetProperty("inclusionProof", out var inclusion)) { inclusionProof = inclusion.GetProperty("logIndex").GetRawText(); } return new TransparencyLogEntry(id, location, logIndex, inclusionProof); } }