Refactor DTOs and simplify PdfDigitalSignatureService

Refactored `PdfSignOptionsDto` and `PdfVisualSignatureOptionsDto`:
- Converted both to immutable `record` types with non-nullable properties.
- Added XML documentation comments for properties and parameters.

Simplified `PdfDigitalSignatureService`:
- Updated `SignPdfAsync` to enforce non-nullable `PdfSignOptionsDto`.
- Removed redundant null checks and inlined `CreateSignatureBounds`.
- Simplified signature appearance and image data logic.
- Refactored `TryGetKeySize` to use `bool` with `out` parameter.

General improvements:
- Enhanced code readability and maintainability.
- Reduced nullable references and enforced stricter type safety.
This commit is contained in:
2026-10-16 10:43:11 +02:00
parent 198eabcbae
commit 57a08e8a89
3 changed files with 85 additions and 60 deletions

View File

@@ -2,15 +2,53 @@ using EnvelopeGenerator.Application.Common.Enums;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public sealed record PdfSignOptionsDto
{
public string? UUID { get; set; }
public string? Name { get; set; }
public string? Reason { get; set; }
public string? Location { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string? ApplicationName { get; set; }
public PdfSignatureCertificationLevel? CertificationLevel { get; set; }
public PdfVisualSignatureOptionsDto? VisualSignature { get; set; }
/// <summary>
///
/// </summary>
public string UUID { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Reason { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Location { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Email { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Phone { get; set; } = null!;
/// <summary>
///
/// </summary>
public string ApplicationName { get; set; } = null!;
/// <summary>
///
/// </summary>
public PdfSignatureCertificationLevel CertificationLevel { get; set; }
/// <summary>
///
/// </summary>
public PdfVisualSignatureOptionsDto VisualSignature { get; set; } = null!;
}

View File

@@ -1,5 +1,15 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
/// <param name="ImageBytes"></param>
/// <param name="PageNumber"></param>
/// <param name="Left"></param>
/// <param name="Bottom"></param>
/// <param name="Width"></param>
/// <param name="Height"></param>
/// <param name="ShowLabels"></param>
public sealed record PdfVisualSignatureOptionsDto(
byte[] ImageBytes,
int PageNumber,

View File

@@ -18,10 +18,8 @@ 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? optionsModel = null, CancellationToken cancellationToken = default)
public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(inputPdf);
ArgumentNullException.ThrowIfNull(outputPdf);
cancellationToken.ThrowIfCancellationRequested();
var (workingInput, ownsWorkingInput) = await EnsureSeekableInputAsync(inputPdf, cancellationToken);
@@ -33,7 +31,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
workingInput.Position = 0;
var pkcs11Options = options.Value;
var request = optionsModel ?? new PdfSignOptionsDto();
var uuid = ResolveUuid(request.UUID);
var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, uuid, sourceHashHex);
@@ -47,9 +45,9 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"
};
var signatureBounds = CreateSignatureBounds(request.VisualSignature);
if (signatureBounds is not null)
signatureFieldInfo.SignatureBounds = signatureBounds;
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)
{
@@ -64,21 +62,18 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
?? PdfCertificationLevel.NoCertification
};
if(request.VisualSignature is PdfVisualSignatureOptionsDto visualSignature)
builder.SetSignatureAppearance(new PdfSignatureAppearance
{
builder.SetSignatureAppearance(new PdfSignatureAppearance
{
AppearanceType = PdfSignatureAppearanceType.Name,
ShowLabels = visualSignature.ShowLabels,
ShowName = true,
ShowDate = false,
ShowReason = false,
ShowLocation = false,
ShowDistinguishedName = false
});
builder.SetImageData(visualSignature.ImageBytes);
}
AppearanceType = PdfSignatureAppearanceType.Name,
ShowLabels = request.VisualSignature.ShowLabels,
ShowName = true,
ShowDate = false,
ShowReason = false,
ShowLocation = false,
ShowDistinguishedName = false
});
builder.SetImageData(request.VisualSignature.ImageBytes);
var deferredSigner = signer.SignDeferred(builder);
var externalSigner = new Pkcs11ExternalSigner(pkcs11Options);
deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid));
@@ -92,9 +87,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
public async Task SignPdfAsync(Stream inputPdf, Stream outputPdf, IReadOnlyList<PdfSignOptionsDto> optionsList, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(inputPdf);
ArgumentNullException.ThrowIfNull(outputPdf);
ArgumentNullException.ThrowIfNull(optionsList);
cancellationToken.ThrowIfCancellationRequested();
if (optionsList.Count == 0)
@@ -138,7 +130,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
public async Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(inputPdf);
cancellationToken.ThrowIfCancellationRequested();
var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken);
@@ -246,8 +237,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
public async Task ExtractOriginalPdfAsync(Stream inputPdf, Stream outputPdf, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(inputPdf);
ArgumentNullException.ThrowIfNull(outputPdf);
cancellationToken.ThrowIfCancellationRequested();
var pdfBytes = await ReadAllBytesAsync(inputPdf, cancellationToken);
@@ -338,18 +327,25 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
KeyAlgorithm: certificate.PublicKey.Oid?.FriendlyName ?? certificate.PublicKey.Oid?.Value,
SignatureAlgorithm: certificate.SignatureAlgorithm.FriendlyName ?? certificate.SignatureAlgorithm.Value,
Pem: ToPem(certificate.RawData),
KeySizeBits: TryGetKeySize(certificate));
KeySizeBits: TryGetKeySize(certificate, out var keySize) ? keySize : null);
}
private static int? TryGetKeySize(X509Certificate2 certificate)
private static bool TryGetKeySize(X509Certificate2 certificate, out int keySize)
{
keySize = 0;
try
{
return certificate.PublicKey.Key.KeySize;
using var rsa = certificate.GetRSAPublicKey();
if (rsa is null)
return false;
keySize = rsa.KeySize;
return true;
}
catch
catch (CryptographicException)
{
return null;
return false;
}
}
@@ -567,25 +563,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
return pageNumber;
}
private static PdfRectangle? CreateSignatureBounds(PdfVisualSignatureOptionsDto? visualOptions)
{
if (visualOptions is null)
return null;
var left = visualOptions.Left;
var bottom = visualOptions.Bottom;
var width = visualOptions.Width;
var height = visualOptions.Height;
if (left < 0 || bottom < 0)
throw new ArgumentOutOfRangeException(nameof(visualOptions), "VisualSignature.Left and VisualSignature.Bottom cannot be negative.");
if (width <= 0 || height <= 0)
throw new ArgumentOutOfRangeException(nameof(visualOptions), "VisualSignature.Width and VisualSignature.Height must be greater than zero.");
return new PdfRectangle(left, bottom, left + width, bottom + height);
}
private static string BuildReasonWithMeta(string? businessReason, string uuid, string sourceHashHex)
{
var trimmedReason = string.IsNullOrWhiteSpace(businessReason) ? null : businessReason.Trim();