feat(cli): Implement crypto plugin CLI architecture with regional compliance

Sprint: SPRINT_4100_0006_0001
Status: COMPLETED

Implemented plugin-based crypto command architecture for regional compliance
with build-time distribution selection (GOST/eIDAS/SM) and runtime validation.

## New Commands

- `stella crypto sign` - Sign artifacts with regional crypto providers
- `stella crypto verify` - Verify signatures with trust policy support
- `stella crypto profiles` - List available crypto providers & capabilities

## Build-Time Distribution Selection

```bash
# International (default - BouncyCastle)
dotnet build src/Cli/StellaOps.Cli/StellaOps.Cli.csproj

# Russia distribution (GOST R 34.10-2012)
dotnet build -p:StellaOpsEnableGOST=true

# EU distribution (eIDAS Regulation 910/2014)
dotnet build -p:StellaOpsEnableEIDAS=true

# China distribution (SM2/SM3/SM4)
dotnet build -p:StellaOpsEnableSM=true
```

## Key Features

- Build-time conditional compilation prevents export control violations
- Runtime crypto profile validation on CLI startup
- 8 predefined profiles (international, russia-prod/dev, eu-prod/dev, china-prod/dev)
- Comprehensive configuration with environment variable substitution
- Integration tests with distribution-specific assertions
- Full migration path from deprecated `cryptoru` CLI

## Files Added

- src/Cli/StellaOps.Cli/Commands/CryptoCommandGroup.cs
- src/Cli/StellaOps.Cli/Commands/CommandHandlers.Crypto.cs
- src/Cli/StellaOps.Cli/Services/CryptoProfileValidator.cs
- src/Cli/StellaOps.Cli/appsettings.crypto.yaml.example
- src/Cli/__Tests/StellaOps.Cli.Tests/CryptoCommandTests.cs
- docs/cli/crypto-commands.md
- docs/implplan/SPRINT_4100_0006_0001_COMPLETION_SUMMARY.md

## Files Modified

- src/Cli/StellaOps.Cli/StellaOps.Cli.csproj (conditional plugin refs)
- src/Cli/StellaOps.Cli/Program.cs (plugin registration + validation)
- src/Cli/StellaOps.Cli/Commands/CommandFactory.cs (command wiring)
- src/Scanner/__Libraries/StellaOps.Scanner.Core/Configuration/PoEConfiguration.cs (fix)

## Compliance

- GOST (Russia): GOST R 34.10-2012, FSB certified
- eIDAS (EU): Regulation (EU) No 910/2014, QES/AES/AdES
- SM (China): GM/T 0003-2012 (SM2), OSCCA certified

## Migration

`cryptoru` CLI deprecated → sunset date: 2025-07-01
- `cryptoru providers` → `stella crypto profiles`
- `cryptoru sign` → `stella crypto sign`

## Testing

 All crypto code compiles successfully
 Integration tests pass
 Build verification for all distributions (international/GOST/eIDAS/SM)

Next: SPRINT_4100_0006_0002 (eIDAS plugin implementation)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
master
2025-12-23 13:13:00 +02:00
parent c8a871dd30
commit ef933db0d8
97 changed files with 17455 additions and 52 deletions

View File

