diff --git a/DocumentOperator.Application/Common/Configurations/Pkcs11Options.cs b/DocumentOperator.Application/Common/Configurations/Pkcs11Options.cs new file mode 100644 index 0000000..7ed64e6 --- /dev/null +++ b/DocumentOperator.Application/Common/Configurations/Pkcs11Options.cs @@ -0,0 +1,94 @@ +using DocumentOperator.Application.Common.Enums; +using System.ComponentModel.DataAnnotations; + +namespace DocumentOperator.Application.Common.Configurations; + +/// +/// Represents PKCS#11 signing configuration for PDF signing workflows. +/// This options model maps to the Crypt:Pkcs11 configuration section. +/// +public sealed class Pkcs11Options +{ + /// + /// Configuration section path used for binding PKCS#11 options. + /// + public const string SectionName = "Crypt:Pkcs11"; + + /// + /// Optional base directory that contains the pkcs11 folder. + /// If null/empty, runtime uses the application base directory. + /// + public string? AssetsRootPath { get; set; } + + /// + /// Optional numeric slot identifier used to directly select the target token slot. + /// + public ulong? SlotId { get; set; } + + /// + /// Optional token label used as a fallback token selector when is not provided. + /// + public string? TokenLabel { get; set; } + + /// + /// Optional PKCS#11 proxy endpoint (for example tcp://host:port). + /// + public string? ProxySocket { get; set; } + + /// + /// Resolved absolute PKCS#11 proxy module path used at runtime. + /// Computed from and runtime defaults. + /// + public string ResolvedLibraryPath { get; private set; } = string.Empty; + + /// + /// Resolved absolute pkcs11-tool path used at runtime. + /// Computed from and runtime defaults. + /// Can be null when tool fallback is not available. + /// + public string? ResolvedToolPath { get; private set; } + + public void SetResolvedPaths(string resolvedLibraryPath, string? resolvedToolPath) + { + ResolvedLibraryPath = resolvedLibraryPath; + ResolvedToolPath = resolvedToolPath; + } + + /// + /// User PIN used to authenticate to the PKCS#11 token session. + /// + [Required] + [MinLength(1)] + public required string UserPin { get; set; } + + /// + /// Label of the private key object used to create digital signatures. + /// + [Required] + [MinLength(1)] + public required string PrivateKeyLabel { get; set; } + + /// + /// Label of the certificate object returned/embedded with the signature. + /// + [Required] + [MinLength(1)] + public required string CertificateLabel { get; set; } + + /// + /// Optional global default signing reason used when request-level options do not specify a value. + /// + public string? DefaultReason { get; set; } + + /// + /// Application name embedded into PDF signature metadata. + /// This value is controlled via app settings and is not client-overridable. + /// + public string? ApplicationName { get; set; } + + /// + /// Controls how fallback signing time is created when request-level options do not specify a value. + /// true uses , false uses . + /// + public bool UseUtcSigningTime { get; set; } = true; +} diff --git a/DocumentOperator.Application/Common/Dto/PdfSignOptionsDto.cs b/DocumentOperator.Application/Common/Dto/PdfSignOptionsDto.cs new file mode 100644 index 0000000..47d5e27 --- /dev/null +++ b/DocumentOperator.Application/Common/Dto/PdfSignOptionsDto.cs @@ -0,0 +1,54 @@ +using DocumentOperator.Application.Common.Enums; + +namespace DocumentOperator.Application.Common.Dto; + +/// +/// +/// +public sealed record PdfSignOptionsDto +{ + /// + /// + /// + public long DocumentElementId { get; set; } + + /// + /// + /// + public string Name { get; set; } = null!; + + /// + /// + /// + public string Reason { get; set; } = null!; + + /// + /// + /// + public string Location { get; set; } = null!; + + /// + /// + /// + public string? Email { get; set; } + + /// + /// + /// + public string? Phone { get; set; } + + /// + /// + /// + public string ApplicationName { get; set; } = null!; + + /// + /// + /// + public PdfSignatureCertificationLevel CertificationLevel { get; set; } + + /// + /// + /// + public PdfSignatureAppearanceDto? Appearance { get; set; } +} diff --git a/DocumentOperator.Application/Common/Dto/PdfSignatureAppearanceDto.cs b/DocumentOperator.Application/Common/Dto/PdfSignatureAppearanceDto.cs new file mode 100644 index 0000000..4497d36 --- /dev/null +++ b/DocumentOperator.Application/Common/Dto/PdfSignatureAppearanceDto.cs @@ -0,0 +1,96 @@ +namespace DocumentOperator.Application.Common.Dto; + +/// +/// +/// +public enum PdfSignatureCoordinateUnit +{ + /// + /// + /// + Point = 0, + + /// + /// + /// + Inch = 1 +} + +/// +/// +/// +public enum PdfSignatureCoordinateOrigin +{ + /// + /// + /// + BottomLeft = 0, + + /// + /// + /// + TopLeft = 1 +} + +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +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) +{ + /// + /// + /// + /// + /// + /// + /// + /// + public sealed record FontDto(string FamilyName = "Helvetica", float Size = 10, bool IsBold = false, bool IsItalic = false, bool IsUnderline = false) + { + /// + /// + /// + public RgbaColorDto Color { get; init; } = new(); + } + + /// + /// + /// + /// + /// + /// + public sealed record RgbaColorDto(double R = 0, double G = 0, double B = 0); + + /// + /// + /// + public FontDto Font { get; init; } = new(); +} diff --git a/DocumentOperator.Application/Common/Dto/PdfSignatureCheckResultDto.cs b/DocumentOperator.Application/Common/Dto/PdfSignatureCheckResultDto.cs new file mode 100644 index 0000000..2aa4575 --- /dev/null +++ b/DocumentOperator.Application/Common/Dto/PdfSignatureCheckResultDto.cs @@ -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 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 Signatures); diff --git a/DocumentOperator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs b/DocumentOperator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs new file mode 100644 index 0000000..81751f8 --- /dev/null +++ b/DocumentOperator.Application/Common/Dto/Pkcs11SelfTestStatusDto.cs @@ -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); diff --git a/DocumentOperator.Application/Common/Enums/PdfSignatureCertificationLevel.cs b/DocumentOperator.Application/Common/Enums/PdfSignatureCertificationLevel.cs new file mode 100644 index 0000000..d8078e9 --- /dev/null +++ b/DocumentOperator.Application/Common/Enums/PdfSignatureCertificationLevel.cs @@ -0,0 +1,27 @@ +namespace DocumentOperator.Application.Common.Enums; + +/// +/// +/// +public enum PdfSignatureCertificationLevel +{ + /// + /// All changes are allowed + /// + NoCertification = 0, + + /// + /// No changes are allowed + /// + NoChangesAllowed, + + /// + /// Allows users to fill in forms and sign the document. + /// + FillForms, + + /// + /// Allows users to fill in forms, create, modify and delete annotations, and sign the document. + /// + FillFormsAndAnnotate +} diff --git a/DocumentOperator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs b/DocumentOperator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs new file mode 100644 index 0000000..5dc5fdb --- /dev/null +++ b/DocumentOperator.Application/Common/Interfaces/Services/IPdfDigitalSignatureService.cs @@ -0,0 +1,29 @@ +using DocumentOperator.Application.Common.Dto; + +namespace DocumentOperator.Application.Common.Interfaces.Services; + +/// +/// Defines PDF digital-signing operations for the application boundary. +/// +public interface IPdfDigitalSignatureService +{ + /// + /// Creates a digital signature for the provided PDF bytes and returns + /// a new signed PDF document. + /// + /// Source PDF content stream. + /// Destination stream for signed PDF output. + /// Optional signing options that map to PDF signature metadata fields. + /// Optional image stream for signature appearance. + /// Cancellation token for cooperative cancellation. + Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default); + + /// + /// Checks whether the provided PDF contains signature field(s) and whether + /// those signatures are cryptographically valid. + /// + /// PDF content stream to inspect. + /// Cancellation token for cooperative cancellation. + /// A structured result containing signature presence and per-signature details. + Task CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default); +} diff --git a/DocumentOperator.Application/Common/Interfaces/Services/IPkcs11SelfTestStatusProvider.cs b/DocumentOperator.Application/Common/Interfaces/Services/IPkcs11SelfTestStatusProvider.cs new file mode 100644 index 0000000..7633b1d --- /dev/null +++ b/DocumentOperator.Application/Common/Interfaces/Services/IPkcs11SelfTestStatusProvider.cs @@ -0,0 +1,8 @@ +using DocumentOperator.Application.Common.Dto; + +namespace DocumentOperator.Application.Common.Interfaces.Services; + +public interface IPkcs11SelfTestStatusProvider +{ + Pkcs11SelfTestStatusDto GetStatus(); +} diff --git a/DocumentOperator.Application/DependencyInjection.cs b/DocumentOperator.Application/DependencyInjection.cs new file mode 100644 index 0000000..a5087ae --- /dev/null +++ b/DocumentOperator.Application/DependencyInjection.cs @@ -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("LuckyPennySoftLicenseKey"); + cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly); + }); + + return services; + } +} diff --git a/DocumentOperator.Application/DocumentOperator.Application.csproj b/DocumentOperator.Application/DocumentOperator.Application.csproj new file mode 100644 index 0000000..dd6be39 --- /dev/null +++ b/DocumentOperator.Application/DocumentOperator.Application.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/DocumentOperator.Application/SignPdf/SignPdfCommand.cs b/DocumentOperator.Application/SignPdf/SignPdfCommand.cs new file mode 100644 index 0000000..d88967a --- /dev/null +++ b/DocumentOperator.Application/SignPdf/SignPdfCommand.cs @@ -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 +{ + public required Stream InputPdf { get; init; } + public required PdfSignOptionsDto Options { get; init; } + public Stream? SignatureImage { get; init; } +} + +public sealed class SignPdfCommandHandler(IPdfDigitalSignatureService signer) : IRequestHandler +{ + public async Task Handle(SignPdfCommand command, CancellationToken cancellationToken) + { + using var output = new MemoryStream(); + await signer.SignPdfAsync(command.InputPdf, output, command.Options, command.SignatureImage, cancellationToken); + return output.ToArray(); + } +}