release orchestrator v1 draft and build fixes
This commit is contained in:
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: []
|
||||
Reference in New Issue
Block a user