feat: Implement IsolatedReplayContext for deterministic audit replay

- Added IsolatedReplayContext class to provide an isolated environment for replaying audit bundles without external calls.
- Introduced methods for initializing the context, verifying input digests, and extracting inputs for policy evaluation.
- Created supporting interfaces and options for context configuration.

feat: Create ReplayExecutor for executing policy re-evaluation and verdict comparison

- Developed ReplayExecutor class to handle the execution of replay processes, including input verification and verdict comparison.
- Implemented detailed drift detection and error handling during replay execution.
- Added interfaces for policy evaluation and replay execution options.

feat: Add ScanSnapshotFetcher for fetching scan data and snapshots

- Introduced ScanSnapshotFetcher class to retrieve necessary scan data and snapshots for audit bundle creation.
- Implemented methods to fetch scan metadata, advisory feeds, policy snapshots, and VEX statements.
- Created supporting interfaces for scan data, feed snapshots, and policy snapshots.
This commit is contained in:
StellaOps Bot
2025-12-23 07:46:34 +02:00
parent e47627cfff
commit 7e384ab610
77 changed files with 153346 additions and 209 deletions

View File

@@ -0,0 +1,541 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_4100_0004_0001 - Security State Delta & Verdict
// Task: T3 - Implement DeltaComputer
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy.Deltas;
/// <summary>
/// Computes security state deltas between baseline and target snapshots.
/// </summary>
public sealed class DeltaComputer : IDeltaComputer
{
private readonly ISnapshotService _snapshotService;
private readonly ILogger<DeltaComputer> _logger;
public DeltaComputer(
ISnapshotService snapshotService,
ILogger<DeltaComputer> logger)
{
_snapshotService = snapshotService;
_logger = logger;
}
/// <inheritdoc />
public async Task<SecurityStateDelta> ComputeDeltaAsync(
string baselineSnapshotId,
string targetSnapshotId,
ArtifactRef artifact,
CancellationToken ct = default)
{
_logger.LogInformation(
"Computing delta between {Baseline} and {Target} for artifact {Artifact}",
baselineSnapshotId, targetSnapshotId, artifact.Digest);
// Load snapshots
var baseline = await _snapshotService.GetSnapshotAsync(baselineSnapshotId, ct)
?? throw new InvalidOperationException($"Baseline snapshot {baselineSnapshotId} not found");
var target = await _snapshotService.GetSnapshotAsync(targetSnapshotId, ct)
?? throw new InvalidOperationException($"Target snapshot {targetSnapshotId} not found");
// Compute component deltas
var sbomDelta = ComputeSbomDelta(baseline, target);
var reachabilityDelta = ComputeReachabilityDelta(baseline, target);
var vexDelta = ComputeVexDelta(baseline, target);
var policyDelta = ComputePolicyDelta(baseline, target);
var unknownsDelta = ComputeUnknownsDelta(baseline, target);
// Identify drivers
var drivers = IdentifyDrivers(sbomDelta, reachabilityDelta, vexDelta, policyDelta, unknownsDelta);
// Compute summary
var summary = ComputeSummary(sbomDelta, reachabilityDelta, vexDelta, policyDelta, drivers);
var delta = new SecurityStateDelta
{
DeltaId = "", // Computed below
ComputedAt = DateTimeOffset.UtcNow,
BaselineSnapshotId = baselineSnapshotId,
TargetSnapshotId = targetSnapshotId,
Artifact = artifact,
Sbom = sbomDelta,
Reachability = reachabilityDelta,
Vex = vexDelta,
Policy = policyDelta,
Unknowns = unknownsDelta,
Drivers = drivers,
Summary = summary
};
// Compute content-addressed ID
var deltaId = ComputeDeltaId(delta);
_logger.LogInformation(
"Computed delta {DeltaId} with {DriverCount} drivers, risk direction: {RiskDirection}",
deltaId, drivers.Count, summary.RiskDirection);
return delta with { DeltaId = deltaId };
}
private SbomDelta ComputeSbomDelta(SnapshotData baseline, SnapshotData target)
{
var baselinePackages = baseline.Packages.ToDictionary(p => p.Purl);
var targetPackages = target.Packages.ToDictionary(p => p.Purl);
var addedPackages = new List<PackageChange>();
var removedPackages = new List<PackageChange>();
var versionChanges = new List<PackageVersionChange>();
// Find added packages
foreach (var (purl, pkg) in targetPackages)
{
if (!baselinePackages.ContainsKey(purl))
{
addedPackages.Add(new PackageChange(purl, pkg.License));
}
}
// Find removed packages
foreach (var (purl, pkg) in baselinePackages)
{
if (!targetPackages.ContainsKey(purl))
{
removedPackages.Add(new PackageChange(purl, pkg.License));
}
}
// Find version changes (same package name, different version in PURL)
foreach (var (purl, targetPkg) in targetPackages)
{
if (baselinePackages.TryGetValue(purl, out var baselinePkg))
{
if (targetPkg.Version != baselinePkg.Version)
{
versionChanges.Add(new PackageVersionChange(
purl,
baselinePkg.Version ?? "unknown",
targetPkg.Version ?? "unknown"));
}
}
}
return new SbomDelta
{
PackagesAdded = addedPackages.Count,
PackagesRemoved = removedPackages.Count,
PackagesModified = versionChanges.Count,
AddedPackages = addedPackages,
RemovedPackages = removedPackages,
VersionChanges = versionChanges
};
}
private ReachabilityDelta ComputeReachabilityDelta(SnapshotData baseline, SnapshotData target)
{
var baselineReach = baseline.Reachability.ToDictionary(r => (r.CveId, r.Purl));
var targetReach = target.Reachability.ToDictionary(r => (r.CveId, r.Purl));
var changes = new List<ReachabilityChange>();
int newReachable = 0, newUnreachable = 0, changedReachability = 0;
// Find changes in reachability
foreach (var (key, targetState) in targetReach)
{
if (baselineReach.TryGetValue(key, out var baselineState))
{
if (baselineState.IsReachable != targetState.IsReachable)
{
changes.Add(new ReachabilityChange(
key.CveId,
key.Purl,
baselineState.IsReachable,
targetState.IsReachable));
changedReachability++;
if (targetState.IsReachable && !baselineState.IsReachable)
newReachable++;
else if (!targetState.IsReachable && baselineState.IsReachable)
newUnreachable++;
}
}
else if (targetState.IsReachable)
{
// New reachable CVE
changes.Add(new ReachabilityChange(key.CveId, key.Purl, false, true));
newReachable++;
}
}
return new ReachabilityDelta
{
NewReachable = newReachable,
NewUnreachable = newUnreachable,
ChangedReachability = changedReachability,
Changes = changes
};
}
private VexDelta ComputeVexDelta(SnapshotData baseline, SnapshotData target)
{
var baselineVex = baseline.VexStatements.ToDictionary(v => v.CveId);
var targetVex = target.VexStatements.ToDictionary(v => v.CveId);
var changes = new List<VexChange>();
int newStatements = 0, revokedStatements = 0;
int coverageIncrease = 0, coverageDecrease = 0;
// Find new VEX statements
foreach (var (cveId, targetStatement) in targetVex)
{
if (!baselineVex.TryGetValue(cveId, out var baselineStatement))
{
changes.Add(new VexChange(cveId, null, targetStatement.Status));
newStatements++;
if (targetStatement.Status == "not_affected")
coverageIncrease++;
}
else if (baselineStatement.Status != targetStatement.Status)
{
changes.Add(new VexChange(cveId, baselineStatement.Status, targetStatement.Status));
if (baselineStatement.Status == "not_affected" && targetStatement.Status != "not_affected")
coverageDecrease++;
else if (baselineStatement.Status != "not_affected" && targetStatement.Status == "not_affected")
coverageIncrease++;
}
}
// Find revoked VEX statements
foreach (var (cveId, baselineStatement) in baselineVex)
{
if (!targetVex.ContainsKey(cveId))
{
changes.Add(new VexChange(cveId, baselineStatement.Status, null));
revokedStatements++;
if (baselineStatement.Status == "not_affected")
coverageDecrease++;
}
}
return new VexDelta
{
NewVexStatements = newStatements,
RevokedVexStatements = revokedStatements,
CoverageIncrease = coverageIncrease,
CoverageDecrease = coverageDecrease,
Changes = changes
};
}
private PolicyDelta ComputePolicyDelta(SnapshotData baseline, SnapshotData target)
{
var baselineViolations = baseline.PolicyViolations.ToDictionary(v => v.RuleId);
var targetViolations = target.PolicyViolations.ToDictionary(v => v.RuleId);
var changes = new List<PolicyChange>();
int newViolations = 0, resolvedViolations = 0;
// Find new violations
foreach (var (ruleId, violation) in targetViolations)
{
if (!baselineViolations.ContainsKey(ruleId))
{
changes.Add(new PolicyChange(ruleId, "new-violation", violation.Message));
newViolations++;
}
}
// Find resolved violations
foreach (var (ruleId, violation) in baselineViolations)
{
if (!targetViolations.ContainsKey(ruleId))
{
changes.Add(new PolicyChange(ruleId, "resolved-violation", violation.Message));
resolvedViolations++;
}
}
// Check policy version change
int policyVersionChanged = baseline.PolicyVersion != target.PolicyVersion ? 1 : 0;
return new PolicyDelta
{
NewViolations = newViolations,
ResolvedViolations = resolvedViolations,
PolicyVersionChanged = policyVersionChanged,
Changes = changes
};
}
private UnknownsDelta ComputeUnknownsDelta(SnapshotData baseline, SnapshotData target)
{
var baselineUnknowns = baseline.Unknowns.ToDictionary(u => u.Id);
var targetUnknowns = target.Unknowns.ToDictionary(u => u.Id);
var newUnknowns = targetUnknowns.Keys.Except(baselineUnknowns.Keys).Count();
var resolvedUnknowns = baselineUnknowns.Keys.Except(targetUnknowns.Keys).Count();
// Count by reason code
var byReasonCode = targetUnknowns.Values
.Where(u => !baselineUnknowns.ContainsKey(u.Id))
.GroupBy(u => u.ReasonCode)
.ToDictionary(g => g.Key, g => g.Count());
return new UnknownsDelta
{
NewUnknowns = newUnknowns,
ResolvedUnknowns = resolvedUnknowns,
TotalBaselineUnknowns = baselineUnknowns.Count,
TotalTargetUnknowns = targetUnknowns.Count,
ByReasonCode = byReasonCode
};
}
private IReadOnlyList<DeltaDriver> IdentifyDrivers(
SbomDelta sbom,
ReachabilityDelta reach,
VexDelta vex,
PolicyDelta policy,
UnknownsDelta unknowns)
{
var drivers = new List<DeltaDriver>();
// New reachable CVEs are critical drivers
foreach (var change in reach.Changes.Where(c => !c.WasReachable && c.IsReachable))
{
drivers.Add(new DeltaDriver
{
Type = "new-reachable-cve",
Severity = DeltaDriverSeverity.Critical,
Description = $"CVE {change.CveId} is now reachable",
CveId = change.CveId,
Purl = change.Purl
});
}
// Lost VEX coverage
foreach (var change in vex.Changes.Where(c => c.OldStatus == "not_affected" && c.NewStatus is null))
{
drivers.Add(new DeltaDriver
{
Type = "lost-vex-coverage",
Severity = DeltaDriverSeverity.High,
Description = $"VEX coverage lost for {change.CveId}",
CveId = change.CveId
});
}
// VEX status downgrade (not_affected -> affected or other)
foreach (var change in vex.Changes.Where(c =>
c.OldStatus == "not_affected" && c.NewStatus is not null && c.NewStatus != "not_affected"))
{
drivers.Add(new DeltaDriver
{
Type = "vex-status-downgrade",
Severity = DeltaDriverSeverity.High,
Description = $"VEX status changed from not_affected to {change.NewStatus} for {change.CveId}",
CveId = change.CveId
});
}
// New policy violations
foreach (var change in policy.Changes.Where(c => c.ChangeType == "new-violation"))
{
drivers.Add(new DeltaDriver
{
Type = "new-policy-violation",
Severity = DeltaDriverSeverity.High,
Description = change.Description ?? $"New violation of rule {change.RuleId}"
});
}
// High-risk packages added
foreach (var pkg in sbom.AddedPackages.Where(IsHighRiskPackage))
{
drivers.Add(new DeltaDriver
{
Type = "high-risk-package-added",
Severity = DeltaDriverSeverity.Medium,
Description = $"New high-risk package: {pkg.Purl}",
Purl = pkg.Purl
});
}
// Increased unknowns
if (unknowns.NewUnknowns > 0)
{
var severity = unknowns.NewUnknowns > 10
? DeltaDriverSeverity.High
: DeltaDriverSeverity.Medium;
drivers.Add(new DeltaDriver
{
Type = "new-unknowns",
Severity = severity,
Description = $"{unknowns.NewUnknowns} new unknown(s) introduced",
Details = unknowns.ByReasonCode.ToDictionary(kv => kv.Key, kv => kv.Value.ToString())
});
}
// CVEs becoming unreachable (positive)
foreach (var change in reach.Changes.Where(c => c.WasReachable && !c.IsReachable))
{
drivers.Add(new DeltaDriver
{
Type = "cve-now-unreachable",
Severity = DeltaDriverSeverity.Low,
Description = $"CVE {change.CveId} is now unreachable (risk reduced)",
CveId = change.CveId,
Purl = change.Purl
});
}
// New VEX coverage (positive)
foreach (var change in vex.Changes.Where(c =>
c.OldStatus is null && c.NewStatus == "not_affected"))
{
drivers.Add(new DeltaDriver
{
Type = "new-vex-coverage",
Severity = DeltaDriverSeverity.Low,
Description = $"New VEX coverage for {change.CveId}: not_affected",
CveId = change.CveId
});
}
return drivers.OrderByDescending(d => d.Severity).ToList();
}
private DeltaSummary ComputeSummary(
SbomDelta sbom,
ReachabilityDelta reach,
VexDelta vex,
PolicyDelta policy,
IReadOnlyList<DeltaDriver> drivers)
{
var totalChanges = sbom.PackagesAdded + sbom.PackagesRemoved + sbom.PackagesModified +
reach.NewReachable + reach.NewUnreachable + reach.ChangedReachability +
vex.NewVexStatements + vex.RevokedVexStatements +
policy.NewViolations + policy.ResolvedViolations;
var riskIncreasing = drivers.Count(d =>
d.Severity is DeltaDriverSeverity.Critical or DeltaDriverSeverity.High &&
!IsPositiveDriver(d.Type));
var riskDecreasing = drivers.Count(d => IsPositiveDriver(d.Type));
var neutral = Math.Max(0, totalChanges - riskIncreasing - riskDecreasing);
var riskScore = ComputeRiskScore(drivers);
var riskDirection = riskIncreasing > riskDecreasing ? "increasing" :
riskIncreasing < riskDecreasing ? "decreasing" : "stable";
return new DeltaSummary
{
TotalChanges = totalChanges,
RiskIncreasing = riskIncreasing,
RiskDecreasing = riskDecreasing,
Neutral = neutral,
RiskScore = riskScore,
RiskDirection = riskDirection
};
}
private static bool IsPositiveDriver(string driverType) =>
driverType is "cve-now-unreachable" or "new-vex-coverage" or "resolved-violation";
private static decimal ComputeRiskScore(IReadOnlyList<DeltaDriver> drivers)
{
return drivers.Sum(d => d.Severity switch
{
DeltaDriverSeverity.Critical => 20m,
DeltaDriverSeverity.High => 10m,
DeltaDriverSeverity.Medium => 5m,
DeltaDriverSeverity.Low => 1m,
_ => 0m
});
}
private static bool IsHighRiskPackage(PackageChange pkg)
{
// Check for known high-risk characteristics
var purl = pkg.Purl.ToLowerInvariant();
return purl.Contains("native") ||
purl.Contains("crypto") ||
purl.Contains("ssl") ||
purl.Contains("auth") ||
purl.Contains("shell") ||
purl.Contains("exec");
}
private static string ComputeDeltaId(SecurityStateDelta delta)
{
// Create a deterministic representation for hashing
var deterministicDelta = delta with
{
DeltaId = "",
ComputedAt = default // Exclude timestamp for determinism
};
var json = JsonSerializer.Serialize(deterministicDelta, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
});
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
var hashHex = Convert.ToHexStringLower(hash);
return $"delta:sha256:{hashHex}";
}
}
/// <summary>
/// Interface for computing security state deltas.
/// </summary>
public interface IDeltaComputer
{
/// <summary>
/// Computes the delta between two knowledge snapshots for an artifact.
/// </summary>
Task<SecurityStateDelta> ComputeDeltaAsync(
string baselineSnapshotId,
string targetSnapshotId,
ArtifactRef artifact,
CancellationToken ct = default);
}
/// <summary>
/// Interface for accessing snapshot data.
/// </summary>
public interface ISnapshotService
{
/// <summary>
/// Gets snapshot data by ID.
/// </summary>
Task<SnapshotData?> GetSnapshotAsync(string snapshotId, CancellationToken ct = default);
}
/// <summary>
/// Snapshot data for delta computation.
/// </summary>
public sealed record SnapshotData
{
public required string SnapshotId { get; init; }
public IReadOnlyList<PackageData> Packages { get; init; } = [];
public IReadOnlyList<ReachabilityData> Reachability { get; init; } = [];
public IReadOnlyList<VexStatementData> VexStatements { get; init; } = [];
public IReadOnlyList<PolicyViolationData> PolicyViolations { get; init; } = [];
public IReadOnlyList<UnknownData> Unknowns { get; init; } = [];
public string? PolicyVersion { get; init; }
}
public sealed record PackageData(string Purl, string? Version, string? License);
public sealed record ReachabilityData(string CveId, string Purl, bool IsReachable);
public sealed record VexStatementData(string CveId, string Status, string? Justification);
public sealed record PolicyViolationData(string RuleId, string Severity, string? Message);
public sealed record UnknownData(string Id, string ReasonCode, string? Description);

