Add PKCS#11-based PDF signing infrastructure

Introduced a cryptographic infrastructure for PDF signing and validation using PKCS#11-based hardware security modules (HSMs) or software tokens. Key changes include:

- Added `PdfDigitalSignatureService` for signing PDFs, multi-signature support, and signature validation.
- Implemented `Pkcs11ExternalSigner` and `Pkcs11PdfSigningRsa` for PKCS#11-based signing with DevExpress PDF infrastructure.
- Added utilities like `Pkcs11SessionRunner`, `Pkcs11InteropBootstrap`, and `Pkcs11ToolBridge` for session management, library initialization, and fallback mechanisms.
- Introduced `Pkcs11Options` for managing PKCS#11 configurations.
- Updated project dependencies to include `DevExpress.Document.Processor`, `System.Security.Cryptography.Pkcs`, and `Pkcs11Interop`.
- Enhanced error handling and fallback support for PKCS#11 operations.

These changes enable robust and extensible PDF signing capabilities with detailed signature metadata extraction and validation.
This commit is contained in:
2026-10-13 14:45:47 +02:00
parent 7766e2ea25
commit f361898ba4
9 changed files with 897 additions and 0 deletions

View File

@@ -0,0 +1,353 @@
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Enums;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using DevExpress.Office.DigitalSignatures;
using DevExpress.Pdf;
using Microsoft.Extensions.Options;
using System.Formats.Asn1;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options) : IPdfDigitalSignatureService
{
public Task<byte[]> SignPdfAsync(byte[] pdfBytes, PdfSignOptionsDto? optionsModel = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfBytes);
var pkcs11Options = options.Value;
var request = optionsModel ?? new PdfSignOptionsDto();
var fallbackSigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
using var inputPdf = new MemoryStream(pdfBytes);
using var signer = new PdfDocumentSigner(inputPdf);
var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256);
var signerInfo = new ExternalSignerInfo(PdfSignatureType.Pkcs7, 32768, digestCalculator);
var signatureFieldInfo = new PdfSignatureFieldInfo(1)
{
Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"
};
var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo)
{
Name = request.Name ?? pkcs11Options.CertificateLabel,
Reason = request.Reason ?? pkcs11Options.DefaultReason,
Location = request.Location,
ContactInfo = request.ContactInfo,
SigningTime = request.SigningTime ?? fallbackSigningTime,
ApplicationName = request.ApplicationName ?? pkcs11Options.DefaultApplicationName,
CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel)
?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel)
?? PdfCertificationLevel.NoCertification
};
var deferredSigner = signer.SignDeferred(builder);
var externalSigner = new Pkcs11ExternalSigner(pkcs11Options);
byte[] signature = externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid);
using var outputPdf = new MemoryStream();
deferredSigner.Sign(outputPdf, signature);
return Task.FromResult(outputPdf.ToArray());
}
public async Task<byte[]> SignPdfAsync(byte[] pdfBytes, IReadOnlyList<PdfSignOptionsDto> optionsList, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfBytes);
ArgumentNullException.ThrowIfNull(optionsList);
if (optionsList.Count == 0)
return pdfBytes;
var currentPdf = pdfBytes;
foreach (var signOptions in optionsList)
{
cancellationToken.ThrowIfCancellationRequested();
currentPdf = await SignPdfAsync(currentPdf, signOptions, cancellationToken);
}
return currentPdf;
}
public Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfBytes);
using var inputPdf = new MemoryStream(pdfBytes);
using var signer = new PdfDocumentSigner(inputPdf);
var signatures = signer.GetSignatureInfo();
if (signatures is null || signatures.Count == 0)
{
return Task.FromResult(new PdfSignatureCheckResultDto(
Signed: false,
Signatures: []));
}
var items = new List<PdfSignatureCheckItemDto>(signatures.Count);
foreach (var signature in signatures)
{
try
{
var pkcs7 = signer.GetPdfPkcs7Signature(signature.FieldName);
bool valid = pkcs7.VerifySignature();
bool coversWholeDocument = true;
var certificate = pkcs7.GetSignatureCertificate();
var now = DateTimeOffset.UtcNow;
var serialNumber = ToColonSerial(certificate?.SerialNumber);
DateTimeOffset? validFrom = certificate is null ? null : new DateTimeOffset(certificate.NotBefore).ToUniversalTime();
DateTimeOffset? validTo = certificate is null ? null : new DateTimeOffset(certificate.NotAfter).ToUniversalTime();
var issuedTo = ParsePrincipal(certificate?.Subject);
var issuedBy = ParsePrincipal(certificate?.Issuer);
var certificateInfo = BuildCertificateInfo(certificate, now);
var digestAlgorithm = InferDigestAlgorithm(certificate);
var signatureAlgorithm = InferSignatureAlgorithm(certificate);
var signerDisplayName = issuedTo.CommonName ?? signature.SignerName;
var messageDigestHex = TryExtractMessageDigestHex(signature.Contents);
items.Add(new PdfSignatureCheckItemDto(
CoversWholeDoc: coversWholeDocument,
Reason: signature.Reason,
Location: signature.Location,
DeclaredName: signature.SignerName,
ContactInfo: signature.ContactInfo,
DigestAlgorithm: digestAlgorithm,
SignatureAlgorithm: signatureAlgorithm,
SignerName: signerDisplayName,
Issuer: issuedBy.Full,
SerialNumber: serialNumber,
ValidFrom: validFrom,
ValidTo: validTo,
ExpiryCheckedAgainst: "now",
Certificates: [certificateInfo],
Unchanged: valid,
FieldName: signature.FieldName,
IsCryptographicallyValid: valid,
SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"),
Filter: signature.Filter,
SubFilter: signature.SubFilter,
ApplicationName: null,
MessageDigestHex: messageDigestHex,
Error: null));
}
catch (Exception ex)
{
items.Add(new PdfSignatureCheckItemDto(
CoversWholeDoc: false,
Reason: signature.Reason,
Location: signature.Location,
DeclaredName: signature.SignerName,
ContactInfo: signature.ContactInfo,
DigestAlgorithm: null,
SignatureAlgorithm: null,
SignerName: signature.SignerName,
Issuer: null,
SerialNumber: null,
ValidFrom: null,
ValidTo: null,
ExpiryCheckedAgainst: "now",
Certificates: [],
Unchanged: false,
FieldName: signature.FieldName,
IsCryptographicallyValid: false,
SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"),
Filter: signature.Filter,
SubFilter: signature.SubFilter,
ApplicationName: null,
MessageDigestHex: null,
Error: ex.Message));
}
}
return Task.FromResult(new PdfSignatureCheckResultDto(
Signed: true,
Signatures: items));
}
private static PdfSignatureCertificateInfoDto BuildCertificateInfo(X509Certificate2? certificate, DateTimeOffset now)
{
if (certificate is null)
{
return new PdfSignatureCertificateInfoDto(
IssuedTo: new PdfSignaturePrincipalDto(null, null, null, null),
IssuedBy: new PdfSignaturePrincipalDto(null, null, null, null),
SerialNumber: null,
ValidFrom: null,
ValidTo: null,
SelfSigned: false,
Expired: false,
NotYetValid: false,
IsSigner: true,
Version: null,
KeyAlgorithm: null,
SignatureAlgorithm: null,
Pem: null,
KeySizeBits: null);
}
var issuedTo = ParsePrincipal(certificate.Subject);
var issuedBy = ParsePrincipal(certificate.Issuer);
var validFrom = new DateTimeOffset(certificate.NotBefore).ToUniversalTime();
var validTo = new DateTimeOffset(certificate.NotAfter).ToUniversalTime();
return new PdfSignatureCertificateInfoDto(
IssuedTo: issuedTo,
IssuedBy: issuedBy,
SerialNumber: ToColonSerial(certificate.SerialNumber),
ValidFrom: validFrom,
ValidTo: validTo,
SelfSigned: string.Equals(certificate.Subject, certificate.Issuer, StringComparison.OrdinalIgnoreCase),
Expired: validTo < now,
NotYetValid: validFrom > now,
IsSigner: true,
Version: certificate.Version,
KeyAlgorithm: certificate.PublicKey.Oid?.FriendlyName ?? certificate.PublicKey.Oid?.Value,
SignatureAlgorithm: certificate.SignatureAlgorithm.FriendlyName ?? certificate.SignatureAlgorithm.Value,
Pem: ToPem(certificate.RawData),
KeySizeBits: TryGetKeySize(certificate));
}
private static int? TryGetKeySize(X509Certificate2 certificate)
{
try
{
return certificate.PublicKey.Key.KeySize;
}
catch
{
return null;
}
}
private static string? InferDigestAlgorithm(X509Certificate2? certificate)
{
if (certificate?.SignatureAlgorithm is null)
return null;
var source = (certificate.SignatureAlgorithm.FriendlyName ?? certificate.SignatureAlgorithm.Value ?? string.Empty).ToUpperInvariant();
if (source.Contains("SHA512", StringComparison.Ordinal)) return "SHA-512";
if (source.Contains("SHA384", StringComparison.Ordinal)) return "SHA-384";
if (source.Contains("SHA256", StringComparison.Ordinal)) return "SHA-256";
if (source.Contains("SHA1", StringComparison.Ordinal)) return "SHA-1";
return certificate.SignatureAlgorithm.FriendlyName ?? certificate.SignatureAlgorithm.Value;
}
private static string? InferSignatureAlgorithm(X509Certificate2? certificate)
{
var oidName = certificate?.PublicKey.Oid?.FriendlyName?.ToUpperInvariant();
if (oidName is not null && oidName.Contains("RSA", StringComparison.Ordinal))
return "RSA";
if (oidName is not null && oidName.Contains("ECDSA", StringComparison.Ordinal))
return "ECDSA";
return certificate?.PublicKey.Oid?.FriendlyName ?? certificate?.PublicKey.Oid?.Value;
}
private static PdfSignaturePrincipalDto ParsePrincipal(string? dn)
{
if (string.IsNullOrWhiteSpace(dn))
return new PdfSignaturePrincipalDto(null, null, null, dn);
string? commonName = null;
string? organization = null;
string? country = null;
foreach (var part in dn.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
var equalsIndex = part.IndexOf('=');
if (equalsIndex <= 0 || equalsIndex >= part.Length - 1)
continue;
var key = part[..equalsIndex].Trim();
var value = part[(equalsIndex + 1)..].Trim();
if (key.Equals("CN", StringComparison.OrdinalIgnoreCase)) commonName = value;
else if (key.Equals("O", StringComparison.OrdinalIgnoreCase)) organization = value;
else if (key.Equals("C", StringComparison.OrdinalIgnoreCase)) country = value;
}
return new PdfSignaturePrincipalDto(commonName, organization, country, dn);
}
private static PdfCertificationLevel? ToDevExpressCertificationLevel(PdfSignatureCertificationLevel? value)
{
return value is null ? null : (PdfCertificationLevel)(int)value.Value;
}
private static string? ToColonSerial(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var normalized = raw.Replace(":", string.Empty, StringComparison.Ordinal).Trim();
if (normalized.Length % 2 != 0)
return normalized;
return string.Join(':', Enumerable.Range(0, normalized.Length / 2).Select(i => normalized.Substring(i * 2, 2).ToUpperInvariant()));
}
private static string ToPem(byte[] der)
{
var base64 = Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks);
var sb = new StringBuilder();
sb.AppendLine("-----BEGIN CERTIFICATE-----");
sb.AppendLine(base64);
sb.Append("-----END CERTIFICATE-----");
return sb.ToString();
}
private static string? TryExtractMessageDigestHex(byte[]? signatureContents)
{
if (signatureContents is null || signatureContents.Length == 0)
return null;
try
{
var cms = new SignedCms();
cms.Decode(signatureContents);
var signerInfo = cms.SignerInfos.Count > 0 ? cms.SignerInfos[0] : null;
if (signerInfo is null)
return null;
foreach (CryptographicAttributeObject attr in signerInfo.SignedAttributes)
{
if (!string.Equals(attr.Oid?.Value, "1.2.840.113549.1.9.4", StringComparison.Ordinal))
continue;
foreach (var value in attr.Values)
{
var octets = TryReadOctetString(value.RawData);
if (octets is not null && octets.Length > 0)
return Convert.ToHexString(octets);
}
}
}
catch
{
return null;
}
return null;
}
private static byte[]? TryReadOctetString(byte[] rawData)
{
try
{
var reader = new AsnReader(rawData, AsnEncodingRules.BER);
var octets = reader.ReadOctetString();
if (reader.HasData)
return null;
return octets;
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,43 @@
using System.Security.Cryptography;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
internal static class Pkcs11DigestInfoBuilder
{
public static byte[] Build(byte[] hash, HashAlgorithmName hashAlgorithm)
{
byte[] prefix = hashAlgorithm.Name switch
{
"SHA256" =>
[
0x30, 0x31,
0x30, 0x0d,
0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
0x05, 0x00,
0x04, 0x20
],
"SHA384" =>
[
0x30, 0x41,
0x30, 0x0d,
0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02,
0x05, 0x00,
0x04, 0x30
],
"SHA512" =>
[
0x30, 0x51,
0x30, 0x0d,
0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03,
0x05, 0x00,
0x04, 0x40
],
_ => throw new NotSupportedException($"Hash algorithm '{hashAlgorithm.Name}' is not supported.")
};
var digestInfo = new byte[prefix.Length + hash.Length];
Buffer.BlockCopy(prefix, 0, digestInfo, 0, prefix.Length);
Buffer.BlockCopy(hash, 0, digestInfo, prefix.Length, hash.Length);
return digestInfo;
}
}

View File

@@ -0,0 +1,49 @@
using EnvelopeGenerator.Application.Common.Configurations;
using DevExpress.Office.DigitalSignatures;
using DevExpress.Pdf;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
internal sealed class Pkcs11ExternalSigner(Pkcs11Options options) : Pkcs7SignerBase
{
private readonly Pkcs11PdfSigningRsa rsa = new(options);
private readonly DigestCalculator digestCalculator = new(HashAlgorithmType.SHA256);
private readonly IReadOnlyList<byte[]> certificates = [ReadCertificateBytesWithFallback(options)];
protected override IDigestCalculator DigestCalculator => digestCalculator;
protected override string SigningAlgorithmOID => "1.2.840.113549.1.1.1";
protected override IEnumerable<byte[]> GetCertificates() => certificates;
protected override byte[] SignDigest(byte[] digest)
{
return rsa.SignHash(digest, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1);
}
private static byte[] ReadCertificateBytesWithFallback(Pkcs11Options options)
{
try
{
return Pkcs11SessionRunner.Run(options, session =>
{
var attrs = new List<Net.Pkcs11Interop.HighLevelAPI.IObjectAttribute>
{
session.Factories.ObjectAttributeFactory.Create(Net.Pkcs11Interop.Common.CKA.CKA_CLASS, Net.Pkcs11Interop.Common.CKO.CKO_CERTIFICATE),
session.Factories.ObjectAttributeFactory.Create(Net.Pkcs11Interop.Common.CKA.CKA_LABEL, options.CertificateLabel)
};
var handles = session.FindAllObjects(attrs);
if (handles.Count == 0)
throw new InvalidOperationException($"Certificate '{options.CertificateLabel}' not found in token.");
var values = session.GetAttributeValue(handles[0], new List<Net.Pkcs11Interop.Common.CKA> { Net.Pkcs11Interop.Common.CKA.CKA_VALUE });
return values[0].GetValueAsByteArray();
});
}
catch when (Pkcs11ToolBridge.IsAvailable(options))
{
return Pkcs11ToolBridge.ReadCertificateBytes(options);
}
}
}

View File

@@ -0,0 +1,87 @@
using EnvelopeGenerator.Application.Common.Configurations;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
internal static class Pkcs11InteropBootstrap
{
private static readonly object Sync = new();
private static bool Initialized;
public static void EnsureLoaded(Pkcs11Options options)
{
if (Initialized)
return;
lock (Sync)
{
if (Initialized)
return;
EnsureNativeDependencySearchPath(options.LibraryPath);
if (!string.IsNullOrWhiteSpace(options.ProxySocket))
{
Environment.SetEnvironmentVariable("PKCS11_PROXY_SOCKET", options.ProxySocket);
Environment.SetEnvironmentVariable("PKCS11_DAEMON_SOCKET", options.ProxySocket);
}
if (!string.IsNullOrWhiteSpace(options.ToolPath))
{
var toolDir = Path.GetDirectoryName(options.ToolPath);
if (!string.IsNullOrWhiteSpace(toolDir))
EnsurePathContains(toolDir);
}
WarmupWithPkcs11Tool(options);
Initialized = true;
}
}
private static void WarmupWithPkcs11Tool(Pkcs11Options options)
{
if (string.IsNullOrWhiteSpace(options.ToolPath))
return;
if (!File.Exists(options.ToolPath))
return;
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = options.ToolPath,
Arguments = $"--module \"{options.LibraryPath}\" -L",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit(15000);
}
private static void EnsureNativeDependencySearchPath(string libraryPath)
{
var directory = Path.GetDirectoryName(libraryPath);
if (string.IsNullOrWhiteSpace(directory))
return;
EnsurePathContains(directory);
}
private static void EnsurePathContains(string directory)
{
var currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
var entries = currentPath.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (entries.Any(p => string.Equals(p, directory, StringComparison.OrdinalIgnoreCase)))
return;
var updatedPath = string.IsNullOrWhiteSpace(currentPath)
? directory
: directory + ";" + currentPath;
Environment.SetEnvironmentVariable("PATH", updatedPath);
}
}

View File

@@ -0,0 +1,64 @@
using EnvelopeGenerator.Application.Common.Configurations;
using Net.Pkcs11Interop.Common;
using Net.Pkcs11Interop.HighLevelAPI;
using System.Security.Cryptography;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
internal sealed class Pkcs11PdfSigningRsa(Pkcs11Options options) : RSA
{
public override int KeySize
{
get => KeySizeValue;
set => KeySizeValue = value;
}
public override KeySizes[] LegalKeySizes => [new(1024, 8192, 8)];
public override string? KeyExchangeAlgorithm => "RSA";
public override string SignatureAlgorithm => "RSA";
public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
{
if (padding != RSASignaturePadding.Pkcs1)
throw new NotSupportedException("Only RSA PKCS#1 v1.5 padding is supported.");
byte[] digestInfo = Pkcs11DigestInfoBuilder.Build(hash, hashAlgorithm);
try
{
return Pkcs11SessionRunner.Run(options, session =>
{
var attrs = new List<IObjectAttribute>
{
session.Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_PRIVATE_KEY),
session.Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, options.PrivateKeyLabel)
};
var handles = session.FindAllObjects(attrs);
if (handles.Count == 0)
throw new InvalidOperationException($"Private key '{options.PrivateKeyLabel}' not found in token.");
var mechanism = session.Factories.MechanismFactory.Create(CKM.CKM_RSA_PKCS);
return session.Sign(mechanism, handles[0], digestInfo);
});
}
catch when (Pkcs11ToolBridge.IsAvailable(options))
{
return Pkcs11ToolBridge.SignDigestInfo(options, digestInfo);
}
}
public override bool VerifyHash(byte[] hash, byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
=> throw new NotSupportedException();
public override byte[] Decrypt(byte[] data, RSAEncryptionPadding padding)
=> throw new NotSupportedException();
public override byte[] Encrypt(byte[] data, RSAEncryptionPadding padding)
=> throw new NotSupportedException();
public override RSAParameters ExportParameters(bool includePrivateParameters)
=> throw new NotSupportedException();
public override void ImportParameters(RSAParameters parameters)
=> throw new NotSupportedException();
}

View File

@@ -0,0 +1,124 @@
using EnvelopeGenerator.Application.Common.Configurations;
using Net.Pkcs11Interop.Common;
using Net.Pkcs11Interop.HighLevelAPI;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
internal static class Pkcs11SessionRunner
{
public static T Run<T>(Pkcs11Options options, Func<ISession, T> action)
{
Pkcs11InteropBootstrap.EnsureLoaded(options);
var factories = new Pkcs11InteropFactories();
using var pkcs11 = LoadLibraryWithFallback(factories, options.LibraryPath, options.ProxySocket);
var slot = ResolveSlot(pkcs11.GetSlotList(SlotsType.WithTokenPresent), options);
using var session = slot.OpenSession(SessionType.ReadOnly);
session.Login(CKU.CKU_USER, options.UserPin);
try
{
return action(session);
}
finally
{
session.Logout();
}
}
private static IPkcs11Library LoadLibraryWithFallback(Pkcs11InteropFactories factories, string libraryPath, string? proxySocket)
{
List<Exception> errors = [];
foreach (string candidate in BuildLibraryCandidates(libraryPath))
{
try
{
return factories.Pkcs11LibraryFactory.LoadPkcs11Library(factories, candidate, AppType.MultiThreaded);
}
catch (Exception ex)
{
errors.Add(new InvalidOperationException($"{candidate} | MultiThreaded+WithFunctionList", ex));
}
try
{
return factories.Pkcs11LibraryFactory.LoadPkcs11Library(factories, candidate, AppType.SingleThreaded);
}
catch (Exception ex)
{
errors.Add(new InvalidOperationException($"{candidate} | SingleThreaded+WithFunctionList", ex));
}
try
{
return factories.Pkcs11LibraryFactory.LoadPkcs11Library(factories, candidate, AppType.MultiThreaded, InitType.WithoutFunctionList);
}
catch (Exception ex)
{
errors.Add(new InvalidOperationException($"{candidate} | MultiThreaded+WithoutFunctionList", ex));
}
try
{
return factories.Pkcs11LibraryFactory.LoadPkcs11Library(factories, candidate, AppType.SingleThreaded, InitType.WithoutFunctionList);
}
catch (Exception ex)
{
errors.Add(new InvalidOperationException($"{candidate} | SingleThreaded+WithoutFunctionList", ex));
}
}
var socketInfo = string.IsNullOrWhiteSpace(proxySocket) ? "<empty>" : proxySocket;
throw new AggregateException($"Unable to initialize PKCS#11 library '{libraryPath}' with any supported initialization mode. PKCS11_PROXY_SOCKET={socketInfo}", errors);
}
private static IEnumerable<string> BuildLibraryCandidates(string configuredPath)
{
var unique = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
configuredPath
};
var directory = Path.GetDirectoryName(configuredPath);
if (!string.IsNullOrWhiteSpace(directory))
{
string[] alternates =
[
"client.dll",
"pkcs11-proxy.dll",
"libpkcs11-proxy.dll"
];
foreach (var fileName in alternates)
{
var candidate = Path.Combine(directory, fileName);
if (File.Exists(candidate))
unique.Add(candidate);
}
}
return unique;
}
private static ISlot ResolveSlot(IEnumerable<ISlot> slots, Pkcs11Options options)
{
if (options.SlotId.HasValue)
{
var bySlotId = slots.FirstOrDefault(s => s.SlotId == options.SlotId.Value);
if (bySlotId is not null)
return bySlotId;
}
if (!string.IsNullOrWhiteSpace(options.TokenLabel))
{
var byTokenLabel = slots.FirstOrDefault(s =>
string.Equals(s.GetTokenInfo().Label?.Trim(), options.TokenLabel.Trim(), StringComparison.Ordinal));
if (byTokenLabel is not null)
return byTokenLabel;
}
throw new InvalidOperationException("Unable to resolve PKCS#11 slot. Check Crypt:Pkcs11:SlotId / TokenLabel.");
}
}

