92 lines
3.8 KiB
C#
92 lines
3.8 KiB
C#
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<RekorHttpClient> _logger;
|
|
|
|
public RekorHttpClient(HttpClient httpClient, IOptions<RekorHttpClientOptions> options, ILogger<RekorHttpClient> 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<TransparencyLogEntry> 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<JsonElement>(cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
var entry = ParseEntryLocation(entryLocation, body);
|
|
_logger.LogInformation("Rekor entry recorded at {Location}", entry.Location);
|
|
return entry;
|
|
}
|
|
|
|
public async ValueTask<bool> 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);
|
|
}
|
|
}
|