feat(application): add DocumentOperator.Application PDF signing contracts

This commit is contained in:
2026-10-22 02:26:07 +02:00
parent de5e2d613f
commit 3b4134e873
11 changed files with 430 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
using DocumentOperator.Application.Common.Enums;
using System.ComponentModel.DataAnnotations;
namespace DocumentOperator.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>
/// Optional base directory that contains the <c>pkcs11</c> folder.
/// If null/empty, runtime uses the application base directory.
/// </summary>
public string? AssetsRootPath { 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>
/// Resolved absolute PKCS#11 proxy module path used at runtime.
/// Computed from <see cref="AssetsRootPath"/> and runtime defaults.
/// </summary>
public string ResolvedLibraryPath { get; private set; } = string.Empty;
/// <summary>
/// Resolved absolute pkcs11-tool path used at runtime.
/// Computed from <see cref="AssetsRootPath"/> and runtime defaults.
/// Can be null when tool fallback is not available.
/// </summary>
public string? ResolvedToolPath { get; private set; }
public void SetResolvedPaths(string resolvedLibraryPath, string? resolvedToolPath)
{
ResolvedLibraryPath = resolvedLibraryPath;
ResolvedToolPath = resolvedToolPath;
}
/// <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>
/// Application name embedded into PDF signature metadata.
/// This value is controlled via app settings and is not client-overridable.
/// </summary>
public string? ApplicationName { 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;
}

View File

@@ -0,0 +1,54 @@
using DocumentOperator.Application.Common.Enums;
namespace DocumentOperator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public sealed record PdfSignOptionsDto
{
/// <summary>
///
/// </summary>
public long DocumentElementId { get; set; }
/// <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; }
/// <summary>
///
/// </summary>
public string? Phone { get; set; }
/// <summary>
///
/// </summary>
public string ApplicationName { get; set; } = null!;
/// <summary>
///
/// </summary>
public PdfSignatureCertificationLevel CertificationLevel { get; set; }
/// <summary>
///
/// </summary>
public PdfSignatureAppearanceDto? Appearance { get; set; }
}

View File

@@ -0,0 +1,96 @@
namespace DocumentOperator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public enum PdfSignatureCoordinateUnit
{
/// <summary>
///
/// </summary>
Point = 0,
/// <summary>
///
/// </summary>
Inch = 1
}
/// <summary>
///
/// </summary>
public enum PdfSignatureCoordinateOrigin
{
/// <summary>
///
/// </summary>
BottomLeft = 0,
/// <summary>
///
/// </summary>
TopLeft = 1
}
/// <summary>
///
/// </summary>
/// <param name="PageNumber"></param>
/// <param name="Left"></param>
/// <param name="Bottom"></param>
/// <param name="Width"></param>
/// <param name="Height"></param>
/// <param name="ShowLabels"></param>
/// <param name="ShowName"></param>
/// <param name="ShowDate"></param>
/// <param name="ShowReason"></param>
/// <param name="ShowLocation"></param>
/// <param name="ShowDistinguishedName"></param>
/// <param name="DateTimeFormat"></param>
/// <param name="CoordinateUnit"></param>
/// <param name="CoordinateOrigin"></param>
public sealed record PdfSignatureAppearanceDto(
int PageNumber,
double Left,
double Bottom,
double Width,
double Height,
bool ShowLabels = false,
bool ShowName = false,
bool ShowDate = false,
bool ShowReason = false,
bool ShowLocation = false,
bool ShowDistinguishedName = false,
string DateTimeFormat = "dd.MM.yyyy HH:mm:ss zzz",
PdfSignatureCoordinateUnit CoordinateUnit = PdfSignatureCoordinateUnit.Point,
PdfSignatureCoordinateOrigin CoordinateOrigin = PdfSignatureCoordinateOrigin.BottomLeft)
{
/// <summary>
///
/// </summary>
/// <param name="FamilyName"></param>
/// <param name="Size"></param>
/// <param name="IsBold"></param>
/// <param name="IsItalic"></param>
/// <param name="IsUnderline"></param>
public sealed record FontDto(string FamilyName = "Helvetica", float Size = 10, bool IsBold = false, bool IsItalic = false, bool IsUnderline = false)
{
/// <summary>
///
/// </summary>
public RgbaColorDto Color { get; init; } = new();
}
/// <summary>
///
/// </summary>
/// <param name="R"></param>
/// <param name="G"></param>
/// <param name="B"></param>
public sealed record RgbaColorDto(double R = 0, double G = 0, double B = 0);
/// <summary>
///
/// </summary>
public FontDto Font { get; init; } = new();
}

View File

@@ -0,0 +1,57 @@
namespace DocumentOperator.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? BusinessReason,
string? Location,
string? DeclaredName,
string? ContactInfo,
string? Email,
string? Phone,
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,
long? DocumentElementId,
string? OriginalDocumentSha256,
string? MessageDigestHex,
string? Error);
public sealed record PdfSignatureCheckResultDto(
bool Signed,
IReadOnlyList<PdfSignatureCheckItemDto> Signatures);

