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:
master
2025-12-23 14:06:48 +02:00
parent ef933db0d8
commit 84d97fd22c
51 changed files with 4353 additions and 747 deletions

View File

@@ -1,6 +1,6 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
using StellaOps.Scanner.Reachability.Models;
// Models are now in the same namespace
namespace StellaOps.Attestor;
@@ -23,7 +23,7 @@ public interface IProofEmitter
/// Canonical PoE JSON bytes (unsigned). Hash these bytes to get poe_hash.
/// </returns>
Task<byte[]> EmitPoEAsync(
Subgraph subgraph,
PoESubgraph subgraph,
ProofMetadata metadata,
string graphHash,
string? imageDigest = null,
@@ -67,7 +67,7 @@ public interface IProofEmitter
/// Dictionary mapping vuln_id to (poe_bytes, poe_hash).
/// </returns>
Task<IReadOnlyDictionary<string, (byte[] PoeBytes, string PoeHash)>> EmitPoEBatchAsync(
IReadOnlyList<Subgraph> subgraphs,
IReadOnlyList<PoESubgraph> subgraphs,
ProofMetadata metadata,
string graphHash,
string? imageDigest = null,

View File

@@ -4,7 +4,7 @@ using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.Serialization;
using StellaOps.Scanner.Reachability.Models;
// Models are now in the same namespace
namespace StellaOps.Attestor;
@@ -30,7 +30,7 @@ public class PoEArtifactGenerator : IProofEmitter
}
public Task<byte[]> EmitPoEAsync(
Subgraph subgraph,
PoESubgraph subgraph,
ProofMetadata metadata,
string graphHash,
string? imageDigest = null,
@@ -106,7 +106,7 @@ public class PoEArtifactGenerator : IProofEmitter
}
public async Task<IReadOnlyDictionary<string, (byte[] PoeBytes, string PoeHash)>> EmitPoEBatchAsync(
IReadOnlyList<Subgraph> subgraphs,
IReadOnlyList<PoESubgraph> subgraphs,
ProofMetadata metadata,
string graphHash,
string? imageDigest = null,
@@ -135,12 +135,12 @@ public class PoEArtifactGenerator : IProofEmitter
/// Build ProofOfExposure record from subgraph and metadata.
/// </summary>
private ProofOfExposure BuildProofOfExposure(
Subgraph subgraph,
PoESubgraph subgraph,
ProofMetadata metadata,
string graphHash,
string? imageDigest)
{
// Convert Subgraph to SubgraphData (flatten for JSON)
// Convert PoESubgraph to SubgraphData (flatten for JSON)
var nodes = subgraph.Nodes.Select(n => new NodeData(
Id: n.Id,
ModuleHash: n.ModuleHash,

View File

@@ -0,0 +1,239 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
using System.Text.Json.Serialization;
namespace StellaOps.Attestor;
/// <summary>
/// Represents a function identifier in a subgraph with module, symbol, address, and optional source location.
/// </summary>
/// <param name="ModuleHash">SHA-256 hash of the module/library containing this function</param>
/// <param name="Symbol">Human-readable symbol name (e.g., "main()", "Foo.bar()")</param>
/// <param name="Addr">Hexadecimal address (e.g., "0x401000")</param>
/// <param name="File">Optional source file path</param>
/// <param name="Line">Optional source line number</param>
[method: JsonConstructor]
public record FunctionId(
[property: JsonPropertyName("moduleHash")] string ModuleHash,
[property: JsonPropertyName("symbol")] string Symbol,
[property: JsonPropertyName("addr")] string Addr,
[property: JsonPropertyName("file")] string? File = null,
[property: JsonPropertyName("line")] int? Line = null
)
{
/// <summary>
/// Gets the canonical identifier for this function (symbol_id or code_id).
/// </summary>
[JsonIgnore]
public string Id => Symbol;
}
/// <summary>
/// Represents a call edge between two functions with optional guard predicates.
/// </summary>
/// <param name="Caller">Calling function identifier</param>
/// <param name="Callee">Called function identifier</param>
/// <param name="Guards">Guard predicates controlling this edge (e.g., ["feature:dark-mode", "platform:linux"])</param>
/// <param name="Confidence">Confidence score for this edge [0.0, 1.0]</param>
[method: JsonConstructor]
public record Edge(
[property: JsonPropertyName("from")] string Caller,
[property: JsonPropertyName("to")] string Callee,
[property: JsonPropertyName("guards")] string[] Guards,
[property: JsonPropertyName("confidence")] double Confidence = 1.0
);
/// <summary>
/// Represents a minimal PoE subgraph showing call paths from entry points to vulnerable sinks.
/// </summary>
/// <param name="BuildId">Deterministic build identifier (e.g., "gnu-build-id:5f0c7c3c...")</param>
/// <param name="ComponentRef">PURL package reference (e.g., "pkg:maven/log4j@2.14.1")</param>
/// <param name="VulnId">CVE identifier (e.g., "CVE-2021-44228")</param>
/// <param name="Nodes">Function nodes in the subgraph</param>
/// <param name="Edges">Call edges in the subgraph</param>
/// <param name="EntryRefs">Entry point node IDs (where execution begins)</param>
/// <param name="SinkRefs">Vulnerable sink node IDs (CVE-affected functions)</param>
/// <param name="PolicyDigest">SHA-256 hash of policy version used during extraction</param>
/// <param name="ToolchainDigest">SHA-256 hash of scanner version/toolchain</param>
[method: JsonConstructor]
public record PoESubgraph(
[property: JsonPropertyName("buildId")] string BuildId,
[property: JsonPropertyName("componentRef")] string ComponentRef,
[property: JsonPropertyName("vulnId")] string VulnId,
[property: JsonPropertyName("nodes")] IReadOnlyList<FunctionId> Nodes,
[property: JsonPropertyName("edges")] IReadOnlyList<Edge> Edges,
[property: JsonPropertyName("entryRefs")] string[] EntryRefs,
[property: JsonPropertyName("sinkRefs")] string[] SinkRefs,
[property: JsonPropertyName("policyDigest")] string PolicyDigest,
[property: JsonPropertyName("toolchainDigest")] string ToolchainDigest
);
/// <summary>
/// Metadata for Proof of Exposure artifact generation.
/// </summary>
/// <param name="GeneratedAt">Timestamp when PoE was generated</param>
/// <param name="AnalyzerName">Analyzer identifier (e.g., "stellaops-scanner")</param>
/// <param name="AnalyzerVersion">Semantic version (e.g., "1.2.0")</param>
/// <param name="ToolchainDigest">SHA-256 hash of analyzer binary/container</param>
/// <param name="PolicyDigest">SHA-256 hash of policy document</param>
/// <param name="ReproSteps">Minimal steps to reproduce this PoE</param>
[method: JsonConstructor]
public record ProofMetadata(
[property: JsonPropertyName("generatedAt")] DateTime GeneratedAt,
[property: JsonPropertyName("analyzer")] AnalyzerInfo Analyzer,
[property: JsonPropertyName("policy")] PolicyInfo Policy,
[property: JsonPropertyName("reproSteps")] string[] ReproSteps
);
/// <summary>
/// Analyzer information for PoE provenance.
/// </summary>
[method: JsonConstructor]
public record AnalyzerInfo(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("version")] string Version,
[property: JsonPropertyName("toolchainDigest")] string ToolchainDigest
);
/// <summary>
/// Policy information for PoE provenance.
/// </summary>
[method: JsonConstructor]
public record PolicyInfo(
[property: JsonPropertyName("policyId")] string PolicyId,
[property: JsonPropertyName("policyDigest")] string PolicyDigest,
[property: JsonPropertyName("evaluatedAt")] DateTime EvaluatedAt
);
/// <summary>
/// Complete Proof of Exposure artifact.
/// </summary>
/// <param name="Schema">Schema version (e.g., "stellaops.dev/poe@v1")</param>
/// <param name="Subgraph">Minimal subgraph with call paths</param>
/// <param name="Metadata">Provenance and reproduction metadata</param>
/// <param name="GraphHash">Parent richgraph-v1 BLAKE3 hash</param>
/// <param name="SbomRef">Optional reference to SBOM artifact</param>
/// <param name="VexClaimUri">Optional reference to VEX claim</param>
[method: JsonConstructor]
public record ProofOfExposure(
[property: JsonPropertyName("@type")] string Type,
[property: JsonPropertyName("schema")] string Schema,
[property: JsonPropertyName("subject")] SubjectInfo Subject,
[property: JsonPropertyName("subgraph")] SubgraphData SubgraphData,
[property: JsonPropertyName("metadata")] ProofMetadata Metadata,
[property: JsonPropertyName("evidence")] EvidenceInfo Evidence
);
/// <summary>
/// Subject information identifying what this PoE is about.
/// </summary>
[method: JsonConstructor]
public record SubjectInfo(
[property: JsonPropertyName("buildId")] string BuildId,
[property: JsonPropertyName("componentRef")] string ComponentRef,
[property: JsonPropertyName("vulnId")] string VulnId,
[property: JsonPropertyName("imageDigest")] string? ImageDigest = null
);
/// <summary>
/// Subgraph data structure for PoE JSON.
/// </summary>
[method: JsonConstructor]
public record SubgraphData(
[property: JsonPropertyName("nodes")] NodeData[] Nodes,
[property: JsonPropertyName("edges")] EdgeData[] Edges,
[property: JsonPropertyName("entryRefs")] string[] EntryRefs,
[property: JsonPropertyName("sinkRefs")] string[] SinkRefs
);
/// <summary>
/// Node data for PoE JSON serialization.
/// </summary>
[method: JsonConstructor]
public record NodeData(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("moduleHash")] string ModuleHash,
[property: JsonPropertyName("symbol")] string Symbol,
[property: JsonPropertyName("addr")] string Addr,
[property: JsonPropertyName("file")] string? File = null,
[property: JsonPropertyName("line")] int? Line = null
);
/// <summary>
/// Edge data for PoE JSON serialization.
/// </summary>
[method: JsonConstructor]
public record EdgeData(
[property: JsonPropertyName("from")] string From,
[property: JsonPropertyName("to")] string To,
[property: JsonPropertyName("guards")] string[]? Guards = null,
[property: JsonPropertyName("confidence")] double Confidence = 1.0
);
/// <summary>
/// Evidence links to related artifacts.
/// </summary>
[method: JsonConstructor]
public record EvidenceInfo(
[property: JsonPropertyName("graphHash")] string GraphHash,
[property: JsonPropertyName("sbomRef")] string? SbomRef = null,
[property: JsonPropertyName("vexClaimUri")] string? VexClaimUri = null,
[property: JsonPropertyName("runtimeFactsUri")] string? RuntimeFactsUri = null
);
/// <summary>
/// Represents a matched vulnerability for PoE generation.
/// </summary>
/// <param name="VulnId">Vulnerability identifier (CVE, GHSA, etc.)</param>
/// <param name="ComponentRef">Component package URL (PURL)</param>
/// <param name="IsReachable">Whether the vulnerability is reachable from entry points</param>
/// <param name="Severity">Vulnerability severity (Critical, High, Medium, Low, Info)</param>
[method: JsonConstructor]
public record VulnerabilityMatch(
[property: JsonPropertyName("vulnId")] string VulnId,
[property: JsonPropertyName("componentRef")] string ComponentRef,
[property: JsonPropertyName("isReachable")] bool IsReachable,
[property: JsonPropertyName("severity")] string Severity
);
/// <summary>
/// PoE scan context for PoE generation.
/// </summary>
/// <param name="ScanId">Unique scan identifier</param>
/// <param name="GraphHash">BLAKE3 hash of the reachability graph</param>
/// <param name="BuildId">GNU build ID or equivalent</param>
/// <param name="ImageDigest">Container image digest</param>
/// <param name="PolicyId">Policy identifier</param>
/// <param name="PolicyDigest">Policy content digest</param>
/// <param name="ScannerVersion">Scanner version</param>
/// <param name="ConfigPath">Scanner configuration path</param>
[method: JsonConstructor]
public record PoEScanContext(
[property: JsonPropertyName("scanId")] string ScanId,
[property: JsonPropertyName("graphHash")] string GraphHash,
[property: JsonPropertyName("buildId")] string BuildId,
[property: JsonPropertyName("imageDigest")] string ImageDigest,
[property: JsonPropertyName("policyId")] string PolicyId,
[property: JsonPropertyName("policyDigest")] string PolicyDigest,
[property: JsonPropertyName("scannerVersion")] string ScannerVersion,
[property: JsonPropertyName("configPath")] string ConfigPath
);
/// <summary>
/// Result from PoE generation for a single vulnerability.
/// </summary>
/// <param name="VulnId">Vulnerability identifier</param>
/// <param name="ComponentRef">Component package URL</param>
/// <param name="PoEHash">Content hash of the PoE artifact</param>
/// <param name="PoERef">CAS reference to the PoE artifact</param>
/// <param name="IsSigned">Whether the PoE is cryptographically signed</param>
/// <param name="PathCount">Number of paths in the subgraph</param>
[method: JsonConstructor]
public record PoEResult(
[property: JsonPropertyName("vulnId")] string VulnId,
[property: JsonPropertyName("componentRef")] string ComponentRef,
[property: JsonPropertyName("poeHash")] string PoEHash,
[property: JsonPropertyName("poeRef")] string? PoERef,
[property: JsonPropertyName("isSigned")] bool IsSigned,
[property: JsonPropertyName("pathCount")] int? PathCount = null
);

View File

@@ -104,33 +104,21 @@ public class VerdictController : ControllerBase
// Create submission context
var context = new SubmissionContext
{
TenantId = "default", // TODO: Extract from auth context
UserId = "system",
SubmitToRekor = request.SubmitToRekor
CallerSubject = "system",
CallerAudience = "policy-engine",
CallerClientId = "policy-engine-verdict-attestor",
CallerTenant = "default" // TODO: Extract from auth context
};
// Sign the predicate
var signResult = await _signingService.SignAsync(signingRequest, context, ct);
if (!signResult.Success)
{
_logger.LogError(
"Failed to sign verdict attestation: {Error}",
signResult.ErrorMessage);
// Extract DSSE envelope from result
var envelope = signResult.Bundle.Dsse;
var envelopeJson = SerializeEnvelope(envelope, signResult.KeyId);
return StatusCode(
StatusCodes.Status500InternalServerError,
new ProblemDetails
{
Title = "Signing Failed",
Detail = signResult.ErrorMessage,
Status = StatusCodes.Status500InternalServerError
});
}
// Extract envelope and Rekor info
var envelopeJson = SerializeEnvelope(signResult);
var rekorLogIndex = signResult.RekorLogIndex;
// Rekor log index (not implemented in minimal handler)
long? rekorLogIndex = null;
// Store in Evidence Locker (via HTTP call)
await StoreVerdictInEvidenceLockerAsync(
@@ -189,26 +177,25 @@ public class VerdictController : ControllerBase
}
/// <summary>
/// Serializes DSSE envelope from signing result.
/// Serializes DSSE envelope to JSON.
/// </summary>
private static string SerializeEnvelope(AttestationSignResult signResult)
private static string SerializeEnvelope(
StellaOps.Attestor.Core.Submission.AttestorSubmissionRequest.DsseEnvelope envelope,
string keyId)
{
// Simple DSSE envelope structure
var envelope = new
// DSSE envelope structure (already populated by signing service)
var envelopeObj = new
{
payloadType = signResult.PayloadType,
payload = signResult.PayloadBase64,
signatures = new[]
payloadType = envelope.PayloadType,
payload = envelope.PayloadBase64,
signatures = envelope.Signatures.Select(s => new
{
new
{
keyid = signResult.KeyId,
sig = signResult.SignatureBase64
}
}
keyid = keyId,
sig = s.Signature
}).ToArray()
};
return JsonSerializer.Serialize(envelope, new JsonSerializerOptions
return JsonSerializer.Serialize(envelopeObj, new JsonSerializerOptions
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@@ -225,28 +212,63 @@ public class VerdictController : ControllerBase
AttestationSignResult signResult,
CancellationToken ct)
{
// Skip storage if HttpClientFactory not configured
if (_httpClientFactory is null)
{
_logger.LogWarning(
"HttpClientFactory not configured - skipping Evidence Locker storage for {VerdictId}",
verdictId);
return;
}
try
{
// NOTE: This is a placeholder implementation.
// In production, this would:
// 1. Call Evidence Locker API via HttpClient
// 2. Or inject IVerdictRepository directly
// For now, we log and skip storage (attestation is returned to caller)
_logger.LogInformation(
"Verdict attestation {VerdictId} ready for storage (Evidence Locker integration pending)",
"Storing verdict attestation {VerdictId} in Evidence Locker",
verdictId);
// TODO: Implement Evidence Locker storage
// Example:
// if (_httpClientFactory != null)
// {
// var client = _httpClientFactory.CreateClient("EvidenceLocker");
// var storeRequest = new { verdictId, findingId, envelope = envelopeJson };
// await client.PostAsJsonAsync("/api/v1/verdicts", storeRequest, ct);
// }
var client = _httpClientFactory.CreateClient("EvidenceLocker");
await Task.CompletedTask;
// Parse envelope to get predicate for digest calculation
var envelope = JsonSerializer.Deserialize<JsonElement>(envelopeJson);
var payloadBase64 = envelope.GetProperty("payload").GetString() ?? string.Empty;
var predicateBytes = Convert.FromBase64String(payloadBase64);
var predicateDigest = $"sha256:{Convert.ToHexString(SHA256.HashData(predicateBytes)).ToLowerInvariant()}";
// Create Evidence Locker storage request
var storeRequest = new
{
verdict_id = verdictId,
tenant_id = "default", // TODO: Extract from auth context
policy_run_id = "unknown", // TODO: Pass from caller
policy_id = "unknown", // TODO: Pass from caller
policy_version = 1, // TODO: Pass from caller
finding_id = findingId,
verdict_status = "unknown", // TODO: Extract from predicate
verdict_severity = "unknown", // TODO: Extract from predicate
verdict_score = 0.0m, // TODO: Extract from predicate
evaluated_at = DateTimeOffset.UtcNow,
envelope = JsonSerializer.Deserialize<object>(envelopeJson),
predicate_digest = predicateDigest,
determinism_hash = (string?)null, // TODO: Pass from predicate
rekor_log_index = (long?)null // Not implemented yet
};
var response = await client.PostAsJsonAsync("/api/v1/verdicts", storeRequest, ct);
if (response.IsSuccessStatusCode)
{
_logger.LogInformation(
"Successfully stored verdict {VerdictId} in Evidence Locker",
verdictId);
}
else
{
_logger.LogWarning(
"Failed to store verdict {VerdictId} in Evidence Locker: {StatusCode}",
verdictId,
response.StatusCode);
}
}
catch (Exception ex)
{

View File

@@ -158,6 +158,18 @@ builder.Services.AddScoped<StellaOps.Attestor.WebService.Services.IPredicateType
StellaOps.Attestor.WebService.Services.PredicateTypeRouter>();
builder.Services.AddHttpContextAccessor();
// Configure HttpClient for Evidence Locker integration
builder.Services.AddHttpClient("EvidenceLocker", client =>
{
// TODO: Configure base address from configuration
// For now, use localhost default (will be overridden by actual configuration)
var evidenceLockerUrl = builder.Configuration.GetValue<string>("EvidenceLockerUrl")
?? "http://localhost:9090";
client.BaseAddress = new Uri(evidenceLockerUrl);
client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy());

View File

@@ -14,6 +14,7 @@
<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="..\..\..\Feedser\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.SourceIntel\StellaOps.Concelier.SourceIntel.csproj" />
</ItemGroup>