View File

@@ -0,0 +1,147 @@
using EnvelopeGenerator.Application.Common.Configurations;
using System.Diagnostics;
using System.Security.Cryptography;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
internal static class Pkcs11ToolBridge
{
public static bool IsAvailable(Pkcs11Options options)
{
return !string.IsNullOrWhiteSpace(options.ToolPath) && File.Exists(options.ToolPath);
}
public static byte[] ReadCertificateBytes(Pkcs11Options options)
{
EnsureAvailable(options);
var outputFile = Path.Combine(Path.GetTempPath(), $"pkcs11-cert-{Guid.NewGuid():N}.der");
try
{
var args = CreateBaseArgs(options);
args.Add("--read-object");
args.Add("--type");
args.Add("cert");
args.Add("--label");
args.Add(options.CertificateLabel);
args.Add("--output-file");
args.Add(outputFile);
Execute(options, args);
return File.ReadAllBytes(outputFile);
}
finally
{
DeleteIfExists(outputFile);
}
}
public static byte[] SignDigestInfo(Pkcs11Options options, byte[] digestInfo)
{
EnsureAvailable(options);
var inputFile = Path.Combine(Path.GetTempPath(), $"pkcs11-sign-in-{Guid.NewGuid():N}.bin");
var outputFile = Path.Combine(Path.GetTempPath(), $"pkcs11-sign-out-{Guid.NewGuid():N}.bin");
try
{
File.WriteAllBytes(inputFile, digestInfo);
var args = CreateBaseArgs(options);
args.Add("--sign");
args.Add("--mechanism");
args.Add("RSA-PKCS");
args.Add("--type");
args.Add("privkey");
args.Add("--label");
args.Add(options.PrivateKeyLabel);
args.Add("--input-file");
args.Add(inputFile);
args.Add("--output-file");
args.Add(outputFile);
Execute(options, args);
return File.ReadAllBytes(outputFile);
}
finally
{
DeleteIfExists(inputFile);
DeleteIfExists(outputFile);
}
}
private static List<string> CreateBaseArgs(Pkcs11Options options)
{
var args = new List<string>
{
"--module",
options.LibraryPath,
"--login",
"--pin",
options.UserPin
};
if (options.SlotId.HasValue)
{
args.Add("--slot");
args.Add(options.SlotId.Value.ToString());
}
else if (!string.IsNullOrWhiteSpace(options.TokenLabel))
{
args.Add("--token-label");
args.Add(options.TokenLabel);
}
return args;
}
private static void Execute(Pkcs11Options options, IReadOnlyList<string> arguments)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = options.ToolPath!,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
foreach (var argument in arguments)
process.StartInfo.ArgumentList.Add(argument);
if (!string.IsNullOrWhiteSpace(options.ProxySocket))
{
process.StartInfo.Environment["PKCS11_PROXY_SOCKET"] = options.ProxySocket;
process.StartInfo.Environment["PKCS11_DAEMON_SOCKET"] = options.ProxySocket;
}
process.Start();
string stdOut = process.StandardOutput.ReadToEnd();
string stdErr = process.StandardError.ReadToEnd();
if (!process.WaitForExit(30000))
{
try { process.Kill(entireProcessTree: true); } catch { }
throw new TimeoutException($"pkcs11-tool timed out. Args: {string.Join(' ', process.StartInfo.ArgumentList)}");
}
if (process.ExitCode != 0)
throw new InvalidOperationException($"pkcs11-tool failed with exit code {process.ExitCode}. StdOut: {stdOut} StdErr: {stdErr}");
}
private static void EnsureAvailable(Pkcs11Options options)
{
if (!IsAvailable(options))
throw new InvalidOperationException("pkcs11-tool is not available. Configure Crypt:Pkcs11:ToolPath.");
}
private static void DeleteIfExists(string path)
{
if (File.Exists(path))
File.Delete(path);
}
}