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:
@@ -11,37 +11,37 @@ public interface IPdfDigitalSignatureService
|
||||
/// Creates a digital signature for the provided PDF bytes and returns
|
||||
/// a new signed PDF document.
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes">Source PDF content as a byte array.</param>
|
||||
/// <param name="inputPdf">Source PDF content stream.</param>
|
||||
/// <param name="outputPdf">Destination stream for signed PDF output.</param>
|
||||
/// <param name="options">Optional signing options that map to PDF signature metadata fields.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
|
||||
/// <returns>The signed PDF as a byte array.</returns>
|
||||
Task<byte[]> SignPdfAsync(byte[] pdfBytes, PdfSignOptionsDto? options = null, CancellationToken cancellationToken = default);
|
||||
Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Applies multiple signatures to the provided PDF in sequence and returns
|
||||
/// the final signed PDF document.
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes">Source PDF content as a byte array.</param>
|
||||
/// <param name="inputPdf">Source PDF content stream.</param>
|
||||
/// <param name="outputPdf">Destination stream for signed PDF output.</param>
|
||||
/// <param name="optionsList">Ordered signing options; one signature is applied per item.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
|
||||
/// <returns>The signed PDF as a byte array.</returns>
|
||||
Task<byte[]> SignPdfAsync(byte[] pdfBytes, IReadOnlyList<PdfSignOptionsDto> optionsList, CancellationToken cancellationToken = default);
|
||||
Task SignPdfAsync(Stream inputPdf, Stream outputPdf, IReadOnlyList<PdfSignOptionsDto> optionsList, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the provided PDF contains signature field(s) and whether
|
||||
/// those signatures are cryptographically valid.
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes">PDF content to inspect.</param>
|
||||
/// <param name="inputPdf">PDF content stream to inspect.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
|
||||
/// <returns>A structured result containing signature presence and per-signature details.</returns>
|
||||
Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the original signed revision from a signed PDF.
|
||||
/// For multi-revision files this returns the first signed revision bytes.
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes">Signed PDF bytes.</param>
|
||||
/// <param name="inputPdf">Signed PDF content stream.</param>
|
||||
/// <param name="outputPdf">Destination stream for original revision output.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
|
||||
/// <returns>PDF bytes of the original signed revision.</returns>
|
||||
Task<byte[]> ExtractOriginalPdfAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
Task ExtractOriginalPdfAsync(Stream inputPdf, Stream outputPdf, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user