Improve PDF signing and validation logic

Updated `.gitignore` to exclude new files and directories, including `FodyWeavers.xsd` and `appsettings.Secrets.json`.

Refactored `PdfDigitalSignatureService`:
- Improved signing time handling by calculating fallback inline.
- Simplified `SignPdfAsync` by inlining signature building.
- Added `ParseByteRanges` to extract `/ByteRange` values from PDFs.
- Introduced `CoversWholeDocument` to validate document coverage.

Enhanced signature validation:
- Integrated `ParseByteRanges` and `CoversWholeDocument`.
- Streamlined `PdfSignatureCheckItemDto` construction.
- Updated properties to calculate values inline.
- Improved `Unchanged` property to check cryptographic validity and document coverage.

These changes enhance maintainability, readability, and functionality.
This commit is contained in:
2026-10-13 18:52:28 +02:00
parent 1942d78155
commit c8ddfac8d0
2 changed files with 73 additions and 24 deletions

1
.gitignore vendored
View File

@@ -370,3 +370,4 @@ FodyWeavers.xsd
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output /EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md /EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/ignore/EnvelopeGenerator.Form /ignore/EnvelopeGenerator.Form
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json

View File

@@ -10,6 +10,7 @@ using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
namespace EnvelopeGenerator.Infrastructure.Crypt.Services; namespace EnvelopeGenerator.Infrastructure.Crypt.Services;
@@ -20,7 +21,6 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
ArgumentNullException.ThrowIfNull(pdfBytes); ArgumentNullException.ThrowIfNull(pdfBytes);
var pkcs11Options = options.Value; var pkcs11Options = options.Value;
var request = optionsModel ?? new PdfSignOptionsDto(); var request = optionsModel ?? new PdfSignOptionsDto();
var fallbackSigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
using var inputPdf = new MemoryStream(pdfBytes); using var inputPdf = new MemoryStream(pdfBytes);
using var signer = new PdfDocumentSigner(inputPdf); using var signer = new PdfDocumentSigner(inputPdf);
@@ -38,7 +38,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
Reason = request.Reason ?? pkcs11Options.DefaultReason, Reason = request.Reason ?? pkcs11Options.DefaultReason,
Location = request.Location, Location = request.Location,
ContactInfo = request.ContactInfo, ContactInfo = request.ContactInfo,
SigningTime = request.SigningTime ?? fallbackSigningTime, SigningTime = request.SigningTime ?? (pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now),
ApplicationName = request.ApplicationName ?? pkcs11Options.DefaultApplicationName, ApplicationName = request.ApplicationName ?? pkcs11Options.DefaultApplicationName,
CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel) CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel)
?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel) ?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel)
@@ -47,10 +47,9 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
var deferredSigner = signer.SignDeferred(builder); var deferredSigner = signer.SignDeferred(builder);
var externalSigner = new Pkcs11ExternalSigner(pkcs11Options); var externalSigner = new Pkcs11ExternalSigner(pkcs11Options);
byte[] signature = externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid);
using var outputPdf = new MemoryStream(); using var outputPdf = new MemoryStream();
deferredSigner.Sign(outputPdf, signature); deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid));
return Task.FromResult(outputPdf.ToArray()); return Task.FromResult(outputPdf.ToArray());
} }
@@ -80,6 +79,7 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
using var inputPdf = new MemoryStream(pdfBytes); using var inputPdf = new MemoryStream(pdfBytes);
using var signer = new PdfDocumentSigner(inputPdf); using var signer = new PdfDocumentSigner(inputPdf);
var signatures = signer.GetSignatureInfo(); var signatures = signer.GetSignatureInfo();
var byteRanges = ParseByteRanges(pdfBytes);
if (signatures is null || signatures.Count == 0) if (signatures is null || signatures.Count == 0)
{ {
@@ -90,25 +90,18 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
var items = new List<PdfSignatureCheckItemDto>(signatures.Count); var items = new List<PdfSignatureCheckItemDto>(signatures.Count);
foreach (var signature in signatures) for (var i = 0; i < signatures.Count; i++)
{ {
var signature = signatures[i];
try try
{ {
var pkcs7 = signer.GetPdfPkcs7Signature(signature.FieldName); var pkcs7 = signer.GetPdfPkcs7Signature(signature.FieldName);
bool valid = pkcs7.VerifySignature(); bool valid = pkcs7.VerifySignature();
bool coversWholeDocument = true; var byteRange = i < byteRanges.Count ? byteRanges[i] : null;
var coversWholeDocument = CoversWholeDocument(byteRange, pdfBytes.LongLength);
var certificate = pkcs7.GetSignatureCertificate(); var certificate = pkcs7.GetSignatureCertificate();
var now = DateTimeOffset.UtcNow;
var serialNumber = ToColonSerial(certificate?.SerialNumber);
DateTimeOffset? validFrom = certificate is null ? null : new DateTimeOffset(certificate.NotBefore).ToUniversalTime();
DateTimeOffset? validTo = certificate is null ? null : new DateTimeOffset(certificate.NotAfter).ToUniversalTime();
var issuedTo = ParsePrincipal(certificate?.Subject); var issuedTo = ParsePrincipal(certificate?.Subject);
var issuedBy = ParsePrincipal(certificate?.Issuer); var issuedBy = ParsePrincipal(certificate?.Issuer);
var certificateInfo = BuildCertificateInfo(certificate, now);
var digestAlgorithm = InferDigestAlgorithm(certificate);
var signatureAlgorithm = InferSignatureAlgorithm(certificate);
var signerDisplayName = issuedTo.CommonName ?? signature.SignerName;
var messageDigestHex = TryExtractMessageDigestHex(signature.Contents);
items.Add(new PdfSignatureCheckItemDto( items.Add(new PdfSignatureCheckItemDto(
CoversWholeDoc: coversWholeDocument, CoversWholeDoc: coversWholeDocument,
@@ -116,23 +109,23 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
Location: signature.Location, Location: signature.Location,
DeclaredName: signature.SignerName, DeclaredName: signature.SignerName,
ContactInfo: signature.ContactInfo, ContactInfo: signature.ContactInfo,
DigestAlgorithm: digestAlgorithm, DigestAlgorithm: InferDigestAlgorithm(certificate),
SignatureAlgorithm: signatureAlgorithm, SignatureAlgorithm: InferSignatureAlgorithm(certificate),
SignerName: signerDisplayName, SignerName: issuedTo.CommonName ?? signature.SignerName,
Issuer: issuedBy.Full, Issuer: issuedBy.Full,
SerialNumber: serialNumber, SerialNumber: ToColonSerial(certificate?.SerialNumber),
ValidFrom: validFrom, ValidFrom: certificate is null ? null : new DateTimeOffset(certificate.NotBefore).ToUniversalTime(),
ValidTo: validTo, ValidTo: certificate is null ? null : new DateTimeOffset(certificate.NotAfter).ToUniversalTime(),
ExpiryCheckedAgainst: "now", ExpiryCheckedAgainst: "now",
Certificates: [certificateInfo], Certificates: [BuildCertificateInfo(certificate, DateTimeOffset.UtcNow)],
Unchanged: valid, Unchanged: valid && coversWholeDocument,
FieldName: signature.FieldName, FieldName: signature.FieldName,
IsCryptographicallyValid: valid, IsCryptographicallyValid: valid,
SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"), SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"),
Filter: signature.Filter, Filter: signature.Filter,
SubFilter: signature.SubFilter, SubFilter: signature.SubFilter,
ApplicationName: null, ApplicationName: null,
MessageDigestHex: messageDigestHex, MessageDigestHex: TryExtractMessageDigestHex(signature.Contents),
Error: null)); Error: null));
} }
catch (Exception ex) catch (Exception ex)
@@ -350,4 +343,59 @@ public sealed class PdfDigitalSignatureService(IOptions<Pkcs11Options> options)
return null; return null;
} }
} }
private static List<long[]> ParseByteRanges(byte[] pdfBytes)
{
var text = Encoding.Latin1.GetString(pdfBytes);
var matches = Regex.Matches(text, @"/ByteRange\s*\[(?<values>[\d\s]+)\]", RegexOptions.CultureInvariant);
var ranges = new List<long[]>(matches.Count);
foreach (Match match in matches)
{
var raw = match.Groups["values"].Value;
var parts = raw.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
var values = new long[parts.Length];
var ok = true;
for (var i = 0; i < parts.Length; i++)
{
if (!long.TryParse(parts[i], out values[i]))
{
ok = false;
break;
}
}
if (ok && values.Length >= 4 && values.Length % 2 == 0)
ranges.Add(values);
}
return ranges;
}
private static bool CoversWholeDocument(long[]? byteRange, long fileLength)
{
if (byteRange is null || byteRange.Length < 4 || byteRange.Length % 2 != 0)
return false;
if (byteRange[0] != 0)
return false;
long previousEnd = -1;
for (var i = 0; i < byteRange.Length; i += 2)
{
var start = byteRange[i];
var length = byteRange[i + 1];
if (start < 0 || length < 0)
return false;
var end = start + length;
if (previousEnd > start)
return false;
previousEnd = end;
}
return previousEnd == fileLength;
}
} }