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);
}
}