release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Org.BouncyCastle.X509;
|
||||
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 X509Certificate = Org.BouncyCastle.X509.X509Certificate;
|
||||
|
||||
/// <summary>
|
||||
/// eIDAS cryptography plugin for EU qualified electronic signatures.
|
||||
/// Implements ETSI TS 119 312 compliant signature operations.
|
||||
/// </summary>
|
||||
public sealed class EidasPlugin : CryptoPluginBase
|
||||
{
|
||||
private EidasOptions? _options;
|
||||
private X509Certificate2? _signingCertificate;
|
||||
private X509Certificate? _bcCertificate;
|
||||
private RSA? _privateKey;
|
||||
|
||||
/// <inheritdoc />
|
||||
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: "AGPL-3.0-or-later");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
||||
{
|
||||
"eIDAS-RSA-SHA256",
|
||||
"eIDAS-RSA-SHA384",
|
||||
"eIDAS-RSA-SHA512",
|
||||
"eIDAS-ECDSA-SHA256",
|
||||
"eIDAS-ECDSA-SHA384",
|
||||
"eIDAS-CAdES-BES",
|
||||
"eIDAS-XAdES-BES"
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct)
|
||||
{
|
||||
_options = context.Configuration.Bind<EidasOptions>() ?? 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");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanHandle(CryptoOperation operation, string algorithm)
|
||||
{
|
||||
return algorithm.StartsWith("eIDAS", StringComparison.OrdinalIgnoreCase) &&
|
||||
SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> 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 = CreateCadesSignature(data.ToArray());
|
||||
}
|
||||
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 Task.FromResult(signature);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = options.Algorithm;
|
||||
bool isValid;
|
||||
|
||||
if (algorithm.Contains("CAdES", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isValid = VerifyCadesSignature(data.ToArray(), signature.ToArray());
|
||||
}
|
||||
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 Task.FromResult(isValid);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> EncryptAsync(ReadOnlyMemory<byte> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> HashAsync(ReadOnlyMemory<byte> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for eIDAS cryptography plugin.
|
||||
/// </summary>
|
||||
public sealed class EidasOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to PKCS#12/PFX certificate file.
|
||||
/// </summary>
|
||||
public string? CertificatePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Password for the certificate file.
|
||||
/// </summary>
|
||||
public string? CertificatePassword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Certificate thumbprint for loading from Windows certificate store.
|
||||
/// </summary>
|
||||
public string? CertificateThumbprint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Certificate store location (CurrentUser or LocalMachine).
|
||||
/// </summary>
|
||||
public StoreLocation CertificateStoreLocation { get; init; } = StoreLocation.CurrentUser;
|
||||
|
||||
/// <summary>
|
||||
/// Trusted timestamp authority URL for qualified signatures.
|
||||
/// </summary>
|
||||
public string? TimestampAuthorityUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to validate certificate chain during operations.
|
||||
/// </summary>
|
||||
public bool ValidateCertificateChain { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" />
|
||||
<PackageReference Include="System.Security.Cryptography.Pkcs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin\StellaOps.Cryptography.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="plugin.yaml" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,50 @@
|
||||
plugin:
|
||||
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)
|
||||
license: AGPL-3.0-or-later
|
||||
|
||||
entryPoint: StellaOps.Cryptography.Plugin.Eidas.EidasPlugin
|
||||
|
||||
minPlatformVersion: 1.0.0
|
||||
|
||||
capabilities:
|
||||
- type: crypto
|
||||
id: eidas
|
||||
algorithms:
|
||||
- eIDAS-RSA-SHA256
|
||||
- eIDAS-RSA-SHA384
|
||||
- eIDAS-RSA-SHA512
|
||||
- eIDAS-ECDSA-SHA256
|
||||
- eIDAS-ECDSA-SHA384
|
||||
- eIDAS-CAdES-BES
|
||||
- eIDAS-XAdES-BES
|
||||
|
||||
configSchema:
|
||||
type: object
|
||||
properties:
|
||||
certificatePath:
|
||||
type: string
|
||||
description: Path to PKCS#12/PFX certificate file
|
||||
certificatePassword:
|
||||
type: string
|
||||
description: Password for the certificate file
|
||||
certificateThumbprint:
|
||||
type: string
|
||||
description: Certificate thumbprint for Windows certificate store
|
||||
certificateStoreLocation:
|
||||
type: string
|
||||
enum: [CurrentUser, LocalMachine]
|
||||
default: CurrentUser
|
||||
description: Certificate store location
|
||||
timestampAuthorityUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Trusted timestamp authority URL
|
||||
validateCertificateChain:
|
||||
type: boolean
|
||||
default: true
|
||||
description: Validate certificate chain during operations
|
||||
required: []
|
||||
Reference in New Issue
Block a user