license switch agpl -> busl1, sprints work, new product advisories

This commit is contained in:
master
2026-01-20 15:32:20 +02:00
parent 4903395618
commit c32fff8f86
1835 changed files with 38630 additions and 4359 deletions

View File

@@ -4,6 +4,7 @@ using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using StellaOps.Cryptography.Plugin.Eidas.Timestamping;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
@@ -21,6 +22,10 @@ public sealed class EidasPlugin : CryptoPluginBase
private X509Certificate2? _signingCertificate;
private X509Certificate? _bcCertificate;
private RSA? _privateKey;
private ICadesSignatureBuilder? _cadesBuilder;
private ITimestampModeSelector? _timestampModeSelector;
private IQualifiedTimestampVerifier? _timestampVerifier;
private QualifiedTimestampingConfiguration? _timestampingConfiguration;
/// <inheritdoc />
public override PluginInfo Info => new(
@@ -29,7 +34,7 @@ public sealed class EidasPlugin : CryptoPluginBase
Version: "1.0.0",
Vendor: "Stella Ops",
Description: "EU eIDAS qualified electronic signatures (ETSI TS 119 312)",
LicenseId: "AGPL-3.0-or-later");
LicenseId: "BUSL-1.1");
/// <inheritdoc />
public override IReadOnlyList<string> SupportedAlgorithms => new[]
@@ -40,6 +45,9 @@ public sealed class EidasPlugin : CryptoPluginBase
"eIDAS-ECDSA-SHA256",
"eIDAS-ECDSA-SHA384",
"eIDAS-CAdES-BES",
"eIDAS-CAdES-T",
"eIDAS-CAdES-LT",
"eIDAS-CAdES-LTA",
"eIDAS-XAdES-BES"
};
@@ -63,6 +71,12 @@ public sealed class EidasPlugin : CryptoPluginBase
Context?.Logger.Warning("eIDAS provider initialized without certificate - signing operations will fail");
}
_timestampingConfiguration = context.Configuration.Bind<QualifiedTimestampingConfiguration>("timestamping")
?? new QualifiedTimestampingConfiguration();
_cadesBuilder = context.Services.GetService<ICadesSignatureBuilder>();
_timestampModeSelector = context.Services.GetService<ITimestampModeSelector>();
_timestampVerifier = context.Services.GetService<IQualifiedTimestampVerifier>();
return Task.CompletedTask;
}
@@ -74,7 +88,7 @@ public sealed class EidasPlugin : CryptoPluginBase
}
/// <inheritdoc />
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
public override async Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
@@ -89,7 +103,7 @@ public sealed class EidasPlugin : CryptoPluginBase
if (algorithm.Contains("CAdES", StringComparison.OrdinalIgnoreCase))
{
signature = CreateCadesSignature(data.ToArray());
signature = await SignCadesAsync(data, options, ct).ConfigureAwait(false);
}
else if (algorithm.Contains("ECDSA", StringComparison.OrdinalIgnoreCase))
{
@@ -104,11 +118,11 @@ public sealed class EidasPlugin : CryptoPluginBase
Context?.Logger.Debug("Signed {DataLength} bytes with {Algorithm}", data.Length, algorithm);
return Task.FromResult(signature);
return signature;
}
/// <inheritdoc />
public override Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
public override async Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
@@ -118,7 +132,7 @@ public sealed class EidasPlugin : CryptoPluginBase
if (algorithm.Contains("CAdES", StringComparison.OrdinalIgnoreCase))
{
isValid = VerifyCadesSignature(data.ToArray(), signature.ToArray());
isValid = await VerifyCadesSignatureAsync(data, signature, options, ct).ConfigureAwait(false);
}
else
{
@@ -149,7 +163,171 @@ public sealed class EidasPlugin : CryptoPluginBase
Context?.Logger.Debug("Verified signature: {IsValid}", isValid);
return Task.FromResult(isValid);
return isValid;
}
private async Task<byte[]> SignCadesAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
{
if (_signingCertificate == null)
{
throw new InvalidOperationException("Certificate not loaded for CAdES signing.");
}
var timestampContext = BuildTimestampContext(options.Metadata);
var policy = ResolveTimestampPolicy(timestampContext);
var requestedLevel = ResolveCadesLevel(options.Algorithm, policy);
var provider = FindProvider(policy.TsaProvider);
var signatureOptions = new CadesSignatureOptions
{
DigestAlgorithm = ResolveDigestAlgorithm(options.Algorithm),
TsaProvider = provider?.Name ?? policy.TsaProvider,
TsaUrl = provider?.Url ?? _options?.TimestampAuthorityUrl,
RequireQualifiedTsa = policy.IsQualified,
IncludeRevocationData = requestedLevel >= CadesLevel.CadesLT
};
if (_cadesBuilder is null)
{
Context?.Logger.Warning("CAdES builder not available; falling back to CAdES-BES.");
return CreateCadesSignature(data.ToArray());
}
return requestedLevel switch
{
CadesLevel.CadesB => await _cadesBuilder.CreateCadesBAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
CadesLevel.CadesT => await _cadesBuilder.CreateCadesTAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
CadesLevel.CadesLT => await _cadesBuilder.CreateCadesLTAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
CadesLevel.CadesLTA => await _cadesBuilder.CreateCadesLTAAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
CadesLevel.CadesC => await _cadesBuilder.CreateCadesLTAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
_ => await _cadesBuilder.CreateCadesBAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false)
};
}
private async Task<bool> VerifyCadesSignatureAsync(
ReadOnlyMemory<byte> data,
ReadOnlyMemory<byte> signature,
CryptoVerifyOptions options,
CancellationToken ct)
{
if (_timestampVerifier is null)
{
return VerifyCadesSignature(data.ToArray(), signature.ToArray());
}
var requestedLevel = ResolveCadesLevel(options.Algorithm, ResolveTimestampPolicy(BuildTimestampContext(null)));
var mode = _options?.TimestampMode ?? _timestampingConfiguration?.DefaultMode ?? TimestampMode.Rfc3161;
var verifyOptions = new QualifiedTimestampVerificationOptions
{
RequireQualifiedTsa = mode == TimestampMode.Qualified || mode == TimestampMode.QualifiedLtv,
VerifyLtvCompleteness = requestedLevel >= CadesLevel.CadesLT
};
var result = await _timestampVerifier.VerifyCadesAsync(signature, data, verifyOptions, ct).ConfigureAwait(false);
return result.IsSignatureValid;
}
private TimestampPolicy ResolveTimestampPolicy(TimestampContext context)
{
if (_timestampModeSelector is not null)
{
return _timestampModeSelector.GetPolicy(context);
}
var mode = _options?.TimestampMode ?? _timestampingConfiguration?.DefaultMode ?? TimestampMode.Rfc3161;
var provider = ResolveDefaultProvider(mode);
return new TimestampPolicy
{
Mode = mode,
TsaProvider = provider,
SignatureFormat = _options?.SignatureFormat ?? CadesLevel.CadesT,
MatchedPolicy = "options"
};
}
private TimestampContext BuildTimestampContext(IReadOnlyDictionary<string, string>? metadata)
{
if (metadata is null)
{
return new TimestampContext();
}
metadata.TryGetValue("environment", out var environment);
metadata.TryGetValue("repository", out var repository);
metadata.TryGetValue("tags", out var tagsValue);
metadata.TryGetValue("artifactType", out var artifactType);
metadata.TryGetValue("artifactDigest", out var artifactDigest);
return new TimestampContext
{
Environment = environment,
Repository = repository,
Tags = ParseTags(tagsValue),
ArtifactType = artifactType,
ArtifactDigest = artifactDigest,
Properties = metadata
};
}
private QualifiedTsaProvider? FindProvider(string providerName)
{
return _timestampingConfiguration?.Providers
.FirstOrDefault(p => p.Name.Equals(providerName, StringComparison.OrdinalIgnoreCase));
}
private string ResolveDefaultProvider(TimestampMode mode)
{
var isQualified = mode == TimestampMode.Qualified || mode == TimestampMode.QualifiedLtv;
var provider = _timestampingConfiguration?.Providers
.FirstOrDefault(p => p.Qualified == isQualified);
return provider?.Name ?? _timestampingConfiguration?.Providers.FirstOrDefault()?.Name ?? "default";
}
private static IReadOnlyList<string>? ParseTags(string? tagsValue)
{
if (string.IsNullOrWhiteSpace(tagsValue))
{
return null;
}
return tagsValue
.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static string ResolveDigestAlgorithm(string algorithm)
{
if (algorithm.Contains("512", StringComparison.OrdinalIgnoreCase))
{
return "SHA512";
}
if (algorithm.Contains("384", StringComparison.OrdinalIgnoreCase))
{
return "SHA384";
}
return "SHA256";
}
private static CadesLevel ResolveCadesLevel(string algorithm, TimestampPolicy policy)
{
var level = algorithm switch
{
var a when a.Contains("CAdES-LTA", StringComparison.OrdinalIgnoreCase) => CadesLevel.CadesLTA,
var a when a.Contains("CAdES-LT", StringComparison.OrdinalIgnoreCase) => CadesLevel.CadesLT,
var a when a.Contains("CAdES-T", StringComparison.OrdinalIgnoreCase) => CadesLevel.CadesT,
var a when a.Contains("CAdES-B", StringComparison.OrdinalIgnoreCase) => CadesLevel.CadesB,
_ => policy.SignatureFormat
};
if (policy.Mode == TimestampMode.None)
{
return CadesLevel.CadesB;
}
return level;
}
/// <inheritdoc />
@@ -322,6 +500,16 @@ public sealed class EidasOptions
/// </summary>
public string? TimestampAuthorityUrl { get; init; }
/// <summary>
/// Timestamp mode to use (Rfc3161, Qualified, QualifiedLtv).
/// </summary>
public TimestampMode? TimestampMode { get; init; }
/// <summary>
/// CAdES signature format level.
/// </summary>
public CadesLevel? SignatureFormat { get; init; }
/// <summary>
/// Whether to validate certificate chain during operations.
/// </summary>

View File

@@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="System.Security.Cryptography.Pkcs" />
<PackageReference Include="System.Security.Cryptography.Xml" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,7 +5,10 @@
// Description: Implementation of EU Trusted List service.
// -----------------------------------------------------------------------------
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -258,6 +261,8 @@ public sealed class EuTrustListService : IEuTrustListService
}
}
var certificates = ParseServiceCertificates(serviceInfo);
entries.Add(new TrustListEntry
{
TspName = tspName,
@@ -269,7 +274,8 @@ public sealed class EuTrustListService : IEuTrustListService
ServiceTypeIdentifier = serviceType ?? "",
CountryCode = ExtractCountryCode(tspName),
ServiceSupplyPoints = supplyPoints,
StatusHistory = historyList
StatusHistory = historyList,
ServiceCertificates = certificates
});
}
}
@@ -336,9 +342,64 @@ public sealed class EuTrustListService : IEuTrustListService
private void VerifyTrustListSignature(string xmlContent)
{
// Would verify the XML signature on the trust list
// Using XmlDsig signature verification
_logger.LogDebug("Verifying trust list signature");
// Implementation would use System.Security.Cryptography.Xml
var xmlDoc = new XmlDocument
{
PreserveWhitespace = true,
XmlResolver = null
};
xmlDoc.LoadXml(xmlContent);
var nsManager = new XmlNamespaceManager(xmlDoc.NameTable);
nsManager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
var signatureNode = xmlDoc.SelectSingleNode("//ds:Signature", nsManager) as XmlElement;
if (signatureNode is null)
{
throw new CryptographicException("Trust list signature element not found.");
}
var signedXml = new SignedXml(xmlDoc);
signedXml.LoadXml(signatureNode);
if (!signedXml.CheckSignature())
{
throw new CryptographicException("Trust list signature validation failed.");
}
}
private static IReadOnlyList<X509Certificate2>? ParseServiceCertificates(XElement serviceInfo)
{
var certElements = serviceInfo.Descendants()
.Where(e => e.Name.LocalName.Equals("X509Certificate", StringComparison.OrdinalIgnoreCase))
.Select(e => e.Value)
.Where(v => !string.IsNullOrWhiteSpace(v))
.ToList();
if (certElements.Count == 0)
{
return null;
}
var certificates = new List<X509Certificate2>();
foreach (var certBase64 in certElements)
{
try
{
var raw = Convert.FromBase64String(certBase64.Trim());
certificates.Add(X509CertificateLoader.LoadCertificate(raw));
}
catch (FormatException)
{
// Ignore malformed certificate entries.
}
catch (CryptographicException)
{
// Ignore malformed certificate entries.
}
}
return certificates.Count > 0 ? certificates : null;
}
}