View File

@@ -0,0 +1,10 @@
namespace DocumentOperator.Application.Common.Dto;
public sealed record Pkcs11SelfTestStatusDto(
bool Completed,
bool Success,
string Mode,
string Summary,
DateTimeOffset? CheckedAtUtc,
string? InteropError,
string? ToolError);

View File

@@ -0,0 +1,27 @@
namespace DocumentOperator.Application.Common.Enums;
/// <summary>
///
/// </summary>
public enum PdfSignatureCertificationLevel
{
/// <summary>
/// All changes are allowed
/// </summary>
NoCertification = 0,
/// <summary>
/// No changes are allowed
/// </summary>
NoChangesAllowed,
/// <summary>
/// Allows users to fill in forms and sign the document.
/// </summary>
FillForms,
/// <summary>
/// Allows users to fill in forms, create, modify and delete annotations, and sign the document.
/// </summary>
FillFormsAndAnnotate
}

View File

@@ -0,0 +1,29 @@
using DocumentOperator.Application.Common.Dto;
namespace DocumentOperator.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="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="image">Optional image stream for signature appearance.</param>
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default);
/// <summary>
/// Checks whether the provided PDF contains signature field(s) and whether
/// those signatures are cryptographically valid.
/// </summary>
/// <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(Stream inputPdf, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,8 @@
using DocumentOperator.Application.Common.Dto;
namespace DocumentOperator.Application.Common.Interfaces.Services;
public interface IPkcs11SelfTestStatusProvider
{
Pkcs11SelfTestStatusDto GetStatus();
}

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DocumentOperator.Application;
public static class DependencyInjection
{
public static IServiceCollection AddDocumentOperatorApplication(this IServiceCollection services, IConfiguration configuration)
{
services.AddMediatR(cfg =>
{
cfg.LicenseKey = configuration.GetValue<string>("LuckyPennySoftLicenseKey");
cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
});
return services;
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="14.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
using DocumentOperator.Application.Common.Dto;
using DocumentOperator.Application.Common.Interfaces.Services;
using MediatR;
namespace DocumentOperator.Application.SignPdf;
public sealed record SignPdfCommand : IRequest<byte[]>
{
public required Stream InputPdf { get; init; }
public required PdfSignOptionsDto Options { get; init; }
public Stream? SignatureImage { get; init; }
}
public sealed class SignPdfCommandHandler(IPdfDigitalSignatureService signer) : IRequestHandler<SignPdfCommand, byte[]>
{
public async Task<byte[]> Handle(SignPdfCommand command, CancellationToken cancellationToken)
{
using var output = new MemoryStream();
await signer.SignPdfAsync(command.InputPdf, output, command.Options, command.SignatureImage, cancellationToken);
return output.ToArray();
}
}