using Microsoft.Extensions.Options; using Net.Pkcs11Interop.Common; using Net.Pkcs11Interop.HighLevelAPI; using Net.Pkcs11Interop.HighLevelAPI.Factories; using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using static StellaOps.Localization.T; namespace StellaOps.Cryptography.Kms; internal sealed partial class Pkcs11InteropFacade : IPkcs11Facade { private readonly Pkcs11Options _options; private readonly Pkcs11InteropFactories _factories; private readonly IPkcs11Library _library; private readonly ISlot _slot; private readonly ConcurrentDictionary _attributeCache = new(StringComparer.Ordinal); private readonly TimeProvider _timeProvider; public Pkcs11InteropFacade(Pkcs11Options options, TimeProvider? timeProvider = null) { _options = options ?? throw new ArgumentNullException(nameof(options)); _timeProvider = timeProvider ?? TimeProvider.System; if (string.IsNullOrWhiteSpace(_options.LibraryPath)) { throw new ArgumentException("PKCS#11 library path must be provided.", nameof(options)); } _factories = new Pkcs11InteropFactories(); _library = _factories.Pkcs11LibraryFactory.LoadPkcs11Library(_factories, _options.LibraryPath, AppType.MultiThreaded); _slot = ResolveSlot(_library, _options) ?? throw new InvalidOperationException(_t("crypto.pkcs11.slot_not_found")); } public Pkcs11InteropFacade(IOptions options, TimeProvider timeProvider) : this(options?.Value ?? new Pkcs11Options(), timeProvider) { } public async Task GetKeyAsync(CancellationToken cancellationToken) { using var context = await OpenSessionAsync(cancellationToken).ConfigureAwait(false); var session = context.Session; var privateHandle = FindKey(session, CKO.CKO_PRIVATE_KEY, _options.PrivateKeyLabel); if (privateHandle is null) { throw new InvalidOperationException(_t("crypto.pkcs11.private_key_not_found")); } var labelAttr = GetAttribute(session, privateHandle, CKA.CKA_LABEL); var label = labelAttr?.GetValueAsString(); return new Pkcs11KeyDescriptor( KeyId: label ?? privateHandle.ObjectId.ToString(), Label: label, CreatedAt: _timeProvider.GetUtcNow()); } public async Task GetPublicKeyAsync(CancellationToken cancellationToken) { using var context = await OpenSessionAsync(cancellationToken).ConfigureAwait(false); var session = context.Session; var publicHandle = FindKey(session, CKO.CKO_PUBLIC_KEY, _options.PublicKeyLabel ?? _options.PrivateKeyLabel); if (publicHandle is null) { throw new InvalidOperationException(_t("crypto.pkcs11.public_key_not_found")); } var pointAttr = GetAttribute(session, publicHandle, CKA.CKA_EC_POINT) ?? throw new InvalidOperationException(_t("crypto.pkcs11.missing_ec_point")); var paramsAttr = GetAttribute(session, publicHandle, CKA.CKA_EC_PARAMS) ?? throw new InvalidOperationException(_t("crypto.pkcs11.missing_ec_params")); var ecPoint = ExtractEcPoint(pointAttr.GetValueAsByteArray()); var (curve, coordinateSize) = DecodeCurve(paramsAttr.GetValueAsByteArray()); if (ecPoint.Length != 1 + (coordinateSize * 2) || ecPoint[0] != 0x04) { throw new InvalidOperationException(_t("crypto.pkcs11.unsupported_point_format")); } var qx = ecPoint.AsSpan(1, coordinateSize).ToArray(); var qy = ecPoint.AsSpan(1 + coordinateSize, coordinateSize).ToArray(); var keyId = GetAttribute(session, publicHandle, CKA.CKA_LABEL)?.GetValueAsString() ?? publicHandle.ObjectId.ToString(); return new Pkcs11PublicKeyMaterial( keyId, curve, qx, qy); } public async Task SignDigestAsync(ReadOnlyMemory digest, CancellationToken cancellationToken) { using var context = await OpenSessionAsync(cancellationToken).ConfigureAwait(false); var session = context.Session; var privateHandle = FindKey(session, CKO.CKO_PRIVATE_KEY, _options.PrivateKeyLabel) ?? throw new InvalidOperationException(_t("crypto.pkcs11.private_key_not_found")); var mechanism = _factories.MechanismFactory.Create(_options.MechanismId); return session.Sign(mechanism, privateHandle, digest.ToArray()); } public void Dispose() { _library.Dispose(); } }