Add PKCS#11 startup self-test health reporting

This commit is contained in:
2026-10-14 13:49:38 +02:00
parent 8f2744afc7
commit b8a96330c0
13 changed files with 259 additions and 25 deletions

View File

@@ -16,7 +16,11 @@ public static class ServiceCollectionExtensions
.Bind(configuration.GetSection(Pkcs11Options.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IPdfDigitalSignatureService, PdfDigitalSignatureService>();
services.AddSingleton<Pkcs11SelfTestStatusStore>();
services.AddSingleton<IPkcs11SelfTestStatusProvider>(sp => sp.GetRequiredService<Pkcs11SelfTestStatusStore>());
services.AddHostedService<Pkcs11StartupSelfTestHostedService>();
return services;
}
}

View File

@@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.6" />
@@ -24,4 +25,11 @@
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="resources\pkcs11\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>pkcs11\%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
</None>
</ItemGroup>
</Project>

View File

@@ -6,6 +6,8 @@ internal static class Pkcs11InteropBootstrap
{
private static readonly object Sync = new();
private static bool Initialized;
private const string ProxyRelativePath = "pkcs11\\windows\\proxy\\libpkcs11-proxy.dll";
private const string ToolRelativePath = "pkcs11\\windows\\opensc\\tools\\pkcs11-tool.exe";
public static void EnsureLoaded(Pkcs11Options options)
{
@@ -17,7 +19,11 @@ internal static class Pkcs11InteropBootstrap
if (Initialized)
return;
EnsureNativeDependencySearchPath(options.LibraryPath);
var libraryPath = ResolveLibraryPath(options);
var toolPath = ResolveToolPath(options);
options.SetResolvedPaths(libraryPath, toolPath);
EnsureNativeDependencySearchPath(options.ResolvedLibraryPath);
if (!string.IsNullOrWhiteSpace(options.ProxySocket))
{
@@ -25,9 +31,9 @@ internal static class Pkcs11InteropBootstrap
Environment.SetEnvironmentVariable("PKCS11_DAEMON_SOCKET", options.ProxySocket);
}
if (!string.IsNullOrWhiteSpace(options.ToolPath))
if (!string.IsNullOrWhiteSpace(options.ResolvedToolPath))
{
var toolDir = Path.GetDirectoryName(options.ToolPath);
var toolDir = Path.GetDirectoryName(options.ResolvedToolPath);
if (!string.IsNullOrWhiteSpace(toolDir))
EnsurePathContains(toolDir);
}
@@ -37,20 +43,46 @@ internal static class Pkcs11InteropBootstrap
}
}
private static string ResolveLibraryPath(Pkcs11Options options)
{
var assetsRoot = string.IsNullOrWhiteSpace(options.AssetsRootPath)
? AppContext.BaseDirectory
: options.AssetsRootPath;
var resolved = Path.Combine(assetsRoot, ProxyRelativePath);
if (File.Exists(resolved))
return resolved;
throw new FileNotFoundException($"PKCS#11 proxy module not found. Checked path: {resolved}", resolved);
}
private static string? ResolveToolPath(Pkcs11Options options)
{
var assetsRoot = string.IsNullOrWhiteSpace(options.AssetsRootPath)
? AppContext.BaseDirectory
: options.AssetsRootPath;
var resolved = Path.Combine(assetsRoot, ToolRelativePath);
if (File.Exists(resolved))
return resolved;
return null;
}
private static void WarmupWithPkcs11Tool(Pkcs11Options options)
{
if (string.IsNullOrWhiteSpace(options.ToolPath))
if (string.IsNullOrWhiteSpace(options.ResolvedToolPath))
return;
if (!File.Exists(options.ToolPath))
if (!File.Exists(options.ResolvedToolPath))
return;
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = options.ToolPath,
Arguments = $"--module \"{options.LibraryPath}\" -L",
FileName = options.ResolvedToolPath,
Arguments = $"--module \"{options.ResolvedLibraryPath}\" -L",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,

View File

@@ -0,0 +1,24 @@
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using System.Threading;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
internal sealed class Pkcs11SelfTestStatusStore : IPkcs11SelfTestStatusProvider
{
private Pkcs11SelfTestStatusDto _status = new(
Completed: false,
Success: false,
Mode: "pending",
Summary: "PKCS#11 startup self-test has not run yet.",
CheckedAtUtc: null,
InteropError: null,
ToolError: null);
public Pkcs11SelfTestStatusDto GetStatus() => Volatile.Read(ref _status);
public void SetStatus(Pkcs11SelfTestStatusDto status)
{
Volatile.Write(ref _status, status);
}
}

View File

@@ -11,7 +11,7 @@ internal static class Pkcs11SessionRunner
Pkcs11InteropBootstrap.EnsureLoaded(options);
var factories = new Pkcs11InteropFactories();
using var pkcs11 = LoadLibraryWithFallback(factories, options.LibraryPath, options.ProxySocket);
using var pkcs11 = LoadLibraryWithFallback(factories, options.ResolvedLibraryPath, options.ProxySocket);
var slot = ResolveSlot(pkcs11.GetSlotList(SlotsType.WithTokenPresent), options);
using var session = slot.OpenSession(SessionType.ReadOnly);

View File

@@ -0,0 +1,112 @@
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<Pkcs11Options> options,
Pkcs11SelfTestStatusStore statusStore,
ILogger<Pkcs11StartupSelfTestHostedService> 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<IObjectAttribute>
{
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.");
}
}

View File

@@ -8,7 +8,7 @@ internal static class Pkcs11ToolBridge
{
public static bool IsAvailable(Pkcs11Options options)
{
return !string.IsNullOrWhiteSpace(options.ToolPath) && File.Exists(options.ToolPath);
return !string.IsNullOrWhiteSpace(options.ResolvedToolPath) && File.Exists(options.ResolvedToolPath);
}
public static byte[] ReadCertificateBytes(Pkcs11Options options)
@@ -75,7 +75,7 @@ internal static class Pkcs11ToolBridge
var args = new List<string>
{
"--module",
options.LibraryPath,
options.ResolvedLibraryPath,
"--login",
"--pin",
options.UserPin
@@ -101,7 +101,7 @@ internal static class Pkcs11ToolBridge
{
StartInfo = new ProcessStartInfo
{
FileName = options.ToolPath!,
FileName = options.ResolvedToolPath!,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
@@ -136,7 +136,7 @@ internal static class Pkcs11ToolBridge
private static void EnsureAvailable(Pkcs11Options options)
{
if (!IsAvailable(options))
throw new InvalidOperationException("pkcs11-tool is not available. Configure Crypt:Pkcs11:ToolPath.");
throw new InvalidOperationException("pkcs11-tool is not available at resolved pkcs11 assets path.");
}
private static void DeleteIfExists(string path)