From 68d9dbdcdfb6118562d069bf3a6b89b53ab8a068 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 14 Oct 2026 14:24:51 +0200 Subject: [PATCH] Add UUID and metadata support for PDF signing Enhanced PDF signing with UUID and metadata handling: - Added `UUID` and `OriginalDocumentSha256` to `PdfSignOptionsDto` and `PdfSignatureCheckItemDto`. - Introduced `BusinessReason` to separate metadata from signature reasons. - Updated `PdfDigitalSignatureService` to embed and parse metadata in signature reasons. - Improved error handling to include metadata in validation results. - Updated `PdfSignatureController` to handle `UUID` in signing requests. Refactored PKCS#11 configuration: - Replaced `LibraryPath` with `AssetsRootPath` for asset resolution. - Added `ProxySocket` and `DefaultApplicationName` options. - Clarified runtime behavior for asset resolution in `README.md`. Updated documentation and examples to reflect these changes. --- .../Common/Dto/PdfSignOptionsDto.cs | 1 + .../Common/Dto/PdfSignatureCheckResultDto.cs | 3 ++ .../README.md | 18 ++++--- .../Services/PdfDigitalSignatureService.cs | 49 ++++++++++++++++++- .../resources/INFRASTRUCTURE_PLAN.md | 4 +- .../Controllers/PdfSignatureController.cs | 2 + 6 files changed, 66 insertions(+), 11 deletions(-) diff --git a/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs b/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs index a6c0ed26..ab9fa245 100644 --- a/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/PdfSignOptionsDto.cs @@ -4,6 +4,7 @@ namespace EnvelopeGenerator.Application.Common.Dto; public sealed record PdfSignOptionsDto { + public string? UUID { get; set; } public string? Name { get; set; } public string? Reason { get; set; } public string? Location { get; set; } diff --git a/EnvelopeGenerator.Application/Common/Dto/PdfSignatureCheckResultDto.cs b/EnvelopeGenerator.Application/Common/Dto/PdfSignatureCheckResultDto.cs index 3029ed2c..d694b224 100644 --- a/EnvelopeGenerator.Application/Common/Dto/PdfSignatureCheckResultDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/PdfSignatureCheckResultDto.cs @@ -25,6 +25,7 @@ public sealed record PdfSignatureCertificateInfoDto( public sealed record PdfSignatureCheckItemDto( bool CoversWholeDoc, string? Reason, + string? BusinessReason, string? Location, string? DeclaredName, string? ContactInfo, @@ -44,6 +45,8 @@ public sealed record PdfSignatureCheckItemDto( string? Filter, string? SubFilter, string? ApplicationName, + string? UUID, + string? OriginalDocumentSha256, string? MessageDigestHex, string? Error); diff --git a/EnvelopeGenerator.Infrastructure.Crypt/README.md b/EnvelopeGenerator.Infrastructure.Crypt/README.md index 678b7abb..1c07d7f7 100644 --- a/EnvelopeGenerator.Infrastructure.Crypt/README.md +++ b/EnvelopeGenerator.Infrastructure.Crypt/README.md @@ -42,7 +42,8 @@ Supported deployment patterns: Critical platform rule: - A Windows process cannot load a Linux `.so` directly. -- `Crypt:Pkcs11:LibraryPath` must always point to a native library compatible with the host OS of `EnvelopeGenerator.Server`. +- `Crypt:Pkcs11:AssetsRootPath` must resolve to a directory that contains the expected native assets for the host OS. +- If `AssetsRootPath` is empty, runtime resolves assets from `AppContext.BaseDirectory`. --- @@ -104,14 +105,15 @@ Configuration section: `Crypt:Pkcs11` { "Crypt": { "Pkcs11": { - "LibraryPath": "/usr/lib/softhsm/libsofthsm2.so", + "AssetsRootPath": "E:\\DataFiles\\Digital Data\\EnvelopeGenerator", "SlotId": 1720207650, "TokenLabel": "DevToken", + "ProxySocket": "tcp://172.24.12.64:5657", "UserPin": "***", "PrivateKeyLabel": "sign-key", - "PrivateKeyIdHex": null, "CertificateLabel": "sign-cert", - "CertificateIdHex": null + "DefaultApplicationName": "signFLOW", + "UseUtcSigningTime": true } } } @@ -139,7 +141,7 @@ Crypt tests read PKCS#11 values from `EnvelopeGenerator.Tests/appsettings.json` Required keys: -- `Crypt:Pkcs11:LibraryPath` +- `Crypt:Pkcs11:AssetsRootPath` (optional; if missing, app base directory is used) - `Crypt:Pkcs11:SlotId` (or `Crypt:Pkcs11:TokenLabel`) - `Crypt:Pkcs11:UserPin` - `Crypt:Pkcs11:PrivateKeyLabel` @@ -371,11 +373,11 @@ Checkpoint: Server runtime (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`) must include: -- `Crypt:Pkcs11:LibraryPath` = native PKCS#11 library path for the server host OS - - Windows host: Windows DLL path (vendor DLL or proxy client DLL) - - Linux host: Linux `.so` path +- `Crypt:Pkcs11:AssetsRootPath` = base directory that contains `pkcs11\windows\proxy\libpkcs11-proxy.dll` + - If null/empty, runtime resolves assets from application output (`AppContext.BaseDirectory`) - `Crypt:Pkcs11:SlotId` = `2021549410` (or current company slot) - `Crypt:Pkcs11:TokenLabel` = `CompanyProdToken` +- `Crypt:Pkcs11:ProxySocket` = `tcp://172.24.12.64:5657` - `Crypt:Pkcs11:UserPin` = real user pin - `Crypt:Pkcs11:PrivateKeyLabel` = `company-sign-key-v1` - `Crypt:Pkcs11:CertificateLabel` = `company-sign-cert-v1` diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs index 25a627ec..4592df7f 100644 --- a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs +++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs @@ -16,6 +16,8 @@ namespace EnvelopeGenerator.Infrastructure.Crypt.Services; public sealed class PdfDigitalSignatureService(IOptions options) : IPdfDigitalSignatureService { + private static readonly Regex SignatureReasonMetaRegex = new(@"\|\s*SFMETA:v1;doc=(?[0-9a-fA-F-]{36});sha256=(?[A-Fa-f0-9]{64})$", RegexOptions.CultureInvariant | RegexOptions.Compiled); + public Task SignPdfAsync(byte[] pdfBytes, PdfSignOptionsDto? optionsModel = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(pdfBytes); @@ -27,6 +29,9 @@ public sealed class PdfDigitalSignatureService(IOptions options) var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256); var signerInfo = new ExternalSignerInfo(PdfSignatureType.Pkcs7, 32768, digestCalculator); + var sourceHashHex = Convert.ToHexString(SHA256.HashData(pdfBytes)); + var uuid = ResolveUuid(request.UUID); + var reasonWithMeta = BuildReasonWithMeta(request.Reason ?? pkcs11Options.DefaultReason, uuid, sourceHashHex); var signatureFieldInfo = new PdfSignatureFieldInfo(1) { Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}" @@ -35,7 +40,7 @@ public sealed class PdfDigitalSignatureService(IOptions options) var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo) { Name = request.Name ?? pkcs11Options.CertificateLabel, - Reason = request.Reason ?? pkcs11Options.DefaultReason, + Reason = reasonWithMeta, Location = request.Location, ContactInfo = request.ContactInfo, SigningTime = request.SigningTime ?? (pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now), @@ -102,10 +107,12 @@ public sealed class PdfDigitalSignatureService(IOptions options) var certificate = pkcs7.GetSignatureCertificate(); var issuedTo = ParsePrincipal(certificate?.Subject); var issuedBy = ParsePrincipal(certificate?.Issuer); + var reasonMeta = ParseReasonMeta(signature.Reason); items.Add(new PdfSignatureCheckItemDto( CoversWholeDoc: coversWholeDocument, Reason: signature.Reason, + BusinessReason: reasonMeta.BusinessReason, Location: signature.Location, DeclaredName: signature.SignerName, ContactInfo: signature.ContactInfo, @@ -125,14 +132,19 @@ public sealed class PdfDigitalSignatureService(IOptions options) Filter: signature.Filter, SubFilter: signature.SubFilter, ApplicationName: null, + UUID: reasonMeta.UUID, + OriginalDocumentSha256: reasonMeta.OriginalDocumentSha256, MessageDigestHex: TryExtractMessageDigestHex(signature.Contents), Error: null)); } catch (Exception ex) { + var reasonMeta = ParseReasonMeta(signature.Reason); + items.Add(new PdfSignatureCheckItemDto( CoversWholeDoc: false, Reason: signature.Reason, + BusinessReason: reasonMeta.BusinessReason, Location: signature.Location, DeclaredName: signature.SignerName, ContactInfo: signature.ContactInfo, @@ -152,6 +164,8 @@ public sealed class PdfDigitalSignatureService(IOptions options) Filter: signature.Filter, SubFilter: signature.SubFilter, ApplicationName: null, + UUID: reasonMeta.UUID, + OriginalDocumentSha256: reasonMeta.OriginalDocumentSha256, MessageDigestHex: null, Error: ex.Message)); } @@ -434,4 +448,37 @@ public sealed class PdfDigitalSignatureService(IOptions options) return start + length; } + + private static string ResolveUuid(string? uuid) + { + return Guid.TryParse(uuid, out var parsed) + ? parsed.ToString() + : Guid.NewGuid().ToString(); + } + + private static string BuildReasonWithMeta(string? businessReason, string uuid, string sourceHashHex) + { + var trimmedReason = string.IsNullOrWhiteSpace(businessReason) ? null : businessReason.Trim(); + var prefix = string.IsNullOrWhiteSpace(trimmedReason) ? string.Empty : trimmedReason + " "; + return $"{prefix}| SFMETA:v1;doc={uuid};sha256={sourceHashHex}"; + } + + private static (string? BusinessReason, string? UUID, string? OriginalDocumentSha256) ParseReasonMeta(string? reason) + { + if (string.IsNullOrWhiteSpace(reason)) + return (reason, null, null); + + var match = SignatureReasonMetaRegex.Match(reason); + if (!match.Success) + return (reason, null, null); + + var businessReason = reason[..match.Index].Trim(); + if (string.IsNullOrWhiteSpace(businessReason)) + businessReason = null; + + return ( + businessReason, + match.Groups["doc"].Value, + match.Groups["sha"].Value.ToUpperInvariant()); + } } diff --git a/EnvelopeGenerator.Infrastructure.Crypt/resources/INFRASTRUCTURE_PLAN.md b/EnvelopeGenerator.Infrastructure.Crypt/resources/INFRASTRUCTURE_PLAN.md index d877e878..3f5bda2c 100644 --- a/EnvelopeGenerator.Infrastructure.Crypt/resources/INFRASTRUCTURE_PLAN.md +++ b/EnvelopeGenerator.Infrastructure.Crypt/resources/INFRASTRUCTURE_PLAN.md @@ -234,7 +234,7 @@ EnvelopeGenerator.Infrastructure.Crypt/ Use options (bound from host config) with environment override support: - `Crypt:Provider = PKCS11` -- `Crypt:Pkcs11:LibraryPath` +- `Crypt:Pkcs11:AssetsRootPath` (base path that contains bundled `pkcs11` assets) - `Crypt:Pkcs11:SlotId` or token selectors - `Crypt:Pkcs11:UserPin` (from secret store/env, not plain committed value) - `Crypt:Pkcs11:PrivateKeyLabel` @@ -243,7 +243,7 @@ Use options (bound from host config) with environment override support: Validation rules: -- library path required, +- resolved module path must exist under configured/default assets root, - at least one key locator required, - pin required for signing operations. diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs index 6fb95308..bfe37d5f 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfSignatureController.cs @@ -13,6 +13,7 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService public sealed class SignPdfForm { public IFormFile? File { get; set; } + public string? UUID { get; set; } public string? Name { get; set; } public string? Reason { get; set; } public string? Location { get; set; } @@ -50,6 +51,7 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService var signOptions = new PdfSignOptionsDto { + UUID = request.UUID, Name = request.Name, Reason = request.Reason, Location = request.Location,