View File

@@ -0,0 +1,374 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_4100_0004_0001 - Security State Delta & Verdict
// Task: T5 - Create DeltaVerdictStatement
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy.Deltas;
/// <summary>
/// Creates in-toto statements for delta verdicts.
/// </summary>
public static class DeltaVerdictStatement
{
/// <summary>
/// Predicate type for delta verdict attestations.
/// </summary>
public const string PredicateType = "https://stellaops.io/predicates/delta-verdict@v1";
/// <summary>
/// Creates an in-toto statement from a delta verdict.
/// </summary>
public static InTotoStatement CreateStatement(
SecurityStateDelta delta,
DeltaVerdict verdict)
{
return new InTotoStatement
{
Type = "https://in-toto.io/Statement/v1",
Subject = new[]
{
new InTotoSubject
{
Name = delta.Artifact.Name ?? delta.Artifact.Digest,
Digest = new Dictionary<string, string>
{
["sha256"] = delta.Artifact.Digest.Replace("sha256:", "")
}
}
},
PredicateType = PredicateType,
Predicate = new DeltaVerdictPredicate
{
DeltaId = delta.DeltaId,
VerdictId = verdict.VerdictId,
Status = verdict.Status.ToString(),
BaselineSnapshotId = delta.BaselineSnapshotId,
TargetSnapshotId = delta.TargetSnapshotId,
RecommendedGate = verdict.RecommendedGate.ToString(),
RiskPoints = verdict.RiskPoints,
Summary = new DeltaSummaryPredicate
{
TotalChanges = delta.Summary.TotalChanges,
RiskIncreasing = delta.Summary.RiskIncreasing,
RiskDecreasing = delta.Summary.RiskDecreasing,
RiskDirection = delta.Summary.RiskDirection,
RiskScore = delta.Summary.RiskScore
},
BlockingDrivers = verdict.BlockingDrivers
.Select(d => new DriverPredicate
{
Type = d.Type,
Severity = d.Severity.ToString(),
Description = d.Description,
CveId = d.CveId,
Purl = d.Purl
})
.ToList(),
WarningDrivers = verdict.WarningDrivers
.Select(d => new DriverPredicate
{
Type = d.Type,
Severity = d.Severity.ToString(),
Description = d.Description,
CveId = d.CveId,
Purl = d.Purl
})
.ToList(),
AppliedExceptions = verdict.AppliedExceptions.ToList(),
Explanation = verdict.Explanation,
Recommendations = verdict.Recommendations.ToList(),
EvaluatedAt = verdict.EvaluatedAt.ToString("o")
}
};
}
/// <summary>
/// Serializes the statement to JSON.
/// </summary>
public static string ToJson(InTotoStatement statement)
{
return JsonSerializer.Serialize(statement, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
/// <summary>
/// Serializes the statement to bytes for signing.
/// </summary>
public static byte[] ToBytes(InTotoStatement statement)
{
return JsonSerializer.SerializeToUtf8Bytes(statement, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
}
/// <summary>
/// in-toto statement structure.
/// </summary>
public sealed record InTotoStatement
{
[JsonPropertyName("_type")]
public required string Type { get; init; }
[JsonPropertyName("subject")]
public required IReadOnlyList<InTotoSubject> Subject { get; init; }
[JsonPropertyName("predicateType")]
public required string PredicateType { get; init; }
[JsonPropertyName("predicate")]
public required DeltaVerdictPredicate Predicate { get; init; }
}
/// <summary>
/// in-toto subject (artifact reference).
/// </summary>
public sealed record InTotoSubject
{
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("digest")]
public required IReadOnlyDictionary<string, string> Digest { get; init; }
}
/// <summary>
/// Delta verdict predicate for attestation.
/// </summary>
public sealed record DeltaVerdictPredicate
{
[JsonPropertyName("deltaId")]
public required string DeltaId { get; init; }
[JsonPropertyName("verdictId")]
public required string VerdictId { get; init; }
[JsonPropertyName("status")]
public required string Status { get; init; }
[JsonPropertyName("baselineSnapshotId")]
public required string BaselineSnapshotId { get; init; }
[JsonPropertyName("targetSnapshotId")]
public required string TargetSnapshotId { get; init; }
[JsonPropertyName("recommendedGate")]
public required string RecommendedGate { get; init; }
[JsonPropertyName("riskPoints")]
public int RiskPoints { get; init; }
[JsonPropertyName("summary")]
public required DeltaSummaryPredicate Summary { get; init; }
[JsonPropertyName("blockingDrivers")]
public required IReadOnlyList<DriverPredicate> BlockingDrivers { get; init; }
[JsonPropertyName("warningDrivers")]
public required IReadOnlyList<DriverPredicate> WarningDrivers { get; init; }
[JsonPropertyName("appliedExceptions")]
public required IReadOnlyList<string> AppliedExceptions { get; init; }
[JsonPropertyName("explanation")]
public string? Explanation { get; init; }
[JsonPropertyName("recommendations")]
public required IReadOnlyList<string> Recommendations { get; init; }
[JsonPropertyName("evaluatedAt")]
public required string EvaluatedAt { get; init; }
}
/// <summary>
/// Summary section of the predicate.
/// </summary>
public sealed record DeltaSummaryPredicate
{
[JsonPropertyName("totalChanges")]
public int TotalChanges { get; init; }
[JsonPropertyName("riskIncreasing")]
public int RiskIncreasing { get; init; }
[JsonPropertyName("riskDecreasing")]
public int RiskDecreasing { get; init; }
[JsonPropertyName("riskDirection")]
public required string RiskDirection { get; init; }
[JsonPropertyName("riskScore")]
public decimal RiskScore { get; init; }
}
/// <summary>
/// Driver details in the predicate.
/// </summary>
public sealed record DriverPredicate
{
[JsonPropertyName("type")]
public required string Type { get; init; }
[JsonPropertyName("severity")]
public required string Severity { get; init; }
[JsonPropertyName("description")]
public required string Description { get; init; }
[JsonPropertyName("cveId")]
public string? CveId { get; init; }
[JsonPropertyName("purl")]
public string? Purl { get; init; }
}
/// <summary>
/// DSSE (Dead Simple Signing Envelope) structure.
/// </summary>
public sealed record DsseEnvelope
{
[JsonPropertyName("payloadType")]
public required string PayloadType { get; init; }
[JsonPropertyName("payload")]
public required string Payload { get; init; }
[JsonPropertyName("signatures")]
public required IReadOnlyList<DsseSignature> Signatures { get; init; }
}
/// <summary>
/// DSSE signature structure.
/// </summary>
public sealed record DsseSignature
{
[JsonPropertyName("keyid")]
public required string KeyId { get; init; }
[JsonPropertyName("sig")]
public required string Sig { get; init; }
}
/// <summary>
/// Service for creating and signing delta verdict attestations.
/// </summary>
public sealed class DeltaVerdictAttestor : IDeltaVerdictAttestor
{
private readonly ISigner _signer;
private readonly ILogger<DeltaVerdictAttestor> _logger;
public DeltaVerdictAttestor(ISigner signer, ILogger<DeltaVerdictAttestor> logger)
{
_signer = signer;
_logger = logger;
}
/// <inheritdoc />
public async Task<DsseEnvelope> AttestAsync(
SecurityStateDelta delta,
DeltaVerdict verdict,
CancellationToken ct = default)
{
var statement = DeltaVerdictStatement.CreateStatement(delta, verdict);
var payload = DeltaVerdictStatement.ToBytes(statement);
var signature = await _signer.SignAsync(payload, ct);
_logger.LogInformation(
"Created delta verdict attestation for {DeltaId} with status {Status}",
delta.DeltaId, verdict.Status);
return new DsseEnvelope
{
PayloadType = "application/vnd.in-toto+json",
Payload = Convert.ToBase64String(payload),
Signatures = new[]
{
new DsseSignature
{
KeyId = _signer.KeyId,
Sig = Convert.ToBase64String(signature)
}
}
};
}
/// <inheritdoc />
public async Task<bool> VerifyAsync(
DsseEnvelope envelope,
CancellationToken ct = default)
{
if (envelope.Signatures.Count == 0)
{
_logger.LogWarning("No signatures found in envelope");
return false;
}
var payload = Convert.FromBase64String(envelope.Payload);
foreach (var sig in envelope.Signatures)
{
var signature = Convert.FromBase64String(sig.Sig);
var isValid = await _signer.VerifyAsync(payload, signature, sig.KeyId, ct);
if (!isValid)
{
_logger.LogWarning("Invalid signature for key {KeyId}", sig.KeyId);
return false;
}
}
return true;
}
}
/// <summary>
/// Interface for signing and verifying attestations.
/// </summary>
public interface ISigner
{
/// <summary>
/// Gets the key ID for the current signing key.
/// </summary>
string KeyId { get; }
/// <summary>
/// Signs the payload.
/// </summary>
Task<byte[]> SignAsync(byte[] payload, CancellationToken ct = default);
/// <summary>
/// Verifies a signature.
/// </summary>
Task<bool> VerifyAsync(byte[] payload, byte[] signature, string keyId, CancellationToken ct = default);
}
/// <summary>
/// Interface for creating and verifying delta verdict attestations.
/// </summary>
public interface IDeltaVerdictAttestor
{
/// <summary>
/// Creates a signed attestation for a delta verdict.
/// </summary>
Task<DsseEnvelope> AttestAsync(
SecurityStateDelta delta,
DeltaVerdict verdict,
CancellationToken ct = default);
/// <summary>
/// Verifies a delta verdict attestation.
/// </summary>
Task<bool> VerifyAsync(
DsseEnvelope envelope,
CancellationToken ct = default);
}