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.
This commit is contained in:
2026-10-14 14:24:51 +02:00
parent 7f3d1afcb9
commit 68d9dbdcdf
6 changed files with 66 additions and 11 deletions

View File

@@ -4,6 +4,7 @@ namespace EnvelopeGenerator.Application.Common.Dto;
public sealed record PdfSignOptionsDto public sealed record PdfSignOptionsDto
{ {
public string? UUID { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
public string? Reason { get; set; } public string? Reason { get; set; }
public string? Location { get; set; } public string? Location { get; set; }

View File

@@ -25,6 +25,7 @@ public sealed record PdfSignatureCertificateInfoDto(
public sealed record PdfSignatureCheckItemDto( public sealed record PdfSignatureCheckItemDto(
bool CoversWholeDoc, bool CoversWholeDoc,
string? Reason, string? Reason,
string? BusinessReason,
string? Location, string? Location,
string? DeclaredName, string? DeclaredName,
string? ContactInfo, string? ContactInfo,
@@ -44,6 +45,8 @@ public sealed record PdfSignatureCheckItemDto(
string? Filter, string? Filter,
string? SubFilter, string? SubFilter,
string? ApplicationName, string? ApplicationName,
string? UUID,
string? OriginalDocumentSha256,
string? MessageDigestHex, string? MessageDigestHex,
string? Error); string? Error);

View File

@@ -42,7 +42,8 @@ Supported deployment patterns:
Critical platform rule: Critical platform rule:
- A Windows process cannot load a Linux `.so` directly. - 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": { "Crypt": {
"Pkcs11": { "Pkcs11": {
"LibraryPath": "/usr/lib/softhsm/libsofthsm2.so", "AssetsRootPath": "E:\\DataFiles\\Digital Data\\EnvelopeGenerator",
"SlotId": 1720207650, "SlotId": 1720207650,
"TokenLabel": "DevToken", "TokenLabel": "DevToken",
"ProxySocket": "tcp://172.24.12.64:5657",
"UserPin": "***", "UserPin": "***",
"PrivateKeyLabel": "sign-key", "PrivateKeyLabel": "sign-key",
"PrivateKeyIdHex": null,
"CertificateLabel": "sign-cert", "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: 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:SlotId` (or `Crypt:Pkcs11:TokenLabel`)
- `Crypt:Pkcs11:UserPin` - `Crypt:Pkcs11:UserPin`
- `Crypt:Pkcs11:PrivateKeyLabel` - `Crypt:Pkcs11:PrivateKeyLabel`
@@ -371,11 +373,11 @@ Checkpoint:
Server runtime (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`) must include: Server runtime (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`) must include:
- `Crypt:Pkcs11:LibraryPath` = native PKCS#11 library path for the server host OS - `Crypt:Pkcs11:AssetsRootPath` = base directory that contains `pkcs11\windows\proxy\libpkcs11-proxy.dll`
- Windows host: Windows DLL path (vendor DLL or proxy client DLL) - If null/empty, runtime resolves assets from application output (`AppContext.BaseDirectory`)
- Linux host: Linux `.so` path
- `Crypt:Pkcs11:SlotId` = `2021549410` (or current company slot) - `Crypt:Pkcs11:SlotId` = `2021549410` (or current company slot)
- `Crypt:Pkcs11:TokenLabel` = `CompanyProdToken` - `Crypt:Pkcs11:TokenLabel` = `CompanyProdToken`
- `Crypt:Pkcs11:ProxySocket` = `tcp://172.24.12.64:5657`
- `Crypt:Pkcs11:UserPin` = real user pin - `Crypt:Pkcs11:UserPin` = real user pin
- `Crypt:Pkcs11:PrivateKeyLabel` = `company-sign-key-v1` - `Crypt:Pkcs11:PrivateKeyLabel` = `company-sign-key-v1`
- `Crypt:Pkcs11:CertificateLabel` = `company-sign-cert-v1` - `Crypt:Pkcs11:CertificateLabel` = `company-sign-cert-v1`

View File

@@ -16,6 +16,8 @@ namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options) : IPdfDigitalSignatureService public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options) : IPdfDigitalSignatureService
{ {
private static readonly Regex SignatureReasonMetaRegex = new(@"\|\s*SFMETA:v1;doc=(?<doc>[0-9a-fA-F-]{36});sha256=(?<sha>[A-Fa-f0-9]{64})$", RegexOptions.CultureInvariant | RegexOptions.Compiled);
public Task<byte[]> SignPdfAsync(byte[] pdfBytes, PdfSignOptionsDto? optionsModel = null, CancellationToken cancellationToken = default) public Task<byte[]> SignPdfAsync(byte[] pdfBytes, PdfSignOptionsDto? optionsModel = null, CancellationToken cancellationToken = default)
{ {
ArgumentNullException.ThrowIfNull(pdfBytes); ArgumentNullException.ThrowIfNull(pdfBytes);
@@ -27,6 +29,9 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256); var digestCalculator = new DigestCalculator(HashAlgorithmType.SHA256);
var signerInfo = new ExternalSignerInfo(PdfSignatureType.Pkcs7, 32768, digestCalculator); 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) var signatureFieldInfo = new PdfSignatureFieldInfo(1)
{ {
Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}" Name = $"Sig_{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"
@@ -35,7 +40,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo) var builder = new PdfDeferredSignatureBuilder(signerInfo, signatureFieldInfo)
{ {
Name = request.Name ?? pkcs11Options.CertificateLabel, Name = request.Name ?? pkcs11Options.CertificateLabel,
Reason = request.Reason ?? pkcs11Options.DefaultReason, Reason = reasonWithMeta,
Location = request.Location, Location = request.Location,
ContactInfo = request.ContactInfo, ContactInfo = request.ContactInfo,
SigningTime = request.SigningTime ?? (pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now), SigningTime = request.SigningTime ?? (pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now),
@@ -102,10 +107,12 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
var certificate = pkcs7.GetSignatureCertificate(); var certificate = pkcs7.GetSignatureCertificate();
var issuedTo = ParsePrincipal(certificate?.Subject); var issuedTo = ParsePrincipal(certificate?.Subject);
var issuedBy = ParsePrincipal(certificate?.Issuer); var issuedBy = ParsePrincipal(certificate?.Issuer);
var reasonMeta = ParseReasonMeta(signature.Reason);
items.Add(new PdfSignatureCheckItemDto( items.Add(new PdfSignatureCheckItemDto(
CoversWholeDoc: coversWholeDocument, CoversWholeDoc: coversWholeDocument,
Reason: signature.Reason, Reason: signature.Reason,
BusinessReason: reasonMeta.BusinessReason,
Location: signature.Location, Location: signature.Location,
DeclaredName: signature.SignerName, DeclaredName: signature.SignerName,
ContactInfo: signature.ContactInfo, ContactInfo: signature.ContactInfo,
@@ -125,14 +132,19 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
Filter: signature.Filter, Filter: signature.Filter,
SubFilter: signature.SubFilter, SubFilter: signature.SubFilter,
ApplicationName: null, ApplicationName: null,
UUID: reasonMeta.UUID,
OriginalDocumentSha256: reasonMeta.OriginalDocumentSha256,
MessageDigestHex: TryExtractMessageDigestHex(signature.Contents), MessageDigestHex: TryExtractMessageDigestHex(signature.Contents),
Error: null)); Error: null));
} }
catch (Exception ex) catch (Exception ex)
{ {
var reasonMeta = ParseReasonMeta(signature.Reason);
items.Add(new PdfSignatureCheckItemDto( items.Add(new PdfSignatureCheckItemDto(
CoversWholeDoc: false, CoversWholeDoc: false,
Reason: signature.Reason, Reason: signature.Reason,
BusinessReason: reasonMeta.BusinessReason,
Location: signature.Location, Location: signature.Location,
DeclaredName: signature.SignerName, DeclaredName: signature.SignerName,
ContactInfo: signature.ContactInfo, ContactInfo: signature.ContactInfo,
@@ -152,6 +164,8 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
Filter: signature.Filter, Filter: signature.Filter,
SubFilter: signature.SubFilter, SubFilter: signature.SubFilter,
ApplicationName: null, ApplicationName: null,
UUID: reasonMeta.UUID,
OriginalDocumentSha256: reasonMeta.OriginalDocumentSha256,
MessageDigestHex: null, MessageDigestHex: null,
Error: ex.Message)); Error: ex.Message));
} }
@@ -434,4 +448,37 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
return start + length; 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());
}
} }

View File

@@ -234,7 +234,7 @@ EnvelopeGenerator.Infrastructure.Crypt/
Use options (bound from host config) with environment override support: Use options (bound from host config) with environment override support:
- `Crypt:Provider = PKCS11` - `Crypt:Provider = PKCS11`
- `Crypt:Pkcs11:LibraryPath` - `Crypt:Pkcs11:AssetsRootPath` (base path that contains bundled `pkcs11` assets)
- `Crypt:Pkcs11:SlotId` or token selectors - `Crypt:Pkcs11:SlotId` or token selectors
- `Crypt:Pkcs11:UserPin` (from secret store/env, not plain committed value) - `Crypt:Pkcs11:UserPin` (from secret store/env, not plain committed value)
- `Crypt:Pkcs11:PrivateKeyLabel` - `Crypt:Pkcs11:PrivateKeyLabel`
@@ -243,7 +243,7 @@ Use options (bound from host config) with environment override support:
Validation rules: Validation rules:
- library path required, - resolved module path must exist under configured/default assets root,
- at least one key locator required, - at least one key locator required,
- pin required for signing operations. - pin required for signing operations.

View File

@@ -13,6 +13,7 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService
public sealed class SignPdfForm public sealed class SignPdfForm
{ {
public IFormFile? File { get; set; } public IFormFile? File { get; set; }
public string? UUID { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
public string? Reason { get; set; } public string? Reason { get; set; }
public string? Location { get; set; } public string? Location { get; set; }
@@ -50,6 +51,7 @@ public class PdfSignatureController(IPdfDigitalSignatureService signatureService
var signOptions = new PdfSignOptionsDto var signOptions = new PdfSignOptionsDto
{ {
UUID = request.UUID,
Name = request.Name, Name = request.Name,
Reason = request.Reason, Reason = request.Reason,
Location = request.Location, Location = request.Location,