Refactor PDF signing logic and appearance handling
Refactored `Pkcs11Options` by removing `DefaultCertificationLevel` and improving property documentation. Replaced `VisualSignature` in `PdfSignOptionsDto` with `Appearance` using the new `PdfSignatureAppearanceDto` class, which encapsulates signature appearance details, including font and color settings. Enhanced `PdfSignatureCertificationLevel` enum with more descriptive values and added XML documentation. Updated `IPdfDigitalSignatureService` to support optional signature images and removed redundant methods. Refactored `PdfDigitalSignatureService` to streamline appearance handling, validate inputs, and improve maintainability. Removed unused methods and added support for `PdfSignatureAppearanceDto`. Simplified `PdfSignatureController` by consolidating form properties into `PdfSignOptionsDto`. Improved code readability and flexibility while removing redundant logic.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Enums;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using DevExpress.Office.DigitalSignatures;
|
||||
using DevExpress.Pdf;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
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.Security.Cryptography.Pkcs;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -18,7 +20,7 @@ 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 async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto request, CancellationToken cancellationToken = default)
|
||||
public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto request, Stream? image = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -32,48 +34,66 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
|
||||
var pkcs11Options = options.Value;
|
||||
|
||||
var uuid = ResolveUuid(request.UUID);
|
||||
var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, uuid, sourceHashHex);
|
||||
var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, request.UUID, sourceHashHex);
|
||||
|
||||
using var signer = new PdfDocumentSigner(workingInput);
|
||||
var signaturePageNumber = ResolveSignaturePageNumber(request.VisualSignature?.PageNumber, signer.PageCount);
|
||||
|
||||
var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256);
|
||||
var signerInfo = new ExternalSignerInfo(PdfSignatureType.Pkcs7, 32768, digestCalculator);
|
||||
var signatureFieldInfo = new PdfSignatureFieldInfo(signaturePageNumber)
|
||||
{
|
||||
Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"
|
||||
};
|
||||
|
||||
var left = request.VisualSignature!.Left;
|
||||
var bottom = request.VisualSignature.Bottom;
|
||||
signatureFieldInfo.SignatureBounds = new PdfRectangle(left, bottom, left + request.VisualSignature.Width, bottom + request.VisualSignature.Height);
|
||||
|
||||
var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo)
|
||||
var builder = request.Appearance is null ? new PdfDeferredSignatureBuilder(signerInfo) : new (signerInfo, new PdfSignatureFieldInfo(request.Appearance.PageNumber)
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
builder.SetSignatureAppearance(new PdfSignatureAppearance
|
||||
{
|
||||
AppearanceType = PdfSignatureAppearanceType.Name,
|
||||
ShowLabels = request.VisualSignature.ShowLabels,
|
||||
ShowName = true,
|
||||
ShowDate = false,
|
||||
ShowReason = false,
|
||||
ShowLocation = false,
|
||||
ShowDistinguishedName = false
|
||||
Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}",
|
||||
SignatureBounds = new PdfRectangle(request.Appearance.Left, request.Appearance.Bottom, request.Appearance.Left + request.Appearance.Width, request.Appearance.Bottom + request.Appearance.Height)
|
||||
});
|
||||
builder.SetImageData(request.VisualSignature.ImageBytes);
|
||||
|
||||
|
||||
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));
|
||||
@@ -85,49 +105,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, IReadOnlyList<PdfSignOptionsDto> optionsList, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (optionsList.Count == 0)
|
||||
{
|
||||
await inputPdf.CopyToAsync(outputPdf, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: verify and optimize (remove memory stream etc.) the check algortihm
|
||||
public async Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -135,7 +113,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken);
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
using var signer = new PdfDocumentSigner(pdfStream);
|
||||
using var signer = new PdfDocumentSigner(inputPdf);
|
||||
var signatures = signer.GetSignatureInfo();
|
||||
var byteRanges = ParseByteRanges(pdfBytes);
|
||||
|
||||
@@ -235,31 +213,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
Signatures: items);
|
||||
}
|
||||
|
||||
public async Task ExtractOriginalPdfAsync(Stream inputPdf, Stream outputPdf, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken);
|
||||
|
||||
var byteRanges = ParseByteRanges(pdfBytes);
|
||||
if (byteRanges.Count == 0)
|
||||
{
|
||||
await outputPdf.WriteAsync(pdfBytes, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var revisionLength = byteRanges
|
||||
.Select(GetSignedRevisionLength)
|
||||
.Where(x => x > 0)
|
||||
.DefaultIfEmpty(pdfBytes.LongLength)
|
||||
.Min();
|
||||
|
||||
if (revisionLength > pdfBytes.LongLength)
|
||||
throw new InvalidOperationException("Invalid ByteRange in signed PDF.");
|
||||
|
||||
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)
|
||||
@@ -398,11 +351,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
return new PdfSignaturePrincipalDto(commonName, organization, country, dn);
|
||||
}
|
||||
|
||||
private static PdfCertificationLevel? ToDevExpressCertificationLevel(PdfSignatureCertificationLevel? value)
|
||||
{
|
||||
return value is null ? null : (PdfCertificationLevel)(int)value.Value;
|
||||
}
|
||||
|
||||
private static string? ToColonSerial(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
@@ -544,25 +492,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
|
||||
return start + length;
|
||||
}
|
||||
|
||||
private static string ResolveUuid(string? uuid)
|
||||
{
|
||||
return Guid.TryParse(uuid, out var parsed)
|
||||
? parsed.ToString()
|
||||
: Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
private static int ResolveSignaturePageNumber(int? requestedPageNumber, int pageCount)
|
||||
{
|
||||
var pageNumber = requestedPageNumber ?? 1;
|
||||
if (pageNumber < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(requestedPageNumber), "SignaturePageNumber must be greater than or equal to 1.");
|
||||
|
||||
if (pageCount > 0 && pageNumber > pageCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(requestedPageNumber), $"SignaturePageNumber {pageNumber} exceeds page count {pageCount}.");
|
||||
|
||||
return pageNumber;
|
||||
}
|
||||
|
||||
private static string BuildReasonWithMeta(string? businessReason, string uuid, string sourceHashHex)
|
||||
{
|
||||
var trimmedReason = string.IsNullOrWhiteSpace(businessReason) ? null : businessReason.Trim();
|
||||
|
||||
Reference in New Issue
Block a user