View File

@@ -171,6 +171,7 @@ public sealed class QualifiedTimestampVerifier : IQualifiedTimestampVerifier
// Detect CAdES level
var level = DetectCadesLevel(signedCms);
ValidateCadesFormat(signedCms, level, errors);
// Get signing time
DateTimeOffset? signingTime = null;
@@ -211,18 +212,19 @@ public sealed class QualifiedTimestampVerifier : IQualifiedTimestampVerifier
}
else
{
warnings.Add("Expected timestamp attribute not found");
errors.Add("Expected timestamp attribute not found");
}
}
// Check LTV completeness if required
var isLtvComplete = false;
if (options.VerifyLtvCompleteness && level >= CadesLevel.CadesLT)
var shouldCheckLtv = options.VerifyLtvCompleteness || level >= CadesLevel.CadesLT;
if (shouldCheckLtv)
{
isLtvComplete = VerifyLtvCompleteness(signedCms);
if (!isLtvComplete)
{
warnings.Add("LTV data is incomplete");
errors.Add("LTV data is incomplete");
}
}
@@ -344,6 +346,54 @@ public sealed class QualifiedTimestampVerifier : IQualifiedTimestampVerifier
return hasRevocationValues && hasCertValues;
}
private static void ValidateCadesFormat(SignedCms signedCms, CadesLevel level, List<string> errors)
{
var signerInfo = signedCms.SignerInfos[0];
var unsignedAttrs = signerInfo.UnsignedAttributes;
const string archiveTimestampOid = "1.2.840.113549.1.9.16.2.48";
const string revocationValuesOid = "1.2.840.113549.1.9.16.2.24";
const string certValuesOid = "1.2.840.113549.1.9.16.2.23";
const string revocationRefsOid = "1.2.840.113549.1.9.16.2.22";
if (level >= CadesLevel.CadesT &&
!HasUnsignedAttribute(unsignedAttrs, SignatureTimestampOid))
{
errors.Add("Missing signature timestamp attribute for CAdES-T");
}
if (level >= CadesLevel.CadesC &&
!HasUnsignedAttribute(unsignedAttrs, revocationRefsOid))
{
errors.Add("Missing revocation references for CAdES-C");
}
if (level >= CadesLevel.CadesLT)
{
if (!HasUnsignedAttribute(unsignedAttrs, revocationValuesOid))
{
errors.Add("Missing revocation values for CAdES-LT");
}
if (!HasUnsignedAttribute(unsignedAttrs, certValuesOid))
{
errors.Add("Missing certificate values for CAdES-LT");
}
}
if (level >= CadesLevel.CadesLTA &&
!HasUnsignedAttribute(unsignedAttrs, archiveTimestampOid))
{
errors.Add("Missing archive timestamp for CAdES-LTA");
}
}
private static bool HasUnsignedAttribute(CryptographicAttributeObjectCollection attributes, string oid)
{
return attributes.Cast<CryptographicAttributeObject>()
.Any(a => a.Oid?.Value == oid);
}
private sealed class TstInfoData
{
public required string DigestAlgorithm { get; init; }

View File

@@ -61,13 +61,16 @@ public sealed partial class TimestampModeSelector : ITimestampModeSelector
overridePolicy.Mode,
provider);
return new TimestampPolicy
var policy = new TimestampPolicy
{
Mode = overridePolicy.Mode,
TsaProvider = provider,
SignatureFormat = overridePolicy.SignatureFormat ?? CadesLevel.CadesT,
MatchedPolicy = $"override:{context.Environment ?? "default"}"
};
LogDecision(context, policy);
return policy;
}
}
@@ -80,26 +83,45 @@ public sealed partial class TimestampModeSelector : ITimestampModeSelector
"Provider {Provider} required for context, using qualified mode",
provider.Name);
return new TimestampPolicy
var policy = new TimestampPolicy
{
Mode = TimestampMode.Qualified,
TsaProvider = provider.Name,
SignatureFormat = provider.SignatureFormat,
MatchedPolicy = $"provider:{provider.Name}"
};
LogDecision(context, policy);
return policy;
}
}
// Use defaults
var defaultProvider = GetDefaultProviderForMode(_config.DefaultMode);
return new TimestampPolicy
var defaultPolicy = new TimestampPolicy
{
Mode = _config.DefaultMode,
TsaProvider = defaultProvider,
SignatureFormat = CadesLevel.CadesT,
MatchedPolicy = "default"
};
LogDecision(context, defaultPolicy);
return defaultPolicy;
}
private void LogDecision(TimestampContext context, TimestampPolicy policy)
{
_logger.LogInformation(
"Timestamp policy decision: mode={Mode}, provider={Provider}, format={Format}, policy={Policy}, env={Environment}, repo={Repository}, tags={Tags}",
policy.Mode,
policy.TsaProvider,
policy.SignatureFormat,
policy.MatchedPolicy ?? "unknown",
context.Environment ?? "(none)",
context.Repository ?? "(none)",
context.Tags is null ? "(none)" : string.Join(",", context.Tags));
}
private static bool MatchesOverride(TimestampContext context, TimestampPolicyOverride policy)

View File

@@ -4,7 +4,7 @@ plugin:
version: 1.0.0
vendor: Stella Ops
description: EU eIDAS qualified electronic signatures (ETSI TS 119 312)
license: AGPL-3.0-or-later
license: BUSL-1.1
entryPoint: StellaOps.Cryptography.Plugin.Eidas.EidasPlugin