From 11482e869c4b5ea33a89b70a1667ecb9176e2055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9E=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9F=D0=B0=D0=BD=D0=B0=D1=81=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Tue, 11 Aug 2026 14:28:06 +0300 Subject: [PATCH] Give each signature field a unique name The partial name of the signature field was hard-coded to "Signature1" (with a TODO). Signing a document a second time therefore produced two fields named "Signature1". The fully qualified names of interactive form fields must be unique; validators like Acrobat either merge or reject the duplicate. The first unused "SignatureN" is now chosen by scanning the names of the fields at the root of the interactive form. --- .../Pdf.Signatures/DigitalSignatureHandler.cs | 45 ++++++- .../Pdf.Signatures/SignatureFieldNameTests.cs | 115 ++++++++++++++++++ .../Pdf.Signatures/TestSigner.cs | 46 +++++++ 3 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/SignatureFieldNameTests.cs create mode 100644 src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/TestSigner.cs diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureHandler.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureHandler.cs index a6d00f3c..c36a1119 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureHandler.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Signatures/DigitalSignatureHandler.cs @@ -175,8 +175,11 @@ internal async Task AddSignatureComponentsAsync() // #US321 TODO Use appropriate _contentsPlaceholder = new(2 * signatureSize + 2); _byteRangePlaceholder = new(ByteRangePlaceholderLength); + var catalog = Document.Catalog; + var acroForm = catalog.GetOrCreateAcroForm(); + var signatureDictionary = GetSignatureDictionary(_contentsPlaceholder, _byteRangePlaceholder); - var signatureField = GetSignatureField(signatureDictionary); + var signatureField = GetSignatureField(signatureDictionary, ChooseFieldName(acroForm)); var page = Document.Pages[Options.PageIndex]; var annotations = page.Elements.GetArray(PdfPage.Keys.Annots); @@ -184,9 +187,6 @@ internal async Task AddSignatureComponentsAsync() // #US321 TODO Use appropriate page.Elements.Add(PdfPage.Keys.Annots, new PdfArray(Document, signatureField)); else annotations.Elements.Add(signatureField); - - var catalog = Document.Catalog; - var acroForm = catalog.GetOrCreateAcroForm(); if (!acroForm.Elements.ContainsKey(PdfForm.Keys.SigFlags)) acroForm.Elements.Add(PdfForm.Keys.SigFlags, new PdfInteger(3, true)); @@ -200,7 +200,40 @@ internal async Task AddSignatureComponentsAsync() // #US321 TODO Use appropriate acroForm.Fields.Elements.Add(signatureField); } - PdfFormSignatureField GetSignatureField(PdfSignature signatureDic) // #US321 TODO Use appropriate classes. + /// + /// Chooses the partial field name of the signature field to be created, e.g. “Signature1”, “Signature2”, … + /// Interactive form fields must have unique fully qualified names. If a document is signed more than once, + /// reusing a name leads to two fields with the same name, which validators either merge or reject. + /// Therefore, the first unused name is taken. + /// + /// The interactive form the new field is added to. + static string ChooseFieldName(PdfForm acroForm) + { + // Only the names of the fields at the root of the form are relevant here, because the new field + // is added there. The name of a field below the root is relative to the name of its parent field + // and therefore cannot collide with a name at the root. + var usedNames = new HashSet(StringComparer.Ordinal); + var fields = acroForm.Elements.GetArray(PdfForm.Keys.Fields); + if (fields != null) + { + int count = fields.Elements.Count; + for (int idx = 0; idx < count; idx++) + { + var name = fields.Elements.GetDictionary(idx)?.Elements.GetString(PdfFormField.Keys.T); + if (!String.IsNullOrEmpty(name)) + usedNames.Add(name!); + } + } + + for (int idx = 1; ; idx++) + { + var name = Invariant($"Signature{idx}"); + if (!usedNames.Contains(name)) + return name; + } + } + + PdfFormSignatureField GetSignatureField(PdfSignature signatureDic, string fieldName) // #US321 TODO Use appropriate classes. { var signatureField = new PdfFormSignatureField(Document); @@ -209,7 +242,7 @@ PdfFormSignatureField GetSignatureField(PdfSignature signatureDic) // #US321 TOD // #AcroForms // Annotation keys. signatureField.Elements.Add(PdfFormField.Keys.FT, new PdfName(PdfFormFieldType.Signature)); - signatureField.Elements.Add(PdfFormField.Keys.T, new PdfString("Signature1")); // TODO If already exists, will it cause error? implement a name chooser if yes. + signatureField.Elements.Add(PdfFormField.Keys.T, new PdfString(fieldName)); signatureField.Elements.Add(PdfFormField.Keys.Ff, new PdfInteger(132)); // signatureField.Elements.Add(PdfFormField.Keys.DR, new PdfDictionary()); TODO COMPILE signatureField.Elements.Add(PdfAnnotation.Keys.Type, new PdfName("/Annot")); diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/SignatureFieldNameTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/SignatureFieldNameTests.cs new file mode 100644 index 00000000..2778bb96 --- /dev/null +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/SignatureFieldNameTests.cs @@ -0,0 +1,115 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +#if WPF +using System.IO; +#endif +using FluentAssertions; +using PdfSharp.Diagnostics; +using PdfSharp.Drawing; +using PdfSharp.Pdf; +using PdfSharp.Pdf.Forms; +using PdfSharp.Pdf.IO; +using PdfSharp.Pdf.Signatures; +#if CORE +using PdfSharp.Fonts; +using PdfSharp.Quality; +#endif +using Xunit; + +namespace PdfSharp.Tests.Pdf.Signatures +{ + [Collection("PDFsharp")] + public class SignatureFieldNameTests : IDisposable + { + public SignatureFieldNameTests() + { + PdfSharpCore.ResetAll(); +#if CORE + GlobalFontSettings.FontResolver = new UnitTestFontResolver(); +#endif + } + + public void Dispose() + { + PdfSharpCore.ResetAll(); + } + + [Fact] + public void Signature_field_of_a_new_document_is_named_Signature1() + { + using var document = new PdfDocument(); + document.AddPage(); + + var signedBytes = Sign(document); + + FieldNamesOf(signedBytes).Should().Equal("Signature1"); + } + + [Fact] + public void Signature_field_of_an_already_signed_document_gets_an_unused_name() + { + using var document = new PdfDocument(); + document.AddPage(); + var signedBytes = Sign(document); + + // Sign the signed document again. The fully qualified names of interactive form fields must be + // unique, so the second field must not be named "Signature1" again. + using var signedDocument = PdfReader.Open(new MemoryStream(signedBytes), PdfDocumentOpenMode.Modify); + var twiceSignedBytes = Sign(signedDocument); + + FieldNamesOf(twiceSignedBytes).Should().Equal("Signature1", "Signature2"); + } + + [Fact] + public void Signature_field_does_not_take_the_name_of_an_existing_field() + { + using var document = new PdfDocument(); + document.AddPage(); + + // A text field named "Signature1" already occupies the name the signature field would get. + var acroForm = document.Catalog.GetOrCreateAcroForm(); + var textField = new PdfDictionary(document); + textField.Elements.SetName(PdfFormField.Keys.FT, PdfFormFieldType.Text); + textField.Elements.SetString(PdfFormField.Keys.T, "Signature1"); + document.Internals.AddObject(textField); + acroForm.Fields.Elements.Add(textField); + + var signedBytes = Sign(document); + + FieldNamesOf(signedBytes).Should().Equal("Signature1", "Signature2"); + } + + static byte[] Sign(PdfDocument document) + { + var options = new DigitalSignatureOptions + { + ContactInfo = "John Doe", + Location = "Seattle", + Reason = "License Agreement", + Rectangle = new XRect(36, 36, 200, 50), + AppearanceHandler = new EmptyAppearanceHandler() + }; + _ = DigitalSignatureHandler.ForDocument(document, new TestSigner(), options); + + using var stream = new MemoryStream(); + document.Save(stream, false); + return stream.ToArray(); + } + + /// + /// Gets the partial names of the fields at the root of the interactive form. + /// + internal static List FieldNamesOf(byte[] pdfBytes) + { + using var document = PdfReader.Open(new MemoryStream(pdfBytes), PdfDocumentOpenMode.Import); + var fields = document.Catalog.GetAcroForm()?.Elements.GetArray(PdfForm.Keys.Fields); + fields.Should().NotBeNull(); + + var names = new List(); + for (int idx = 0; idx < fields!.Elements.Count; idx++) + names.Add(fields.Elements.GetDictionary(idx)!.Elements.GetString(PdfFormField.Keys.T)); + return names; + } + } +} diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/TestSigner.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/TestSigner.cs new file mode 100644 index 00000000..a6a85008 --- /dev/null +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf.Signatures/TestSigner.cs @@ -0,0 +1,46 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +#if WPF +using System.IO; +#endif +using PdfSharp.Drawing; +using PdfSharp.Pdf.Annotations; +using PdfSharp.Pdf.Signatures; + +namespace PdfSharp.Tests.Pdf.Signatures +{ + /// + /// A signer that creates a deterministic dummy signature, so that tests need no certificate. + /// + class TestSigner : IDigitalSigner + { + public string CertificateName => "PDFsharp unit test"; + + public Task GetSignatureSizeAsync() => Task.FromResult(SignatureSize); + + public Task GetSignatureAsync(Stream stream) + { + // Read the stream to ensure the ranges to be signed are readable. + var buffer = new byte[4096]; + while (stream.Read(buffer, 0, buffer.Length) > 0) + { } + + var signature = new byte[SignatureSize]; + for (int idx = 0; idx < signature.Length; idx++) + signature[idx] = (byte)idx; + return Task.FromResult(signature); + } + + const int SignatureSize = 512; + } + + /// + /// An appearance handler that draws nothing, so that tests need no font. + /// + class EmptyAppearanceHandler : IAnnotationAppearanceHandler + { + public void DrawAppearance(XGraphics gfx, XRect rect) + { } + } +}