feat(application): add DocumentOperator.Application PDF signing contracts
This commit is contained in:
@@ -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;
|
||||
}
|
||||
54
DocumentOperator.Application/Common/Dto/PdfSignOptionsDto.cs
Normal file
54
DocumentOperator.Application/Common/Dto/PdfSignOptionsDto.cs
Normal 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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using DocumentOperator.Application.Common.Dto;
|
||||
|
||||
namespace DocumentOperator.Application.Common.Interfaces.Services;
|
||||
|
||||
public interface IPkcs11SelfTestStatusProvider
|
||||
{
|
||||
Pkcs11SelfTestStatusDto GetStatus();
|
||||
}
|
||||
18
DocumentOperator.Application/DependencyInjection.cs
Normal file
18
DocumentOperator.Application/DependencyInjection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
22
DocumentOperator.Application/SignPdf/SignPdfCommand.cs
Normal file
22
DocumentOperator.Application/SignPdf/SignPdfCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user