Implement VEX document verification system with issuer management and signature verification

- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation.
- Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments.
- Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats.
- Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats.
- Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction.
- Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
This commit is contained in:
StellaOps Bot
2025-12-06 13:41:22 +02:00
parent 2141196496
commit 5e514532df
112 changed files with 24861 additions and 211 deletions

View File

@@ -0,0 +1,152 @@
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Verification;
namespace StellaOps.VexLens.Trust;
/// <summary>
/// Interface for computing trust weights for VEX statements.
/// </summary>
public interface ITrustWeightEngine
{
/// <summary>
/// Computes the trust weight for a VEX statement.
/// </summary>
Task<TrustWeightResult> ComputeWeightAsync(
TrustWeightRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// Computes trust weights for multiple statements in batch.
/// </summary>
Task<IReadOnlyList<TrustWeightResult>> ComputeWeightsBatchAsync(
IEnumerable<TrustWeightRequest> requests,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the current trust weight configuration.
/// </summary>
TrustWeightConfiguration GetConfiguration();
/// <summary>
/// Updates the trust weight configuration.
/// </summary>
void UpdateConfiguration(TrustWeightConfiguration configuration);
}
/// <summary>
/// Request for trust weight computation.
/// </summary>
public sealed record TrustWeightRequest(
NormalizedStatement Statement,
VexIssuer? Issuer,
SignatureVerificationResult? SignatureVerification,
DateTimeOffset? DocumentIssuedAt,
TrustWeightContext Context);
/// <summary>
/// Context for trust weight computation.
/// </summary>
public sealed record TrustWeightContext(
string? TenantId,
DateTimeOffset EvaluationTime,
IReadOnlyDictionary<string, object?>? CustomFactors);
/// <summary>
/// Result of trust weight computation.
/// </summary>
public sealed record TrustWeightResult(
NormalizedStatement Statement,
double Weight,
TrustWeightBreakdown Breakdown,
IReadOnlyList<TrustWeightFactor> Factors,
IReadOnlyList<string> Warnings);
/// <summary>
/// Breakdown of trust weight by component.
/// </summary>
public sealed record TrustWeightBreakdown(
double IssuerWeight,
double SignatureWeight,
double FreshnessWeight,
double SourceFormatWeight,
double StatusSpecificityWeight,
double CustomWeight);
/// <summary>
/// Individual factor contributing to trust weight.
/// </summary>
public sealed record TrustWeightFactor(
string FactorId,
string Name,
double RawValue,
double WeightedValue,
double Multiplier,
string? Reason);
/// <summary>
/// Configuration for trust weight computation.
/// </summary>
public sealed record TrustWeightConfiguration(
IssuerTrustWeights IssuerWeights,
SignatureTrustWeights SignatureWeights,
FreshnessTrustWeights FreshnessWeights,
SourceFormatWeights SourceFormatWeights,
StatusSpecificityWeights StatusSpecificityWeights,
double MinimumWeight,
double MaximumWeight);
/// <summary>
/// Trust weights based on issuer category and tier.
/// </summary>
public sealed record IssuerTrustWeights(
double VendorMultiplier,
double DistributorMultiplier,
double CommunityMultiplier,
double InternalMultiplier,
double AggregatorMultiplier,
double UnknownIssuerMultiplier,
double AuthoritativeTierBonus,
double TrustedTierBonus,
double UntrustedTierPenalty);
/// <summary>
/// Trust weights based on signature verification.
/// </summary>
public sealed record SignatureTrustWeights(
double ValidSignatureMultiplier,
double InvalidSignaturePenalty,
double NoSignaturePenalty,
double ExpiredCertificatePenalty,
double RevokedCertificatePenalty,
double TimestampedBonus);
/// <summary>
/// Trust weights based on document freshness.
/// </summary>
public sealed record FreshnessTrustWeights(
TimeSpan FreshThreshold,
TimeSpan StaleThreshold,
TimeSpan ExpiredThreshold,
double FreshMultiplier,
double StaleMultiplier,
double ExpiredMultiplier);
/// <summary>
/// Trust weights based on source format.
/// </summary>
public sealed record SourceFormatWeights(
double OpenVexMultiplier,
double CsafVexMultiplier,
double CycloneDxVexMultiplier,
double SpdxVexMultiplier,
double StellaOpsMultiplier);
/// <summary>
/// Trust weights based on status specificity.
/// </summary>
public sealed record StatusSpecificityWeights(
double NotAffectedBonus,
double FixedBonus,
double AffectedNeutral,
double UnderInvestigationPenalty,
double JustificationBonus);

View File

@@ -0,0 +1,445 @@
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Verification;
namespace StellaOps.VexLens.Trust;
/// <summary>
/// Default implementation of <see cref="ITrustWeightEngine"/>.
/// Computes trust weights based on issuer, signature, freshness, and other factors.
/// </summary>
public sealed class TrustWeightEngine : ITrustWeightEngine
{
private TrustWeightConfiguration _configuration;
public TrustWeightEngine(TrustWeightConfiguration? configuration = null)
{
_configuration = configuration ?? CreateDefaultConfiguration();
}
public Task<TrustWeightResult> ComputeWeightAsync(
TrustWeightRequest request,
CancellationToken cancellationToken = default)
{
var factors = new List<TrustWeightFactor>();
var warnings = new List<string>();
// Compute issuer weight
var issuerWeight = ComputeIssuerWeight(request.Issuer, factors);
// Compute signature weight
var signatureWeight = ComputeSignatureWeight(request.SignatureVerification, factors);
// Compute freshness weight
var freshnessWeight = ComputeFreshnessWeight(
request.DocumentIssuedAt,
request.Statement.FirstSeen,
request.Context.EvaluationTime,
factors);
// Compute source format weight
var sourceFormatWeight = ComputeSourceFormatWeight(request.Statement, factors);
// Compute status specificity weight
var statusWeight = ComputeStatusSpecificityWeight(request.Statement, factors);
// Compute custom weight
var customWeight = ComputeCustomWeight(request.Context.CustomFactors, factors);
// Combine weights
var breakdown = new TrustWeightBreakdown(
IssuerWeight: issuerWeight,
SignatureWeight: signatureWeight,
FreshnessWeight: freshnessWeight,
SourceFormatWeight: sourceFormatWeight,
StatusSpecificityWeight: statusWeight,
CustomWeight: customWeight);
var combinedWeight = CombineWeights(breakdown);
// Clamp to configured range
var finalWeight = Math.Clamp(combinedWeight, _configuration.MinimumWeight, _configuration.MaximumWeight);
if (finalWeight != combinedWeight)
{
warnings.Add($"Weight clamped from {combinedWeight:F4} to {finalWeight:F4}");
}
return Task.FromResult(new TrustWeightResult(
Statement: request.Statement,
Weight: finalWeight,
Breakdown: breakdown,
Factors: factors,
Warnings: warnings));
}
public async Task<IReadOnlyList<TrustWeightResult>> ComputeWeightsBatchAsync(
IEnumerable<TrustWeightRequest> requests,
CancellationToken cancellationToken = default)
{
var results = new List<TrustWeightResult>();
foreach (var request in requests)
{
cancellationToken.ThrowIfCancellationRequested();
var result = await ComputeWeightAsync(request, cancellationToken);
results.Add(result);
}
return results;
}
public TrustWeightConfiguration GetConfiguration() => _configuration;
public void UpdateConfiguration(TrustWeightConfiguration configuration)
{
_configuration = configuration;
}
private double ComputeIssuerWeight(VexIssuer? issuer, List<TrustWeightFactor> factors)
{
var config = _configuration.IssuerWeights;
if (issuer == null)
{
factors.Add(new TrustWeightFactor(
FactorId: "issuer_unknown",
Name: "Unknown Issuer",
RawValue: 0.0,
WeightedValue: config.UnknownIssuerMultiplier,
Multiplier: config.UnknownIssuerMultiplier,
Reason: "No issuer information available"));
return config.UnknownIssuerMultiplier;
}
// Base weight from category
var categoryMultiplier = issuer.Category switch
{
IssuerCategory.Vendor => config.VendorMultiplier,
IssuerCategory.Distributor => config.DistributorMultiplier,
IssuerCategory.Community => config.CommunityMultiplier,
IssuerCategory.Internal => config.InternalMultiplier,
IssuerCategory.Aggregator => config.AggregatorMultiplier,
_ => config.UnknownIssuerMultiplier
};
factors.Add(new TrustWeightFactor(
FactorId: "issuer_category",
Name: $"Issuer Category: {issuer.Category}",
RawValue: 1.0,
WeightedValue: categoryMultiplier,
Multiplier: categoryMultiplier,
Reason: $"Category '{issuer.Category}' has multiplier {categoryMultiplier:F2}"));
// Trust tier adjustment
var tierAdjustment = issuer.TrustTier switch
{
TrustTier.Authoritative => config.AuthoritativeTierBonus,
TrustTier.Trusted => config.TrustedTierBonus,
TrustTier.Untrusted => config.UntrustedTierPenalty,
_ => 0.0
};
if (Math.Abs(tierAdjustment) > 0.001)
{
factors.Add(new TrustWeightFactor(
FactorId: "issuer_tier",
Name: $"Trust Tier: {issuer.TrustTier}",
RawValue: tierAdjustment,
WeightedValue: tierAdjustment,
Multiplier: 1.0,
Reason: $"Trust tier '{issuer.TrustTier}' adjustment: {tierAdjustment:+0.00;-0.00}"));
}
return categoryMultiplier + tierAdjustment;
}
private double ComputeSignatureWeight(SignatureVerificationResult? verification, List<TrustWeightFactor> factors)
{
var config = _configuration.SignatureWeights;
if (verification == null)
{
factors.Add(new TrustWeightFactor(
FactorId: "signature_none",
Name: "No Signature",
RawValue: 0.0,
WeightedValue: config.NoSignaturePenalty,
Multiplier: config.NoSignaturePenalty,
Reason: "Document has no signature or signature not verified"));
return config.NoSignaturePenalty;
}
double weight;
string reason;
switch (verification.Status)
{
case SignatureVerificationStatus.Valid:
weight = config.ValidSignatureMultiplier;
reason = "Signature is valid and verified";
break;
case SignatureVerificationStatus.InvalidSignature:
weight = config.InvalidSignaturePenalty;
reason = "Signature verification failed";
break;
case SignatureVerificationStatus.ExpiredCertificate:
weight = config.ExpiredCertificatePenalty;
reason = "Certificate has expired";
break;
case SignatureVerificationStatus.RevokedCertificate:
weight = config.RevokedCertificatePenalty;
reason = "Certificate has been revoked";
break;
case SignatureVerificationStatus.UntrustedIssuer:
weight = config.NoSignaturePenalty;
reason = "Signature from untrusted issuer";
break;
default:
weight = config.NoSignaturePenalty;
reason = $"Signature status: {verification.Status}";
break;
}
factors.Add(new TrustWeightFactor(
FactorId: "signature_status",
Name: $"Signature: {verification.Status}",
RawValue: verification.IsValid ? 1.0 : 0.0,
WeightedValue: weight,
Multiplier: weight,
Reason: reason));
// Timestamp bonus
if (verification.IsValid && verification.Timestamp?.IsValid == true)
{
factors.Add(new TrustWeightFactor(
FactorId: "signature_timestamped",
Name: "Timestamped Signature",
RawValue: 1.0,
WeightedValue: config.TimestampedBonus,
Multiplier: 1.0,
Reason: $"Signature has valid timestamp from {verification.Timestamp.TimestampAuthority}"));
weight += config.TimestampedBonus;
}
return weight;
}
private double ComputeFreshnessWeight(
DateTimeOffset? documentIssuedAt,
DateTimeOffset? statementFirstSeen,
DateTimeOffset evaluationTime,
List<TrustWeightFactor> factors)
{
var config = _configuration.FreshnessWeights;
var referenceTime = documentIssuedAt ?? statementFirstSeen;
if (!referenceTime.HasValue)
{
factors.Add(new TrustWeightFactor(
FactorId: "freshness_unknown",
Name: "Unknown Age",
RawValue: 0.0,
WeightedValue: config.StaleMultiplier,
Multiplier: config.StaleMultiplier,
Reason: "No timestamp available to determine freshness"));
return config.StaleMultiplier;
}
var age = evaluationTime - referenceTime.Value;
double weight;
string category;
if (age < config.FreshThreshold)
{
weight = config.FreshMultiplier;
category = "Fresh";
}
else if (age < config.StaleThreshold)
{
weight = config.StaleMultiplier;
category = "Stale";
}
else
{
weight = config.ExpiredMultiplier;
category = "Expired";
}
factors.Add(new TrustWeightFactor(
FactorId: "freshness",
Name: $"Freshness: {category}",
RawValue: age.TotalDays,
WeightedValue: weight,
Multiplier: weight,
Reason: $"Document age: {FormatAge(age)} ({category})"));
return weight;
}
private double ComputeSourceFormatWeight(NormalizedStatement statement, List<TrustWeightFactor> factors)
{
// Note: We don't have direct access to source format from statement
// This would typically come from the document context
// For now, return neutral weight
var config = _configuration.SourceFormatWeights;
factors.Add(new TrustWeightFactor(
FactorId: "source_format",
Name: "Source Format",
RawValue: 1.0,
WeightedValue: 1.0,
Multiplier: 1.0,
Reason: "Source format weight applied at document level"));
return 1.0;
}
private double ComputeStatusSpecificityWeight(NormalizedStatement statement, List<TrustWeightFactor> factors)
{
var config = _configuration.StatusSpecificityWeights;
var statusWeight = statement.Status switch
{
VexStatus.NotAffected => config.NotAffectedBonus,
VexStatus.Fixed => config.FixedBonus,
VexStatus.Affected => config.AffectedNeutral,
VexStatus.UnderInvestigation => config.UnderInvestigationPenalty,
_ => 0.0
};
factors.Add(new TrustWeightFactor(
FactorId: "status",
Name: $"Status: {statement.Status}",
RawValue: 1.0,
WeightedValue: statusWeight,
Multiplier: 1.0,
Reason: $"Status '{statement.Status}' weight adjustment"));
// Justification bonus for not_affected
if (statement.Status == VexStatus.NotAffected && statement.Justification.HasValue)
{
factors.Add(new TrustWeightFactor(
FactorId: "justification",
Name: $"Justification: {statement.Justification}",
RawValue: 1.0,
WeightedValue: config.JustificationBonus,
Multiplier: 1.0,
Reason: $"Has justification: {statement.Justification}"));
statusWeight += config.JustificationBonus;
}
return statusWeight;
}
private double ComputeCustomWeight(
IReadOnlyDictionary<string, object?>? customFactors,
List<TrustWeightFactor> factors)
{
if (customFactors == null || customFactors.Count == 0)
{
return 0.0;
}
double totalCustomWeight = 0.0;
foreach (var (key, value) in customFactors)
{
if (value is double d)
{
factors.Add(new TrustWeightFactor(
FactorId: $"custom_{key}",
Name: $"Custom: {key}",
RawValue: d,
WeightedValue: d,
Multiplier: 1.0,
Reason: $"Custom factor '{key}'"));
totalCustomWeight += d;
}
}
return totalCustomWeight;
}
private double CombineWeights(TrustWeightBreakdown breakdown)
{
// Multiplicative combination with additive adjustments
var baseWeight = breakdown.IssuerWeight * breakdown.SignatureWeight * breakdown.FreshnessWeight;
var adjustments = breakdown.StatusSpecificityWeight + breakdown.CustomWeight;
return baseWeight + adjustments;
}
private static string FormatAge(TimeSpan age)
{
if (age.TotalDays >= 365)
{
return $"{age.TotalDays / 365:F1} years";
}
if (age.TotalDays >= 30)
{
return $"{age.TotalDays / 30:F1} months";
}
if (age.TotalDays >= 1)
{
return $"{age.TotalDays:F1} days";
}
return $"{age.TotalHours:F1} hours";
}
public static TrustWeightConfiguration CreateDefaultConfiguration()
{
return new TrustWeightConfiguration(
IssuerWeights: new IssuerTrustWeights(
VendorMultiplier: 1.0,
DistributorMultiplier: 0.9,
CommunityMultiplier: 0.7,
InternalMultiplier: 0.8,
AggregatorMultiplier: 0.6,
UnknownIssuerMultiplier: 0.3,
AuthoritativeTierBonus: 0.2,
TrustedTierBonus: 0.1,
UntrustedTierPenalty: -0.3),
SignatureWeights: new SignatureTrustWeights(
ValidSignatureMultiplier: 1.0,
InvalidSignaturePenalty: 0.1,
NoSignaturePenalty: 0.5,
ExpiredCertificatePenalty: 0.3,
RevokedCertificatePenalty: 0.1,
TimestampedBonus: 0.1),
FreshnessWeights: new FreshnessTrustWeights(
FreshThreshold: TimeSpan.FromDays(7),
StaleThreshold: TimeSpan.FromDays(90),
ExpiredThreshold: TimeSpan.FromDays(365),
FreshMultiplier: 1.0,
StaleMultiplier: 0.8,
ExpiredMultiplier: 0.5),
SourceFormatWeights: new SourceFormatWeights(
OpenVexMultiplier: 1.0,
CsafVexMultiplier: 1.0,
CycloneDxVexMultiplier: 0.95,
SpdxVexMultiplier: 0.9,
StellaOpsMultiplier: 1.0),
StatusSpecificityWeights: new StatusSpecificityWeights(
NotAffectedBonus: 0.1,
FixedBonus: 0.05,
AffectedNeutral: 0.0,
UnderInvestigationPenalty: -0.1,
JustificationBonus: 0.1),
MinimumWeight: 0.0,
MaximumWeight: 1.5);
}
}