diff --git a/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs b/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs
index f2a8f092..a6ad386f 100644
--- a/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs
+++ b/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs
@@ -15,11 +15,10 @@ public sealed class Pkcs11Options
public const string SectionName = "Crypt:Pkcs11";
///
- /// Absolute or resolvable file path of the PKCS#11 provider library.
+ /// Optional base directory that contains the pkcs11 folder.
+ /// If null/empty, runtime uses the application base directory.
///
- [Required]
- [MinLength(1)]
- public required string LibraryPath { get; set; }
+ public string? AssetsRootPath { get; set; }
///
/// Optional numeric slot identifier used to directly select the target token slot.
@@ -37,9 +36,23 @@ public sealed class Pkcs11Options
public string? ProxySocket { get; set; }
///
- /// Optional full path to pkcs11-tool, used by diagnostics and fallback paths.
+ /// Resolved absolute PKCS#11 proxy module path used at runtime.
+ /// Computed from and runtime defaults.
///
- public string? ToolPath { get; set; }
+ public string ResolvedLibraryPath { get; private set; } = string.Empty;
+
+ ///
+ /// Resolved absolute pkcs11-tool path used at runtime.
+ /// Computed from and runtime defaults.
+ /// Can be null when tool fallback is not available.
+ ///
+ public string? ResolvedToolPath { get; private set; }
+
+ public void SetResolvedPaths(string resolvedLibraryPath, string? resolvedToolPath)
+ {
+ ResolvedLibraryPath = resolvedLibraryPath;
+ ResolvedToolPath = resolvedToolPath;
+ }
///
/// User PIN used to authenticate to the PKCS#11 token session.
diff --git a/EnvelopeGenerator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs b/EnvelopeGenerator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs
new file mode 100644
index 00000000..43df76ba
--- /dev/null
+++ b/EnvelopeGenerator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs
@@ -0,0 +1,10 @@
+namespace EnvelopeGenerator.Application.Common.Dto;
+
+public sealed record Pkcs11SelfTestStatusDto(
+ bool Completed,
+ bool Success,
+ string Mode,
+ string Summary,
+ DateTimeOffset? CheckedAtUtc,
+ string? InteropError,
+ string? ToolError);
diff --git a/EnvelopeGenerator.Application/Common/Interfaces/Services/IPkcs11SelfTestStatusProvider.cs b/EnvelopeGenerator.Application/Common/Interfaces/Services/IPkcs11SelfTestStatusProvider.cs
new file mode 100644
index 00000000..693b7d02
--- /dev/null
+++ b/EnvelopeGenerator.Application/Common/Interfaces/Services/IPkcs11SelfTestStatusProvider.cs
@@ -0,0 +1,8 @@
+using EnvelopeGenerator.Application.Common.Dto;
+
+namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
+
+public interface IPkcs11SelfTestStatusProvider
+{
+ Pkcs11SelfTestStatusDto GetStatus();
+}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/DependencyInjection/ServiceCollectionExtensions.cs b/EnvelopeGenerator.Infrastructure.Crypt/DependencyInjection/ServiceCollectionExtensions.cs
index de0a8718..b1320e8c 100644
--- a/EnvelopeGenerator.Infrastructure.Crypt/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/EnvelopeGenerator.Infrastructure.Crypt/DependencyInjection/ServiceCollectionExtensions.cs
@@ -16,7 +16,11 @@ public static class ServiceCollectionExtensions
.Bind(configuration.GetSection(Pkcs11Options.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
+
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddHostedService();
return services;
}
}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/EnvelopeGenerator.Infrastructure.Crypt.csproj b/EnvelopeGenerator.Infrastructure.Crypt/EnvelopeGenerator.Infrastructure.Crypt.csproj
index 5d2d97dd..fb0fc085 100644
--- a/EnvelopeGenerator.Infrastructure.Crypt/EnvelopeGenerator.Infrastructure.Crypt.csproj
+++ b/EnvelopeGenerator.Infrastructure.Crypt/EnvelopeGenerator.Infrastructure.Crypt.csproj
@@ -12,6 +12,7 @@
+
@@ -24,4 +25,11 @@
+
+
+ PreserveNewest
+ pkcs11\%(RecursiveDir)%(Filename)%(Extension)
+
+
+
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11InteropBootstrap.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11InteropBootstrap.cs
index 51f88997..833a2516 100644
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11InteropBootstrap.cs
+++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11InteropBootstrap.cs
@@ -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,
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SelfTestStatusStore.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SelfTestStatusStore.cs
new file mode 100644
index 00000000..5dfddb1b
--- /dev/null
+++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SelfTestStatusStore.cs
@@ -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);
+ }
+}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SessionRunner.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SessionRunner.cs
index c03b3ffe..0d53ef8a 100644
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SessionRunner.cs
+++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SessionRunner.cs
@@ -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);
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11StartupSelfTestHostedService.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11StartupSelfTestHostedService.cs
new file mode 100644
index 00000000..74d6e2ac
--- /dev/null
+++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11StartupSelfTestHostedService.cs
@@ -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 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.");
+ }
+}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11ToolBridge.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11ToolBridge.cs
index 8fb26978..9c564763 100644
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11ToolBridge.cs
+++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11ToolBridge.cs
@@ -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
{
"--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)
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HealthController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HealthController.cs
new file mode 100644
index 00000000..6dc171cf
--- /dev/null
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HealthController.cs
@@ -0,0 +1,18 @@
+using EnvelopeGenerator.Application.Common.Interfaces.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace EnvelopeGenerator.Server.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class HealthController(IPkcs11SelfTestStatusProvider pkcs11StatusProvider) : ControllerBase
+{
+ [AllowAnonymous]
+ [HttpGet("crypt")]
+ public IActionResult GetCryptHealth()
+ {
+ var status = pkcs11StatusProvider.GetStatus();
+ return Ok(status);
+ }
+}
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json
index 3940dae0..461e53a4 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json
@@ -1,10 +1,16 @@
{
-"Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
}
-},
+ ,
+ "Crypt": {
+ "Pkcs11": {
+ "AssetsRootPath": null
+ }
+ },
"AuthClientParams": {
"Url": "http://172.24.12.39:9090/auth-hub",
"PublicKeys": [
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
index 420fc04c..2462a81f 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
@@ -57,11 +57,10 @@
},
"Crypt": {
"Pkcs11": {
- "LibraryPath": "C:\\PKCS11\\libpkcs11-proxy.dll",
+ "AssetsRootPath": "E:\\DataFiles\\Digital Data\\EnvelopeGenerator",
"SlotId": 2021549410,
"TokenLabel": "CompanyProdToken",
"ProxySocket": "tcp://172.24.12.64:5657",
- "ToolPath": "C:\\Users\\tekh\\tools\\opensc\\PFiles64\\OpenSC Project\\OpenSC\\tools\\pkcs11-tool.exe",
"UserPin": "",
"PrivateKeyLabel": "company-sign-key-v1",
"CertificateLabel": "company-sign-cert-v1",