release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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: []

View File

@@ -0,0 +1,431 @@
namespace StellaOps.Cryptography.Plugin.Fips;
using System.Security.Cryptography;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
/// <summary>
/// FIPS 140-2 compliant cryptography plugin.
/// Uses .NET's FIPS-validated cryptographic implementations.
/// </summary>
public sealed class FipsPlugin : CryptoPluginBase
{
private FipsOptions? _options;
private RSA? _rsaKey;
private ECDsa? _ecdsaKey;
/// <inheritdoc />
public override PluginInfo Info => new(
Id: "com.stellaops.crypto.fips",
Name: "FIPS 140-2 Cryptography Provider",
Version: "1.0.0",
Vendor: "Stella Ops",
Description: "US FIPS 140-2 compliant cryptographic algorithms",
LicenseId: "AGPL-3.0-or-later");
/// <inheritdoc />
public override IReadOnlyList<string> SupportedAlgorithms => new[]
{
"RSA-SHA256",
"RSA-SHA384",
"RSA-SHA512",
"RSA-PSS-SHA256",
"RSA-PSS-SHA384",
"RSA-PSS-SHA512",
"ECDSA-P256-SHA256",
"ECDSA-P384-SHA384",
"ECDSA-P521-SHA512",
"AES-128-CBC",
"AES-256-CBC",
"AES-128-GCM",
"AES-256-GCM",
"SHA-256",
"SHA-384",
"SHA-512",
"HMAC-SHA256",
"HMAC-SHA384",
"HMAC-SHA512"
};
/// <inheritdoc />
protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct)
{
_options = context.Configuration.Bind<FipsOptions>() ?? new FipsOptions();
// Verify FIPS mode if required
if (_options.RequireFipsMode && !IsFipsModeEnabled())
{
throw new InvalidOperationException(
"FIPS mode is required but not enabled. Set CryptoConfig.AllowOnlyFipsAlgorithms = true.");
}
// Initialize RSA key
if (!string.IsNullOrEmpty(_options.RsaKeyXml))
{
_rsaKey = RSA.Create();
_rsaKey.FromXmlString(_options.RsaKeyXml);
Context?.Logger.Info("FIPS provider initialized with configured RSA key");
}
else if (_options.GenerateKeysOnInit)
{
_rsaKey = RSA.Create(_options.RsaKeySize);
Context?.Logger.Info("FIPS provider initialized with generated {KeySize}-bit RSA key", _options.RsaKeySize);
}
// Initialize ECDSA key
if (!string.IsNullOrEmpty(_options.EcdsaKeyXml))
{
_ecdsaKey = ECDsa.Create();
_ecdsaKey.FromXmlString(_options.EcdsaKeyXml);
}
else if (_options.GenerateKeysOnInit)
{
_ecdsaKey = ECDsa.Create(GetECCurve(_options.EcdsaCurve));
}
return Task.CompletedTask;
}
/// <inheritdoc />
public override bool CanHandle(CryptoOperation operation, string algorithm)
{
return SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
var algorithm = options.Algorithm;
byte[] signature;
if (algorithm.StartsWith("ECDSA", StringComparison.OrdinalIgnoreCase))
{
if (_ecdsaKey == null)
{
throw new InvalidOperationException("No ECDSA key available.");
}
var hashAlgorithm = GetHashAlgorithmName(algorithm);
signature = _ecdsaKey.SignData(data.ToArray(), hashAlgorithm);
}
else if (algorithm.Contains("PSS", StringComparison.OrdinalIgnoreCase))
{
if (_rsaKey == null)
{
throw new InvalidOperationException("No RSA key available.");
}
var hashAlgorithm = GetHashAlgorithmName(algorithm);
signature = _rsaKey.SignData(data.ToArray(), hashAlgorithm, RSASignaturePadding.Pss);
}
else
{
if (_rsaKey == null)
{
throw new InvalidOperationException("No RSA key available.");
}
var hashAlgorithm = GetHashAlgorithmName(algorithm);
signature = _rsaKey.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.StartsWith("ECDSA", StringComparison.OrdinalIgnoreCase))
{
if (_ecdsaKey == null)
{
throw new InvalidOperationException("No ECDSA key available.");
}
var hashAlgorithm = GetHashAlgorithmName(algorithm);
isValid = _ecdsaKey.VerifyData(data.ToArray(), signature.ToArray(), hashAlgorithm);
}
else if (algorithm.Contains("PSS", StringComparison.OrdinalIgnoreCase))
{
if (_rsaKey == null)
{
throw new InvalidOperationException("No RSA key available.");
}
var hashAlgorithm = GetHashAlgorithmName(algorithm);
isValid = _rsaKey.VerifyData(data.ToArray(), signature.ToArray(), hashAlgorithm, RSASignaturePadding.Pss);
}
else
{
if (_rsaKey == null)
{
throw new InvalidOperationException("No RSA key available.");
}
var hashAlgorithm = GetHashAlgorithmName(algorithm);
isValid = _rsaKey.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();
var algorithm = options.Algorithm;
var keyBytes = GetSymmetricKey(options.KeyId, algorithm);
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(AesGcmEncrypt(data.ToArray(), keyBytes, options.Iv, options.Aad));
}
else
{
return Task.FromResult(AesCbcEncrypt(data.ToArray(), keyBytes, options.Iv));
}
}
/// <inheritdoc />
public override Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> data, CryptoDecryptOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
var algorithm = options.Algorithm;
var keyBytes = GetSymmetricKey(options.KeyId, algorithm);
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(AesGcmDecrypt(data.ToArray(), keyBytes, options.Iv, options.Aad));
}
else
{
return Task.FromResult(AesCbcDecrypt(data.ToArray(), keyBytes, options.Iv));
}
}
/// <inheritdoc />
public override Task<byte[]> HashAsync(ReadOnlyMemory<byte> data, string algorithm, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
byte[] hash;
if (algorithm.Contains("HMAC", StringComparison.OrdinalIgnoreCase))
{
// For HMAC, we need a key - use a default for demonstration
var hmacKey = System.Text.Encoding.UTF8.GetBytes("default-hmac-key");
hash = algorithm.ToUpperInvariant() switch
{
var a when a.Contains("512") => HMACSHA512.HashData(hmacKey, data.Span),
var a when a.Contains("384") => HMACSHA384.HashData(hmacKey, data.Span),
_ => HMACSHA256.HashData(hmacKey, data.Span)
};
}
else
{
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 {Algorithm} hash of {DataLength} bytes", algorithm, data.Length);
return Task.FromResult(hash);
}
/// <inheritdoc />
public override ValueTask DisposeAsync()
{
_rsaKey?.Dispose();
_ecdsaKey?.Dispose();
_rsaKey = null;
_ecdsaKey = null;
State = PluginLifecycleState.Stopped;
return ValueTask.CompletedTask;
}
private static bool IsFipsModeEnabled()
{
// Check if FIPS mode is enabled at OS level
try
{
// This will throw if FIPS mode is required but algorithm is not FIPS-compliant
using var _ = SHA256.Create();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
private static ECCurve GetECCurve(string curveName)
{
return curveName.ToUpperInvariant() switch
{
"P-521" or "SECP521R1" => ECCurve.NamedCurves.nistP521,
"P-384" or "SECP384R1" => ECCurve.NamedCurves.nistP384,
_ => ECCurve.NamedCurves.nistP256
};
}
private static HashAlgorithmName GetHashAlgorithmName(string algorithm)
{
return algorithm.ToUpperInvariant() switch
{
var a when a.Contains("512") => HashAlgorithmName.SHA512,
var a when a.Contains("384") => HashAlgorithmName.SHA384,
_ => HashAlgorithmName.SHA256
};
}
private byte[] GetSymmetricKey(string keyId, string algorithm)
{
// Determine key size from algorithm
var keySize = algorithm.Contains("256") ? 32 : 16;
// Derive key from key ID using SHA-256
var hash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(keyId));
return hash.Take(keySize).ToArray();
}
private static byte[] AesCbcEncrypt(byte[] data, byte[] key, byte[]? iv)
{
using var aes = Aes.Create();
aes.Key = key;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
iv ??= aes.IV;
aes.IV = iv;
using var encryptor = aes.CreateEncryptor();
var encrypted = encryptor.TransformFinalBlock(data, 0, data.Length);
// Prepend IV to ciphertext
var result = new byte[iv.Length + encrypted.Length];
Array.Copy(iv, 0, result, 0, iv.Length);
Array.Copy(encrypted, 0, result, iv.Length, encrypted.Length);
return result;
}
private static byte[] AesCbcDecrypt(byte[] data, byte[] key, byte[]? iv)
{
using var aes = Aes.Create();
aes.Key = key;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
if (iv == null)
{
iv = data.Take(16).ToArray();
data = data.Skip(16).ToArray();
}
aes.IV = iv;
using var decryptor = aes.CreateDecryptor();
return decryptor.TransformFinalBlock(data, 0, data.Length);
}
private static byte[] AesGcmEncrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
{
iv ??= RandomNumberGenerator.GetBytes(12);
var tag = new byte[16];
using var aesGcm = new AesGcm(key, 16);
var ciphertext = new byte[data.Length];
aesGcm.Encrypt(iv, data, ciphertext, tag, aad);
// Format: IV (12) + Tag (16) + Ciphertext
var result = new byte[iv.Length + tag.Length + ciphertext.Length];
Array.Copy(iv, 0, result, 0, iv.Length);
Array.Copy(tag, 0, result, iv.Length, tag.Length);
Array.Copy(ciphertext, 0, result, iv.Length + tag.Length, ciphertext.Length);
return result;
}
private static byte[] AesGcmDecrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
{
if (iv == null)
{
iv = data.Take(12).ToArray();
var tag = data.Skip(12).Take(16).ToArray();
var ciphertext = data.Skip(28).ToArray();
using var aesGcm = new AesGcm(key, 16);
var plaintext = new byte[ciphertext.Length];
aesGcm.Decrypt(iv, ciphertext, tag, plaintext, aad);
return plaintext;
}
else
{
var tag = data.Take(16).ToArray();
var ciphertext = data.Skip(16).ToArray();
using var aesGcm = new AesGcm(key, 16);
var plaintext = new byte[ciphertext.Length];
aesGcm.Decrypt(iv, ciphertext, tag, plaintext, aad);
return plaintext;
}
}
}
/// <summary>
/// Configuration options for FIPS cryptography plugin.
/// </summary>
public sealed class FipsOptions
{
/// <summary>
/// Require FIPS mode to be enabled at OS level.
/// </summary>
public bool RequireFipsMode { get; init; }
/// <summary>
/// RSA key in XML format.
/// </summary>
public string? RsaKeyXml { get; init; }
/// <summary>
/// RSA key size in bits (2048, 3072, 4096).
/// </summary>
public int RsaKeySize { get; init; } = 2048;
/// <summary>
/// ECDSA key in XML format.
/// </summary>
public string? EcdsaKeyXml { get; init; }
/// <summary>
/// ECDSA curve name (P-256, P-384, P-521).
/// </summary>
public string EcdsaCurve { get; init; } = "P-256";
/// <summary>
/// Generate keys on initialization if not configured.
/// </summary>
public bool GenerateKeysOnInit { get; init; } = true;
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Cryptography.Plugin\StellaOps.Cryptography.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="plugin.yaml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,64 @@
plugin:
id: com.stellaops.crypto.fips
name: FIPS 140-2 Cryptography Provider
version: 1.0.0
vendor: Stella Ops
description: US FIPS 140-2 compliant cryptographic algorithms
license: AGPL-3.0-or-later
entryPoint: StellaOps.Cryptography.Plugin.Fips.FipsPlugin
minPlatformVersion: 1.0.0
capabilities:
- type: crypto
id: fips
algorithms:
- RSA-SHA256
- RSA-SHA384
- RSA-SHA512
- RSA-PSS-SHA256
- RSA-PSS-SHA384
- RSA-PSS-SHA512
- ECDSA-P256-SHA256
- ECDSA-P384-SHA384
- ECDSA-P521-SHA512
- AES-128-CBC
- AES-256-CBC
- AES-128-GCM
- AES-256-GCM
- SHA-256
- SHA-384
- SHA-512
- HMAC-SHA256
- HMAC-SHA384
- HMAC-SHA512
configSchema:
type: object
properties:
requireFipsMode:
type: boolean
default: false
description: Require FIPS mode to be enabled at OS level
rsaKeyXml:
type: string
description: RSA key in XML format
rsaKeySize:
type: integer
enum: [2048, 3072, 4096]
default: 2048
description: RSA key size in bits
ecdsaKeyXml:
type: string
description: ECDSA key in XML format
ecdsaCurve:
type: string
enum: [P-256, P-384, P-521]
default: P-256
description: ECDSA curve name
generateKeysOnInit:
type: boolean
default: true
description: Generate keys on initialization
required: []

View File

@@ -0,0 +1,342 @@
namespace StellaOps.Cryptography.Plugin.Gost;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Math.EC;
using Org.BouncyCastle.Security;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
/// <summary>
/// GOST cryptography plugin providing Russian Federal cryptographic standards.
/// Implements GOST R 34.10-2012 (signatures) and GOST R 34.11-2012 (hashes).
/// </summary>
public sealed class GostPlugin : CryptoPluginBase
{
private GostOptions? _options;
private AsymmetricCipherKeyPair? _keyPair;
private readonly SecureRandom _random = new();
/// <inheritdoc />
public override PluginInfo Info => new(
Id: "com.stellaops.crypto.gost",
Name: "GOST Cryptography Provider",
Version: "1.0.0",
Vendor: "Stella Ops",
Description: "Russian GOST R 34.10-2012 and R 34.11-2012 cryptographic algorithms",
LicenseId: "AGPL-3.0-or-later");
/// <inheritdoc />
public override IReadOnlyList<string> SupportedAlgorithms => new[]
{
"GOST-R34.10-2012-256",
"GOST-R34.10-2012-512",
"GOST-R34.11-2012-256",
"GOST-R34.11-2012-512",
"GOST-28147-89"
};
/// <inheritdoc />
protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct)
{
_options = context.Configuration.Bind<GostOptions>() ?? new GostOptions();
// Generate or load key pair if configured
if (!string.IsNullOrEmpty(_options.PrivateKeyBase64))
{
// Load existing key - implementation depends on key format
Context?.Logger.Info("GOST provider initialized with configured key");
}
else if (_options.GenerateKeyOnInit)
{
// Generate new GOST-R34.10-2012-256 key pair
_keyPair = GenerateGost2012KeyPair(_options.KeySize);
Context?.Logger.Info("GOST provider initialized with generated {KeySize}-bit key", _options.KeySize);
}
return Task.CompletedTask;
}
/// <inheritdoc />
public override bool CanHandle(CryptoOperation operation, string algorithm)
{
return algorithm.StartsWith("GOST", StringComparison.OrdinalIgnoreCase) &&
SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
if (_keyPair == null)
{
throw new InvalidOperationException("No signing key available. Configure a key or enable GenerateKeyOnInit.");
}
var algorithm = options.Algorithm;
var digestBits = algorithm.Contains("512") ? 512 : 256;
// Create GOST R 34.11-2012 digest
var digest = CreateGost2012Digest(digestBits);
// Create GOST R 34.10-2012 signer
var signer = new ECGost3410Signer();
signer.Init(true, _keyPair.Private);
// Hash the data
digest.BlockUpdate(data.Span.ToArray(), 0, data.Length);
var hash = new byte[digest.GetDigestSize()];
digest.DoFinal(hash, 0);
// Sign the hash
var signature = signer.GenerateSignature(hash);
var sigBytes = EncodeSignature(signature);
Context?.Logger.Debug("Signed {DataLength} bytes with {Algorithm}", data.Length, algorithm);
return Task.FromResult(sigBytes);
}
/// <inheritdoc />
public override Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
if (_keyPair == null)
{
throw new InvalidOperationException("No verification key available.");
}
var algorithm = options.Algorithm;
var digestBits = algorithm.Contains("512") ? 512 : 256;
// Create GOST R 34.11-2012 digest
var digest = CreateGost2012Digest(digestBits);
// Create GOST R 34.10-2012 verifier
var verifier = new ECGost3410Signer();
verifier.Init(false, _keyPair.Public);
// Hash the data
digest.BlockUpdate(data.Span.ToArray(), 0, data.Length);
var hash = new byte[digest.GetDigestSize()];
digest.DoFinal(hash, 0);
// Decode and verify signature
var sigComponents = DecodeSignature(signature.ToArray());
var isValid = verifier.VerifySignature(hash, sigComponents[0], sigComponents[1]);
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 (!options.Algorithm.Contains("28147", StringComparison.Ordinal))
{
throw new NotSupportedException($"Encryption algorithm {options.Algorithm} not supported. Use GOST-28147-89.");
}
// GOST 28147-89 block cipher encryption
var engine = new Gost28147Engine();
var keyBytes = GetEncryptionKey(options.KeyId);
engine.Init(true, new KeyParameter(keyBytes));
var encrypted = ProcessBlocks(engine, data.ToArray());
Context?.Logger.Debug("Encrypted {DataLength} bytes with GOST-28147-89", data.Length);
return Task.FromResult(encrypted);
}
/// <inheritdoc />
public override Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> data, CryptoDecryptOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
// GOST 28147-89 block cipher decryption
var engine = new Gost28147Engine();
var keyBytes = GetEncryptionKey(options.KeyId);
engine.Init(false, new KeyParameter(keyBytes));
var decrypted = ProcessBlocks(engine, data.ToArray());
Context?.Logger.Debug("Decrypted {DataLength} bytes with GOST-28147-89", data.Length);
return Task.FromResult(decrypted);
}
/// <inheritdoc />
public override Task<byte[]> HashAsync(ReadOnlyMemory<byte> data, string algorithm, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
var digestBits = algorithm.Contains("512") ? 512 : 256;
var digest = CreateGost2012Digest(digestBits);
digest.BlockUpdate(data.Span.ToArray(), 0, data.Length);
var hash = new byte[digest.GetDigestSize()];
digest.DoFinal(hash, 0);
Context?.Logger.Debug("Computed {Algorithm} hash of {DataLength} bytes", algorithm, data.Length);
return Task.FromResult(hash);
}
/// <inheritdoc />
public override ValueTask DisposeAsync()
{
_keyPair = null;
State = PluginLifecycleState.Stopped;
return ValueTask.CompletedTask;
}
private static IDigest CreateGost2012Digest(int bits)
{
return bits switch
{
256 => new Gost3411_2012_256Digest(),
512 => new Gost3411_2012_512Digest(),
_ => throw new ArgumentException($"Unsupported digest size: {bits}")
};
}
private AsymmetricCipherKeyPair GenerateGost2012KeyPair(int keySize)
{
// GOST R 34.10-2012 uses specific elliptic curve parameters
// For 256-bit: id-tc26-gost-3410-2012-256-paramSetA
// For 512-bit: id-tc26-gost-3410-2012-512-paramSetA
var generator = new ECKeyPairGenerator("ECGOST3410");
var domainParams = GetGost2012DomainParameters(keySize);
var keyGenParams = new ECKeyGenerationParameters(domainParams, _random);
generator.Init(keyGenParams);
return generator.GenerateKeyPair();
}
private static ECDomainParameters GetGost2012DomainParameters(int keySize)
{
// Simplified: use predefined GOST parameters
// In production, load from OID: 1.2.643.7.1.2.1.1.1 (256-bit) or 1.2.643.7.1.2.1.2.1 (512-bit)
if (keySize == 256)
{
// id-tc26-gost-3410-2012-256-paramSetA
var p = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD97", 16);
var a = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD94", 16);
var b = new BigInteger("00000000000000000000000000000000000000000000000000000000000000A6", 16);
var n = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C611070995AD10045841B09B761B893", 16);
var h = BigInteger.One;
var gx = new BigInteger("0000000000000000000000000000000000000000000000000000000000000001", 16);
var gy = new BigInteger("8D91E471E0989CDA27DF505A453F2B7635294F2DDF23E3B122ACC99C9E9F1E14", 16);
var curve = new FpCurve(p, a, b, n, h);
var g = curve.CreatePoint(gx, gy);
return new ECDomainParameters(curve, g, n, h);
}
else
{
// id-tc26-gost-3410-2012-512-paramSetA (simplified)
throw new NotImplementedException("512-bit GOST parameters not implemented in this example");
}
}
private static byte[] EncodeSignature(BigInteger[] signature)
{
// Encode r and s as fixed-length byte arrays concatenated
var r = signature[0].ToByteArrayUnsigned();
var s = signature[1].ToByteArrayUnsigned();
// Pad to 32 bytes each for 256-bit
var encoded = new byte[64];
Array.Copy(r, 0, encoded, 32 - r.Length, r.Length);
Array.Copy(s, 0, encoded, 64 - s.Length, s.Length);
return encoded;
}
private static BigInteger[] DecodeSignature(byte[] signature)
{
var r = new BigInteger(1, signature.Take(32).ToArray());
var s = new BigInteger(1, signature.Skip(32).Take(32).ToArray());
return new[] { r, s };
}
private byte[] GetEncryptionKey(string keyId)
{
// In production, retrieve from secure key store
// For now, derive a key from the key ID
var digest = new Gost3411_2012_256Digest();
var keyIdBytes = System.Text.Encoding.UTF8.GetBytes(keyId);
digest.BlockUpdate(keyIdBytes, 0, keyIdBytes.Length);
var key = new byte[32];
digest.DoFinal(key, 0);
return key;
}
private static byte[] ProcessBlocks(IBlockCipher engine, byte[] data)
{
var blockSize = engine.GetBlockSize();
var paddedLength = ((data.Length + blockSize - 1) / blockSize) * blockSize;
var padded = new byte[paddedLength];
Array.Copy(data, padded, data.Length);
var output = new byte[paddedLength];
for (var i = 0; i < paddedLength; i += blockSize)
{
engine.ProcessBlock(padded, i, output, i);
}
return output;
}
}
/// <summary>
/// Configuration options for GOST cryptography plugin.
/// </summary>
public sealed class GostOptions
{
/// <summary>
/// Path to key store file.
/// </summary>
public string? KeyStorePath { get; init; }
/// <summary>
/// Default key identifier for signing operations.
/// </summary>
public string? DefaultKeyId { get; init; }
/// <summary>
/// Base64-encoded private key (if not using key store).
/// </summary>
public string? PrivateKeyBase64 { get; init; }
/// <summary>
/// Generate a new key pair on initialization if no key is configured.
/// </summary>
public bool GenerateKeyOnInit { get; init; } = true;
/// <summary>
/// Key size in bits (256 or 512).
/// </summary>
public int KeySize { get; init; } = 256;
}