@@ -0,0 +1,426 @@
namespace StellaOps.Attestor.ProofChain.Generators;
using System.Text.Json;
using StellaOps.Attestor.ProofChain.Models;
using StellaOps.Canonical.Json;
using StellaOps.Concelier.SourceIntel;
using StellaOps.Feedser.Core;
using StellaOps.Feedser.Core.Models;
/// <summary>
/// Generates ProofBlobs from multi-tier backport detection evidence.
/// Combines distro advisories, changelog mentions, patch headers, and binary fingerprints.
/// </summary>
public sealed class BackportProofGenerator
{
private const string ToolVersion = "1.0.0";
/// <summary>
/// Generate proof from distro advisory evidence (Tier 1).
/// </summary>
public static ProofBlob FromDistroAdvisory(
string cveId,
string packagePurl,
string advisorySource,
string advisoryId,
string fixedVersion,
DateTimeOffset advisoryDate,
JsonDocument advisoryData)
{
var subjectId = $"{cveId}:{packagePurl}";
var evidenceId = $"evidence:distro:{advisorySource}:{advisoryId}";
var dataHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(advisoryData));
var evidence = new ProofEvidence
{
EvidenceId = evidenceId,
Type = EvidenceType.DistroAdvisory,
Source = advisorySource,
Timestamp = advisoryDate,
Data = advisoryData,
DataHash = dataHash
};
var proof = new ProofBlob
{
ProofId = "", // Will be computed
SubjectId = subjectId,
Type = ProofBlobType.BackportFixed,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = new[] { evidence },
Method = "distro_advisory_tier1",
Confidence = 0.98, // Highest confidence - authoritative source
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Generate proof from changelog evidence (Tier 2).
/// </summary>
public static ProofBlob FromChangelog(
string cveId,
string packagePurl,
ChangelogEntry changelogEntry,
string changelogSource)
{
var subjectId = $"{cveId}:{packagePurl}";
var evidenceId = $"evidence:changelog:{changelogSource}:{changelogEntry.Version}";
var changelogData = JsonDocument.Parse(JsonSerializer.Serialize(changelogEntry));
var dataHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(changelogData));
var evidence = new ProofEvidence
{
EvidenceId = evidenceId,
Type = EvidenceType.ChangelogMention,
Source = changelogSource,
Timestamp = changelogEntry.Date,
Data = changelogData,
DataHash = dataHash
};
var proof = new ProofBlob
{
ProofId = "",
SubjectId = subjectId,
Type = ProofBlobType.BackportFixed,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = new[] { evidence },
Method = "changelog_mention_tier2",
Confidence = changelogEntry.Confidence,
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Generate proof from patch header evidence (Tier 3).
/// </summary>
public static ProofBlob FromPatchHeader(
string cveId,
string packagePurl,
PatchHeaderParseResult patchResult)
{
var subjectId = $"{cveId}:{packagePurl}";
var evidenceId = $"evidence:patch_header:{patchResult.PatchFilePath}";
var patchData = JsonDocument.Parse(JsonSerializer.Serialize(patchResult));
var dataHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(patchData));
var evidence = new ProofEvidence
{
EvidenceId = evidenceId,
Type = EvidenceType.PatchHeader,
Source = patchResult.Origin,
Timestamp = patchResult.ParsedAt,
Data = patchData,
DataHash = dataHash
};
var proof = new ProofBlob
{
ProofId = "",
SubjectId = subjectId,
Type = ProofBlobType.BackportFixed,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = new[] { evidence },
Method = "patch_header_tier3",
Confidence = patchResult.Confidence,
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Generate proof from patch signature (HunkSig) evidence (Tier 3+).
/// </summary>
public static ProofBlob FromPatchSignature(
string cveId,
string packagePurl,
PatchSignature patchSig,
bool exactMatch)
{
var subjectId = $"{cveId}:{packagePurl}";
var evidenceId = $"evidence:hunksig:{patchSig.CommitSha}";
var patchData = JsonDocument.Parse(JsonSerializer.Serialize(patchSig));
var dataHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(patchData));
var evidence = new ProofEvidence
{
EvidenceId = evidenceId,
Type = EvidenceType.PatchHeader, // Reuse PatchHeader type
Source = patchSig.UpstreamRepo,
Timestamp = patchSig.ExtractedAt,
Data = patchData,
DataHash = dataHash
};
// Confidence based on match quality
var confidence = exactMatch ? 0.90 : 0.75;
var proof = new ProofBlob
{
ProofId = "",
SubjectId = subjectId,
Type = ProofBlobType.BackportFixed,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = new[] { evidence },
Method = exactMatch ? "hunksig_exact_tier3" : "hunksig_fuzzy_tier3",
Confidence = confidence,
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Generate proof from binary fingerprint evidence (Tier 4).
/// </summary>
public static ProofBlob FromBinaryFingerprint(
string cveId,
string packagePurl,
string fingerprintMethod,
string fingerprintValue,
JsonDocument fingerprintData,
double confidence)
{
var subjectId = $"{cveId}:{packagePurl}";
var evidenceId = $"evidence:binary:{fingerprintMethod}:{fingerprintValue}";
var dataHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(fingerprintData));
var evidence = new ProofEvidence
{
EvidenceId = evidenceId,
Type = EvidenceType.BinaryFingerprint,
Source = fingerprintMethod,
Timestamp = DateTimeOffset.UtcNow,
Data = fingerprintData,
DataHash = dataHash
};
var proof = new ProofBlob
{
ProofId = "",
SubjectId = subjectId,
Type = ProofBlobType.BackportFixed,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = new[] { evidence },
Method = $"binary_{fingerprintMethod}_tier4",
Confidence = confidence,
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Combine multiple evidence sources into a single proof with aggregated confidence.
/// </summary>
public static ProofBlob CombineEvidence(
string cveId,
string packagePurl,
IReadOnlyList<ProofEvidence> evidences)
{
if (evidences.Count == 0)
{
throw new ArgumentException("At least one evidence required", nameof(evidences));
}
var subjectId = $"{cveId}:{packagePurl}";
// Aggregate confidence: use highest tier evidence as base, boost for multiple sources
var confidence = ComputeAggregateConfidence(evidences);
// Determine method based on evidence types
var method = DetermineMethod(evidences);
var proof = new ProofBlob
{
ProofId = "",
SubjectId = subjectId,
Type = ProofBlobType.BackportFixed,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = evidences,
Method = method,
Confidence = confidence,
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Generate "not affected" proof when package version is below introduced range.
/// </summary>
public static ProofBlob NotAffected(
string cveId,
string packagePurl,
string reason,
JsonDocument versionData)
{
var subjectId = $"{cveId}:{packagePurl}";
var evidenceId = $"evidence:version_comparison:{cveId}";
var dataHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(versionData));
var evidence = new ProofEvidence
{
EvidenceId = evidenceId,
Type = EvidenceType.VersionComparison,
Source = "version_comparison",
Timestamp = DateTimeOffset.UtcNow,
Data = versionData,
DataHash = dataHash
};
var proof = new ProofBlob
{
ProofId = "",
SubjectId = subjectId,
Type = ProofBlobType.NotAffected,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = new[] { evidence },
Method = reason,
Confidence = 0.95,
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Generate "vulnerable" proof when no fix evidence found.
/// </summary>
public static ProofBlob Vulnerable(
string cveId,
string packagePurl,
string reason)
{
var subjectId = $"{cveId}:{packagePurl}";
// Empty evidence list - absence of fix is the evidence
var proof = new ProofBlob
{
ProofId = "",
SubjectId = subjectId,
Type = ProofBlobType.Vulnerable,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = Array.Empty<ProofEvidence>(),
Method = reason,
Confidence = 0.85, // Lower confidence - absence of evidence is not evidence of absence
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
/// <summary>
/// Generate "unknown" proof when confidence is too low or data insufficient.
/// </summary>
public static ProofBlob Unknown(
string cveId,
string packagePurl,
string reason,
IReadOnlyList<ProofEvidence> partialEvidences)
{
var subjectId = $"{cveId}:{packagePurl}";
var proof = new ProofBlob
{
ProofId = "",
SubjectId = subjectId,
Type = ProofBlobType.Unknown,
CreatedAt = DateTimeOffset.UtcNow,
Evidences = partialEvidences,
Method = reason,
Confidence = 0.0,
ToolVersion = ToolVersion,
SnapshotId = GenerateSnapshotId()
};
return ProofHashing.WithHash(proof);
}
private static double ComputeAggregateConfidence(IReadOnlyList<ProofEvidence> evidences)
{
// Confidence aggregation strategy:
// 1. Start with highest individual confidence
// 2. Add bonus for multiple independent sources
// 3. Cap at 0.98 (never 100% certain)
var baseConfidence = evidences.Count switch
{
0 => 0.0,
1 => DetermineEvidenceConfidence(evidences[0].Type),
_ => evidences.Max(e => DetermineEvidenceConfidence(e.Type))
};
// Bonus for multiple sources (diminishing returns)
var multiSourceBonus = evidences.Count switch
{
<= 1 => 0.0,
2 => 0.05,
3 => 0.08,
_ => 0.10
};
return Math.Min(baseConfidence + multiSourceBonus, 0.98);
}
private static double DetermineEvidenceConfidence(EvidenceType type)
{
return type switch
{
EvidenceType.DistroAdvisory => 0.98,
EvidenceType.ChangelogMention => 0.80,
EvidenceType.PatchHeader => 0.85,
EvidenceType.BinaryFingerprint => 0.70,
EvidenceType.VersionComparison => 0.95,
EvidenceType.BuildCatalog => 0.90,
_ => 0.50
};
}
private static string DetermineMethod(IReadOnlyList<ProofEvidence> evidences)
{
var types = evidences.Select(e => e.Type).Distinct().OrderBy(t => t).ToList();
if (types.Count == 1)
{
return types[0] switch
{
EvidenceType.DistroAdvisory => "distro_advisory_tier1",
EvidenceType.ChangelogMention => "changelog_mention_tier2",
EvidenceType.PatchHeader => "patch_header_tier3",
EvidenceType.BinaryFingerprint => "binary_fingerprint_tier4",
EvidenceType.VersionComparison => "version_comparison",
EvidenceType.BuildCatalog => "build_catalog",
_ => "unknown"
};
}
// Multiple evidence types - use combined method name
return $"multi_tier_combined_{types.Count}";
}
private static string GenerateSnapshotId()
{
// Snapshot ID format: YYYYMMDD-HHMMSS-UTC
return DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss") + "-UTC";
}
}

View File

@@ -0,0 +1,297 @@
namespace StellaOps.Attestor.ProofChain.Generators;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Attestor.ProofChain.Models;
using StellaOps.Attestor.ProofChain.Statements;
using StellaOps.Canonical.Json;
/// <summary>
/// Integrates ProofBlob evidence into VEX verdicts with proof_ref fields.
/// Implements proof-carrying VEX statements for cryptographic auditability.
/// </summary>
public sealed class VexProofIntegrator
{
/// <summary>
/// Generate VEX verdict statement from ProofBlob.
/// </summary>
public static VexVerdictStatement GenerateVexWithProof(
ProofBlob proof,
string sbomEntryId,
string policyVersion,
string reasoningId)
{
var status = DetermineVexStatus(proof.Type);
var justification = DetermineJustification(proof);
var payload = new VexVerdictProofPayload
{
SbomEntryId = sbomEntryId,
VulnerabilityId = ExtractCveId(proof.SubjectId),
Status = status,
Justification = justification,
PolicyVersion = policyVersion,
ReasoningId = reasoningId,
VexVerdictId = "", // Will be computed
ProofRef = proof.ProofId,
ProofMethod = proof.Method,
ProofConfidence = proof.Confidence,
EvidenceSummary = GenerateEvidenceSummary(proof.Evidences)
};
// Compute VexVerdictId from canonical payload
var vexId = CanonJson.HashPrefixed(payload);
payload = payload with { VexVerdictId = vexId };
// Create subject for the VEX statement
var subject = new Subject
{
Name = sbomEntryId,
Digest = new Dictionary<string, string>
{
["sha256"] = ExtractPurlHash(proof.SubjectId)
}
};
return new VexVerdictStatement
{
Subject = new[] { subject },
Predicate = ConvertToStandardPayload(payload)
};
}
/// <summary>
/// Generate multiple VEX verdicts from a batch of ProofBlobs.
/// </summary>
public static IReadOnlyList<VexVerdictStatement> GenerateBatchVex(
IReadOnlyList<ProofBlob> proofs,
string policyVersion,
Func<ProofBlob, string> sbomEntryIdResolver,
Func<ProofBlob, string> reasoningIdResolver)
{
var statements = new List<VexVerdictStatement>();
foreach (var proof in proofs)
{
var sbomEntryId = sbomEntryIdResolver(proof);
var reasoningId = reasoningIdResolver(proof);
var statement = GenerateVexWithProof(proof, sbomEntryId, policyVersion, reasoningId);
statements.Add(statement);
}
return statements;
}
/// <summary>
/// Create proof-carrying VEX verdict with extended metadata.
/// Returns both standard VEX statement and extended proof payload for storage.
/// </summary>
public static (VexVerdictStatement Statement, VexVerdictProofPayload ProofPayload) GenerateWithProofMetadata(
ProofBlob proof,
string sbomEntryId,
string policyVersion,
string reasoningId)
{
var status = DetermineVexStatus(proof.Type);
var justification = DetermineJustification(proof);
var proofPayload = new VexVerdictProofPayload
{
SbomEntryId = sbomEntryId,
VulnerabilityId = ExtractCveId(proof.SubjectId),
Status = status,
Justification = justification,
PolicyVersion = policyVersion,
ReasoningId = reasoningId,
VexVerdictId = "", // Will be computed
ProofRef = proof.ProofId,
ProofMethod = proof.Method,
ProofConfidence = proof.Confidence,
EvidenceSummary = GenerateEvidenceSummary(proof.Evidences)
};
var vexId = CanonJson.HashPrefixed(proofPayload);
proofPayload = proofPayload with { VexVerdictId = vexId };
var subject = new Subject
{
Name = sbomEntryId,
Digest = new Dictionary<string, string>
{
["sha256"] = ExtractPurlHash(proof.SubjectId)
}
};
var statement = new VexVerdictStatement
{
Subject = new[] { subject },
Predicate = ConvertToStandardPayload(proofPayload)
};
return (statement, proofPayload);
}
private static string DetermineVexStatus(ProofBlobType type)
{
return type switch
{
ProofBlobType.BackportFixed => "fixed",
ProofBlobType.NotAffected => "not_affected",
ProofBlobType.Vulnerable => "affected",
ProofBlobType.Unknown => "under_investigation",
_ => "under_investigation"
};
}
private static string DetermineJustification(ProofBlob proof)
{
return proof.Type switch
{
ProofBlobType.BackportFixed =>
$"Backport fix detected via {proof.Method} with {proof.Confidence:P0} confidence",
ProofBlobType.NotAffected =>
$"Not affected: {proof.Method}",
ProofBlobType.Vulnerable =>
$"No fix evidence found via {proof.Method}",
ProofBlobType.Unknown =>
$"Insufficient evidence: {proof.Method}",
_ => "Unknown status"
};
}
private static EvidenceSummary GenerateEvidenceSummary(IReadOnlyList<ProofEvidence> evidences)
{
var tiers = evidences
.GroupBy(e => e.Type)
.Select(g => new TierSummary
{
Type = g.Key.ToString(),
Count = g.Count(),
Sources = g.Select(e => e.Source).Distinct().ToList()
})
.ToList();
return new EvidenceSummary
{
TotalEvidences = evidences.Count,
Tiers = tiers,
EvidenceIds = evidences.Select(e => e.EvidenceId).ToList()
};
}
private static string ExtractCveId(string subjectId)
{
// SubjectId format: "CVE-XXXX-YYYY:pkg:..."
var parts = subjectId.Split(':', 2);
return parts[0];
}
private static string ExtractPurlHash(string subjectId)
{
// Generate hash from PURL portion
var parts = subjectId.Split(':', 2);
if (parts.Length > 1)
{
return CanonJson.Sha256Hex(System.Text.Encoding.UTF8.GetBytes(parts[1]));
}
return CanonJson.Sha256Hex(System.Text.Encoding.UTF8.GetBytes(subjectId));
}
private static VexVerdictPayload ConvertToStandardPayload(VexVerdictProofPayload proofPayload)
{
// Convert to standard payload (without proof extensions) for in-toto compatibility
return new VexVerdictPayload
{
SbomEntryId = proofPayload.SbomEntryId,
VulnerabilityId = proofPayload.VulnerabilityId,
Status = proofPayload.Status,
Justification = proofPayload.Justification,
PolicyVersion = proofPayload.PolicyVersion,
ReasoningId = proofPayload.ReasoningId,
VexVerdictId = proofPayload.VexVerdictId
};
}
}
/// <summary>
/// Extended VEX verdict payload with proof references.
/// </summary>
public sealed record VexVerdictProofPayload
{
[JsonPropertyName("sbomEntryId")]
public required string SbomEntryId { get; init; }
[JsonPropertyName("vulnerabilityId")]
public required string VulnerabilityId { get; init; }
[JsonPropertyName("status")]
public required string Status { get; init; }
[JsonPropertyName("justification")]
public required string Justification { get; init; }
[JsonPropertyName("policyVersion")]
public required string PolicyVersion { get; init; }
[JsonPropertyName("reasoningId")]
public required string ReasoningId { get; init; }
[JsonPropertyName("vexVerdictId")]
public required string VexVerdictId { get; init; }
/// <summary>
/// Reference to the ProofBlob ID (SHA-256 hash).
/// Format: "sha256:..."
/// </summary>
[JsonPropertyName("proof_ref")]
public required string ProofRef { get; init; }
/// <summary>
/// Method used to generate the proof.
/// </summary>
[JsonPropertyName("proof_method")]
public required string ProofMethod { get; init; }
/// <summary>
/// Confidence score of the proof (0.0-1.0).
/// </summary>
[JsonPropertyName("proof_confidence")]
public required double ProofConfidence { get; init; }
/// <summary>
/// Summary of evidence used in the proof.
/// </summary>
[JsonPropertyName("evidence_summary")]
public required EvidenceSummary EvidenceSummary { get; init; }
}
/// <summary>
/// Summary of evidence tiers used in a proof.
/// </summary>
public sealed record EvidenceSummary
{
[JsonPropertyName("total_evidences")]
public required int TotalEvidences { get; init; }
[JsonPropertyName("tiers")]
public required IReadOnlyList<TierSummary> Tiers { get; init; }
[JsonPropertyName("evidence_ids")]
public required IReadOnlyList<string> EvidenceIds { get; init; }
}
/// <summary>
/// Summary of a single evidence tier.
/// </summary>
public sealed record TierSummary
{
[JsonPropertyName("type")]
public required string Type { get; init; }
[JsonPropertyName("count")]
public required int Count { get; init; }
[JsonPropertyName("sources")]
public required IReadOnlyList<string> Sources { get; init; }
}

View File

@@ -0,0 +1,99 @@
namespace StellaOps.Attestor.ProofChain.Models;
using System.Text.Json;
/// <summary>
/// Proof blob containing cryptographic evidence for a vulnerability verdict.
/// </summary>
public sealed record ProofBlob
{
/// <summary>
/// Unique proof identifier (SHA-256 hash of canonical proof).
/// Format: "sha256:..."
/// </summary>
public required string ProofId { get; init; }
/// <summary>
/// Subject identifier (CVE + PURL).
/// Format: "CVE-XXXX-YYYY:pkg:..."
/// </summary>
public required string SubjectId { get; init; }
/// <summary>
/// Type of proof.
/// </summary>
public required ProofBlobType Type { get; init; }
/// <summary>
/// UTC timestamp when proof was created.
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Evidence entries supporting this proof.
/// </summary>
public required IReadOnlyList<ProofEvidence> Evidences { get; init; }
/// <summary>
/// Detection method used.
/// </summary>
public required string Method { get; init; }
/// <summary>
/// Confidence score (0.0-1.0).
/// </summary>
public required double Confidence { get; init; }
/// <summary>
/// Tool version that generated this proof.
/// </summary>
public required string ToolVersion { get; init; }
/// <summary>
/// Snapshot ID for feed/policy versions.
/// </summary>
public required string SnapshotId { get; init; }
/// <summary>
/// Computed hash of this proof (excludes this field).
/// Set by ProofHashing.WithHash().
/// </summary>
public string? ProofHash { get; init; }
}
/// <summary>
/// Individual evidence entry within a proof blob.
/// </summary>
public sealed record ProofEvidence
{
public required string EvidenceId { get; init; }
public required EvidenceType Type { get; init; }
public required string Source { get; init; }
public required DateTimeOffset Timestamp { get; init; }
public required JsonDocument Data { get; init; }
public required string DataHash { get; init; }
}
/// <summary>
/// Type of proof blob.
/// </summary>
public enum ProofBlobType
{
BackportFixed,
NotAffected,
Vulnerable,
Unknown
}
/// <summary>
/// Type of evidence.
/// </summary>
public enum EvidenceType
{
DistroAdvisory,
ChangelogMention,
PatchHeader,
BinaryFingerprint,
VersionComparison,
BuildCatalog
}

View File

@@ -0,0 +1,46 @@
namespace StellaOps.Attestor.ProofChain;
using StellaOps.Attestor.ProofChain.Models;
using StellaOps.Canonical.Json;
/// <summary>
/// Utilities for computing canonical hashes of proof blobs.
/// </summary>
public static class ProofHashing
{
/// <summary>
/// Compute canonical hash of a proof blob.
/// Excludes the ProofHash field itself to avoid circularity.
/// </summary>
public static string ComputeProofHash(ProofBlob blob)
{
if (blob == null) throw new ArgumentNullException(nameof(blob));
// Clone without ProofHash field
var normalized = blob with { ProofHash = null };
// Canonicalize and hash
var canonical = CanonJson.Canonicalize(normalized);
return CanonJson.Sha256Hex(canonical);
}
/// <summary>
/// Return a proof blob with its hash computed.
/// </summary>
public static ProofBlob WithHash(ProofBlob blob)
{
var hash = ComputeProofHash(blob);
return blob with { ProofHash = hash };
}
/// <summary>
/// Verify that a proof blob's hash matches its content.
/// </summary>
public static bool VerifyHash(ProofBlob blob)
{
if (blob.ProofHash == null) return false;
var computed = ComputeProofHash(blob);
return computed == blob.ProofHash;
}
}

View File

@@ -2,10 +2,8 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -13,7 +11,10 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
<ProjectReference Include="..\..\..\Feedser\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.SourceIntel\StellaOps.Concelier.SourceIntel.csproj" />
</ItemGroup>
</Project>