license switch agpl -> busl1, sprints work, new product advisories
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user