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.