using EnvelopeGenerator.Application.Common.Configurations; using EnvelopeGenerator.Application.Common.Dto; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Net.Pkcs11Interop.Common; using Net.Pkcs11Interop.HighLevelAPI; using System.Security.Cryptography; namespace EnvelopeGenerator.Infrastructure.Crypt.Services; internal sealed class Pkcs11StartupSelfTestHostedService( IOptions options, Pkcs11SelfTestStatusStore statusStore, ILogger logger) : IHostedService { public Task StartAsync(CancellationToken cancellationToken) { var pkcs11 = options.Value; try { RunInteropProbe(pkcs11); statusStore.SetStatus(new Pkcs11SelfTestStatusDto( Completed: true, Success: true, Mode: "interop", Summary: "PKCS#11 startup self-test succeeded via interop.", CheckedAtUtc: DateTimeOffset.UtcNow, InteropError: null, ToolError: null)); logger.LogInformation("PKCS#11 startup self-test succeeded (mode=interop, library={LibraryPath}, proxy={ProxySocket}).", pkcs11.ResolvedLibraryPath, pkcs11.ProxySocket); return Task.CompletedTask; } catch (Exception interopEx) when (Pkcs11ToolBridge.IsAvailable(pkcs11)) { try { RunToolProbe(pkcs11); statusStore.SetStatus(new Pkcs11SelfTestStatusDto( Completed: true, Success: true, Mode: "pkcs11-tool", Summary: "PKCS#11 interop failed, tool fallback succeeded.", CheckedAtUtc: DateTimeOffset.UtcNow, InteropError: interopEx.Message, ToolError: null)); logger.LogWarning(interopEx, "PKCS#11 interop self-test failed, but tool fallback is healthy (mode=pkcs11-tool, tool={ToolPath}).", pkcs11.ResolvedToolPath); return Task.CompletedTask; } catch (Exception toolEx) { statusStore.SetStatus(new Pkcs11SelfTestStatusDto( Completed: true, Success: false, Mode: "failed", Summary: "PKCS#11 startup self-test failed for both interop and tool fallback.", CheckedAtUtc: DateTimeOffset.UtcNow, InteropError: interopEx.Message, ToolError: toolEx.Message)); logger.LogError(toolEx, "PKCS#11 startup self-test failed for both interop and tool fallback. library={LibraryPath}, proxy={ProxySocket}, tool={ToolPath}", pkcs11.ResolvedLibraryPath, pkcs11.ProxySocket, pkcs11.ResolvedToolPath); return Task.CompletedTask; } } catch (Exception ex) { statusStore.SetStatus(new Pkcs11SelfTestStatusDto( Completed: true, Success: false, Mode: "failed", Summary: "PKCS#11 startup self-test failed via interop and no tool fallback is available.", CheckedAtUtc: DateTimeOffset.UtcNow, InteropError: ex.Message, ToolError: null)); logger.LogError(ex, "PKCS#11 startup self-test failed (mode=interop, no fallback available). library={LibraryPath}, proxy={ProxySocket}", pkcs11.ResolvedLibraryPath, pkcs11.ProxySocket); return Task.CompletedTask; } } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; private static void RunInteropProbe(Pkcs11Options options) { Pkcs11SessionRunner.Run(options, session => { EnsureObjectExists(session, CKO.CKO_CERTIFICATE, options.CertificateLabel, "certificate"); EnsureObjectExists(session, CKO.CKO_PRIVATE_KEY, options.PrivateKeyLabel, "private key"); return true; }); } private static void RunToolProbe(Pkcs11Options options) { _ = Pkcs11ToolBridge.ReadCertificateBytes(options); var testDigest = SHA256.HashData([1, 2, 3, 4]); var digestInfo = Pkcs11DigestInfoBuilder.Build(testDigest, HashAlgorithmName.SHA256); _ = Pkcs11ToolBridge.SignDigestInfo(options, digestInfo); } private static void EnsureObjectExists(ISession session, CKO objectClass, string label, string objectType) { var attrs = new List { session.Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, objectClass), session.Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, label) }; var handles = session.FindAllObjects(attrs); if (handles.Count == 0) throw new InvalidOperationException($"PKCS#11 {objectType} '{label}' not found in token."); } }