sprints completion. new product advisories prepared

This commit is contained in:
master
2026-01-16 16:30:03 +02:00
parent a927d924e3
commit 4ca3ce8fb4
255 changed files with 42434 additions and 1020 deletions

View File

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

View File

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

View File

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