sprints completion. new product advisories prepared
This commit is contained in:
@@ -2,8 +2,8 @@ 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.Capabilities;
|
||||
using StellaOps.Plugin.Abstractions.Health;
|
||||
using StellaOps.Plugin.Abstractions.Lifecycle;
|
||||
|
||||
@@ -373,12 +373,13 @@ internal sealed class SimulatedHsmClient : IHsmClient
|
||||
|
||||
/// <summary>
|
||||
/// PKCS#11 HSM client implementation stub.
|
||||
/// In production, this would use a PKCS#11 library like PKCS11Interop.
|
||||
/// In production, use Pkcs11HsmClientImpl for full PKCS#11 support.
|
||||
/// </summary>
|
||||
internal sealed class Pkcs11HsmClient : IHsmClient
|
||||
{
|
||||
private readonly string _libraryPath;
|
||||
private readonly IPluginLogger? _logger;
|
||||
private Pkcs11HsmClientImpl? _impl;
|
||||
|
||||
public Pkcs11HsmClient(string libraryPath, IPluginLogger? logger)
|
||||
{
|
||||
@@ -386,43 +387,55 @@ internal sealed class Pkcs11HsmClient : IHsmClient
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task ConnectAsync(int slotId, string? pin, CancellationToken ct)
|
||||
public async 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.");
|
||||
_impl = new Pkcs11HsmClientImpl(_libraryPath, _logger);
|
||||
await _impl.ConnectAsync(slotId, pin, ct);
|
||||
}
|
||||
|
||||
public Task DisconnectAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
_impl?.Dispose();
|
||||
_impl = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> PingAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return _impl?.PingAsync(ct) ?? Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<byte[]> SignAsync(string keyId, byte[] data, HsmMechanism mechanism, CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
EnsureConnected();
|
||||
return _impl!.SignAsync(keyId, data, mechanism, ct);
|
||||
}
|
||||
|
||||
public Task<bool> VerifyAsync(string keyId, byte[] data, byte[] signature, HsmMechanism mechanism, CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
EnsureConnected();
|
||||
return _impl!.VerifyAsync(keyId, data, signature, mechanism, ct);
|
||||
}
|
||||
|
||||
public Task<byte[]> EncryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
EnsureConnected();
|
||||
return _impl!.EncryptAsync(keyId, data, mechanism, iv, ct);
|
||||
}
|
||||
|
||||
public Task<byte[]> DecryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
EnsureConnected();
|
||||
return _impl!.DecryptAsync(keyId, data, mechanism, iv, ct);
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_impl == null)
|
||||
{
|
||||
throw new InvalidOperationException("HSM not connected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,717 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_017_CRYPTO_pkcs11_hsm_implementation
|
||||
// Tasks: HSM-002, HSM-003, HSM-004, HSM-005, HSM-006, HSM-007
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using Net.Pkcs11Interop.Common;
|
||||
using Net.Pkcs11Interop.HighLevelAPI;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Hsm;
|
||||
|
||||
/// <summary>
|
||||
/// PKCS#11 HSM client implementation using Pkcs11Interop.
|
||||
/// Provides session pooling, multi-slot failover, and key management.
|
||||
/// </summary>
|
||||
public sealed class Pkcs11HsmClientImpl : IHsmClient, IDisposable
|
||||
{
|
||||
private readonly string _libraryPath;
|
||||
private readonly IPluginLogger? _logger;
|
||||
private readonly Pkcs11HsmClientOptions _options;
|
||||
|
||||
private IPkcs11Library? _pkcs11Library;
|
||||
private readonly ConcurrentDictionary<int, SlotContext> _slotContexts = new();
|
||||
private readonly SemaphoreSlim _connectionLock = new(1, 1);
|
||||
private volatile bool _connected;
|
||||
private int _primarySlotId;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PKCS#11 HSM client.
|
||||
/// </summary>
|
||||
public Pkcs11HsmClientImpl(
|
||||
string libraryPath,
|
||||
IPluginLogger? logger = null,
|
||||
Pkcs11HsmClientOptions? options = null)
|
||||
{
|
||||
_libraryPath = libraryPath ?? throw new ArgumentNullException(nameof(libraryPath));
|
||||
_logger = logger;
|
||||
_options = options ?? new Pkcs11HsmClientOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConnectAsync(int slotId, string? pin, CancellationToken ct)
|
||||
{
|
||||
await _connectionLock.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
if (_connected)
|
||||
{
|
||||
_logger?.Debug("HSM already connected");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger?.Info("Loading PKCS#11 library: {LibraryPath}", _libraryPath);
|
||||
|
||||
// Create PKCS#11 library wrapper
|
||||
var factories = new Pkcs11InteropFactories();
|
||||
_pkcs11Library = factories.Pkcs11LibraryFactory.LoadPkcs11Library(
|
||||
factories,
|
||||
_libraryPath,
|
||||
AppType.MultiThreaded);
|
||||
|
||||
_primarySlotId = slotId;
|
||||
|
||||
// Connect to primary slot
|
||||
await ConnectToSlotAsync(slotId, pin, ct);
|
||||
|
||||
// Connect to failover slots if configured
|
||||
if (_options.FailoverSlotIds?.Count > 0)
|
||||
{
|
||||
foreach (var failoverSlotId in _options.FailoverSlotIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ConnectToSlotAsync(failoverSlotId, pin, ct);
|
||||
_logger?.Info("Connected to failover slot {SlotId}", failoverSlotId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Warning("Failed to connect to failover slot {SlotId}: {Error}",
|
||||
failoverSlotId, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_connected = true;
|
||||
_logger?.Info("HSM connected to {SlotCount} slot(s), primary={PrimarySlotId}",
|
||||
_slotContexts.Count, _primarySlotId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DisconnectAsync(CancellationToken ct)
|
||||
{
|
||||
await _connectionLock.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
foreach (var context in _slotContexts.Values)
|
||||
{
|
||||
context.Dispose();
|
||||
}
|
||||
_slotContexts.Clear();
|
||||
|
||||
_pkcs11Library?.Dispose();
|
||||
_pkcs11Library = null;
|
||||
_connected = false;
|
||||
|
||||
_logger?.Info("HSM disconnected");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> PingAsync(CancellationToken ct)
|
||||
{
|
||||
if (!_connected || _pkcs11Library == null)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try to get slot info as a ping
|
||||
var slots = _pkcs11Library.GetSlotList(SlotsType.WithTokenPresent);
|
||||
return Task.FromResult(slots.Count > 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> SignAsync(string keyId, byte[] data, HsmMechanism mechanism, CancellationToken ct)
|
||||
{
|
||||
var context = GetActiveSlotContext();
|
||||
var session = await context.GetSessionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
// Find the private key
|
||||
var key = FindKey(session, keyId, CKO.CKO_PRIVATE_KEY);
|
||||
if (key == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Private key not found: {keyId}");
|
||||
}
|
||||
|
||||
// Verify key attributes (CKA_SIGN must be true)
|
||||
ValidateKeyAttribute(session, key, CKA.CKA_SIGN, true, "signing");
|
||||
|
||||
// Get PKCS#11 mechanism
|
||||
var pkcs11Mechanism = GetPkcs11Mechanism(mechanism);
|
||||
|
||||
// Sign the data
|
||||
var signature = session.Sign(pkcs11Mechanism, key, data);
|
||||
|
||||
_logger?.Debug("HSM signed {DataLength} bytes with key {KeyId}", data.Length, keyId);
|
||||
|
||||
return signature;
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.ReturnSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> VerifyAsync(string keyId, byte[] data, byte[] signature, HsmMechanism mechanism, CancellationToken ct)
|
||||
{
|
||||
var context = GetActiveSlotContext();
|
||||
var session = await context.GetSessionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
// Find the public key
|
||||
var key = FindKey(session, keyId, CKO.CKO_PUBLIC_KEY);
|
||||
if (key == null)
|
||||
{
|
||||
// Try private key (some HSMs store both in one object)
|
||||
key = FindKey(session, keyId, CKO.CKO_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Key not found for verification: {keyId}");
|
||||
}
|
||||
|
||||
// Verify key attributes (CKA_VERIFY must be true)
|
||||
ValidateKeyAttribute(session, key, CKA.CKA_VERIFY, true, "verification");
|
||||
|
||||
// Get PKCS#11 mechanism
|
||||
var pkcs11Mechanism = GetPkcs11Mechanism(mechanism);
|
||||
|
||||
// Verify the signature
|
||||
session.Verify(pkcs11Mechanism, key, data, signature, out bool isValid);
|
||||
|
||||
_logger?.Debug("HSM verified signature with key {KeyId}: {IsValid}", keyId, isValid);
|
||||
|
||||
return isValid;
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.ReturnSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> EncryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct)
|
||||
{
|
||||
var context = GetActiveSlotContext();
|
||||
var session = await context.GetSessionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
var key = FindKey(session, keyId, CKO.CKO_SECRET_KEY);
|
||||
if (key == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Secret key not found: {keyId}");
|
||||
}
|
||||
|
||||
ValidateKeyAttribute(session, key, CKA.CKA_ENCRYPT, true, "encryption");
|
||||
|
||||
var pkcs11Mechanism = GetAesMechanism(mechanism, iv);
|
||||
var ciphertext = session.Encrypt(pkcs11Mechanism, key, data);
|
||||
|
||||
_logger?.Debug("HSM encrypted {DataLength} bytes with key {KeyId}", data.Length, keyId);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.ReturnSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> DecryptAsync(string keyId, byte[] data, HsmMechanism mechanism, byte[]? iv, CancellationToken ct)
|
||||
{
|
||||
var context = GetActiveSlotContext();
|
||||
var session = await context.GetSessionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
var key = FindKey(session, keyId, CKO.CKO_SECRET_KEY);
|
||||
if (key == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Secret key not found: {keyId}");
|
||||
}
|
||||
|
||||
ValidateKeyAttribute(session, key, CKA.CKA_DECRYPT, true, "decryption");
|
||||
|
||||
var pkcs11Mechanism = GetAesMechanism(mechanism, iv);
|
||||
var plaintext = session.Decrypt(pkcs11Mechanism, key, data);
|
||||
|
||||
_logger?.Debug("HSM decrypted {DataLength} bytes with key {KeyId}", data.Length, keyId);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.ReturnSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets metadata about a key.
|
||||
/// </summary>
|
||||
public async Task<HsmKeyMetadata?> GetKeyMetadataAsync(string keyId, CancellationToken ct)
|
||||
{
|
||||
var context = GetActiveSlotContext();
|
||||
var session = await context.GetSessionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
// Try to find the key in various object classes
|
||||
IObjectHandle? key = FindKey(session, keyId, CKO.CKO_PRIVATE_KEY)
|
||||
?? FindKey(session, keyId, CKO.CKO_PUBLIC_KEY)
|
||||
?? FindKey(session, keyId, CKO.CKO_SECRET_KEY);
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read key attributes
|
||||
var attributeValues = session.GetAttributeValue(key, new List<CKA>
|
||||
{
|
||||
CKA.CKA_CLASS,
|
||||
CKA.CKA_KEY_TYPE,
|
||||
CKA.CKA_LABEL,
|
||||
CKA.CKA_ID,
|
||||
CKA.CKA_EXTRACTABLE,
|
||||
CKA.CKA_SENSITIVE,
|
||||
CKA.CKA_PRIVATE,
|
||||
CKA.CKA_MODIFIABLE,
|
||||
});
|
||||
|
||||
return new HsmKeyMetadata
|
||||
{
|
||||
KeyId = keyId,
|
||||
Label = attributeValues[2].GetValueAsString() ?? keyId,
|
||||
KeyClass = GetKeyClassName((CKO)attributeValues[0].GetValueAsUlong()),
|
||||
KeyType = GetKeyTypeName((CKK)attributeValues[1].GetValueAsUlong()),
|
||||
IsExtractable = attributeValues[4].GetValueAsBool(),
|
||||
IsSensitive = attributeValues[5].GetValueAsBool(),
|
||||
IsPrivate = attributeValues[6].GetValueAsBool(),
|
||||
IsModifiable = attributeValues[7].GetValueAsBool(),
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.ReturnSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all keys in the HSM.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<HsmKeyMetadata>> ListKeysAsync(CancellationToken ct)
|
||||
{
|
||||
var context = GetActiveSlotContext();
|
||||
var session = await context.GetSessionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
var keys = new List<HsmKeyMetadata>();
|
||||
|
||||
// Find all key objects
|
||||
foreach (var keyClass in new[] { CKO.CKO_PRIVATE_KEY, CKO.CKO_PUBLIC_KEY, CKO.CKO_SECRET_KEY })
|
||||
{
|
||||
var searchAttributes = new List<IObjectAttribute>
|
||||
{
|
||||
session.Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, (ulong)keyClass),
|
||||
};
|
||||
|
||||
var foundObjects = session.FindAllObjects(searchAttributes);
|
||||
|
||||
foreach (var obj in foundObjects)
|
||||
{
|
||||
try
|
||||
{
|
||||
var attributeValues = session.GetAttributeValue(obj, new List<CKA>
|
||||
{
|
||||
CKA.CKA_ID,
|
||||
CKA.CKA_LABEL,
|
||||
CKA.CKA_KEY_TYPE,
|
||||
});
|
||||
|
||||
var keyId = BitConverter.ToString(attributeValues[0].GetValueAsByteArray() ?? []).Replace("-", "");
|
||||
var label = attributeValues[1].GetValueAsString() ?? keyId;
|
||||
|
||||
keys.Add(new HsmKeyMetadata
|
||||
{
|
||||
KeyId = keyId,
|
||||
Label = label,
|
||||
KeyClass = GetKeyClassName(keyClass),
|
||||
KeyType = GetKeyTypeName((CKK)attributeValues[2].GetValueAsUlong()),
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Warning("Failed to read key attributes: {Error}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.ReturnSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var context in _slotContexts.Values)
|
||||
{
|
||||
context.Dispose();
|
||||
}
|
||||
_slotContexts.Clear();
|
||||
_pkcs11Library?.Dispose();
|
||||
_connectionLock.Dispose();
|
||||
}
|
||||
|
||||
private async Task ConnectToSlotAsync(int slotId, string? pin, CancellationToken ct)
|
||||
{
|
||||
if (_pkcs11Library == null)
|
||||
{
|
||||
throw new InvalidOperationException("PKCS#11 library not loaded");
|
||||
}
|
||||
|
||||
var slots = _pkcs11Library.GetSlotList(SlotsType.WithTokenPresent);
|
||||
var slot = slots.FirstOrDefault(s => (int)s.GetSlotInfo().SlotId == slotId);
|
||||
|
||||
if (slot == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Slot {slotId} not found or has no token");
|
||||
}
|
||||
|
||||
var tokenInfo = slot.GetTokenInfo();
|
||||
_logger?.Info("Connecting to token: {TokenLabel} in slot {SlotId}",
|
||||
tokenInfo.Label, slotId);
|
||||
|
||||
// Create session pool for this slot
|
||||
var context = new SlotContext(slot, pin, _options, _logger);
|
||||
await context.InitializeAsync(ct);
|
||||
|
||||
_slotContexts[slotId] = context;
|
||||
}
|
||||
|
||||
private SlotContext GetActiveSlotContext()
|
||||
{
|
||||
// Try primary slot first
|
||||
if (_slotContexts.TryGetValue(_primarySlotId, out var context) && context.IsHealthy)
|
||||
{
|
||||
return context;
|
||||
}
|
||||
|
||||
// Try failover slots
|
||||
foreach (var kvp in _slotContexts)
|
||||
{
|
||||
if (kvp.Value.IsHealthy)
|
||||
{
|
||||
_logger?.Warning("Primary slot unhealthy, using failover slot {SlotId}", kvp.Key);
|
||||
return kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No healthy HSM slots available");
|
||||
}
|
||||
|
||||
private static IObjectHandle? FindKey(ISession session, string keyId, CKO keyClass)
|
||||
{
|
||||
// Try finding by CKA_LABEL first
|
||||
var searchByLabel = new List<IObjectAttribute>
|
||||
{
|
||||
session.Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, (ulong)keyClass),
|
||||
session.Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, keyId),
|
||||
};
|
||||
|
||||
var foundObjects = session.FindAllObjects(searchByLabel);
|
||||
if (foundObjects.Count > 0)
|
||||
{
|
||||
return foundObjects[0];
|
||||
}
|
||||
|
||||
// Try finding by CKA_ID (hex string)
|
||||
if (TryParseHexString(keyId, out var keyIdBytes))
|
||||
{
|
||||
var searchById = new List<IObjectAttribute>
|
||||
{
|
||||
session.Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, (ulong)keyClass),
|
||||
session.Factories.ObjectAttributeFactory.Create(CKA.CKA_ID, keyIdBytes),
|
||||
};
|
||||
|
||||
foundObjects = session.FindAllObjects(searchById);
|
||||
if (foundObjects.Count > 0)
|
||||
{
|
||||
return foundObjects[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ValidateKeyAttribute(ISession session, IObjectHandle key, CKA attribute, bool expectedValue, string operation)
|
||||
{
|
||||
var attrs = session.GetAttributeValue(key, new List<CKA> { attribute });
|
||||
var actualValue = attrs[0].GetValueAsBool();
|
||||
|
||||
if (actualValue != expectedValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Key attribute {attribute} is {actualValue}, expected {expectedValue} for {operation}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IMechanism GetPkcs11Mechanism(HsmMechanism mechanism)
|
||||
{
|
||||
return mechanism switch
|
||||
{
|
||||
HsmMechanism.RsaSha256 => MechanismFactory.Create(CKM.CKM_SHA256_RSA_PKCS),
|
||||
HsmMechanism.RsaSha384 => MechanismFactory.Create(CKM.CKM_SHA384_RSA_PKCS),
|
||||
HsmMechanism.RsaSha512 => MechanismFactory.Create(CKM.CKM_SHA512_RSA_PKCS),
|
||||
HsmMechanism.RsaPssSha256 => CreateRsaPssMechanism(CKM.CKM_SHA256_RSA_PKCS_PSS, CKM.CKM_SHA256, 32),
|
||||
HsmMechanism.EcdsaP256 => MechanismFactory.Create(CKM.CKM_ECDSA_SHA256),
|
||||
HsmMechanism.EcdsaP384 => MechanismFactory.Create(CKM.CKM_ECDSA_SHA384),
|
||||
_ => throw new NotSupportedException($"Mechanism not supported: {mechanism}"),
|
||||
};
|
||||
}
|
||||
|
||||
private static IMechanism GetAesMechanism(HsmMechanism mechanism, byte[]? iv)
|
||||
{
|
||||
if (mechanism is not (HsmMechanism.Aes128Gcm or HsmMechanism.Aes256Gcm))
|
||||
{
|
||||
throw new NotSupportedException($"AES mechanism not supported: {mechanism}");
|
||||
}
|
||||
|
||||
iv ??= new byte[12]; // Default GCM nonce size
|
||||
|
||||
// For AES-GCM, we need to create a mechanism with GCM parameters
|
||||
return MechanismFactory.Create(CKM.CKM_AES_GCM, iv);
|
||||
}
|
||||
|
||||
private static IMechanism CreateRsaPssMechanism(CKM mechanism, CKM hashAlg, int saltLen)
|
||||
{
|
||||
// RSA-PSS requires additional parameters
|
||||
// This is a simplified version; full implementation would use CK_RSA_PKCS_PSS_PARAMS
|
||||
return MechanismFactory.Create(mechanism);
|
||||
}
|
||||
|
||||
private static bool TryParseHexString(string hex, out byte[] bytes)
|
||||
{
|
||||
bytes = [];
|
||||
if (string.IsNullOrEmpty(hex) || hex.Length % 2 != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bytes = Convert.FromHexString(hex);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetKeyClassName(CKO keyClass) => keyClass switch
|
||||
{
|
||||
CKO.CKO_PRIVATE_KEY => "PrivateKey",
|
||||
CKO.CKO_PUBLIC_KEY => "PublicKey",
|
||||
CKO.CKO_SECRET_KEY => "SecretKey",
|
||||
_ => keyClass.ToString(),
|
||||
};
|
||||
|
||||
private static string GetKeyTypeName(CKK keyType) => keyType switch
|
||||
{
|
||||
CKK.CKK_RSA => "RSA",
|
||||
CKK.CKK_EC => "EC",
|
||||
CKK.CKK_AES => "AES",
|
||||
CKK.CKK_GENERIC_SECRET => "GenericSecret",
|
||||
_ => keyType.ToString(),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Helper factory for creating mechanisms.
|
||||
/// </summary>
|
||||
private static class MechanismFactory
|
||||
{
|
||||
private static readonly Pkcs11InteropFactories Factories = new();
|
||||
|
||||
public static IMechanism Create(CKM mechanism)
|
||||
{
|
||||
return Factories.MechanismFactory.Create(mechanism);
|
||||
}
|
||||
|
||||
public static IMechanism Create(CKM mechanism, byte[] parameter)
|
||||
{
|
||||
return Factories.MechanismFactory.Create(mechanism, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages sessions for a single HSM slot with pooling and health monitoring.
|
||||
/// </summary>
|
||||
internal sealed class SlotContext : IDisposable
|
||||
{
|
||||
private readonly ISlot _slot;
|
||||
private readonly string? _pin;
|
||||
private readonly Pkcs11HsmClientOptions _options;
|
||||
private readonly IPluginLogger? _logger;
|
||||
private readonly ConcurrentBag<ISession> _sessionPool = new();
|
||||
private readonly SemaphoreSlim _poolSemaphore;
|
||||
private volatile bool _isHealthy = true;
|
||||
private int _consecutiveFailures;
|
||||
|
||||
public bool IsHealthy => _isHealthy;
|
||||
|
||||
public SlotContext(ISlot slot, string? pin, Pkcs11HsmClientOptions options, IPluginLogger? logger)
|
||||
{
|
||||
_slot = slot;
|
||||
_pin = pin;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
_poolSemaphore = new SemaphoreSlim(options.MaxSessionPoolSize, options.MaxSessionPoolSize);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken ct)
|
||||
{
|
||||
// Pre-create some sessions
|
||||
for (int i = 0; i < _options.MinSessionPoolSize; i++)
|
||||
{
|
||||
var session = await CreateSessionAsync(ct);
|
||||
_sessionPool.Add(session);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ISession> GetSessionAsync(CancellationToken ct)
|
||||
{
|
||||
await _poolSemaphore.WaitAsync(ct);
|
||||
|
||||
if (_sessionPool.TryTake(out var session))
|
||||
{
|
||||
return session;
|
||||
}
|
||||
|
||||
// Create new session
|
||||
return await CreateSessionAsync(ct);
|
||||
}
|
||||
|
||||
public void ReturnSession(ISession session)
|
||||
{
|
||||
_sessionPool.Add(session);
|
||||
_poolSemaphore.Release();
|
||||
|
||||
// Reset failure counter on successful operation
|
||||
Interlocked.Exchange(ref _consecutiveFailures, 0);
|
||||
_isHealthy = true;
|
||||
}
|
||||
|
||||
public void ReportFailure()
|
||||
{
|
||||
var failures = Interlocked.Increment(ref _consecutiveFailures);
|
||||
if (failures >= _options.FailureThreshold)
|
||||
{
|
||||
_isHealthy = false;
|
||||
_logger?.Warning("Slot marked unhealthy after {Failures} consecutive failures", failures);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ISession> CreateSessionAsync(CancellationToken ct)
|
||||
{
|
||||
var session = _slot.OpenSession(SessionType.ReadWrite);
|
||||
|
||||
if (!string.IsNullOrEmpty(_pin))
|
||||
{
|
||||
await Task.Run(() => session.Login(CKU.CKU_USER, _pin), ct);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
while (_sessionPool.TryTake(out var session))
|
||||
{
|
||||
try
|
||||
{
|
||||
session.Logout();
|
||||
session.CloseSession();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
_poolSemaphore.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for PKCS#11 HSM client.
|
||||
/// </summary>
|
||||
public sealed record Pkcs11HsmClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum number of sessions to keep in the pool.
|
||||
/// </summary>
|
||||
public int MinSessionPoolSize { get; init; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of concurrent sessions.
|
||||
/// </summary>
|
||||
public int MaxSessionPoolSize { get; init; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Number of consecutive failures before marking slot unhealthy.
|
||||
/// </summary>
|
||||
public int FailureThreshold { get; init; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// IDs of failover slots.
|
||||
/// </summary>
|
||||
public IReadOnlyList<int>? FailoverSlotIds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Connection timeout in milliseconds.
|
||||
/// </summary>
|
||||
public int ConnectionTimeoutMs { get; init; } = 30000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata about a key stored in the HSM.
|
||||
/// </summary>
|
||||
public sealed record HsmKeyMetadata
|
||||
{
|
||||
public required string KeyId { get; init; }
|
||||
public required string Label { get; init; }
|
||||
public required string KeyClass { get; init; }
|
||||
public required string KeyType { get; init; }
|
||||
public bool IsExtractable { get; init; }
|
||||
public bool IsSensitive { get; init; }
|
||||
public bool IsPrivate { get; init; }
|
||||
public bool IsModifiable { get; init; }
|
||||
}
|
||||
@@ -8,8 +8,13 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Pkcs11Interop" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin\StellaOps.Cryptography.Plugin.csproj" />
|
||||
<ProjectReference Include="..\..\Plugin\StellaOps.Plugin.Abstractions\StellaOps.Plugin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user