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:
@@ -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; }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user