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; using StellaOps.Plugin.Abstractions.Health; using StellaOps.Plugin.Abstractions.Lifecycle; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using X509Certificate = Org.BouncyCastle.X509.X509Certificate; namespace StellaOps.Cryptography.Plugin.Eidas; /// /// eIDAS cryptography plugin for EU qualified electronic signatures. /// Implements ETSI TS 119 312 compliant signature operations. /// public sealed class EidasPlugin : CryptoPluginBase { private EidasOptions? _options; private X509Certificate2? _signingCertificate; private X509Certificate? _bcCertificate; private RSA? _privateKey; private ICadesSignatureBuilder? _cadesBuilder; private ITimestampModeSelector? _timestampModeSelector; private IQualifiedTimestampVerifier? _timestampVerifier; private QualifiedTimestampingConfiguration? _timestampingConfiguration; /// public override PluginInfo Info => new( Id: "com.stellaops.crypto.eidas", Name: "eIDAS Cryptography Provider", Version: "1.0.0", Vendor: "Stella Ops", Description: "EU eIDAS qualified electronic signatures (ETSI TS 119 312)", LicenseId: "BUSL-1.1"); /// public override IReadOnlyList SupportedAlgorithms => new[] { "eIDAS-RSA-SHA256", "eIDAS-RSA-SHA384", "eIDAS-RSA-SHA512", "eIDAS-ECDSA-SHA256", "eIDAS-ECDSA-SHA384", "eIDAS-CAdES-BES", "eIDAS-CAdES-T", "eIDAS-CAdES-LT", "eIDAS-CAdES-LTA", "eIDAS-XAdES-BES" }; /// protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct) { _options = context.Configuration.Bind() ?? new EidasOptions(); if (!string.IsNullOrEmpty(_options.CertificatePath)) { LoadCertificate(_options.CertificatePath, _options.CertificatePassword); Context?.Logger.Info("eIDAS provider initialized with certificate from {Path}", _options.CertificatePath); } else if (!string.IsNullOrEmpty(_options.CertificateThumbprint)) { LoadCertificateFromStore(_options.CertificateThumbprint, _options.CertificateStoreLocation); Context?.Logger.Info("eIDAS provider initialized with certificate from store"); } else { Context?.Logger.Warning("eIDAS provider initialized without certificate - signing operations will fail"); } _timestampingConfiguration = context.Configuration.Bind("timestamping") ?? new QualifiedTimestampingConfiguration(); _cadesBuilder = context.Services.GetService(); _timestampModeSelector = context.Services.GetService(); _timestampVerifier = context.Services.GetService(); return Task.CompletedTask; } /// public override bool CanHandle(CryptoOperation operation, string algorithm) { return algorithm.StartsWith("eIDAS", StringComparison.OrdinalIgnoreCase) && SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase); } /// public override async Task SignAsync(ReadOnlyMemory data, CryptoSignOptions options, CancellationToken ct) { EnsureActive(); ct.ThrowIfCancellationRequested(); if (_signingCertificate == null || _privateKey == null) { throw new InvalidOperationException("No signing certificate configured."); } var algorithm = options.Algorithm; byte[] signature; if (algorithm.Contains("CAdES", StringComparison.OrdinalIgnoreCase)) { signature = await SignCadesAsync(data, options, ct).ConfigureAwait(false); } else if (algorithm.Contains("ECDSA", StringComparison.OrdinalIgnoreCase)) { throw new NotSupportedException("ECDSA not yet implemented for eIDAS. Use RSA algorithms."); } else { // Standard RSA signature var hashAlgorithm = GetHashAlgorithmName(algorithm); signature = _privateKey.SignData(data.ToArray(), hashAlgorithm, RSASignaturePadding.Pkcs1); } Context?.Logger.Debug("Signed {DataLength} bytes with {Algorithm}", data.Length, algorithm); return signature; } /// public override async Task VerifyAsync(ReadOnlyMemory data, ReadOnlyMemory signature, CryptoVerifyOptions options, CancellationToken ct) { EnsureActive(); ct.ThrowIfCancellationRequested(); var algorithm = options.Algorithm; bool isValid; if (algorithm.Contains("CAdES", StringComparison.OrdinalIgnoreCase)) { isValid = await VerifyCadesSignatureAsync(data, signature, options, ct).ConfigureAwait(false); } else { // Load verification certificate X509Certificate2 verificationCert; if (!string.IsNullOrEmpty(options.CertificateChain)) { verificationCert = X509CertificateLoader.LoadCertificate(Convert.FromBase64String(options.CertificateChain)); } else if (_signingCertificate != null) { verificationCert = _signingCertificate; } else { throw new InvalidOperationException("No verification certificate available."); } using var rsa = verificationCert.GetRSAPublicKey(); if (rsa == null) { throw new InvalidOperationException("Certificate does not contain RSA public key."); } var hashAlgorithm = GetHashAlgorithmName(algorithm); isValid = rsa.VerifyData(data.ToArray(), signature.ToArray(), hashAlgorithm, RSASignaturePadding.Pkcs1); } Context?.Logger.Debug("Verified signature: {IsValid}", isValid); return isValid; } private async Task SignCadesAsync(ReadOnlyMemory 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 VerifyCadesSignatureAsync( ReadOnlyMemory data, ReadOnlyMemory 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? 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? 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; } /// public override Task EncryptAsync(ReadOnlyMemory data, CryptoEncryptOptions options, CancellationToken ct) { EnsureActive(); ct.ThrowIfCancellationRequested(); if (_signingCertificate == null) { throw new InvalidOperationException("No certificate configured for encryption."); } using var rsa = _signingCertificate.GetRSAPublicKey() ?? throw new InvalidOperationException("Certificate does not contain RSA public key."); var encrypted = rsa.Encrypt(data.ToArray(), RSAEncryptionPadding.OaepSHA256); Context?.Logger.Debug("Encrypted {DataLength} bytes", data.Length); return Task.FromResult(encrypted); } /// public override Task DecryptAsync(ReadOnlyMemory data, CryptoDecryptOptions options, CancellationToken ct) { EnsureActive(); ct.ThrowIfCancellationRequested(); if (_privateKey == null) { throw new InvalidOperationException("No private key configured for decryption."); } var decrypted = _privateKey.Decrypt(data.ToArray(), RSAEncryptionPadding.OaepSHA256); Context?.Logger.Debug("Decrypted {DataLength} bytes", data.Length); return Task.FromResult(decrypted); } /// public override Task HashAsync(ReadOnlyMemory data, string algorithm, CancellationToken ct) { EnsureActive(); ct.ThrowIfCancellationRequested(); byte[] hash = algorithm.ToUpperInvariant() switch { var a when a.Contains("512") => SHA512.HashData(data.Span), var a when a.Contains("384") => SHA384.HashData(data.Span), _ => SHA256.HashData(data.Span) }; Context?.Logger.Debug("Computed hash of {DataLength} bytes", data.Length); return Task.FromResult(hash); } /// public override ValueTask DisposeAsync() { _privateKey?.Dispose(); _signingCertificate?.Dispose(); _privateKey = null; _signingCertificate = null; _bcCertificate = null; State = PluginLifecycleState.Stopped; return ValueTask.CompletedTask; } private void LoadCertificate(string path, string? password) { _signingCertificate = X509CertificateLoader.LoadPkcs12FromFile(path, password, X509KeyStorageFlags.Exportable); _privateKey = _signingCertificate.GetRSAPrivateKey(); _bcCertificate = DotNetUtilities.FromX509Certificate(_signingCertificate); } private void LoadCertificateFromStore(string thumbprint, StoreLocation storeLocation) { using var store = new X509Store(StoreName.My, storeLocation); store.Open(OpenFlags.ReadOnly); var certs = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false); if (certs.Count == 0) { throw new InvalidOperationException($"Certificate with thumbprint {thumbprint} not found in store."); } _signingCertificate = certs[0]; _privateKey = _signingCertificate.GetRSAPrivateKey(); _bcCertificate = DotNetUtilities.FromX509Certificate(_signingCertificate); } private byte[] CreateCadesSignature(byte[] data) { if (_signingCertificate == null || _privateKey == null) { throw new InvalidOperationException("Certificate not loaded for CAdES signing."); } // Simplified CAdES-BES: Use .NET CMS for signing // For full CAdES compliance, a dedicated library like iText or DSS should be used var contentInfo = new System.Security.Cryptography.Pkcs.ContentInfo(data); var signedCms = new System.Security.Cryptography.Pkcs.SignedCms(contentInfo, detached: true); var signer = new System.Security.Cryptography.Pkcs.CmsSigner(_signingCertificate) { DigestAlgorithm = new Oid("2.16.840.1.101.3.4.2.1") // SHA-256 }; signedCms.ComputeSignature(signer); return signedCms.Encode(); } private bool VerifyCadesSignature(byte[] data, byte[] signature) { try { // Use .NET CMS verification var contentInfo = new System.Security.Cryptography.Pkcs.ContentInfo(data); var signedCms = new System.Security.Cryptography.Pkcs.SignedCms(contentInfo, detached: true); signedCms.Decode(signature); signedCms.CheckSignature(verifySignatureOnly: true); return true; } catch { return false; } } private static HashAlgorithmName GetHashAlgorithmName(string algorithm) { return algorithm.ToUpperInvariant() switch { var a when a.Contains("SHA512") => HashAlgorithmName.SHA512, var a when a.Contains("SHA384") => HashAlgorithmName.SHA384, _ => HashAlgorithmName.SHA256 }; } } /// /// Configuration options for eIDAS cryptography plugin. /// public sealed class EidasOptions { /// /// Path to PKCS#12/PFX certificate file. /// public string? CertificatePath { get; init; } /// /// Password for the certificate file. /// public string? CertificatePassword { get; init; } /// /// Certificate thumbprint for loading from Windows certificate store. /// public string? CertificateThumbprint { get; init; } /// /// Certificate store location (CurrentUser or LocalMachine). /// public StoreLocation CertificateStoreLocation { get; init; } = StoreLocation.CurrentUser; /// /// Trusted timestamp authority URL for qualified signatures. /// public string? TimestampAuthorityUrl { get; init; } /// /// Timestamp mode to use (Rfc3161, Qualified, QualifiedLtv). /// public TimestampMode? TimestampMode { get; init; } /// /// CAdES signature format level. /// public CadesLevel? SignatureFormat { get; init; } /// /// Whether to validate certificate chain during operations. /// public bool ValidateCertificateChain { get; init; } = true; }