View File

@@ -0,0 +1,23 @@
<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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Cryptography.Plugin\StellaOps.Cryptography.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="plugin.yaml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,44 @@
plugin:
id: com.stellaops.crypto.gost
name: GOST Cryptography Provider
version: 1.0.0
vendor: Stella Ops
description: Russian GOST R 34.10-2012 and R 34.11-2012 cryptographic algorithms
license: AGPL-3.0-or-later
entryPoint: StellaOps.Cryptography.Plugin.Gost.GostPlugin
minPlatformVersion: 1.0.0
capabilities:
- type: crypto
id: gost
algorithms:
- GOST-R34.10-2012-256
- GOST-R34.10-2012-512
- GOST-R34.11-2012-256
- GOST-R34.11-2012-512
- GOST-28147-89
configSchema:
type: object
properties:
keyStorePath:
type: string
description: Path to GOST key store
defaultKeyId:
type: string
description: Default key identifier for signing
privateKeyBase64:
type: string
description: Base64-encoded private key
generateKeyOnInit:
type: boolean
default: true
description: Generate new key pair on initialization if no key configured
keySize:
type: integer
enum: [256, 512]
default: 256
description: Key size in bits
required: []

View File

@@ -0,0 +1,464 @@
namespace StellaOps.Cryptography.Plugin.Hsm;
using System.Security.Cryptography;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
/// <summary>
/// Hardware Security Module (HSM) cryptography plugin.
/// Provides integration with PKCS#11 compliant HSMs for secure key storage and operations.
/// </summary>
public sealed class HsmPlugin : CryptoPluginBase
{
private HsmOptions? _options;
private IHsmClient? _hsmClient;
private bool _isConnected;
/// <inheritdoc />
public override PluginInfo Info => new(
Id: "com.stellaops.crypto.hsm",
Name: "HSM Cryptography Provider",
Version: "1.0.0",
Vendor: "Stella Ops",
Description: "Hardware Security Module integration via PKCS#11",
LicenseId: "AGPL-3.0-or-later");
/// <inheritdoc />
public override IReadOnlyList<string> SupportedAlgorithms => new[]
{
"HSM-RSA-SHA256",
"HSM-RSA-SHA384",
"HSM-RSA-SHA512",
"HSM-RSA-PSS-SHA256",
"HSM-ECDSA-P256",
"HSM-ECDSA-P384",
"HSM-AES-128-GCM",
"HSM-AES-256-GCM"
};
/// <inheritdoc />
protected override async Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct)
{
_options = context.Configuration.Bind<HsmOptions>() ?? new HsmOptions();
if (string.IsNullOrEmpty(_options.LibraryPath))
{
Context?.Logger.Warning("HSM provider initialized in simulation mode (no library configured)");
_hsmClient = new SimulatedHsmClient();
}
else
{
_hsmClient = new Pkcs11HsmClient(_options.LibraryPath, Context?.Logger);
}
await _hsmClient.ConnectAsync(
_options.SlotId,
_options.Pin,
ct);
_isConnected = true;
Context?.Logger.Info("HSM provider connected to slot {SlotId}", _options.SlotId);
}
/// <inheritdoc />
public override async Task<HealthCheckResult> HealthCheckAsync(CancellationToken ct)
{
if (!_isConnected || _hsmClient == null)
{
return HealthCheckResult.Unhealthy("HSM not connected");
}
try
{
var isHealthy = await _hsmClient.PingAsync(ct);
if (!isHealthy)
{
return HealthCheckResult.Degraded("HSM responding slowly");
}
return HealthCheckResult.Healthy().WithDetails(new Dictionary<string, object>
{
["slot"] = _options?.SlotId ?? 0,
["library"] = _options?.LibraryPath ?? "simulated"
});
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(ex);
}
}
/// <inheritdoc />
public override bool CanHandle(CryptoOperation operation, string algorithm)
{
return algorithm.StartsWith("HSM-", StringComparison.OrdinalIgnoreCase) &&
SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public override async Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
{
EnsureActive();
EnsureConnected();
ct.ThrowIfCancellationRequested();
var keyId = options.KeyId;
var mechanism = GetSigningMechanism(options.Algorithm);
var signature = await _hsmClient!.SignAsync(keyId, data.ToArray(), mechanism, ct);
Context?.Logger.Debug("HSM signed {DataLength} bytes with key {KeyId}", data.Length, keyId);
return signature;
}
/// <inheritdoc />
public override async Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
{
EnsureActive();
EnsureConnected();
ct.ThrowIfCancellationRequested();
var keyId = options.KeyId;
var mechanism = GetSigningMechanism(options.Algorithm);
var isValid = await _hsmClient!.VerifyAsync(keyId, data.ToArray(), signature.ToArray(), mechanism, ct);
Context?.Logger.Debug("HSM verified signature with key {KeyId}: {IsValid}", keyId, isValid);
return isValid;
}
/// <inheritdoc />
public override async Task<byte[]> EncryptAsync(ReadOnlyMemory<byte> data, CryptoEncryptOptions options, CancellationToken ct)
{
EnsureActive();
EnsureConnected();
ct.ThrowIfCancellationRequested();
var keyId = options.KeyId;
var mechanism = GetEncryptionMechanism(options.Algorithm);
var encrypted = await _hsmClient!.EncryptAsync(keyId, data.ToArray(), mechanism, options.Iv, ct);
Context?.Logger.Debug("HSM encrypted {DataLength} bytes with key {KeyId}", data.Length, keyId);
return encrypted;
}
/// <inheritdoc />
public override async Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> data, CryptoDecryptOptions options, CancellationToken ct)
{
EnsureActive();
EnsureConnected();
ct.ThrowIfCancellationRequested();
var keyId = options.KeyId;
var mechanism = GetEncryptionMechanism(options.Algorithm);
var decrypted = await _hsmClient!.DecryptAsync(keyId, data.ToArray(), mechanism, options.Iv, ct);
Context?.Logger.Debug("HSM decrypted {DataLength} bytes with key {KeyId}", data.Length, keyId);
return decrypted;
}
/// <inheritdoc />
public override Task<byte[]> HashAsync(ReadOnlyMemory<byte> data, string algorithm, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
// Hashing can be done locally - no need to use HSM
var 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 async ValueTask DisposeAsync()
{
if (_hsmClient != null)
{
await _hsmClient.DisconnectAsync(CancellationToken.None);
_hsmClient = null;
}
_isConnected = false;
State = PluginLifecycleState.Stopped;
}
private void EnsureConnected()
{
if (!_isConnected || _hsmClient == null)
{
throw new InvalidOperationException("HSM is not connected.");
}
}
private static HsmMechanism GetSigningMechanism(string algorithm)
{
return algorithm.ToUpperInvariant() switch
{
var a when a.Contains("PSS") && a.Contains("256") => HsmMechanism.RsaPssSha256,
var a when a.Contains("RSA") && a.Contains("512") => HsmMechanism.RsaSha512,
var a when a.Contains("RSA") && a.Contains("384") => HsmMechanism.RsaSha384,
var a when a.Contains("RSA") => HsmMechanism.RsaSha256,
var a when a.Contains("ECDSA") && a.Contains("384") => HsmMechanism.EcdsaP384,
var a when a.Contains("ECDSA") => HsmMechanism.EcdsaP256,
_ => throw new NotSupportedException($"Signing mechanism not supported: {algorithm}")
};
}
private static HsmMechanism GetEncryptionMechanism(string algorithm)
{
return algorithm.ToUpperInvariant() switch
{
var a when a.Contains("256-GCM") => HsmMechanism.Aes256Gcm,
var a when a.Contains("128-GCM") => HsmMechanism.Aes128Gcm,
_ => throw new NotSupportedException($"Encryption mechanism not supported: {algorithm}")
};
}
}
/// <summary>
/// HSM mechanism identifiers.
/// </summary>
public enum HsmMechanism
{
RsaSha256,
RsaSha384,
RsaSha512,
RsaPssSha256,
EcdsaP256,
EcdsaP384,
Aes128Gcm,
Aes256Gcm
}
/// <summary>
/// Interface for HSM client implementations.
/// </summary>
public interface IHsmClient
{
Task ConnectAsync(int slotId, string? pin, CancellationToken ct);
Task DisconnectAsync(CancellationToken ct);
Task<bool> PingAsync(CancellationToken ct);
Task<byte[]> SignAsync(string keyId, byte[] data, HsmMechanism mechanism, CancellationToken ct);
Task<bool> VerifyAsync(string keyId, byte[] data, byte[] signature, HsmMechanism mechanism, CancellationToken ct);
Task<byte[]> EncryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct);
Task<byte[]> DecryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct);
}
/// <summary>
/// Simulated HSM client for testing without actual HSM hardware.
/// </summary>
internal sealed class SimulatedHsmClient : IHsmClient
{
private readonly Dictionary<string, RSA> _rsaKeys = new();
private readonly Dictionary<string, byte[]> _aesKeys = new();
private bool _connected;
public Task ConnectAsync(int slotId, string? pin, CancellationToken ct)
{
_connected = true;
// Generate some default keys for simulation
_rsaKeys["default"] = RSA.Create(2048);
_aesKeys["default"] = RandomNumberGenerator.GetBytes(32);
return Task.CompletedTask;
}
public Task DisconnectAsync(CancellationToken ct)
{
foreach (var key in _rsaKeys.Values)
{
key.Dispose();
}
_rsaKeys.Clear();
_aesKeys.Clear();
_connected = false;
return Task.CompletedTask;
}
public Task<bool> PingAsync(CancellationToken ct)
{
return Task.FromResult(_connected);
}
public Task<byte[]> SignAsync(string keyId, byte[] data, HsmMechanism mechanism, CancellationToken ct)
{
if (!_rsaKeys.TryGetValue(keyId, out var rsa))
{
rsa = _rsaKeys["default"];
}
var (hashAlg, padding) = GetRsaParameters(mechanism);
var signature = rsa.SignData(data, hashAlg, padding);
return Task.FromResult(signature);
}
public Task<bool> VerifyAsync(string keyId, byte[] data, byte[] signature, HsmMechanism mechanism, CancellationToken ct)
{
if (!_rsaKeys.TryGetValue(keyId, out var rsa))
{
rsa = _rsaKeys["default"];
}
var (hashAlg, padding) = GetRsaParameters(mechanism);
var isValid = rsa.VerifyData(data, signature, hashAlg, padding);
return Task.FromResult(isValid);
}
public Task<byte[]> EncryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct)
{
if (!_aesKeys.TryGetValue(keyId, out var key))
{
key = _aesKeys["default"];
}
var keyToUse = mechanism == HsmMechanism.Aes128Gcm ? key.Take(16).ToArray() : key;
iv ??= RandomNumberGenerator.GetBytes(12);
var tag = new byte[16];
using var aesGcm = new AesGcm(keyToUse, 16);
var ciphertext = new byte[data.Length];
aesGcm.Encrypt(iv, data, ciphertext, tag);
var result = new byte[iv.Length + tag.Length + ciphertext.Length];
Array.Copy(iv, 0, result, 0, iv.Length);
Array.Copy(tag, 0, result, iv.Length, tag.Length);
Array.Copy(ciphertext, 0, result, iv.Length + tag.Length, ciphertext.Length);
return Task.FromResult(result);
}
public Task<byte[]> DecryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct)
{
if (!_aesKeys.TryGetValue(keyId, out var key))
{
key = _aesKeys["default"];
}
var keyToUse = mechanism == HsmMechanism.Aes128Gcm ? key.Take(16).ToArray() : key;
iv ??= data.Take(12).ToArray();
var tag = data.Skip(12).Take(16).ToArray();
var ciphertext = data.Skip(28).ToArray();
using var aesGcm = new AesGcm(keyToUse, 16);
var plaintext = new byte[ciphertext.Length];
aesGcm.Decrypt(iv, ciphertext, tag, plaintext);
return Task.FromResult(plaintext);
}
private static (HashAlgorithmName, RSASignaturePadding) GetRsaParameters(HsmMechanism mechanism)
{
return mechanism switch
{
HsmMechanism.RsaPssSha256 => (HashAlgorithmName.SHA256, RSASignaturePadding.Pss),
HsmMechanism.RsaSha512 => (HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1),
HsmMechanism.RsaSha384 => (HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1),
_ => (HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
};
}
}
/// <summary>
/// PKCS#11 HSM client implementation stub.
/// In production, this would use a PKCS#11 library like PKCS11Interop.
/// </summary>
internal sealed class Pkcs11HsmClient : IHsmClient
{
private readonly string _libraryPath;
private readonly IPluginLogger? _logger;
public Pkcs11HsmClient(string libraryPath, IPluginLogger? logger)
{
_libraryPath = libraryPath;
_logger = logger;
}
public Task ConnectAsync(int slotId, string? pin, CancellationToken ct)
{
_logger?.Info("Connecting to HSM via PKCS#11 library: {LibraryPath}", _libraryPath);
// In production: Load PKCS#11 library, open session, login
throw new NotImplementedException(
"PKCS#11 implementation requires Net.Pkcs11Interop package. " +
"Use simulation mode for testing.");
}
public Task DisconnectAsync(CancellationToken ct)
{
throw new NotImplementedException();
}
public Task<bool> PingAsync(CancellationToken ct)
{
throw new NotImplementedException();
}
public Task<byte[]> SignAsync(string keyId, byte[] data, HsmMechanism mechanism, CancellationToken ct)
{
throw new NotImplementedException();
}
public Task<bool> VerifyAsync(string keyId, byte[] data, byte[] signature, HsmMechanism mechanism, CancellationToken ct)
{
throw new NotImplementedException();
}
public Task<byte[]> EncryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct)
{
throw new NotImplementedException();
}
public Task<byte[]> DecryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Configuration options for HSM cryptography plugin.
/// </summary>
public sealed class HsmOptions
{
/// <summary>
/// Path to PKCS#11 library (.so/.dll).
/// Leave empty for simulation mode.
/// </summary>
public string? LibraryPath { get; init; }
/// <summary>
/// HSM slot identifier.
/// </summary>
public int SlotId { get; init; }
/// <summary>
/// PIN for HSM authentication.
/// </summary>
public string? Pin { get; init; }
/// <summary>
/// Token label for identifying the HSM.
/// </summary>
public string? TokenLabel { get; init; }
/// <summary>
/// Connection timeout in seconds.
/// </summary>
public int ConnectionTimeoutSeconds { get; init; } = 30;
/// <summary>
/// Whether to use read-only session (no key generation/modification).
/// </summary>
public bool ReadOnlySession { get; init; } = true;
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Cryptography.Plugin\StellaOps.Cryptography.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="plugin.yaml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
plugin:
id: com.stellaops.crypto.hsm
name: HSM Cryptography Provider
version: 1.0.0
vendor: Stella Ops
description: Hardware Security Module integration via PKCS#11
license: AGPL-3.0-or-later
entryPoint: StellaOps.Cryptography.Plugin.Hsm.HsmPlugin
minPlatformVersion: 1.0.0
capabilities:
- type: crypto
id: hsm
algorithms:
- HSM-RSA-SHA256
- HSM-RSA-SHA384
- HSM-RSA-SHA512
- HSM-RSA-PSS-SHA256
- HSM-ECDSA-P256
- HSM-ECDSA-P384
- HSM-AES-128-GCM
- HSM-AES-256-GCM
configSchema:
type: object
properties:
libraryPath:
type: string
description: Path to PKCS#11 library (.so/.dll). Leave empty for simulation mode.
slotId:
type: integer
default: 0
description: HSM slot identifier
pin:
type: string
description: PIN for HSM authentication
tokenLabel:
type: string
description: Token label for identifying the HSM
connectionTimeoutSeconds:
type: integer
default: 30
description: Connection timeout in seconds
readOnlySession:
type: boolean
default: true
description: Use read-only session (no key generation/modification)
required: []

View File

@@ -0,0 +1,364 @@
namespace StellaOps.Cryptography.Plugin.Sm;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Math.EC;
using Org.BouncyCastle.Security;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
/// <summary>
/// Chinese national cryptographic standards plugin.
/// Implements SM2 (signatures), SM3 (hash), and SM4 (symmetric encryption).
/// </summary>
public sealed class SmPlugin : CryptoPluginBase
{
private SmOptions? _options;
private AsymmetricCipherKeyPair? _keyPair;
private readonly SecureRandom _random = new();
/// <inheritdoc />
public override PluginInfo Info => new(
Id: "com.stellaops.crypto.sm",
Name: "Chinese SM Cryptography Provider",
Version: "1.0.0",
Vendor: "Stella Ops",
Description: "Chinese national cryptographic standards SM2/SM3/SM4 (GM/T 0003-0004)",
LicenseId: "AGPL-3.0-or-later");
/// <inheritdoc />
public override IReadOnlyList<string> SupportedAlgorithms => new[]
{
"SM2-SM3",
"SM2-SHA256",
"SM3",
"SM4-CBC",
"SM4-ECB",
"SM4-GCM"
};
/// <inheritdoc />
protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct)
{
_options = context.Configuration.Bind<SmOptions>() ?? new SmOptions();
if (!string.IsNullOrEmpty(_options.PrivateKeyHex))
{
LoadKeyFromHex(_options.PrivateKeyHex);
Context?.Logger.Info("SM provider initialized with configured key");
}
else if (_options.GenerateKeyOnInit)
{
_keyPair = GenerateSm2KeyPair();
Context?.Logger.Info("SM provider initialized with generated key pair");
}
return Task.CompletedTask;
}
/// <inheritdoc />
public override bool CanHandle(CryptoOperation operation, string algorithm)
{
return algorithm.StartsWith("SM", StringComparison.OrdinalIgnoreCase) &&
SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
if (_keyPair == null)
{
throw new InvalidOperationException("No signing key available.");
}
// SM2 signature with SM3 digest
var signer = new SM2Signer();
signer.Init(true, _keyPair.Private);
signer.BlockUpdate(data.Span.ToArray(), 0, data.Length);
var signature = signer.GenerateSignature();
Context?.Logger.Debug("Signed {DataLength} bytes with SM2", data.Length);
return Task.FromResult(signature);
}
/// <inheritdoc />
public override Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
if (_keyPair == null)
{
throw new InvalidOperationException("No verification key available.");
}
var verifier = new SM2Signer();
verifier.Init(false, _keyPair.Public);
verifier.BlockUpdate(data.Span.ToArray(), 0, data.Length);
var isValid = verifier.VerifySignature(signature.ToArray());
Context?.Logger.Debug("Verified SM2 signature: {IsValid}", isValid);
return Task.FromResult(isValid);
}
/// <inheritdoc />
public override Task<byte[]> EncryptAsync(ReadOnlyMemory<byte> data, CryptoEncryptOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
var algorithm = options.Algorithm;
var keyBytes = GetSymmetricKey(options.KeyId);
byte[] encrypted;
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
{
encrypted = Sm4GcmEncrypt(data.ToArray(), keyBytes, options.Iv, options.Aad);
}
else if (algorithm.Contains("CBC", StringComparison.OrdinalIgnoreCase))
{
encrypted = Sm4CbcEncrypt(data.ToArray(), keyBytes, options.Iv);
}
else
{
encrypted = Sm4EcbEncrypt(data.ToArray(), keyBytes);
}
Context?.Logger.Debug("Encrypted {DataLength} bytes with {Algorithm}", data.Length, algorithm);
return Task.FromResult(encrypted);
}
/// <inheritdoc />
public override Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> data, CryptoDecryptOptions options, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
var algorithm = options.Algorithm;
var keyBytes = GetSymmetricKey(options.KeyId);
byte[] decrypted;
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
{
decrypted = Sm4GcmDecrypt(data.ToArray(), keyBytes, options.Iv, options.Aad);
}
else if (algorithm.Contains("CBC", StringComparison.OrdinalIgnoreCase))
{
decrypted = Sm4CbcDecrypt(data.ToArray(), keyBytes, options.Iv);
}
else
{
decrypted = Sm4EcbDecrypt(data.ToArray(), keyBytes);
}
Context?.Logger.Debug("Decrypted {DataLength} bytes with {Algorithm}", data.Length, algorithm);
return Task.FromResult(decrypted);
}
/// <inheritdoc />
public override Task<byte[]> HashAsync(ReadOnlyMemory<byte> data, string algorithm, CancellationToken ct)
{
EnsureActive();
ct.ThrowIfCancellationRequested();
var digest = new SM3Digest();
digest.BlockUpdate(data.Span.ToArray(), 0, data.Length);
var hash = new byte[digest.GetDigestSize()];
digest.DoFinal(hash, 0);
Context?.Logger.Debug("Computed SM3 hash of {DataLength} bytes", data.Length);
return Task.FromResult(hash);
}
/// <inheritdoc />
public override ValueTask DisposeAsync()
{
_keyPair = null;
State = PluginLifecycleState.Stopped;
return ValueTask.CompletedTask;
}
private AsymmetricCipherKeyPair GenerateSm2KeyPair()
{
var domainParams = GetSm2DomainParameters();
var generator = new ECKeyPairGenerator();
generator.Init(new ECKeyGenerationParameters(domainParams, _random));
return generator.GenerateKeyPair();
}
private void LoadKeyFromHex(string privateKeyHex)
{
var d = new BigInteger(privateKeyHex, 16);
var domainParams = GetSm2DomainParameters();
var privateKey = new ECPrivateKeyParameters(d, domainParams);
var q = domainParams.G.Multiply(d);
var publicKey = new ECPublicKeyParameters(q, domainParams);
_keyPair = new AsymmetricCipherKeyPair(publicKey, privateKey);
}
private static ECDomainParameters GetSm2DomainParameters()
{
// SM2 recommended parameters (GM/T 0003.5-2012)
var p = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16);
var a = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", 16);
var b = new BigInteger("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", 16);
var n = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16);
var h = BigInteger.One;
var gx = new BigInteger("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16);
var gy = new BigInteger("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16);
var curve = new FpCurve(p, a, b, n, h);
var g = curve.CreatePoint(gx, gy);
return new ECDomainParameters(curve, g, n, h);
}
private byte[] GetSymmetricKey(string keyId)
{
// Derive 128-bit key from key ID using SM3
var digest = new SM3Digest();
var keyIdBytes = System.Text.Encoding.UTF8.GetBytes(keyId);
digest.BlockUpdate(keyIdBytes, 0, keyIdBytes.Length);
var hash = new byte[32];
digest.DoFinal(hash, 0);
return hash.Take(16).ToArray(); // SM4 uses 128-bit keys
}
private byte[] Sm4EcbEncrypt(byte[] data, byte[] key)
{
var engine = new SM4Engine();
engine.Init(true, new KeyParameter(key));
return ProcessBlocks(engine, data);
}
private byte[] Sm4EcbDecrypt(byte[] data, byte[] key)
{
var engine = new SM4Engine();
engine.Init(false, new KeyParameter(key));
return ProcessBlocks(engine, data);
}
private byte[] Sm4CbcEncrypt(byte[] data, byte[] key, byte[]? iv)
{
iv ??= GenerateIv(16);
var cipher = new CbcBlockCipher(new SM4Engine());
cipher.Init(true, new ParametersWithIV(new KeyParameter(key), iv));
var encrypted = ProcessBlocks(cipher, data);
// Prepend IV to ciphertext
var result = new byte[iv.Length + encrypted.Length];
Array.Copy(iv, 0, result, 0, iv.Length);
Array.Copy(encrypted, 0, result, iv.Length, encrypted.Length);
return result;
}
private byte[] Sm4CbcDecrypt(byte[] data, byte[] key, byte[]? iv)
{
if (iv == null)
{
// Extract IV from ciphertext
iv = data.Take(16).ToArray();
data = data.Skip(16).ToArray();
}
var cipher = new CbcBlockCipher(new SM4Engine());
cipher.Init(false, new ParametersWithIV(new KeyParameter(key), iv));
return ProcessBlocks(cipher, data);
}
private byte[] Sm4GcmEncrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
{
iv ??= GenerateIv(12);
var cipher = new GcmBlockCipher(new SM4Engine());
var parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad ?? Array.Empty<byte>());
cipher.Init(true, parameters);
var output = new byte[cipher.GetOutputSize(data.Length)];
var len = cipher.ProcessBytes(data, 0, data.Length, output, 0);
cipher.DoFinal(output, len);
// Prepend IV to ciphertext
var result = new byte[iv.Length + output.Length];
Array.Copy(iv, 0, result, 0, iv.Length);
Array.Copy(output, 0, result, iv.Length, output.Length);
return result;
}
private byte[] Sm4GcmDecrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
{
if (iv == null)
{
iv = data.Take(12).ToArray();
data = data.Skip(12).ToArray();
}
var cipher = new GcmBlockCipher(new SM4Engine());
var parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad ?? Array.Empty<byte>());
cipher.Init(false, parameters);
var output = new byte[cipher.GetOutputSize(data.Length)];
var len = cipher.ProcessBytes(data, 0, data.Length, output, 0);
cipher.DoFinal(output, len);
return output;
}
private static byte[] ProcessBlocks(IBlockCipher engine, byte[] data)
{
var blockSize = engine.GetBlockSize();
var paddedLength = ((data.Length + blockSize - 1) / blockSize) * blockSize;
var padded = new byte[paddedLength];
Array.Copy(data, padded, data.Length);
var output = new byte[paddedLength];
for (var i = 0; i < paddedLength; i += blockSize)
{
engine.ProcessBlock(padded, i, output, i);
}
return output;
}
private byte[] GenerateIv(int length)
{
var iv = new byte[length];
_random.NextBytes(iv);
return iv;
}
}
/// <summary>
/// Configuration options for SM cryptography plugin.
/// </summary>
public sealed class SmOptions
{
/// <summary>
/// Private key in hexadecimal format.
/// </summary>
public string? PrivateKeyHex { get; init; }
/// <summary>
/// Generate a new key pair on initialization if no key configured.
/// </summary>
public bool GenerateKeyOnInit { get; init; } = true;
/// <summary>
/// User identifier for SM2 signature (ZA computation).
/// </summary>
public string UserId { get; init; } = "1234567812345678";
}

