227 lines
9.3 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|