diff --git a/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs b/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs
deleted file mode 100644
index 5215cc71..00000000
--- a/EnvelopeGenerator.Application/Common/Configurations/Pkcs11Options.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using EnvelopeGenerator.Application.Common.Enums;
-using System.ComponentModel.DataAnnotations;
-
-namespace EnvelopeGenerator.Application.Common.Configurations;
-
-///
-/// Represents PKCS#11 signing configuration for PDF signing workflows.
-/// This options model maps to the Crypt:Pkcs11 configuration section.
-///
-public sealed class Pkcs11Options
-{
- ///
- /// Configuration section path used for binding PKCS#11 options.
- ///
- public const string SectionName = "Crypt:Pkcs11";
-
- ///
- /// Optional base directory that contains the pkcs11 folder.
- /// If null/empty, runtime uses the application base directory.
- ///
- public string? AssetsRootPath { get; set; }
-
- ///
- /// Optional numeric slot identifier used to directly select the target token slot.
- ///
- public ulong? SlotId { get; set; }
-
- ///
- /// Optional token label used as a fallback token selector when is not provided.
- ///
- public string? TokenLabel { get; set; }
-
- ///
- /// Optional PKCS#11 proxy endpoint (for example tcp://host:port).
- ///
- public string? ProxySocket { get; set; }
-
- ///
- /// Resolved absolute PKCS#11 proxy module path used at runtime.
- /// Computed from and runtime defaults.
- ///
- 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.
- ///
- [Required]
- [MinLength(1)]
- public required string UserPin { get; set; }
-
- ///
- /// Label of the private key object used to create digital signatures.
- ///
- [Required]
- [MinLength(1)]
- public required string PrivateKeyLabel { get; set; }
-
- ///
- /// Label of the certificate object returned/embedded with the signature.
- ///
- [Required]
- [MinLength(1)]
- public required string CertificateLabel { get; set; }
-
- ///
- /// Optional global default signing reason used when request-level options do not specify a value.
- ///
- public string? DefaultReason { get; set; }
-
- ///
- /// Application name embedded into PDF signature metadata.
- /// This value is controlled via app settings and is not client-overridable.
- ///
- public string? ApplicationName { get; set; }
-
- ///
- /// Controls how fallback signing time is created when request-level options do not specify a value.
- /// true uses , false uses .
- ///
- public bool UseUtcSigningTime { get; set; } = true;
-}
diff --git a/EnvelopeGenerator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs b/EnvelopeGenerator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs
deleted file mode 100644
index 43df76ba..00000000
--- a/EnvelopeGenerator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index 693b7d02..00000000
--- a/EnvelopeGenerator.Application/Common/Interfaces/Services/IPkcs11SelfTestStatusProvider.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using EnvelopeGenerator.Application.Common.Dto;
-
-namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
-
-public interface IPkcs11SelfTestStatusProvider
-{
- Pkcs11SelfTestStatusDto GetStatus();
-}
diff --git a/EnvelopeGenerator.Application/PdfSigning/FinalizeEnvelopeCommand.cs b/EnvelopeGenerator.Application/PdfSigning/FinalizeEnvelopeCommand.cs
index 67ac39a5..7c082981 100644
--- a/EnvelopeGenerator.Application/PdfSigning/FinalizeEnvelopeCommand.cs
+++ b/EnvelopeGenerator.Application/PdfSigning/FinalizeEnvelopeCommand.cs
@@ -44,6 +44,7 @@ public record FinalizeEnvelopeCommand : EnvelopeQueryBase, IRequest
///
///
///
+///
public class FinalizeEnvelopeCommandHandler(IRepository Repo, ISender Sender, IEnvelopeNotificationService NtfServer) : IRequestHandler
{
///
@@ -90,7 +91,7 @@ public class FinalizeEnvelopeCommandHandler(IRepository Repo, ISender
using var ms = new MemoryStream();
docStream.CopyTo(ms);
byte[] doc = ms.ToArray();
-
+ string base64 = Convert.ToBase64String(doc);
foreach (var er in envelope.EnvelopeReceivers)
{
await NtfServer.Notify(er.Receiver.EmailAddress, new DocumentSignedNotification()
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/DependencyInjection/ServiceCollectionExtensions.cs b/EnvelopeGenerator.Infrastructure.Crypt/DependencyInjection/ServiceCollectionExtensions.cs
index b1320e8c..6b51a23b 100644
--- a/EnvelopeGenerator.Infrastructure.Crypt/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/EnvelopeGenerator.Infrastructure.Crypt/DependencyInjection/ServiceCollectionExtensions.cs
@@ -1,9 +1,9 @@
-using EnvelopeGenerator.Application.Common.Configurations;
+using DocumentService.Client.Configuration;
+using DocumentService.Client.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Infrastructure.Crypt.Services;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Options;
+using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Infrastructure.Crypt.DependencyInjection;
@@ -11,16 +11,13 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCryptInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
- services
- .AddOptions()
- .Bind(configuration.GetSection(Pkcs11Options.SectionName))
- .ValidateDataAnnotations()
- .ValidateOnStart();
+ var baseUrl = configuration["DocumentService:BaseUrl"]
+ ?? throw new InvalidOperationException("DocumentService:BaseUrl is not configured.");
+
+ services.AddDocumentServiceClients(options => options.BaseUrl = baseUrl);
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 8de3c60e..1ef00efb 100644
--- a/EnvelopeGenerator.Infrastructure.Crypt/EnvelopeGenerator.Infrastructure.Crypt.csproj
+++ b/EnvelopeGenerator.Infrastructure.Crypt/EnvelopeGenerator.Infrastructure.Crypt.csproj
@@ -7,30 +7,12 @@
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
- pkcs11\%(RecursiveDir)%(Filename)%(Extension)
-
-
-
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Helper/PdfCertificationLevelExtensions.cs b/EnvelopeGenerator.Infrastructure.Crypt/Helper/PdfCertificationLevelExtensions.cs
deleted file mode 100644
index c6c24250..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Helper/PdfCertificationLevelExtensions.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using DevExpress.Pdf;
-using EnvelopeGenerator.Application.Common.Enums;
-
-namespace EnvelopeGenerator.Infrastructure.Crypt.Helper;
-
-public static class PdfCertificationLevelExtensions
-{
- public static PdfCertificationLevel ToDevExpress(this PdfSignatureCertificationLevel value) => value switch
- {
- PdfSignatureCertificationLevel.NoChangesAllowed => PdfCertificationLevel.NoChangesAllowed,
- PdfSignatureCertificationLevel.FillForms => PdfCertificationLevel.FillForms,
- PdfSignatureCertificationLevel.NoCertification => PdfCertificationLevel.NoCertification,
- PdfSignatureCertificationLevel.FillFormsAndAnnotate => PdfCertificationLevel.FillFormsAndAnnotate,
- _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
- };
-}
\ No newline at end of file
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs
index 6f26379d..9a39011b 100644
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs
+++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs
@@ -1,631 +1,77 @@
-using DevExpress.Office.DigitalSignatures;
-using DevExpress.Pdf;
-using DigitalData.Core.Exceptions;
-using EnvelopeGenerator.Application.Common.Configurations;
+using DocumentService.Client.Interfaces;
+using DocumentService.Client.Models.Requests;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
-using EnvelopeGenerator.Infrastructure.Crypt.Helper;
-using Microsoft.Extensions.Options;
-using System.Formats.Asn1;
-using System.IO;
-using System.Security.Cryptography;
-using System.Security.Cryptography.Pkcs;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.RegularExpressions;
+using ClientCertLevel = DocumentService.Client.Models.Requests.PdfSignatureCertificationLevel;
+using ClientCoordUnit = DocumentService.Client.Models.Requests.PdfSignatureCoordinateUnit;
+using ClientCoordOrigin = DocumentService.Client.Models.Requests.PdfSignatureCoordinateOrigin;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
-public sealed class PdfDigitalSignatureService(IOptions options) : IPdfDigitalSignatureService
+///
+/// Proxy that forwards PDF signing to the remote DocumentService API via .
+///
+public sealed class PdfDigitalSignatureService(IPdfSignatureClient signatureClient) : IPdfDigitalSignatureService
{
- private static readonly Regex SignatureReasonMetaRegex = new(@"\|\s*SFMETA:v1;deid=(?\d+);sha256=(?[A-Fa-f0-9]{64})$", RegexOptions.CultureInvariant | RegexOptions.Compiled);
-
- public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto request, Stream? image = null, CancellationToken cancellationToken = default)
+ public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default)
{
- cancellationToken.ThrowIfCancellationRequested();
-
- var (workingInput, ownsWorkingInput) = await EnsureSeekableInputAsync(inputPdf, cancellationToken);
-
- try
- {
- workingInput.Position = 0;
- var sourceHashHex = Convert.ToHexString(await SHA256.HashDataAsync(workingInput, cancellationToken));
- workingInput.Position = 0;
-
- var pkcs11Options = options.Value;
-
- var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, request.DocumentElementId, sourceHashHex);
-
- using var signer = new PdfDocumentSigner(workingInput);
-
- var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256);
- var signerInfo = new ExternalSignerInfo(PdfSignatureType.Pkcs7, 32768, digestCalculator);
-
- var builder = request.Appearance is null ? new PdfDeferredSignatureBuilder(signerInfo) : new (signerInfo, new PdfSignatureFieldInfo(request.Appearance.PageNumber)
- {
- Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}",
- SignatureBounds = BuildSignatureBounds(request.Appearance, workingInput)
- });
-
- builder.Name = request.Name;
- builder.Reason = reasonWithMeta;
- builder.Location = request.Location;
- builder.ContactInfo = BuildContactInfo(request.Email, request.Phone);
- builder.SigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
- builder.ApplicationName = pkcs11Options.ApplicationName;
- builder.CertificationLevel = request.CertificationLevel.ToDevExpress();
-
- if (request.Appearance is not null)
- {
- var fColor = request.Appearance.Font.Color;
- var font = new PdfSignatureFontSettings(request.Appearance.Font.FamilyName)
- {
- Size = request.Appearance.Font.Size,
- Color = new PdfRGBColor(fColor.R / 255.0, fColor.G / 255.0, fColor.B / 255.0),
- Bold = false,
- Italic = true
- };
-
- var appearance = new PdfSignatureAppearance
- {
- ShowLabels = request.Appearance.ShowLabels,
- ShowName = request.Appearance.ShowName,
- ShowDate = request.Appearance.ShowDate,
- ShowReason = request.Appearance.ShowReason,
- ShowLocation = request.Appearance.ShowLocation,
- ShowDistinguishedName = request.Appearance.ShowDistinguishedName,
- DateTimeFormat = request.Appearance.DateTimeFormat,
- SignatureDetailsFont = font,
- SignatureNameFont = font,
- AppearanceType = PdfSignatureAppearanceType.Image,
- };
-
-
- if (image is not null)
- using (var memoryStream = new MemoryStream())
- {
- image.CopyTo(memoryStream);
- var bytes = memoryStream.ToArray();
- appearance.SetBackgroundImageData(bytes);
- }
-
- builder.SetSignatureAppearance(appearance);
- }
- else if (image is not null)
- throw new BadRequestException("Signature appearance is not specified, but signature image is provided.");
-
- var deferredSigner = signer.SignDeferred(builder);
- var externalSigner = new Pkcs11ExternalSigner(pkcs11Options);
- deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid));
- }
- finally
- {
- if (ownsWorkingInput)
- await workingInput.DisposeAsync();
- }
- }
-
- private static PdfRectangle BuildSignatureBounds(PdfSignatureAppearanceDto appearance, Stream inputPdf)
- {
- var left = appearance.Left;
- var bottom = appearance.Bottom;
- var width = appearance.Width;
- var height = appearance.Height;
-
- if (appearance.CoordinateUnit == PdfSignatureCoordinateUnit.Inch)
- {
- left *= 72.0;
- bottom *= 72.0;
- width *= 72.0;
- height *= 72.0;
- }
-
- if (width <= 0 || height <= 0)
- throw new BadRequestException("Signature appearance width and height must be greater than zero.");
-
- if (appearance.CoordinateOrigin == PdfSignatureCoordinateOrigin.TopLeft)
- {
- var pageHeight = GetPageHeightInPoints(inputPdf, appearance.PageNumber);
- bottom = pageHeight - bottom - height;
- }
-
- return new PdfRectangle(left, bottom, left + width, bottom + height);
- }
-
- private static double GetPageHeightInPoints(Stream inputPdf, int pageNumber)
- {
- if (pageNumber <= 0)
- throw new BadRequestException("Signature page number must be greater than zero.");
-
- var previousPosition = inputPdf.CanSeek ? inputPdf.Position : 0;
-
- try
- {
- if (inputPdf.CanSeek)
- inputPdf.Position = 0;
-
- using var processor = new PdfDocumentProcessor();
- processor.LoadDocument(inputPdf);
-
- var pageCount = processor.Document.Pages.Count;
- if (pageNumber > pageCount)
- throw new BadRequestException($"Signature page number {pageNumber} is out of range. Document has {pageCount} page(s).");
-
- return processor.Document.Pages[pageNumber - 1].CropBox.Height;
- }
- finally
- {
- if (inputPdf.CanSeek)
- inputPdf.Position = previousPosition;
- }
- }
-
- //TODO: verify and optimize (remove memory stream etc.) the check algortihm
- public async Task CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken);
+ var imageBytes = image is null ? null : await ReadAllBytesAsync(image, cancellationToken);
- using var pdfStream = new MemoryStream(pdfBytes);
- using var signer = new PdfDocumentSigner(inputPdf);
- var signatures = signer.GetSignatureInfo();
- var byteRanges = ParseByteRanges(pdfBytes);
-
- if (signatures is null || signatures.Count == 0)
- {
- return new PdfSignatureCheckResultDto(
- Signed: false,
- Signatures: []);
- }
-
- var items = new List(signatures.Count);
-
- for (var i = 0; i < signatures.Count; i++)
- {
- var signature = signatures[i];
- try
- {
- var pkcs7 = signer.GetPdfPkcs7Signature(signature.FieldName);
- bool valid = pkcs7.VerifySignature();
- var byteRange = i < byteRanges.Count ? byteRanges[i] : null;
- var coversWholeDocument = CoversWholeDocument(byteRange, pdfBytes.LongLength);
- var certificate = pkcs7.GetSignatureCertificate();
- var issuedTo = ParsePrincipal(certificate?.Subject);
- var issuedBy = ParsePrincipal(certificate?.Issuer);
- var reasonMeta = ParseReasonMeta(signature.Reason);
- var contact = ParseContactInfo(signature.ContactInfo);
-
- items.Add(new PdfSignatureCheckItemDto(
- CoversWholeDoc: coversWholeDocument,
- Reason: signature.Reason,
- BusinessReason: reasonMeta.BusinessReason,
- Location: signature.Location,
- DeclaredName: signature.SignerName,
- ContactInfo: signature.ContactInfo,
- Email: contact.Email,
- Phone: contact.Phone,
- DigestAlgorithm: InferDigestAlgorithm(certificate),
- SignatureAlgorithm: InferSignatureAlgorithm(certificate),
- SignerName: issuedTo.CommonName ?? signature.SignerName,
- Issuer: issuedBy.Full,
- SerialNumber: ToColonSerial(certificate?.SerialNumber),
- ValidFrom: certificate is null ? null : new DateTimeOffset(certificate.NotBefore).ToUniversalTime(),
- ValidTo: certificate is null ? null : new DateTimeOffset(certificate.NotAfter).ToUniversalTime(),
- ExpiryCheckedAgainst: "now",
- Certificates: [BuildCertificateInfo(certificate, DateTimeOffset.UtcNow)],
- Unchanged: valid && coversWholeDocument,
- FieldName: signature.FieldName,
- IsCryptographicallyValid: valid,
- SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"),
- Filter: signature.Filter,
- SubFilter: signature.SubFilter,
- ApplicationName: null,
- DocumentElementId: reasonMeta.DocumentElementId,
- OriginalDocumentSha256: reasonMeta.OriginalDocumentSha256,
- MessageDigestHex: TryExtractMessageDigestHex(signature.Contents),
- Error: null));
- }
- catch (Exception ex)
- {
- var reasonMeta = ParseReasonMeta(signature.Reason);
- var contact = ParseContactInfo(signature.ContactInfo);
-
- items.Add(new PdfSignatureCheckItemDto(
- CoversWholeDoc: false,
- Reason: signature.Reason,
- BusinessReason: reasonMeta.BusinessReason,
- Location: signature.Location,
- DeclaredName: signature.SignerName,
- ContactInfo: signature.ContactInfo,
- Email: contact.Email,
- Phone: contact.Phone,
- 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,
- DocumentElementId: reasonMeta.DocumentElementId,
- OriginalDocumentSha256: reasonMeta.OriginalDocumentSha256,
- MessageDigestHex: null,
- Error: ex.Message));
- }
- }
-
- return new PdfSignatureCheckResultDto(
- Signed: true,
- Signatures: items);
+ using var signed = await signatureClient.SignAsync(pdfBytes, Map(options), imageBytes, cancellationToken);
+ await signed.CopyToAsync(outputPdf, cancellationToken);
}
- private static async Task<(Stream Stream, bool OwnsStream)> EnsureSeekableInputAsync(Stream input, CancellationToken cancellationToken)
+ public Task CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Signature checking is handled by DocumentService and is not available through this proxy.");
+
+ private static SignPdfOptions Map(PdfSignOptionsDto? o) => new()
{
- if (input.CanSeek)
+ DocumentElementId = o?.DocumentElementId ?? 0,
+ Name = o?.Name ?? string.Empty,
+ Reason = o?.Reason,
+ Location = o?.Location,
+ Email = o?.Email,
+ Phone = o?.Phone,
+ ApplicationName = o?.ApplicationName,
+ CertificationLevel = (ClientCertLevel)(o?.CertificationLevel ?? EnvelopeGenerator.Application.Common.Enums.PdfSignatureCertificationLevel.NoCertification),
+ Appearance = o?.Appearance is null ? null : MapAppearance(o.Appearance)
+ };
+
+ private static PdfSignatureAppearance MapAppearance(PdfSignatureAppearanceDto a) => new()
+ {
+ PageNumber = a.PageNumber,
+ Left = a.Left,
+ Bottom = a.Bottom,
+ Width = a.Width,
+ Height = a.Height,
+ ShowLabels = a.ShowLabels,
+ ShowName = a.ShowName,
+ ShowDate = a.ShowDate,
+ ShowReason = a.ShowReason,
+ ShowLocation = a.ShowLocation,
+ ShowDistinguishedName = a.ShowDistinguishedName,
+ DateTimeFormat = a.DateTimeFormat,
+ CoordinateUnit = (ClientCoordUnit)a.CoordinateUnit,
+ CoordinateOrigin = (ClientCoordOrigin)a.CoordinateOrigin,
+ Font = new PdfSignatureFont
{
- input.Position = 0;
- return (input, false);
+ FamilyName = a.Font.FamilyName,
+ Size = a.Font.Size,
+ IsBold = a.Font.IsBold,
+ IsItalic = a.Font.IsItalic,
+ IsUnderline = a.Font.IsUnderline,
+ Color = new PdfSignatureColor { R = a.Font.Color.R, G = a.Font.Color.G, B = a.Font.Color.B }
}
+ };
- var buffered = new MemoryStream();
- await input.CopyToAsync(buffered, cancellationToken);
- buffered.Position = 0;
- return (buffered, true);
- }
-
- private static async Task ReadAllBytesAsync(Stream input, CancellationToken cancellationToken)
+ private static async Task ReadAllBytesAsync(Stream stream, CancellationToken cancellationToken)
{
- if (input is MemoryStream memory && memory.TryGetBuffer(out var buffer))
+ if (stream is MemoryStream memory && memory.TryGetBuffer(out var buffer))
return buffer.AsSpan(0, (int)memory.Length).ToArray();
- if (input.CanSeek)
- input.Position = 0;
-
- using var ms = new MemoryStream();
- await input.CopyToAsync(ms, cancellationToken);
- return ms.ToArray();
- }
-
- 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, out var keySize) ? keySize : null);
- }
-
- private static bool TryGetKeySize(X509Certificate2 certificate, out int keySize)
- {
- keySize = 0;
- try
- {
- using var rsa = certificate.GetRSAPublicKey();
-
- if (rsa is null)
- return false;
-
- keySize = rsa.KeySize;
- return true;
- }
- catch (CryptographicException)
- {
- return false;
- }
- }
-
- 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 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;
- }
- }
-
- private static List ParseByteRanges(byte[] pdfBytes)
- {
- var text = Encoding.Latin1.GetString(pdfBytes);
- var matches = Regex.Matches(text, @"/ByteRange\s*\[(?[\d\s]+)\]", RegexOptions.CultureInvariant);
- var ranges = new List(matches.Count);
-
- foreach (Match match in matches)
- {
- var raw = match.Groups["values"].Value;
- var parts = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
- var values = new long[parts.Length];
- var ok = true;
-
- for (var i = 0; i < parts.Length; i++)
- {
- if (!long.TryParse(parts[i], out values[i]))
- {
- ok = false;
- break;
- }
- }
-
- if (ok && values.Length >= 4 && values.Length % 2 == 0)
- ranges.Add(values);
- }
-
- return ranges;
- }
-
- private static bool CoversWholeDocument(long[]? byteRange, long fileLength)
- {
- if (byteRange is null || byteRange.Length < 4 || byteRange.Length % 2 != 0)
- return false;
-
- if (byteRange[0] != 0)
- return false;
-
- long previousEnd = -1;
- for (var i = 0; i < byteRange.Length; i += 2)
- {
- var start = byteRange[i];
- var length = byteRange[i + 1];
- if (start < 0 || length < 0)
- return false;
-
- var end = start + length;
- if (previousEnd > start)
- return false;
-
- previousEnd = end;
- }
-
- return previousEnd == fileLength;
- }
-
- private static long GetSignedRevisionLength(long[] byteRange)
- {
- if (byteRange.Length < 2)
- return -1;
-
- var start = byteRange[^2];
- var length = byteRange[^1];
- if (start < 0 || length < 0)
- return -1;
-
- return start + length;
- }
-
- private static string BuildReasonWithMeta(string? businessReason, long documentElementId, string sourceHashHex)
- {
- var trimmedReason = string.IsNullOrWhiteSpace(businessReason) ? null : businessReason.Trim();
- var prefix = string.IsNullOrWhiteSpace(trimmedReason) ? string.Empty : trimmedReason + " ";
- return $"{prefix}| SFMETA:v1;deid={documentElementId};sha256={sourceHashHex}";
- }
-
- private static string? BuildContactInfo(string? email, string? phone)
- {
- var normalizedEmail = string.IsNullOrWhiteSpace(email) ? null : email.Trim();
- var normalizedPhone = string.IsNullOrWhiteSpace(phone) ? null : phone.Trim();
-
- if (normalizedEmail is null && normalizedPhone is null)
- return null;
-
- if (normalizedEmail is null)
- return $"tel:{normalizedPhone}";
-
- if (normalizedPhone is null)
- return $"mailto:{normalizedEmail}";
-
- return $"mailto:{normalizedEmail};tel:{normalizedPhone}";
- }
-
- private static (string? Email, string? Phone) ParseContactInfo(string? contactInfo)
- {
- if (string.IsNullOrWhiteSpace(contactInfo))
- return (null, null);
-
- string? email = null;
- string? phone = null;
-
- foreach (var part in contactInfo.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
- {
- if (part.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase))
- {
- email = part[7..].Trim();
- continue;
- }
-
- if (part.StartsWith("tel:", StringComparison.OrdinalIgnoreCase))
- {
- phone = part[4..].Trim();
- continue;
- }
-
- if (email is null && part.Contains('@', StringComparison.Ordinal))
- {
- email = part.Trim();
- continue;
- }
-
- if (phone is null)
- phone = part.Trim();
- }
-
- return (
- string.IsNullOrWhiteSpace(email) ? null : email,
- string.IsNullOrWhiteSpace(phone) ? null : phone);
- }
-
- private static (string? BusinessReason, long? DocumentElementId, string? OriginalDocumentSha256) ParseReasonMeta(string? reason)
- {
- if (string.IsNullOrWhiteSpace(reason))
- return (reason, null, null);
-
- var match = SignatureReasonMetaRegex.Match(reason);
- if (!match.Success)
- return (reason, null, null);
-
- var businessReason = reason[..match.Index].Trim();
- if (string.IsNullOrWhiteSpace(businessReason))
- businessReason = null;
-
- long? parsedDocumentElementId = long.TryParse(match.Groups["deid"].Value, out var deid) ? deid : null;
-
- return (
- businessReason,
- parsedDocumentElementId,
- match.Groups["sha"].Value.ToUpperInvariant());
+ using var copy = new MemoryStream();
+ await stream.CopyToAsync(copy, cancellationToken);
+ return copy.ToArray();
}
}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11DigestInfoBuilder.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11DigestInfoBuilder.cs
deleted file mode 100644
index a1741c06..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11DigestInfoBuilder.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-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;
- }
-}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11ExternalSigner.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11ExternalSigner.cs
deleted file mode 100644
index cfc8914c..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11ExternalSigner.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-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 certificates = [ReadCertificateBytesWithFallback(options)];
-
- protected override IDigestCalculator DigestCalculator => digestCalculator;
-
- protected override string SigningAlgorithmOID => "1.2.840.113549.1.1.1";
-
- protected override IEnumerable 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
- {
- 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.CKA_VALUE });
- return values[0].GetValueAsByteArray();
- });
- }
- catch when (Pkcs11ToolBridge.IsAvailable(options))
- {
- return Pkcs11ToolBridge.ReadCertificateBytes(options);
- }
- }
-}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11InteropBootstrap.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11InteropBootstrap.cs
deleted file mode 100644
index 833a2516..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11InteropBootstrap.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using EnvelopeGenerator.Application.Common.Configurations;
-
-namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
-
-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)
- {
- if (Initialized)
- return;
-
- lock (Sync)
- {
- if (Initialized)
- return;
-
- var libraryPath = ResolveLibraryPath(options);
- var toolPath = ResolveToolPath(options);
- options.SetResolvedPaths(libraryPath, toolPath);
-
- EnsureNativeDependencySearchPath(options.ResolvedLibraryPath);
-
- if (!string.IsNullOrWhiteSpace(options.ProxySocket))
- {
- Environment.SetEnvironmentVariable("PKCS11_PROXY_SOCKET", options.ProxySocket);
- Environment.SetEnvironmentVariable("PKCS11_DAEMON_SOCKET", options.ProxySocket);
- }
-
- if (!string.IsNullOrWhiteSpace(options.ResolvedToolPath))
- {
- var toolDir = Path.GetDirectoryName(options.ResolvedToolPath);
- if (!string.IsNullOrWhiteSpace(toolDir))
- EnsurePathContains(toolDir);
- }
-
- WarmupWithPkcs11Tool(options);
- Initialized = true;
- }
- }
-
- 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.ResolvedToolPath))
- return;
-
- if (!File.Exists(options.ResolvedToolPath))
- return;
-
- var process = new System.Diagnostics.Process
- {
- StartInfo = new System.Diagnostics.ProcessStartInfo
- {
- FileName = options.ResolvedToolPath,
- Arguments = $"--module \"{options.ResolvedLibraryPath}\" -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);
- }
-}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11PdfSigningRsa.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11PdfSigningRsa.cs
deleted file mode 100644
index 3b9b0d68..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11PdfSigningRsa.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-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
- {
- 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();
-}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SelfTestStatusStore.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SelfTestStatusStore.cs
deleted file mode 100644
index 5dfddb1b..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SelfTestStatusStore.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index 0d53ef8a..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11SessionRunner.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-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(Pkcs11Options options, Func action)
- {
- Pkcs11InteropBootstrap.EnsureLoaded(options);
-
- var factories = new Pkcs11InteropFactories();
- using var pkcs11 = LoadLibraryWithFallback(factories, options.ResolvedLibraryPath, 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 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) ? "" : proxySocket;
- throw new AggregateException($"Unable to initialize PKCS#11 library '{libraryPath}' with any supported initialization mode. PKCS11_PROXY_SOCKET={socketInfo}", errors);
- }
-
- private static IEnumerable BuildLibraryCandidates(string configuredPath)
- {
- var unique = new HashSet(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 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.");
- }
-}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11StartupSelfTestHostedService.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11StartupSelfTestHostedService.cs
deleted file mode 100644
index 74d6e2ac..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11StartupSelfTestHostedService.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-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
deleted file mode 100644
index 9c564763..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/Services/Pkcs11ToolBridge.cs
+++ /dev/null
@@ -1,147 +0,0 @@
-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.ResolvedToolPath) && File.Exists(options.ResolvedToolPath);
- }
-
- 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 CreateBaseArgs(Pkcs11Options options)
- {
- var args = new List
- {
- "--module",
- options.ResolvedLibraryPath,
- "--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 arguments)
- {
- var process = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- FileName = options.ResolvedToolPath!,
- 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 at resolved pkcs11 assets path.");
- }
-
- private static void DeleteIfExists(string path)
- {
- if (File.Exists(path))
- File.Delete(path);
- }
-}
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/INFRASTRUCTURE_PLAN.md b/EnvelopeGenerator.Infrastructure.Crypt/resources/INFRASTRUCTURE_PLAN.md
deleted file mode 100644
index 3f5bda2c..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/resources/INFRASTRUCTURE_PLAN.md
+++ /dev/null
@@ -1,301 +0,0 @@
-# FES Signature Infrastructure Plan
-
-## 1. Goal
-
-Build a reusable and modular cryptography infrastructure in `EnvelopeGenerator.Infrastructure.Crypt` to support FES (Advanced Electronic Signature) workflows.
-
-This module must:
-
-- use PKCS#11 providers (SoftHSMv2 first, real HSM later),
-- stay independent from UI/API concerns,
-- expose clean interfaces to `Application` layer use-cases,
-- support future extensions (timestamping, certificate chain validation, multiple key algorithms).
-
----
-
-## 2. Context and Constraints
-
-1. SoftHSMv2 is **not** a REST service. It is a PKCS#11 module loaded as a native library.
-2. `EnvelopeGenerator.Server` is presentation only; cryptographic implementation belongs to infrastructure.
-3. Clean Architecture boundaries must be respected:
- - `Application` depends on abstractions.
- - `Infrastructure.Crypt` implements abstractions.
-4. Initial target framework is `net8.0`.
-5. We need production-ready behavior, not only PoC signing.
-
----
-
-## 3. High-Level Design
-
-## 3.1 Architecture slice
-
-- **Application layer (contracts/use-cases):**
- - request signing hash/document,
- - request certificate/public key material,
- - request verification,
- - consume signature evidence result.
-
-- **Infrastructure.Crypt layer (this project):**
- - PKCS#11 session handling,
- - key/certificate object discovery,
- - hash signing operations,
- - local cryptographic verification,
- - certificate parsing helpers,
- - error normalization.
-
-## 3.2 Provider model
-
-Define provider-agnostic APIs and add one concrete provider first:
-
-- `Pkcs11SignatureProvider` (primary)
-- future providers:
- - `RemoteSigningProvider` (if a dedicated signing gateway is introduced)
- - `MockSignatureProvider` (for deterministic test scenarios)
-
----
-
-## 4. Deliverables (Phase by Phase)
-
-## Phase 0 - Contracts and Domain Language
-
-Create neutral models and interfaces in `Infrastructure.Crypt` (or shared abstractions if needed later):
-
-- `SigningAlgorithm` (enum/value object)
-- `Pkcs11ProviderOptions`
-- `KeyLocator` (label/id/slot/token selectors)
-- `SignHashRequest`
-- `SignHashResult`
-- `VerifySignatureRequest`
-- `VerifySignatureResult`
-- `CertificateDescriptor`
-- `SignatureEvidence` (technical evidence payload)
-
-Interfaces:
-
-- `ISignatureProvider`
-- `ICertificateProvider`
-- `ISignatureVerifier`
-- `ICryptographicHealthCheck`
-
-Acceptance criteria:
-
-- no dependency on ASP.NET types,
-- no UI/API naming leakage,
-- all model names are business-neutral.
-
-## Phase 1 - PKCS#11 Core Adapter (SoftHSM-ready)
-
-Implement:
-
-- `Pkcs11LibraryLoader`
-- `Pkcs11SessionFactory`
-- `Pkcs11ObjectFinder`
-- `Pkcs11SignatureProvider`
-- `Pkcs11CertificateProvider`
-
-Capabilities:
-
-1. load module path from options (`libsofthsm2.so` or proxy module when required),
-2. enumerate slots/tokens,
-3. open read-write session,
-4. login with user pin,
-5. find private key by label/id,
-6. sign digest with selected mechanism,
-7. read certificate object by label/id.
-
-Acceptance criteria:
-
-- deterministic disposal of sessions,
-- no leaked native handles,
-- detailed typed errors (not raw exception strings only).
-
-## Phase 2 - Verification and Certificate Utilities
-
-Implement:
-
-- `DefaultSignatureVerifier`
-- `X509CertificateParser`
-- optional `CertificateChainValidator` (toggleable)
-
-Capabilities:
-
-1. verify signature using returned certificate/public key,
-2. expose certificate metadata (`thumbprint`, `subject`, `issuer`, `notBefore`, `notAfter`),
-3. map validation failures into stable error codes.
-
-Acceptance criteria:
-
-- same request yields same verification outcome,
-- invalid signature returns structured negative result.
-
-## Phase 3 - Evidence Builder
-
-Implement:
-
-- `SignatureEvidenceBuilder`
-
-Evidence payload should contain:
-
-- transaction/correlation id,
-- algorithm,
-- hash metadata,
-- key locator snapshot (safe subset),
-- certificate fingerprint and subject,
-- sign timestamp (UTC),
-- verification outcome,
-- provider type (`PKCS11`).
-
-Acceptance criteria:
-
-- no secret values in evidence (pin, private key id internals),
-- evidence serializable for storage/audit.
-
-## Phase 4 - Dependency Injection Module
-
-Implement:
-
-- `CryptInfrastructureDependencyInjection` extension methods
-
-Methods:
-
-- `AddCryptInfrastructure(...)`
-- `AddPkcs11Provider(...)`
-
-Acceptance criteria:
-
-- one-line registration from composition root,
-- options validation on startup.
-
-## Phase 5 - Test Suite (`EnvelopeGenerator.Tests`)
-
-Add test categories for `net8.0`:
-
-1. `Unit`:
- - request validation,
- - algorithm mapping,
- - evidence building,
- - error mapping.
-2. `Integration` (conditional/manual profile):
- - SoftHSM slot listing,
- - login,
- - sign hash,
- - verify,
- - certificate read.
-
-Acceptance criteria:
-
-- unit tests run in CI without native SoftHSM,
-- integration tests run when module path + pins are provided via environment variables.
-
----
-
-## 5. Proposed Project Structure
-
-```text
-EnvelopeGenerator.Infrastructure.Crypt/
- Abstractions/
- ISignatureProvider.cs
- ICertificateProvider.cs
- ISignatureVerifier.cs
- ICryptographicHealthCheck.cs
- Models/
- SigningAlgorithm.cs
- SignHashRequest.cs
- SignHashResult.cs
- VerifySignatureRequest.cs
- VerifySignatureResult.cs
- SignatureEvidence.cs
- CertificateDescriptor.cs
- KeyLocator.cs
- Options/
- Pkcs11ProviderOptions.cs
- Pkcs11/
- Pkcs11LibraryLoader.cs
- Pkcs11SessionFactory.cs
- Pkcs11ObjectFinder.cs
- Pkcs11SignatureProvider.cs
- Pkcs11CertificateProvider.cs
- Verification/
- DefaultSignatureVerifier.cs
- X509CertificateParser.cs
- Evidence/
- SignatureEvidenceBuilder.cs
- DependencyInjection/
- CryptInfrastructureDependencyInjection.cs
- Exceptions/
- CryptographicOperationException.cs
- Pkcs11ProviderException.cs
-```
-
----
-
-## 6. Configuration Strategy
-
-Use options (bound from host config) with environment override support:
-
-- `Crypt:Provider = PKCS11`
-- `Crypt:Pkcs11:AssetsRootPath` (base path that contains bundled `pkcs11` assets)
-- `Crypt:Pkcs11:SlotId` or token selectors
-- `Crypt:Pkcs11:UserPin` (from secret store/env, not plain committed value)
-- `Crypt:Pkcs11:PrivateKeyLabel`
-- `Crypt:Pkcs11:CertificateLabel`
-- `Crypt:Pkcs11:LoginType`
-
-Validation rules:
-
-- resolved module path must exist under configured/default assets root,
-- at least one key locator required,
-- pin required for signing operations.
-
----
-
-## 7. Error Model
-
-Define stable error codes to prevent provider-specific leakage:
-
-- `CRYPT_PROVIDER_UNAVAILABLE`
-- `CRYPT_SLOT_NOT_FOUND`
-- `CRYPT_LOGIN_FAILED`
-- `CRYPT_KEY_NOT_FOUND`
-- `CRYPT_CERT_NOT_FOUND`
-- `CRYPT_SIGN_FAILED`
-- `CRYPT_VERIFY_FAILED`
-
-Map low-level exceptions to these codes with safe diagnostics.
-
----
-
-## 8. Non-Functional Requirements
-
-1. **Security:** never log pins or raw private key handles.
-2. **Reliability:** deterministic cleanup (`IDisposable`/`IAsyncDisposable`).
-3. **Observability:** structured logs with correlation id.
-4. **Performance:** avoid repeated login per operation when a safe session strategy is available.
-5. **Extensibility:** algorithm/provider mapping must be open for extension.
-
----
-
-## 9. Implementation Order (Small Tasks)
-
-1. Add models + interfaces.
-2. Add options + validators.
-3. Add PKCS#11 low-level loader/session wrappers.
-4. Implement `Pkcs11SignatureProvider` for hash signing.
-5. Implement certificate retrieval.
-6. Implement verifier.
-7. Implement evidence builder.
-8. Add DI registration extension.
-9. Add unit tests.
-10. Add optional integration tests (SoftHSM profile).
-
----
-
-## 10. Definition of Done
-
-Done means:
-
-1. `Infrastructure.Crypt` exposes generic cryptographic services usable by Application use-cases.
-2. SoftHSM-backed PKCS#11 sign/verify path works in integration tests.
-3. All sensitive configuration is externalized.
-4. Structured error codes are returned for expected failure scenarios.
-5. Documentation exists for host registration and required settings.
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/INTEGRATION_PLAN.md b/EnvelopeGenerator.Infrastructure.Crypt/resources/INTEGRATION_PLAN.md
deleted file mode 100644
index 1b46d88c..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/resources/INTEGRATION_PLAN.md
+++ /dev/null
@@ -1,228 +0,0 @@
-# FES (Advanced Electronic Signature) Summary and Integration Plan - EN
-
-This document summarizes the shared diagram flow (`Ablauf SignFlow Unternehmenszertifikat`) and provides a small-step integration plan for the `EnvelopeGenerator` solution.
-
----
-
-## 1) Simplified flow summary
-
-Flow logic:
-
-1. The sender uploads a document and defines signers.
-2. If a valid company certificate already exists in HSM, it is used with the company key pair.
-3. If no valid company certificate exists, CA issuance/renewal flow is required.
-4. The signer receives an e-mail with a signing link.
-5. The signer completes SMS OTP (2FA) and performs UI signature interaction.
-6. Backend computes the hash for the current signing stage and sends it to HSM.
-7. HSM signs the hash with the private key and returns signature + certificate context.
-8. Backend embeds digital signature and certificate into PDF and writes audit trail.
-9. After all signers complete, the final artifact is archived.
-10. The final signed document is distributed to signers.
-
-Important: The visible UI signature is not yet the legal electronic signature. The legal signature is produced in backend+HSM steps.
-
----
-
-## 2) Relation to current codebase (as-is)
-
-- `EnvelopeGenerator.Server` is the presentation layer host (Blazor + API hosting).
-- Business flow is handled in `Application` via MediatR pipelines.
-- Current receiver flow is centered around UI overlay/signature capture.
-- Missing FES core: PKCS#11 hash-sign-verify + certificate-based PDF embedding pipeline.
-
-Critical deployment fact:
-
-- `EnvelopeGenerator.Server` is planned to run on Windows.
-- SoftHSM is currently planned to run on Linux.
-- A Windows process cannot directly load Linux PKCS#11 module (`libsofthsm2.so`).
-- Therefore, architecture must include a compatibility bridge (service or proxy) unless HSM/SoftHSM is local to Windows.
-
-Conclusion: FES is not a replacement of the current flow; it is a cryptographic hardening layer behind it.
-
----
-
-## 3) Why SoftHSM tests are foundational
-
-The tests defined in `SOFTHSM_TEST_KILAVUZU_TR.md` are the technical prerequisite for FES:
-
-- `slots/login` -> HSM connectivity and authentication
-- `certificate` -> certificate retrieval path
-- `sign` -> hash signing with private key
-- `verify` -> signature and certificate consistency validation
-
-Without these tests passing, diagram steps 6-8 should not go live.
-
----
-
-## 4) Target architecture (clean architecture aligned)
-
-### Deployment topology options (must choose one)
-
-1. **Linux signing service (recommended)**
- - Windows `EnvelopeGenerator.Server` calls Linux signing API over HTTPS.
- - Linux signing service performs PKCS#11 operations against SoftHSM.
- - Private keys never leave Linux/HSM boundary.
-
-2. **PKCS#11 proxy bridge**
- - Windows host loads a Windows PKCS#11 proxy client DLL.
- - Proxy forwards PKCS#11 operations to Linux side connected to SoftHSM.
-
-3. **Windows-local SoftHSM (dev/test)**
- - SoftHSM also installed on Windows and used locally.
- - Useful for local testing, not preferred production topology.
-
-Decision note:
-
-- For production-critical FES, choose option 1 by default unless enterprise HSM policy requires option 2.
-- Keep option 3 for development convenience only.
-
-### Layer responsibilities
-
-- `EnvelopeGenerator.Server`:
- - API endpoints, auth orchestration, input validation.
- - FES branch routing: standard-sign path vs external signing-service/proxy path.
- - No private-key material handling.
-
-- `EnvelopeGenerator.Application`:
- - Use cases such as `SignWithHsmCommand`.
- - MediatR pipeline orchestration for status/history/audit.
- - FES evidence model aggregation (ip/email/phone/user-agent/otp proof/timestamps).
-
-- `EnvelopeGenerator.Infrastructure`:
- - Implementations for `IPkcs11Service`, `IPdfSignatureService`, `ITimestampService`.
- - Pkcs11Interop integration and cryptographic operations.
- - Optional client for Linux signing service if topology option 1 is selected.
-
-- `Domain`:
- - Signature evidence and audit-trail models.
-
----
-
-## 5) Small-step integration plan
-
-## Phase 0 - Scope and contracts
-
-1. Finalize signing algorithms (for example `SHA256_RSA_PKCS`).
-2. Confirm strategy: company certificate vs signer-specific certificate.
-3. Freeze mandatory audit fields:
- - identity, OTP proof, IP, user-agent, transaction id, timestamps, hash metadata.
-4. Freeze deployment topology decision:
- - option 1 (Linux signing service), option 2 (PKCS#11 proxy), or option 3 (Windows-local SoftHSM).
-5. Define production host mapping:
- - where `EnvelopeGenerator.Server` runs,
- - where HSM/SoftHSM runs,
- - where certificate lifecycle is managed.
-
-Deliverable: Technical decision record (ADR-style markdown).
-
-## Phase 1 - PKCS#11 connector (infrastructure)
-
-1. Define `IPkcs11Service` interface.
-2. Add `Pkcs11Interop` implementation.
-3. Add config model:
- - library path, slot, token label, key label, certificate label.
-4. Ensure secure secret handling for PIN.
-5. Ensure host-compatible module loading:
- - Windows server must load Windows DLL,
- - Linux server must load Linux `.so`.
-
-Deliverable: Testable PKCS#11 service with health/slot/login/sign/verify capabilities.
-
-## Phase 1.5 - Cross-host bridge implementation
-
-1. If option 1 selected:
- - implement Linux signing service API contract (`/sign`, `/certificate`, `/health`),
- - implement authenticated HTTP client in `EnvelopeGenerator.Server`.
-2. If option 2 selected:
- - install/configure PKCS#11 proxy client DLL on Windows,
- - install/configure proxy server on Linux and bind to SoftHSM.
-
-Deliverable: Windows-to-Linux signing communication path proven end-to-end.
-
-## Phase 2 - Application use case
-
-1. Add `SignDocumentHashCommand` (or equivalent).
-2. Pipeline flow:
- - compute/receive hash,
- - sign with HSM,
- - attach certificate context,
- - verify,
- - produce evidence object.
-3. Add resilient error mapping and retry strategy.
-
-Deliverable: HSM signing use case in Application layer.
-
-## Phase 3 - PDF signature embedding
-
-1. Implement PDF signature embedding via `IPdfSignatureService`.
-2. Build signer-specific audit-trail pages.
-3. Append all audit pages to final PDF artifact.
-
-Deliverable: Signed PDF with certificate context and audit trail pages.
-
-## Phase 4 - Presentation/API integration
-
-1. Integrate FES option into submit flow (feature flag guarded).
-2. Add operational endpoints:
- - HSM health/check
- - signature evidence query (restricted roles)
-3. Keep DTOs safe; avoid exposing sensitive cryptographic material.
-4. In `SignatureController.Submit`, add FES branch that sends signing payload + audit context to selected signing backend.
-
-Deliverable: API-level orchestration with minimal UI disruption.
-
-## Phase 5 - Audit, archive, distribution
-
-1. Persist signature evidence records.
-2. Include hash/cert metadata in archival package.
-3. Add signed artifact reference to distribution e-mails.
-
-Deliverable: Alignment with diagram steps 9-10.
-
-## Phase 6 - Testing and operations
-
-1. Postman collection + Newman automation.
-2. Positive/negative matrix:
- - wrong pin, wrong slot, missing cert, expired cert, HSM timeout.
-3. Observability:
- - structured logs, correlation id, latency metrics.
-
-Deliverable: Production readiness checklist.
-
----
-
-## 6) Transition strategy from test API to production flow
-
-1. Keep `/api/pkcs11-test/*` only for non-production.
-2. In production, use the same internal services through business endpoints only.
-3. Roll out by feature flag:
- - `FesEnabled=false` initially
- - pilot sender group
- - full rollout
-
-4. Keep explicit environment matrix:
- - local dev (option 3),
- - integration/staging (option 1 or 2),
- - production (option 1 or 2 only).
-
----
-
-## 7) Risks and mitigations
-
-- Certificate expiry risk -> proactive monitoring and renewal alerting.
-- HSM connectivity risk -> retry policy, circuit breaker, fallback queue.
-- PIN/secret exposure risk -> vault-backed secrets and rotation policy.
-- Verification failure risk -> block PDF sealing and move envelope to technical hold state.
-- Cross-host connectivity risk -> mTLS/JWT auth + timeout/retry/circuit breaker + health probe.
-
----
-
-## 8) Short done checklist
-
-- [ ] PKCS#11 service interface + implementation
-- [ ] Application command + pipeline behaviors
-- [ ] PDF signature embedding
-- [ ] Audit-trail generation
-- [ ] API integration + feature flag
-- [ ] Postman/Newman regression suite
-- [ ] Operational runbook and alerting rules
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/flow-diagram.drawio b/EnvelopeGenerator.Infrastructure.Crypt/resources/flow-diagram.drawio
deleted file mode 100644
index c45d36f7..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/resources/flow-diagram.drawio
+++ /dev/null
@@ -1,172 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/flow-diagram.pdf b/EnvelopeGenerator.Infrastructure.Crypt/resources/flow-diagram.pdf
deleted file mode 100644
index 34548d50..00000000
Binary files a/EnvelopeGenerator.Infrastructure.Crypt/resources/flow-diagram.pdf and /dev/null differ
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/pkcs11/opensc-pkcs11.dll b/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/pkcs11/opensc-pkcs11.dll
deleted file mode 100644
index 4d43edb1..00000000
Binary files a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/pkcs11/opensc-pkcs11.dll and /dev/null differ
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/tools/opensc.dll b/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/tools/opensc.dll
deleted file mode 100644
index 6be23256..00000000
Binary files a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/tools/opensc.dll and /dev/null differ
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/tools/pkcs11-tool.exe b/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/tools/pkcs11-tool.exe
deleted file mode 100644
index e6cb8459..00000000
Binary files a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/opensc/tools/pkcs11-tool.exe and /dev/null differ
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libgcc_s_seh-1.dll b/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libgcc_s_seh-1.dll
deleted file mode 100644
index 21239225..00000000
Binary files a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libgcc_s_seh-1.dll and /dev/null differ
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libpkcs11-proxy.dll b/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libpkcs11-proxy.dll
deleted file mode 100644
index ae729302..00000000
Binary files a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libpkcs11-proxy.dll and /dev/null differ
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libstdc++-6.dll b/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libstdc++-6.dll
deleted file mode 100644
index 72bf0fc7..00000000
Binary files a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libstdc++-6.dll and /dev/null differ
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libwinpthread-1.dll b/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libwinpthread-1.dll
deleted file mode 100644
index 8bc14837..00000000
Binary files a/EnvelopeGenerator.Infrastructure.Crypt/resources/pkcs11/windows/proxy/libwinpthread-1.dll and /dev/null differ
diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/ticket.md b/EnvelopeGenerator.Infrastructure.Crypt/resources/ticket.md
deleted file mode 100644
index f1f2cb07..00000000
--- a/EnvelopeGenerator.Infrastructure.Crypt/resources/ticket.md
+++ /dev/null
@@ -1,84 +0,0 @@
-10/6/26, 10:46 PM
-
-Test SoftHSM : SWINFRA-54
-
-Projects / Infrastruktur Software / Issues /
-
-Enter search…
-
-SWINFRA-54
-
-All…
-
-Created by Marvin Kamm 3 months ago
-
-Visible to issue readers 1
-
-Updated by Matthias Dewald about 1 month ago
-
-# **Test SoftHSM**
-
-Project Priorität Status **SWI** Infrastruktur Softwa H Hoch O Offen re… Bearbeiter Fälligkeit Projekteinfluss MD _ ? ? Matthias Dewald Kein fälligkeit Kein projektbezug Boards No visible boards
-
-Bitte eine Test-VM mit der Software SoftHSM bereitstellen.
-
-Laut Info von @Henning Emrich soll die Installation unter Debian Linux deutlich einfacher sein, als unter Windows. Bitte prüfen.
-
-@Hakan Tek & @OlgunR
-
-Sollen bitte die API Ansteuerung testen
-
-FYI
-
-@Marlon Schreiber @Jan-Ulrich Hoss @Henning Emrich @Hakan Tek @OlgunR
-
-**Attachments** 1
-
-Files Attached to Comments
-
-**PDF**
-
-Ablauf SignFlow Un ternehme… 83 kB
-
-MD _ **Matthias Dewald** Commented 3 months ago Ist für Dienstag 30.6 10:00 Uhr Eingeplant
-
-HT **Hakan Tek** Commented 3 months ago
-
-Für die Integration auf der .NET-Seite können wir die Pkcs11Interop-Bibliothek und die entsprechende DevExpress-Infrastruktur verwenden. @OlgunR, falls du noch weitere Vorschläge hast, füge sie bitte hinzu. @Marvin Kamm, @Henning Emrich, @Matthias Dewald, um mit den Tests beginnen zu können, benötigen wir die entsprechenden Verbindungsdaten für SoftHSM (Slot-ID, PIN, Bibliothekspfad usw.).
-
-FYI
-
-@Marlon Schreiber @Jan-Ulrich Hoss
-
-bugtracker.dd:8080/issue/SWINFRA-54/Test-SoftHSM
-
-1/2
-
-10/6/26, 10:46 PM
-
-Test SoftHSM : SWINFRA-54
-
-MD
-
-## **Matthias Dewald** Commented 3 months ago
-
-Hallo zusammen, SoftHSM ist auf der 172.24.12.64 installiert:
-
-Der Proxy läuft auf Port 5657 und erwartet Verbindungen über die Proxy-Bibliothek (libpkcs11-proxy.so)
-
-@Hakan Tek Bitte testen
-
-MD
-
-## **Matthias Dewald** Commented about 1 month ago
-
-Ablauf SignFlow Unternehmenszertifikat.drawio.pdf
-
-**PDF**
-
-Ablauf SignFlow Un ternehme… 83 kB
-
-bugtracker.dd:8080/issue/SWINFRA-54/Test-SoftHSM
-
-2/2
-
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HealthController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HealthController.cs
deleted file mode 100644
index 6dc171cf..00000000
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HealthController.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-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/EnvelopeGenerator.Server.csproj b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj
index 7ab7881f..936b7c6c 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj
@@ -10,9 +10,9 @@
Digital Data GmbH
Digital Data GmbH
EnvelopeGenerator.Server
- 1.5.0-beta
- 1.5.0.0
- 1.5.0.0
+ 1.6.0-beta
+ 1.6.0.0
+ 1.6.0.0
Copyright © 2026 Digital Data GmbH. All rights reserved.
bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeFinalizationWorker.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeFinalizationWorker.cs
index bbffbcfd..36853668 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeFinalizationWorker.cs
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeFinalizationWorker.cs
@@ -41,6 +41,7 @@ public sealed class EnvelopeFinalizationWorker(
}
catch(Exception ex)
{
+ // TODO: integrate rabbit mq and handle exception (such as delete)
logger.LogError(ex, "Unexpected exception happened during finalization of envelope. Envelope ID: {EnvelopeId}", envelopeId);
}
}
diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json
index 461e53a4..f7f4aad0 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json
@@ -6,11 +6,6 @@
}
}
,
- "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 59647559..6d7e6810 100644
--- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
+++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json
@@ -58,20 +58,8 @@
"MainCanvasEnableHiDPI": true,
"ZoomStepPercentage": 5
},
- "Crypt": {
- "Pkcs11": {
- "AssetsRootPath": "E:\\DataFiles\\Digital Data\\EnvelopeGenerator",
- "SlotId": 2021549410,
- "TokenLabel": "CompanyProdToken",
- "ProxySocket": "tcp://172.24.12.64:5657",
- "UserPin": "",
- "PrivateKeyLabel": "company-sign-key-v1",
- "CertificateLabel": "company-sign-cert-v1",
- "DefaultReason": null,
- "ApplicationName": "signFLOW",
- "UseUtcSigningTime": true,
- "DefaultCertificationLevel": "NoCertification"
- }
+ "DocumentService": {
+ "BaseUrl": "https://localhost:7186"
},
"PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==",
"Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value.