Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Verification/Checks/SignatureVerificationCheck.cs
2026-03-31 23:26:24 +03:00

227 lines
9.3 KiB
C#

using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Builders;
using StellaOps.Doctor.Plugins.Verification.Configuration;
namespace StellaOps.Doctor.Plugins.Verification.Checks;
/// <summary>
/// Verifies signature and attestations for test artifact.
/// </summary>
public sealed class SignatureVerificationCheck : VerificationCheckBase
{
private const string RunbookUrlValue = "docs/doctor/articles/verification/verification-signature.md";
/// <inheritdoc />
public override string CheckId => "check.verification.signature";
/// <inheritdoc />
public override string Name => "Signature Verification";
/// <inheritdoc />
public override string Description => "Verifies signature and attestations for test artifact (DSSE in Rekor or offline bundle)";
/// <inheritdoc />
public override IReadOnlyList<string> Tags => ["verification", "signature", "dsse", "attestation", "security"];
/// <inheritdoc />
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
/// <inheritdoc />
protected override string RunbookUrl => RunbookUrlValue;
/// <inheritdoc />
public override bool CanRun(DoctorPluginContext context)
{
if (!base.CanRun(context))
return false;
var options = VerificationPlugin.GetOptions(context);
return HasTestArtifactConfigured(options);
}
/// <inheritdoc />
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
DoctorPluginContext context,
VerificationPluginOptions options,
CheckResultBuilder result,
CancellationToken ct)
{
if (!HasTestArtifactConfigured(options))
{
return GetNoTestArtifactConfiguredResult(result, CheckId);
}
// Check for offline bundle
if (!string.IsNullOrEmpty(options.TestArtifact.OfflineBundlePath))
{
return await VerifyFromOfflineBundle(options, result, ct);
}
// Online verification
return await VerifyFromOnline(context, options, result, ct);
}
private static Task<DoctorCheckResult> VerifyFromOfflineBundle(
VerificationPluginOptions options,
CheckResultBuilder result,
CancellationToken ct)
{
var bundlePath = options.TestArtifact.OfflineBundlePath!;
if (!File.Exists(bundlePath))
{
return Task.FromResult(result
.Fail($"Offline bundle not found: {bundlePath}")
.WithEvidence("Verification", e => e
.Add("Mode", "Offline")
.Add("BundlePath", bundlePath)
.Add("FileExists", "false"))
.WithRemediation(r => r
.AddShellStep(1, "Export bundle", "stella verification bundle export --output " + bundlePath)
.WithRunbookUrl(RunbookUrlValue))
.WithVerification($"stella doctor --check check.verification.signature")
.Build());
}
// In a real implementation, we would parse the bundle and verify signatures
// For doctor check, we verify the bundle structure contains signature data
try
{
var content = File.ReadAllText(bundlePath);
// Check for signature indicators in the bundle
var hasSignatures = content.Contains("\"signatures\"", StringComparison.OrdinalIgnoreCase)
|| content.Contains("\"payloadType\"", StringComparison.OrdinalIgnoreCase)
|| content.Contains("\"dsse\"", StringComparison.OrdinalIgnoreCase);
if (!hasSignatures)
{
return Task.FromResult(result
.Warn("Offline bundle may not contain signature data")
.WithEvidence("Verification", e => e
.Add("Mode", "Offline")
.Add("BundlePath", bundlePath)
.Add("SignatureDataFound", "false")
.Add("Note", "Bundle should contain DSSE signatures for verification"))
.WithRemediation(r => r
.AddShellStep(1, "Re-export with signatures", "stella verification bundle export --include-signatures --output " + bundlePath)
.WithRunbookUrl(RunbookUrlValue))
.WithVerification($"stella doctor --check check.verification.signature")
.Build());
}
return Task.FromResult(result
.Pass("Offline bundle contains signature data")
.WithEvidence("Verification", e => e
.Add("Mode", "Offline")
.Add("BundlePath", bundlePath)
.Add("SignatureDataFound", "true")
.Add("Note", "Full signature verification requires runtime attestor service"))
.Build());
}
catch (Exception ex)
{
return Task.FromResult(result
.Fail($"Cannot read offline bundle: {ex.Message}")
.WithEvidence("Verification", e => e
.Add("Mode", "Offline")
.Add("BundlePath", bundlePath)
.Add("Error", ex.Message))
.Build());
}
}
private static async Task<DoctorCheckResult> VerifyFromOnline(
DoctorPluginContext context,
VerificationPluginOptions options,
CheckResultBuilder result,
CancellationToken ct)
{
var reference = options.TestArtifact.Reference!;
var rekorUrl = context.Configuration["Sigstore:RekorUrl"] ?? "https://rekor.sigstore.dev";
// Note: Full signature verification requires the Attestor service
// For doctor check, we verify that the infrastructure is in place
// Check if Sigstore is enabled
var sigstoreEnabled = context.Configuration.GetValue<bool>("Sigstore:Enabled");
if (!sigstoreEnabled)
{
return result
.Info("Signature verification skipped - Sigstore not enabled")
.WithEvidence("Verification", e => e
.Add("Mode", "Online")
.Add("SigstoreEnabled", "false")
.Add("Reference", reference)
.Add("Note", "Enable Sigstore to verify artifact signatures"))
.WithRemediation(r => r
.AddManualStep(1, "Enable Sigstore", "Set Sigstore:Enabled to true")
.AddManualStep(2, "Configure signing", "Set up signing keys or keyless mode")
.WithRunbookUrl(RunbookUrlValue))
.Build();
}
// Check if Rekor is reachable (signature verification requires Rekor)
using var httpClient = CreateHttpClient(options);
try
{
var rekorHealthUrl = $"{rekorUrl.TrimEnd('/')}/api/v1/log";
var response = await httpClient.GetAsync(rekorHealthUrl, ct);
if (!response.IsSuccessStatusCode)
{
return result
.Fail($"Rekor transparency log unavailable ({(int)response.StatusCode})")
.WithEvidence("Verification", e => e
.Add("Mode", "Online")
.Add("RekorUrl", rekorUrl)
.Add("RekorStatus", ((int)response.StatusCode).ToString())
.Add("Reference", reference))
.WithCauses(
"Rekor service is down",
"Network connectivity issue")
.WithRemediation(r => r
.AddShellStep(1, "Test Rekor", $"curl -I {rekorHealthUrl}")
.AddManualStep(2, "Or use offline mode", "Configure offline verification bundle")
.WithRunbookUrl(RunbookUrlValue))
.WithVerification($"stella doctor --check check.verification.signature")
.Build();
}
return result
.Pass("Signature verification infrastructure available")
.WithEvidence("Verification", e => e
.Add("Mode", "Online")
.Add("SigstoreEnabled", "true")
.Add("RekorUrl", rekorUrl)
.Add("RekorReachable", "true")
.Add("Reference", reference)
.Add("Note", "Full signature verification requires runtime attestor service"))
.Build();
}
catch (HttpRequestException ex)
{
return result
.Fail($"Cannot reach Rekor: {ex.Message}")
.WithEvidence("Verification", e => e
.Add("Mode", "Online")
.Add("RekorUrl", rekorUrl)
.Add("Error", ex.Message)
.Add("Reference", reference))
.WithCauses("Network connectivity issue")
.WithRemediation(r => r
.AddManualStep(1, "Check network", "Verify connectivity to Rekor")
.AddManualStep(2, "Use offline mode", "Configure offline verification bundle")
.WithRunbookUrl(RunbookUrlValue))
.WithVerification($"stella doctor --check check.verification.signature")
.Build();
}
}
}