feat(eidas): Implement eIDAS Crypto Plugin with dependency injection and signing capabilities
- Added ServiceCollectionExtensions for eIDAS crypto providers. - Implemented EidasCryptoProvider for handling eIDAS-compliant signatures. - Created LocalEidasProvider for local signing using PKCS#12 keystores. - Defined SignatureLevel and SignatureFormat enums for eIDAS compliance. - Developed TrustServiceProviderClient for remote signing via TSP. - Added configuration support for eIDAS options in the project file. - Implemented unit tests for SM2 compliance and crypto operations. - Introduced dependency injection extensions for SM software and remote plugins.
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
namespace StellaOps.Concelier.ProofService;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.ProofChain.Generators;
|
||||
using StellaOps.Attestor.ProofChain.Models;
|
||||
using StellaOps.Concelier.SourceIntel;
|
||||
using StellaOps.Feedser.BinaryAnalysis;
|
||||
using StellaOps.Feedser.Core;
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates four-tier backport detection and proof generation.
|
||||
/// Queries all evidence tiers and produces cryptographic ProofBlobs.
|
||||
/// </summary>
|
||||
public sealed class BackportProofService
|
||||
{
|
||||
private readonly ILogger<BackportProofService> _logger;
|
||||
private readonly IDistroAdvisoryRepository _advisoryRepo;
|
||||
private readonly ISourceArtifactRepository _sourceRepo;
|
||||
private readonly IPatchRepository _patchRepo;
|
||||
private readonly BinaryFingerprintFactory _fingerprintFactory;
|
||||
|
||||
public BackportProofService(
|
||||
ILogger<BackportProofService> logger,
|
||||
IDistroAdvisoryRepository advisoryRepo,
|
||||
ISourceArtifactRepository sourceRepo,
|
||||
IPatchRepository patchRepo,
|
||||
BinaryFingerprintFactory fingerprintFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_advisoryRepo = advisoryRepo;
|
||||
_sourceRepo = sourceRepo;
|
||||
_patchRepo = patchRepo;
|
||||
_fingerprintFactory = fingerprintFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate proof for a CVE + package combination using all available evidence.
|
||||
/// </summary>
|
||||
/// <param name="cveId">CVE identifier (e.g., CVE-2024-1234)</param>
|
||||
/// <param name="packagePurl">Package URL (e.g., pkg:deb/debian/curl@7.64.0-4)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ProofBlob with aggregated evidence, or null if no evidence found</returns>
|
||||
public async Task<ProofBlob?> GenerateProofAsync(
|
||||
string cveId,
|
||||
string packagePurl,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Generating proof for {CveId} in {Package}", cveId, packagePurl);
|
||||
|
||||
var evidences = new List<ProofEvidence>();
|
||||
|
||||
// Tier 1: Query distro advisories
|
||||
var advisoryEvidence = await QueryDistroAdvisoriesAsync(cveId, packagePurl, cancellationToken);
|
||||
if (advisoryEvidence != null)
|
||||
{
|
||||
evidences.Add(advisoryEvidence);
|
||||
_logger.LogInformation("Found Tier 1 evidence (distro advisory) for {CveId}", cveId);
|
||||
}
|
||||
|
||||
// Tier 2: Query changelog mentions
|
||||
var changelogEvidences = await QueryChangelogsAsync(cveId, packagePurl, cancellationToken);
|
||||
evidences.AddRange(changelogEvidences);
|
||||
if (changelogEvidences.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Found {Count} Tier 2 evidence(s) (changelog) for {CveId}",
|
||||
changelogEvidences.Count, cveId);
|
||||
}
|
||||
|
||||
// Tier 3: Query patch headers and HunkSig
|
||||
var patchEvidences = await QueryPatchesAsync(cveId, packagePurl, cancellationToken);
|
||||
evidences.AddRange(patchEvidences);
|
||||
if (patchEvidences.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Found {Count} Tier 3 evidence(s) (patches) for {CveId}",
|
||||
patchEvidences.Count, cveId);
|
||||
}
|
||||
|
||||
// Tier 4: Query binary fingerprints (if binary path available)
|
||||
// Note: Binary fingerprinting requires actual binary, skipped if unavailable
|
||||
var binaryPath = await ResolveBinaryPathAsync(packagePurl, cancellationToken);
|
||||
if (binaryPath != null)
|
||||
{
|
||||
var binaryEvidences = await QueryBinaryFingerprintsAsync(cveId, binaryPath, cancellationToken);
|
||||
evidences.AddRange(binaryEvidences);
|
||||
if (binaryEvidences.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Found {Count} Tier 4 evidence(s) (binary) for {CveId}",
|
||||
binaryEvidences.Count, cveId);
|
||||
}
|
||||
}
|
||||
|
||||
// If no evidence found, return unknown proof
|
||||
if (evidences.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("No evidence found for {CveId} in {Package}", cveId, packagePurl);
|
||||
return BackportProofGenerator.Unknown(
|
||||
cveId,
|
||||
packagePurl,
|
||||
"no_evidence_found",
|
||||
Array.Empty<ProofEvidence>()
|
||||
);
|
||||
}
|
||||
|
||||
// Aggregate evidences into combined proof
|
||||
var proof = BackportProofGenerator.CombineEvidence(cveId, packagePurl, evidences);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Generated proof {ProofId} for {CveId} with confidence {Confidence:P0} from {EvidenceCount} evidence(s)",
|
||||
proof.ProofId, cveId, proof.Confidence, evidences.Count);
|
||||
|
||||
return proof;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate proofs for multiple CVE + package combinations in batch.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<ProofBlob>> GenerateProofBatchAsync(
|
||||
IEnumerable<(string CveId, string PackagePurl)> requests,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = requests.Select(req =>
|
||||
GenerateProofAsync(req.CveId, req.PackagePurl, cancellationToken));
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
return results.Where(p => p != null).ToList()!;
|
||||
}
|
||||
|
||||
private async Task<ProofEvidence?> QueryDistroAdvisoriesAsync(
|
||||
string cveId,
|
||||
string packagePurl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var advisory = await _advisoryRepo.FindByCveAndPackageAsync(cveId, packagePurl, cancellationToken);
|
||||
if (advisory == null) return null;
|
||||
|
||||
// Create evidence from advisory data
|
||||
var advisoryData = JsonDocument.Parse(JsonSerializer.Serialize(advisory));
|
||||
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
|
||||
StellaOps.Canonical.Json.CanonJson.Canonicalize(advisoryData));
|
||||
|
||||
return new ProofEvidence
|
||||
{
|
||||
EvidenceId = $"evidence:distro:{advisory.DistroName}:{advisory.AdvisoryId}",
|
||||
Type = EvidenceType.DistroAdvisory,
|
||||
Source = advisory.DistroName,
|
||||
Timestamp = advisory.PublishedAt,
|
||||
Data = advisoryData,
|
||||
DataHash = dataHash
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<ProofEvidence>> QueryChangelogsAsync(
|
||||
string cveId,
|
||||
string packagePurl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var evidences = new List<ProofEvidence>();
|
||||
var changelogs = await _sourceRepo.FindChangelogsByCveAsync(cveId, packagePurl, cancellationToken);
|
||||
|
||||
foreach (var changelog in changelogs)
|
||||
{
|
||||
var changelogData = JsonDocument.Parse(JsonSerializer.Serialize(changelog));
|
||||
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
|
||||
StellaOps.Canonical.Json.CanonJson.Canonicalize(changelogData));
|
||||
|
||||
evidences.Add(new ProofEvidence
|
||||
{
|
||||
EvidenceId = $"evidence:changelog:{changelog.Format}:{changelog.Version}",
|
||||
Type = EvidenceType.ChangelogMention,
|
||||
Source = changelog.Format,
|
||||
Timestamp = changelog.Date,
|
||||
Data = changelogData,
|
||||
DataHash = dataHash
|
||||
});
|
||||
}
|
||||
|
||||
return evidences;
|
||||
}
|
||||
|
||||
private async Task<List<ProofEvidence>> QueryPatchesAsync(
|
||||
string cveId,
|
||||
string packagePurl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var evidences = new List<ProofEvidence>();
|
||||
|
||||
// Query patch headers
|
||||
var patchHeaders = await _patchRepo.FindPatchHeadersByCveAsync(cveId, cancellationToken);
|
||||
foreach (var header in patchHeaders)
|
||||
{
|
||||
var headerData = JsonDocument.Parse(JsonSerializer.Serialize(header));
|
||||
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
|
||||
StellaOps.Canonical.Json.CanonJson.Canonicalize(headerData));
|
||||
|
||||
evidences.Add(new ProofEvidence
|
||||
{
|
||||
EvidenceId = $"evidence:patch_header:{header.PatchFilePath}",
|
||||
Type = EvidenceType.PatchHeader,
|
||||
Source = header.Origin ?? "unknown",
|
||||
Timestamp = header.ParsedAt,
|
||||
Data = headerData,
|
||||
DataHash = dataHash
|
||||
});
|
||||
}
|
||||
|
||||
// Query HunkSig matches
|
||||
var patchSigs = await _patchRepo.FindPatchSignaturesByCveAsync(cveId, cancellationToken);
|
||||
foreach (var sig in patchSigs)
|
||||
{
|
||||
var sigData = JsonDocument.Parse(JsonSerializer.Serialize(sig));
|
||||
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
|
||||
StellaOps.Canonical.Json.CanonJson.Canonicalize(sigData));
|
||||
|
||||
evidences.Add(new ProofEvidence
|
||||
{
|
||||
EvidenceId = $"evidence:hunksig:{sig.CommitSha}",
|
||||
Type = EvidenceType.PatchHeader, // Reuse PatchHeader type
|
||||
Source = sig.UpstreamRepo,
|
||||
Timestamp = sig.ExtractedAt,
|
||||
Data = sigData,
|
||||
DataHash = dataHash
|
||||
});
|
||||
}
|
||||
|
||||
return evidences;
|
||||
}
|
||||
|
||||
private async Task<List<ProofEvidence>> QueryBinaryFingerprintsAsync(
|
||||
string cveId,
|
||||
string binaryPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var evidences = new List<ProofEvidence>();
|
||||
|
||||
// Query known fingerprints for this CVE
|
||||
var knownFingerprints = await _patchRepo.FindBinaryFingerprintsByCveAsync(cveId, cancellationToken);
|
||||
if (knownFingerprints.Count == 0) return evidences;
|
||||
|
||||
// Match candidate binary against known fingerprints
|
||||
var matchResult = await _fingerprintFactory.MatchBestAsync(binaryPath, knownFingerprints, cancellationToken);
|
||||
if (matchResult?.IsMatch == true)
|
||||
{
|
||||
var fingerprintData = JsonDocument.Parse(JsonSerializer.Serialize(matchResult));
|
||||
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
|
||||
StellaOps.Canonical.Json.CanonJson.Canonicalize(fingerprintData));
|
||||
|
||||
evidences.Add(new ProofEvidence
|
||||
{
|
||||
EvidenceId = $"evidence:binary:{matchResult.Method}:{matchResult.MatchedFingerprintId}",
|
||||
Type = EvidenceType.BinaryFingerprint,
|
||||
Source = matchResult.Method.ToString(),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = fingerprintData,
|
||||
DataHash = dataHash
|
||||
});
|
||||
}
|
||||
|
||||
return evidences;
|
||||
}
|
||||
|
||||
private async Task<string?> ResolveBinaryPathAsync(string packagePurl, CancellationToken cancellationToken)
|
||||
{
|
||||
// Resolve PURL to actual binary path
|
||||
// This would query package metadata or local package cache
|
||||
// Simplified: return null if not available
|
||||
await Task.CompletedTask;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Repository interfaces (to be implemented by storage layer)
|
||||
|
||||
public interface IDistroAdvisoryRepository
|
||||
{
|
||||
Task<DistroAdvisoryDto?> FindByCveAndPackageAsync(string cveId, string packagePurl, CancellationToken ct);
|
||||
}
|
||||
|
||||
public interface ISourceArtifactRepository
|
||||
{
|
||||
Task<IReadOnlyList<ChangelogDto>> FindChangelogsByCveAsync(string cveId, string packagePurl, CancellationToken ct);
|
||||
}
|
||||
|
||||
public interface IPatchRepository
|
||||
{
|
||||
Task<IReadOnlyList<PatchHeaderDto>> FindPatchHeadersByCveAsync(string cveId, CancellationToken ct);
|
||||
Task<IReadOnlyList<PatchSigDto>> FindPatchSignaturesByCveAsync(string cveId, CancellationToken ct);
|
||||
Task<IReadOnlyList<StellaOps.Feedser.BinaryAnalysis.Models.BinaryFingerprint>> FindBinaryFingerprintsByCveAsync(string cveId, CancellationToken ct);
|
||||
}
|
||||
|
||||
// DTOs for repository results
|
||||
|
||||
public sealed record DistroAdvisoryDto
|
||||
{
|
||||
public required string AdvisoryId { get; init; }
|
||||
public required string DistroName { get; init; }
|
||||
public required DateTimeOffset PublishedAt { get; init; }
|
||||
public required string Status { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangelogDto
|
||||
{
|
||||
public required string Format { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required DateTimeOffset Date { get; init; }
|
||||
public required IReadOnlyList<string> CveIds { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PatchHeaderDto
|
||||
{
|
||||
public required string PatchFilePath { get; init; }
|
||||
public required string? Origin { get; init; }
|
||||
public required DateTimeOffset ParsedAt { get; init; }
|
||||
public required IReadOnlyList<string> CveIds { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PatchSigDto
|
||||
{
|
||||
public required string CommitSha { get; init; }
|
||||
public required string UpstreamRepo { get; init; }
|
||||
public required DateTimeOffset ExtractedAt { get; init; }
|
||||
public required string HunkHash { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Concelier.SourceIntel\StellaOps.Concelier.SourceIntel.csproj" />
|
||||
<ProjectReference Include="..\..\..\Feedser\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\Feedser\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj" />
|
||||
<ProjectReference Include="..\..\..\Attestor\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user