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: []
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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: []
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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: []
|
||||
464
src/Cryptography/StellaOps.Cryptography.Plugin.Hsm/HsmPlugin.cs
Normal file
464
src/Cryptography/StellaOps.Cryptography.Plugin.Hsm/HsmPlugin.cs
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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: []
|
||||
364
src/Cryptography/StellaOps.Cryptography.Plugin.Sm/SmPlugin.cs
Normal file
364
src/Cryptography/StellaOps.Cryptography.Plugin.Sm/SmPlugin.cs
Normal 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";
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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: []
|
||||
@@ -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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user