save progress
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.Core.InToto;
|
||||
using StellaOps.Attestor.Core.Signing;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.InToto;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IInTotoLinkSigningService"/> that integrates
|
||||
/// in-toto link generation with the attestation signing infrastructure.
|
||||
/// </summary>
|
||||
internal sealed class InTotoLinkSigningService : IInTotoLinkSigningService
|
||||
{
|
||||
private readonly ILinkRecorder _linkRecorder;
|
||||
private readonly IAttestationSigningService _signingService;
|
||||
private readonly ILogger<InTotoLinkSigningService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InTotoLinkSigningService(
|
||||
ILinkRecorder linkRecorder,
|
||||
IAttestationSigningService signingService,
|
||||
ILogger<InTotoLinkSigningService> logger,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_linkRecorder = linkRecorder ?? throw new ArgumentNullException(nameof(linkRecorder));
|
||||
_signingService = signingService ?? throw new ArgumentNullException(nameof(signingService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SignedInTotoLinkResult> SignLinkAsync(
|
||||
InTotoLink link,
|
||||
InTotoLinkSigningOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(link);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
_logger.LogDebug("Signing in-toto link for step {StepName}", link.Predicate.Name);
|
||||
|
||||
// Serialize link to JSON payload
|
||||
var payloadJson = link.ToJson();
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
|
||||
var payloadBase64 = Convert.ToBase64String(payloadBytes);
|
||||
|
||||
// Build signing request
|
||||
var request = new AttestationSignRequest
|
||||
{
|
||||
KeyId = options.KeyId ?? string.Empty,
|
||||
PayloadType = InTotoLink.PredicateTypeUri,
|
||||
PayloadBase64 = payloadBase64,
|
||||
Mode = options.Mode,
|
||||
LogPreference = options.LogPreference,
|
||||
Archive = options.Archive,
|
||||
Artifact = new AttestorSubmissionRequest.ArtifactInfo
|
||||
{
|
||||
Kind = "in-toto-link",
|
||||
SubjectUri = $"in-toto:{link.Predicate.Name}"
|
||||
}
|
||||
};
|
||||
|
||||
// Create submission context for signing
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = options.CallerSubject ?? "system",
|
||||
CallerAudience = options.CallerAudience ?? "in-toto-link-signer",
|
||||
CallerClientId = options.CallerClientId ?? "intoto-link-signing-service",
|
||||
CallerTenant = options.CallerTenant
|
||||
};
|
||||
|
||||
// Sign the attestation
|
||||
var signResult = await _signingService.SignAsync(request, context, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Signed in-toto link for step {StepName} with key {KeyId}",
|
||||
link.Predicate.Name,
|
||||
signResult.KeyId);
|
||||
|
||||
// Build DSSE envelope from result
|
||||
var envelope = BuildEnvelopeFromResult(payloadBytes, signResult);
|
||||
|
||||
// Build result
|
||||
var result = new SignedInTotoLinkResult
|
||||
{
|
||||
Link = link,
|
||||
Envelope = envelope,
|
||||
SignerKeyId = signResult.KeyId,
|
||||
Algorithm = signResult.Algorithm,
|
||||
SignedAt = signResult.SignedAt,
|
||||
RekorEntry = ExtractRekorEntry(signResult)
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SignedInTotoLinkResult> RecordAndSignStepAsync(
|
||||
string stepName,
|
||||
Func<Task<int>> action,
|
||||
IEnumerable<MaterialSpec> materials,
|
||||
IEnumerable<ProductSpec> products,
|
||||
InTotoLinkSigningOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(stepName);
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
ArgumentNullException.ThrowIfNull(materials);
|
||||
ArgumentNullException.ThrowIfNull(products);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
_logger.LogDebug("Recording and signing step {StepName}", stepName);
|
||||
|
||||
// Record the step to create link
|
||||
var link = await _linkRecorder.RecordStepAsync(
|
||||
stepName,
|
||||
action,
|
||||
materials,
|
||||
products,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Sign the resulting link
|
||||
return await SignLinkAsync(link, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static global::StellaOps.Attestor.Envelope.DsseEnvelope BuildEnvelopeFromResult(
|
||||
byte[] payloadBytes,
|
||||
AttestationSignResult signResult)
|
||||
{
|
||||
// Extract signature from bundle
|
||||
var signatures = new List<global::StellaOps.Attestor.Envelope.DsseSignature>();
|
||||
|
||||
if (signResult.Bundle.Dsse?.Signatures != null)
|
||||
{
|
||||
foreach (var sig in signResult.Bundle.Dsse.Signatures)
|
||||
{
|
||||
signatures.Add(new global::StellaOps.Attestor.Envelope.DsseSignature(
|
||||
sig.Signature,
|
||||
sig.KeyId));
|
||||
}
|
||||
}
|
||||
|
||||
if (signatures.Count == 0)
|
||||
{
|
||||
// Fallback: create signature from keyId if no envelope signatures present
|
||||
signatures.Add(new global::StellaOps.Attestor.Envelope.DsseSignature(
|
||||
"pending", // Will be replaced by actual signing
|
||||
signResult.KeyId));
|
||||
}
|
||||
|
||||
return new global::StellaOps.Attestor.Envelope.DsseEnvelope(
|
||||
InTotoLink.PredicateTypeUri,
|
||||
new ReadOnlyMemory<byte>(payloadBytes),
|
||||
signatures);
|
||||
}
|
||||
|
||||
// Note: Rekor entry information comes from the submission service after
|
||||
// the envelope is submitted to the transparency log. The signing service
|
||||
// produces the signed envelope, but Rekor submission is a separate step.
|
||||
// For now, we return null and let callers handle Rekor submission separately.
|
||||
private static RekorEntryReference? ExtractRekorEntry(AttestationSignResult signResult)
|
||||
{
|
||||
// The signing result does not include Rekor entry info directly.
|
||||
// Rekor submission happens in a separate step via IAttestorSubmissionService.
|
||||
// Callers who need Rekor transparency should submit the result to Rekor
|
||||
// and capture the entry reference from that operation.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ using StellaOps.Attestor.Infrastructure.Transparency;
|
||||
using StellaOps.Attestor.Infrastructure.Verification;
|
||||
using StellaOps.Attestor.Infrastructure.Bulk;
|
||||
using StellaOps.Attestor.Core.Signing;
|
||||
using StellaOps.Attestor.Core.InToto;
|
||||
using StellaOps.Attestor.Core.InToto.Layout;
|
||||
using StellaOps.Attestor.Infrastructure.InToto;
|
||||
using StellaOps.Attestor.Verify;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure;
|
||||
@@ -67,6 +70,12 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<IAttestorBundleService, AttestorBundleService>();
|
||||
services.AddSingleton<AttestorSigningKeyRegistry>();
|
||||
services.AddSingleton<IAttestationSigningService, AttestorSigningService>();
|
||||
|
||||
// In-toto link generation services
|
||||
services.AddSingleton<ILinkRecorder, LinkRecorder>();
|
||||
services.AddSingleton<ILayoutVerifier, LayoutVerifier>();
|
||||
services.AddSingleton<IInTotoLinkSigningService, InTotoLinkSigningService>();
|
||||
|
||||
services.AddHttpClient<HttpRekorClient>((sp, client) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
|
||||
|
||||
Reference in New Issue
Block a user