Add PKCS#11 support for PDF signing and validation

Introduced `Pkcs11Options` for configuring PKCS#11 signing workflows, including properties for library path, token selection, and signing metadata. Added `PdfSignOptionsDto` to represent signing options and `PdfSignatureCheckResultDto` with related DTOs for signature validation.

Defined `PdfSignatureCertificationLevel` enum for certification levels. Added `IPdfDigitalSignatureService` interface to support signing and validating PDF signatures. These changes enable robust and extensible handling of digital signatures.
This commit is contained in:
2026-10-13 14:44:06 +02:00
parent 63c60a91b2
commit 7766e2ea25
5 changed files with 198 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
using EnvelopeGenerator.Application.Common.Enums;
using System.ComponentModel.DataAnnotations;
namespace EnvelopeGenerator.Application.Common.Configurations;
/// <summary>
/// Represents PKCS#11 signing configuration for PDF signing workflows.
/// This options model maps to the <c>Crypt:Pkcs11</c> configuration section.
/// </summary>
public sealed class Pkcs11Options
{
/// <summary>
/// Configuration section path used for binding PKCS#11 options.
/// </summary>
public const string SectionName = "Crypt:Pkcs11";
/// <summary>
/// Absolute or resolvable file path of the PKCS#11 provider library.
/// </summary>
[Required]
[MinLength(1)]
public required string LibraryPath { get; set; }
/// <summary>
/// Optional numeric slot identifier used to directly select the target token slot.
/// </summary>
public ulong? SlotId { get; set; }
/// <summary>
/// Optional token label used as a fallback token selector when <see cref="SlotId"/> is not provided.
/// </summary>
public string? TokenLabel { get; set; }
/// <summary>
/// Optional PKCS#11 proxy endpoint (for example <c>tcp://host:port</c>).
/// </summary>
public string? ProxySocket { get; set; }
/// <summary>
/// Optional full path to <c>pkcs11-tool</c>, used by diagnostics and fallback paths.
/// </summary>
public string? ToolPath { get; set; }
/// <summary>
/// User PIN used to authenticate to the PKCS#11 token session.
/// </summary>
[Required]
[MinLength(1)]
public required string UserPin { get; set; }
/// <summary>
/// Label of the private key object used to create digital signatures.
/// </summary>
[Required]
[MinLength(1)]
public required string PrivateKeyLabel { get; set; }
/// <summary>
/// Label of the certificate object returned/embedded with the signature.
/// </summary>
[Required]
[MinLength(1)]
public required string CertificateLabel { get; set; }
/// <summary>
/// Optional global default signing reason used when request-level options do not specify a value.
/// </summary>
public string? DefaultReason { get; set; }
/// <summary>
/// Optional global default application name used when request-level options do not specify a value.
/// </summary>
public string? DefaultApplicationName { get; set; }
/// <summary>
/// Controls how fallback signing time is created when request-level options do not specify a value.
/// <c>true</c> uses <see cref="DateTimeOffset.UtcNow"/>, <c>false</c> uses <see cref="DateTimeOffset.Now"/>.
/// </summary>
public bool UseUtcSigningTime { get; set; } = true;
/// <summary>
/// Optional global default certification level used when request-level options do not specify a value.
/// </summary>
public PdfSignatureCertificationLevel? DefaultCertificationLevel { get; set; }
}

View File

@@ -0,0 +1,14 @@
using EnvelopeGenerator.Application.Common.Enums;
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed record PdfSignOptionsDto
{
public string? Name { get; set; }
public string? Reason { get; set; }
public string? Location { get; set; }
public string? ContactInfo { get; set; }
public DateTimeOffset? SigningTime { get; set; }
public string? ApplicationName { get; set; }
public PdfSignatureCertificationLevel? CertificationLevel { get; set; }
}

View File

@@ -0,0 +1,52 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed record PdfSignaturePrincipalDto(
string? CommonName,
string? Organization,
string? Country,
string? Full);
public sealed record PdfSignatureCertificateInfoDto(
PdfSignaturePrincipalDto IssuedTo,
PdfSignaturePrincipalDto IssuedBy,
string? SerialNumber,
DateTimeOffset? ValidFrom,
DateTimeOffset? ValidTo,
bool SelfSigned,
bool Expired,
bool NotYetValid,
bool IsSigner,
int? Version,
string? KeyAlgorithm,
string? SignatureAlgorithm,
string? Pem,
int? KeySizeBits);
public sealed record PdfSignatureCheckItemDto(
bool CoversWholeDoc,
string? Reason,
string? Location,
string? DeclaredName,
string? ContactInfo,
string? DigestAlgorithm,
string? SignatureAlgorithm,
string? SignerName,
string? Issuer,
string? SerialNumber,
DateTimeOffset? ValidFrom,
DateTimeOffset? ValidTo,
string ExpiryCheckedAgainst,
IReadOnlyList<PdfSignatureCertificateInfoDto> Certificates,
bool Unchanged,
string? FieldName,
bool IsCryptographicallyValid,
string? SigningTime,
string? Filter,
string? SubFilter,
string? ApplicationName,
string? MessageDigestHex,
string? Error);
public sealed record PdfSignatureCheckResultDto(
bool Signed,
IReadOnlyList<PdfSignatureCheckItemDto> Signatures);

View File

@@ -0,0 +1,9 @@
namespace EnvelopeGenerator.Application.Common.Enums;
public enum PdfSignatureCertificationLevel
{
NoCertification = 0,
NoChangesAllowed = 1,
FormFillingAndSigning = 2,
FormFillingAndAnnotations = 3
}

View File

@@ -0,0 +1,38 @@
using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
/// <summary>
/// Defines PDF digital-signing operations for the application boundary.
/// </summary>
public interface IPdfDigitalSignatureService
{
/// <summary>
/// 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="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);
/// <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="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);
/// <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="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);
}