109 lines
4.4 KiB
C#
109 lines
4.4 KiB
C#
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<string, string> DefaultMetadata { get; set; } = ImmutableDictionary<string, string>.Empty;
|
|
}
|
|
|
|
public sealed class VexAttestationClient : IVexAttestationClient
|
|
{
|
|
private readonly VexDsseBuilder _builder;
|
|
private readonly ILogger<VexAttestationClient> _logger;
|
|
private readonly TimeProvider _timeProvider;
|
|
private readonly IReadOnlyDictionary<string, string> _defaultMetadata;
|
|
private readonly ITransparencyLogClient? _transparencyLogClient;
|
|
|
|
public VexAttestationClient(
|
|
VexDsseBuilder builder,
|
|
IOptions<VexAttestationClientOptions> options,
|
|
ILogger<VexAttestationClient> 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<VexAttestationResponse> 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<string, string>.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<VexAttestationVerification> VerifyAsync(VexAttestationRequest request, CancellationToken cancellationToken)
|
|
{
|
|
// Placeholder until verification flow is implemented in EXCITITOR-ATTEST-01-003.
|
|
return ValueTask.FromResult(new VexAttestationVerification(true, ImmutableDictionary<string, string>.Empty));
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, string> MergeMetadata(
|
|
IReadOnlyDictionary<string, string> requestMetadata,
|
|
IReadOnlyDictionary<string, string> defaults)
|
|
{
|
|
if (defaults.Count == 0)
|
|
{
|
|
return requestMetadata;
|
|
}
|
|
|
|
var merged = new Dictionary<string, string>(defaults, StringComparer.Ordinal);
|
|
foreach (var kvp in requestMetadata)
|
|
{
|
|
merged[kvp.Key] = kvp.Value;
|
|
}
|
|
|
|
return merged.ToImmutableDictionary(StringComparer.Ordinal);
|
|
}
|
|
}
|