View File

@@ -0,0 +1,23 @@
<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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Cryptography.Plugin\StellaOps.Cryptography.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="plugin.yaml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
plugin:
id: com.stellaops.crypto.sm
name: Chinese SM Cryptography Provider
version: 1.0.0
vendor: Stella Ops
description: Chinese national cryptographic standards SM2/SM3/SM4 (GM/T 0003-0004)
license: AGPL-3.0-or-later
entryPoint: StellaOps.Cryptography.Plugin.Sm.SmPlugin
minPlatformVersion: 1.0.0
capabilities:
- type: crypto
id: sm
algorithms:
- SM2-SM3
- SM2-SHA256
- SM3
- SM4-CBC
- SM4-ECB
- SM4-GCM
configSchema:
type: object
properties:
privateKeyHex:
type: string
description: Private key in hexadecimal format
generateKeyOnInit:
type: boolean
default: true
description: Generate new key pair on initialization
userId:
type: string
default: "1234567812345678"
description: User identifier for SM2 signature ZA computation
required: []

View File

@@ -0,0 +1,167 @@
namespace StellaOps.Cryptography.Plugin;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
/// <summary>
/// Base class for cryptographic plugins providing common functionality.
/// Implements IPlugin and ICryptoCapability interfaces.
/// </summary>
public abstract class CryptoPluginBase : IPlugin, ICryptoCapability
{
/// <summary>
/// Plugin context set during initialization.
/// </summary>
protected IPluginContext? Context { get; private set; }
/// <summary>
/// Plugin information including ID, name, version.
/// </summary>
public abstract PluginInfo Info { get; }
/// <summary>
/// Trust level for crypto plugins - always BuiltIn.
/// </summary>
public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn;
/// <summary>
/// Capabilities provided by this plugin.
/// </summary>
public PluginCapabilities Capabilities => PluginCapabilities.Crypto;
/// <summary>
/// Current lifecycle state.
/// </summary>
public PluginLifecycleState State { get; protected set; } = PluginLifecycleState.Discovered;
/// <summary>
/// List of algorithms supported by this crypto provider.
/// </summary>
public abstract IReadOnlyList<string> SupportedAlgorithms { get; }
/// <summary>
/// Initialize the crypto plugin.
/// </summary>
/// <param name="context">Plugin context with configuration and services.</param>
/// <param name="ct">Cancellation token.</param>
public async Task InitializeAsync(IPluginContext context, CancellationToken ct)
{
Context = context;
State = PluginLifecycleState.Initializing;
try
{
await InitializeCryptoServiceAsync(context, ct);
State = PluginLifecycleState.Active;
context.Logger.Info("{PluginName} initialized successfully", Info.Name);
}
catch (Exception ex)
{
State = PluginLifecycleState.Failed;
context.Logger.Error(ex, "Failed to initialize {PluginName}", Info.Name);
throw;
}
}
/// <summary>
/// Override to initialize the specific crypto service.
/// </summary>
/// <param name="context">Plugin context.</param>
/// <param name="ct">Cancellation token.</param>
protected abstract Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct);
/// <summary>
/// Perform health check on the crypto provider.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Health check result.</returns>
public virtual async Task<HealthCheckResult> HealthCheckAsync(CancellationToken ct)
{
if (State != PluginLifecycleState.Active)
{
return HealthCheckResult.Unhealthy($"Plugin is in state {State}");
}
try
{
// Default health check: verify we can hash test data
var testData = "health-check-test"u8.ToArray();
var algorithm = SelectHealthCheckAlgorithm();
if (algorithm != null)
{
await HashAsync(testData, algorithm, ct);
}
return HealthCheckResult.Healthy();
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(ex);
}
}
/// <summary>
/// Select an algorithm for health checks.
/// </summary>
protected virtual string? SelectHealthCheckAlgorithm()
{
return SupportedAlgorithms.FirstOrDefault(a =>
a.Contains("256", StringComparison.OrdinalIgnoreCase) ||
a.Contains("SHA", StringComparison.OrdinalIgnoreCase) ||
a.Contains("HASH", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Check if this provider can handle the specified operation and algorithm.
/// </summary>
/// <param name="operation">Crypto operation type.</param>
/// <param name="algorithm">Algorithm identifier.</param>
/// <returns>True if supported.</returns>
public abstract bool CanHandle(CryptoOperation operation, string algorithm);
/// <summary>
/// Sign data using the specified algorithm and key.
/// </summary>
public abstract Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct);
/// <summary>
/// Verify a signature.
/// </summary>
public abstract Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct);
/// <summary>
/// Encrypt data.
/// </summary>
public abstract Task<byte[]> EncryptAsync(ReadOnlyMemory<byte> data, CryptoEncryptOptions options, CancellationToken ct);
/// <summary>
/// Decrypt data.
/// </summary>
public abstract Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> data, CryptoDecryptOptions options, CancellationToken ct);
/// <summary>
/// Compute hash of data.
/// </summary>
public abstract Task<byte[]> HashAsync(ReadOnlyMemory<byte> data, string algorithm, CancellationToken ct);
/// <summary>
/// Dispose the crypto plugin.
/// </summary>
public abstract ValueTask DisposeAsync();
/// <summary>
/// Ensure the plugin is active before performing operations.
/// </summary>
/// <exception cref="InvalidOperationException">If plugin is not active.</exception>
protected void EnsureActive()
{
if (State != PluginLifecycleState.Active)
{
throw new InvalidOperationException($"{Info.Name} is not active (current state: {State})");
}
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\Plugin\StellaOps.Plugin.Abstractions\StellaOps.Plugin.Abstractions.csproj" />
</ItemGroup>
</Project>