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.
This commit is contained in:
2026-10-15 11:04:05 +02:00
parent 29108594d9
commit 04cea1ca4b
3 changed files with 147 additions and 86 deletions

View File

@@ -18,79 +18,121 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
{
private static readonly Regex SignatureReasonMetaRegex = new(@"\|\s*SFMETA:v1;doc=(?<doc>[0-9a-fA-F-]{36});sha256=(?<sha>[A-Fa-f0-9]{64})$", RegexOptions.CultureInvariant | RegexOptions.Compiled);
public Task<byte[]> 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<byte[]> SignPdfAsync(byte[] pdfBytes, IReadOnlyList<PdfSignOptionsDto> optionsList, CancellationToken cancellationToken = default)
public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, IReadOnlyList<PdfSignOptionsDto> 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<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
public async Task<PdfSignatureCheckResultDto> 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<PdfSignatureCheckItemDto>(signatures.Count);
@@ -177,19 +219,25 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
}
}
return Task.FromResult(new PdfSignatureCheckResultDto(
return new PdfSignatureCheckResultDto(
Signed: true,
Signatures: items));
Signatures: items);
}
public Task<byte[]> 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<Pkcs11Options> 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<byte[]> 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)