From 04cea1ca4bbdf6c1b1501fc6152da57203f950f7 Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 15 Oct 2026 11:04:05 +0200 Subject: [PATCH] Refactor to use streams for PDF handling Refactored `IPdfDigitalSignatureService` to replace `byte[]` with `Stream` for input and output, improving memory efficiency and scalability. Updated `PdfDigitalSignatureService` implementation to support stream-based operations, including signing, checking, and extracting PDFs. Modified `SignPdfAsync` to handle multiple signatures sequentially using streams and ensured proper disposal of intermediate streams. Updated `CheckPdfSignatureAsync` and `ExtractOriginalPdfAsync` to use streams, reducing memory overhead. Refactored `PdfSignatureController` to use streams for file handling, enabling efficient processing of large files. Introduced helper methods for stream handling and enhanced cancellation support. Improved code maintainability by consolidating common logic and reducing duplication. --- .../Services/IPdfDigitalSignatureService.cs | 22 +-- .../Services/PdfDigitalSignatureService.cs | 185 ++++++++++++------ .../Controllers/PdfSignatureController.cs | 26 +-- 3 files changed, 147 insertions(+), 86 deletions(-) diff --git a/EnvelopeGenerator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs b/EnvelopeGenerator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs index 4bfdd640..cd875279 100644 --- a/EnvelopeGenerator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs +++ b/EnvelopeGenerator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs @@ -11,37 +11,37 @@ public interface IPdfDigitalSignatureService /// Creates a digital signature for the provided PDF bytes and returns /// a new signed PDF document. /// - /// Source PDF content as a byte array. + /// Source PDF content stream. + /// Destination stream for signed PDF output. /// Optional signing options that map to PDF signature metadata fields. /// Cancellation token for cooperative cancellation. - /// The signed PDF as a byte array. - Task SignPdfAsync(byte[] pdfBytes, PdfSignOptionsDto? options = null, CancellationToken cancellationToken = default); + Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, CancellationToken cancellationToken = default); /// /// Applies multiple signatures to the provided PDF in sequence and returns /// the final signed PDF document. /// - /// Source PDF content as a byte array. + /// Source PDF content stream. + /// Destination stream for signed PDF output. /// Ordered signing options; one signature is applied per item. /// Cancellation token for cooperative cancellation. - /// The signed PDF as a byte array. - Task SignPdfAsync(byte[] pdfBytes, IReadOnlyList optionsList, CancellationToken cancellationToken = default); + Task SignPdfAsync(Stream inputPdf, Stream outputPdf, IReadOnlyList optionsList, CancellationToken cancellationToken = default); /// /// Checks whether the provided PDF contains signature field(s) and whether /// those signatures are cryptographically valid. /// - /// PDF content to inspect. + /// PDF content stream to inspect. /// Cancellation token for cooperative cancellation. /// A structured result containing signature presence and per-signature details. - Task CheckPdfSignatureAsync(byte[] pdfBytes, CancellationToken cancellationToken = default); + Task CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default); /// /// Extracts the original signed revision from a signed PDF. /// For multi-revision files this returns the first signed revision bytes. /// - /// Signed PDF bytes. + /// Signed PDF content stream. + /// Destination stream for original revision output. /// Cancellation token for cooperative cancellation. - /// PDF bytes of the original signed revision. - Task ExtractOriginalPdfAsync(byte[] pdfBytes, CancellationToken cancellationToken = default); + Task ExtractOriginalPdfAsync(Stream inputPdf, Stream outputPdf, CancellationToken cancellationToken = default); } diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs index 2c68ddc9..c469359d 100644 --- a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs +++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs @@ -18,79 +18,121 @@ public sealed class PdfDigitalSignatureService(IOptions options) { private static readonly Regex SignatureReasonMetaRegex = new(@"\|\s*SFMETA:v1;doc=(?[0-9a-fA-F-]{36});sha256=(?[A-Fa-f0-9]{64})$", RegexOptions.CultureInvariant | RegexOptions.Compiled); - public Task SignPdfAsync(byte[] pdfBytes, PdfSignOptionsDto? optionsModel = null, CancellationToken cancellationToken = default) + public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? optionsModel = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(pdfBytes); - var pkcs11Options = options.Value; - var request = optionsModel ?? new PdfSignOptionsDto(); + ArgumentNullException.ThrowIfNull(inputPdf); + ArgumentNullException.ThrowIfNull(outputPdf); + cancellationToken.ThrowIfCancellationRequested(); - using var inputPdf = new MemoryStream(pdfBytes); - using var signer = new PdfDocumentSigner(inputPdf); + var (workingInput, ownsWorkingInput) = await EnsureSeekableInputAsync(inputPdf, cancellationToken); - var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256); - var signerInfo = new ExternalSignerInfo(PdfSignatureType.Pkcs7, 32768, digestCalculator); - var sourceHashHex = Convert.ToHexString(SHA256.HashData(pdfBytes)); - var uuid = ResolveUuid(request.UUID); - var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, uuid, sourceHashHex); - var signatureFieldInfo = new PdfSignatureFieldInfo(1) + try { - Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}" - }; + workingInput.Position = 0; + var sourceHashHex = Convert.ToHexString(await SHA256.HashDataAsync(workingInput, cancellationToken)); + workingInput.Position = 0; - var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo) + var pkcs11Options = options.Value; + var request = optionsModel ?? new PdfSignOptionsDto(); + var uuid = ResolveUuid(request.UUID); + var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, uuid, sourceHashHex); + + using var signer = new PdfDocumentSigner(workingInput); + + var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256); + var signerInfo = new ExternalSignerInfo(PdfSignatureType.Pkcs7, 32768, digestCalculator); + var signatureFieldInfo = new PdfSignatureFieldInfo(1) + { + Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}" + }; + + var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo) + { + Name = request.Name ?? pkcs11Options.CertificateLabel, + Reason = reasonWithMeta, + Location = request.Location, + ContactInfo = BuildContactInfo(request.Email, request.Phone), + SigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now, + ApplicationName = pkcs11Options.ApplicationName, + CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel) + ?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel) + ?? PdfCertificationLevel.NoCertification + }; + + var deferredSigner = signer.SignDeferred(builder); + var externalSigner = new Pkcs11ExternalSigner(pkcs11Options); + deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid)); + } + finally { - Name = request.Name ?? pkcs11Options.CertificateLabel, - Reason = reasonWithMeta, - Location = request.Location, - ContactInfo = BuildContactInfo(request.Email, request.Phone), - SigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now, - ApplicationName = pkcs11Options.ApplicationName, - CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel) - ?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel) - ?? PdfCertificationLevel.NoCertification - }; - - var deferredSigner = signer.SignDeferred(builder); - var externalSigner = new Pkcs11ExternalSigner(pkcs11Options); - - using var outputPdf = new MemoryStream(); - deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid)); - return Task.FromResult(outputPdf.ToArray()); + if (ownsWorkingInput) + await workingInput.DisposeAsync(); + } } - public async Task SignPdfAsync(byte[] pdfBytes, IReadOnlyList optionsList, CancellationToken cancellationToken = default) + public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, IReadOnlyList optionsList, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(pdfBytes); + ArgumentNullException.ThrowIfNull(inputPdf); + ArgumentNullException.ThrowIfNull(outputPdf); ArgumentNullException.ThrowIfNull(optionsList); + cancellationToken.ThrowIfCancellationRequested(); if (optionsList.Count == 0) - return pdfBytes; - - var currentPdf = pdfBytes; - - foreach (var signOptions in optionsList) { - cancellationToken.ThrowIfCancellationRequested(); - currentPdf = await SignPdfAsync(currentPdf, signOptions, cancellationToken); + await inputPdf.CopyToAsync(outputPdf, cancellationToken); + return; } - return currentPdf; + var (currentInput, ownsCurrentInput) = await EnsureSeekableInputAsync(inputPdf, cancellationToken); + + try + { + for (var i = 0; i < optionsList.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + var isLast = i == optionsList.Count - 1; + + if (isLast) + { + await SignPdfAsync(currentInput, outputPdf, optionsList[i], cancellationToken); + continue; + } + + var intermediateOutput = new MemoryStream(); + await SignPdfAsync(currentInput, intermediateOutput, optionsList[i], cancellationToken); + + if (ownsCurrentInput) + await currentInput.DisposeAsync(); + + intermediateOutput.Position = 0; + currentInput = intermediateOutput; + ownsCurrentInput = true; + } + } + finally + { + if (ownsCurrentInput) + await currentInput.DisposeAsync(); + } } - public Task CheckPdfSignatureAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + public async Task CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(pdfBytes); + ArgumentNullException.ThrowIfNull(inputPdf); + cancellationToken.ThrowIfCancellationRequested(); - using var inputPdf = new MemoryStream(pdfBytes); - using var signer = new PdfDocumentSigner(inputPdf); + var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken); + + using var pdfStream = new MemoryStream(pdfBytes); + using var signer = new PdfDocumentSigner(pdfStream); var signatures = signer.GetSignatureInfo(); var byteRanges = ParseByteRanges(pdfBytes); if (signatures is null || signatures.Count == 0) { - return Task.FromResult(new PdfSignatureCheckResultDto( + return new PdfSignatureCheckResultDto( Signed: false, - Signatures: [])); + Signatures: []); } var items = new List(signatures.Count); @@ -177,19 +219,25 @@ public sealed class PdfDigitalSignatureService(IOptions options) } } - return Task.FromResult(new PdfSignatureCheckResultDto( + return new PdfSignatureCheckResultDto( Signed: true, - Signatures: items)); + Signatures: items); } - public Task ExtractOriginalPdfAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + public async Task ExtractOriginalPdfAsync(Stream inputPdf, Stream outputPdf, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(pdfBytes); + ArgumentNullException.ThrowIfNull(inputPdf); + ArgumentNullException.ThrowIfNull(outputPdf); cancellationToken.ThrowIfCancellationRequested(); + var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken); + var byteRanges = ParseByteRanges(pdfBytes); if (byteRanges.Count == 0) - return Task.FromResult(pdfBytes); + { + await outputPdf.WriteAsync(pdfBytes, cancellationToken); + return; + } var revisionLength = byteRanges .Select(GetSignedRevisionLength) @@ -200,9 +248,34 @@ public sealed class PdfDigitalSignatureService(IOptions options) if (revisionLength > pdfBytes.LongLength) throw new InvalidOperationException("Invalid ByteRange in signed PDF."); - var originalPdf = new byte[revisionLength]; - Buffer.BlockCopy(pdfBytes, 0, originalPdf, 0, (int)revisionLength); - return Task.FromResult(originalPdf); + await outputPdf.WriteAsync(pdfBytes.AsMemory(0, (int)revisionLength), cancellationToken); + } + + private static async Task<(Stream Stream, bool OwnsStream)> EnsureSeekableInputAsync(Stream input, CancellationToken cancellationToken) + { + if (input.CanSeek) + { + input.Position = 0; + return (input, false); + } + + var buffered = new MemoryStream(); + await input.CopyToAsync(buffered, cancellationToken); + buffered.Position = 0; + return (buffered, true); + } + + private static async Task ReadAllBytesAsync(Stream input, CancellationToken cancellationToken) + { + if (input 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) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs index 1f0500b3..cec1609e 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs @@ -39,14 +39,6 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService if (!file.FileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) return BadRequest("Only PDF files are supported."); - byte[] sourcePdf; - await using (var input = file.OpenReadStream()) - using (var ms = new MemoryStream()) - { - await input.CopyToAsync(ms, cancel); - sourcePdf = ms.ToArray(); - } - var signOptions = new PdfSignOptionsDto { UUID = request.UUID, @@ -58,9 +50,12 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService CertificationLevel = PdfSignatureCertificationLevel.FormFillingAndAnnotations }; - byte[] signedPdf = await signatureService.SignPdfAsync(sourcePdf, signOptions, cancel); + await using var input = file.OpenReadStream(); + await using var output = new MemoryStream(); + await signatureService.SignPdfAsync(input, output, signOptions, cancel); + output.Position = 0; var fileName = Path.GetFileNameWithoutExtension(file.FileName); - return File(signedPdf, "application/pdf", $"{fileName}-signed.pdf"); + return File(output, "application/pdf", $"{fileName}-signed.pdf"); } [AllowAnonymous] @@ -76,15 +71,8 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService if (!file.FileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) return BadRequest("Only PDF files are supported."); - byte[] sourcePdf; - await using (var input = file.OpenReadStream()) - using (var ms = new MemoryStream()) - { - await input.CopyToAsync(ms, cancel); - sourcePdf = ms.ToArray(); - } - - var result = await signatureService.CheckPdfSignatureAsync(sourcePdf, cancel); + await using var input = file.OpenReadStream(); + var result = await signatureService.CheckPdfSignatureAsync(input, cancel); return Ok(result); } }