From c8ddfac8d01a6f42c2dfca4761f4d839675f6ad6 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 13 Oct 2026 18:52:28 +0200 Subject: [PATCH] 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. --- .gitignore | 1 + .../Services/PdfDigitalSignatureService.cs | 96 ++++++++++++++----- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 03c6422e..6bad3f90 100644 --- a/.gitignore +++ b/.gitignore @@ -370,3 +370,4 @@ FodyWeavers.xsd /EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output /EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md /ignore/EnvelopeGenerator.Form +/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json diff --git a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs index 17de0ca6..8295ace6 100644 --- a/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs +++ b/EnvelopeGenerator.Infrastructure.Crypt/Services/PdfDigitalSignatureService.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography.Pkcs; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Text.RegularExpressions; namespace EnvelopeGenerator.Infrastructure.Crypt.Services; @@ -20,7 +21,6 @@ public sealed class PdfDigitalSignatureService(IOptions options) ArgumentNullException.ThrowIfNull(pdfBytes); var pkcs11Options = options.Value; var request = optionsModel ?? new PdfSignOptionsDto(); - var fallbackSigningTime = pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now; using var inputPdf = new MemoryStream(pdfBytes); using var signer = new PdfDocumentSigner(inputPdf); @@ -38,7 +38,7 @@ public sealed class PdfDigitalSignatureService(IOptions options) Reason = request.Reason ?? pkcs11Options.DefaultReason, Location = request.Location, ContactInfo = request.ContactInfo, - SigningTime = request.SigningTime ?? fallbackSigningTime, + SigningTime = request.SigningTime ?? (pkcs11Options.UseUtcSigningTime ? DateTimeOffset.UtcNow : DateTimeOffset.Now), ApplicationName = request.ApplicationName ?? pkcs11Options.DefaultApplicationName, CertificationLevel = ToDevExpressCertificationLevel(request.CertificationLevel) ?? ToDevExpressCertificationLevel(pkcs11Options.DefaultCertificationLevel) @@ -47,10 +47,9 @@ public sealed class PdfDigitalSignatureService(IOptions options) var deferredSigner = signer.SignDeferred(builder); var externalSigner = new Pkcs11ExternalSigner(pkcs11Options); - byte[] signature = externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid); using var outputPdf = new MemoryStream(); - deferredSigner.Sign(outputPdf, signature); + deferredSigner.Sign(outputPdf, externalSigner.BuildSignature(deferredSigner.HashValue, digestCalculator.AlgorithmOid)); return Task.FromResult(outputPdf.ToArray()); } @@ -80,6 +79,7 @@ public sealed class PdfDigitalSignatureService(IOptions options) using var inputPdf = new MemoryStream(pdfBytes); using var signer = new PdfDocumentSigner(inputPdf); var signatures = signer.GetSignatureInfo(); + var byteRanges = ParseByteRanges(pdfBytes); if (signatures is null || signatures.Count == 0) { @@ -90,25 +90,18 @@ public sealed class PdfDigitalSignatureService(IOptions options) var items = new List(signatures.Count); - foreach (var signature in signatures) + for (var i = 0; i < signatures.Count; i++) { + var signature = signatures[i]; try { var pkcs7 = signer.GetPdfPkcs7Signature(signature.FieldName); 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 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 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( CoversWholeDoc: coversWholeDocument, @@ -116,23 +109,23 @@ public sealed class PdfDigitalSignatureService(IOptions options) Location: signature.Location, DeclaredName: signature.SignerName, ContactInfo: signature.ContactInfo, - DigestAlgorithm: digestAlgorithm, - SignatureAlgorithm: signatureAlgorithm, - SignerName: signerDisplayName, + DigestAlgorithm: InferDigestAlgorithm(certificate), + SignatureAlgorithm: InferSignatureAlgorithm(certificate), + SignerName: issuedTo.CommonName ?? signature.SignerName, Issuer: issuedBy.Full, - SerialNumber: serialNumber, - ValidFrom: validFrom, - ValidTo: validTo, + SerialNumber: ToColonSerial(certificate?.SerialNumber), + ValidFrom: certificate is null ? null : new DateTimeOffset(certificate.NotBefore).ToUniversalTime(), + ValidTo: certificate is null ? null : new DateTimeOffset(certificate.NotAfter).ToUniversalTime(), ExpiryCheckedAgainst: "now", - Certificates: [certificateInfo], - Unchanged: valid, + Certificates: [BuildCertificateInfo(certificate, DateTimeOffset.UtcNow)], + Unchanged: valid && coversWholeDocument, FieldName: signature.FieldName, IsCryptographicallyValid: valid, SigningTime: signature.SigningTime?.ToUniversalTime().ToString("O"), Filter: signature.Filter, SubFilter: signature.SubFilter, ApplicationName: null, - MessageDigestHex: messageDigestHex, + MessageDigestHex: TryExtractMessageDigestHex(signature.Contents), Error: null)); } catch (Exception ex) @@ -350,4 +343,59 @@ public sealed class PdfDigitalSignatureService(IOptions options) return null; } } + + private static List ParseByteRanges(byte[] pdfBytes) + { + var text = Encoding.Latin1.GetString(pdfBytes); + var matches = Regex.Matches(text, @"/ByteRange\s*\[(?[\d\s]+)\]", RegexOptions.CultureInvariant); + var ranges = new List(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; + } }