3

So I have been working with the Java implementation of IText, but now I'm pretty much writing a port of our signing process to C#, and I've hit a snag. So when im signing my document using the SetVisibleSignature(rect, page, name) overload, it signs the document as expected(as long as the signature field does not exist), but when i use the overload SetVisibleSignature(name) to sign an existing field, it does not actually sign the document. Am I doing something stupid perhaps and missing something?

Thank you for any help.

Updated Code

    using iTextSharp.text;
using iTextSharp.text.pdf;
using iTextSharp.text.pdf.security;
using Org.BouncyCastle.Security;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using BouncyCastle = Org.BouncyCastle;

    public class DocumentSigner : IDocumentSigner
    {
        private const string _datetimeFormat = "dd/MM/yyyy hh:mm:ss";
        private readonly IDateTimeProvider _dateTimeProvider;
        private readonly ISettingManager _settingManager;

        public DocumentSigner(IDateTimeProvider dateTimeProvider, ISettingManager settingManager)
        {
            Guard.ArgumentNotNull(dateTimeProvider, "dateTimeProvider");
            Guard.ArgumentNotNull(settingManager, "settingManager");

            _dateTimeProvider = dateTimeProvider;
            _settingManager = settingManager;
        }

        public byte[] Sign(Certificate certificate, SigningInformation information, List<SigningBlock> signingBlocks, List<MemberItemSignature> signatureImages, byte[] document, bool certify)
        {
            document = AddMetaData(information, document);
            document = AddSignatureFields(information, signingBlocks, document);
            return SignDocument(certificate, information, signingBlocks, signatureImages, document, certify);
        }

        private byte[] AddMetaData(SigningInformation information, byte[] document)
        {
            if (information.CustomProperties != null && information.CustomProperties.Any())
            {
                using (MemoryStream outputStream = new MemoryStream())
                {
                    using (PdfReader reader = new PdfReader(document))
                    {
                        using (PdfStamper stamper = new PdfStamper(reader, outputStream, '\0', true))
                        {
                            Dictionary<string, string> currentProperties = reader.Info;
                            foreach (string key in information.CustomProperties.Keys)
                            {
                                if (currentProperties.ContainsKey(key))
                                {
                                    currentProperties[key] = information.CustomProperties[key];
                                }
                                else
                                {
                                    currentProperties.Add(key, information.CustomProperties[key]);
                                }
                            }

                            stamper.MoreInfo = currentProperties;
                        }
                    }

                    return outputStream.ToArray();
                }
            }

            return document;
        }

        private byte[] AddSignatureFields(SigningInformation information, List<SigningBlock> signingBlocks, byte[] document)
        {
            for (int i = 0; i < signingBlocks.Count; i++)
            {
                using (MemoryStream outputStream = new MemoryStream())
                {
                    using (PdfReader reader = new PdfReader(document))
                    {
                        using (PdfStamper stamper = new PdfStamper(reader, outputStream, '\0', true))
                        {
                            CreateSignatureField(reader, stamper, signingBlocks[i]);
                        }
                    }

                    document = outputStream.ToArray();
                }
            }

            return document;
        }

        private PdfSignatureAppearance CreatePdfAppearance(PdfStamper stamper, SigningInformation information, bool certify)
        {
            PdfSignatureAppearance appearance = stamper.SignatureAppearance;
            appearance.Location = information.Location;
            appearance.Reason = information.Purpose;
            appearance.SignatureRenderingMode = PdfSignatureAppearance.RenderingMode.DESCRIPTION;
            CreatePdfAppearanceCertifyDocument(appearance, certify);

            return appearance;
        }

        private void CreatePdfAppearanceCertifyDocument(PdfSignatureAppearance appearance, bool certify)
        {
            if (certify)
            {
                appearance.CertificationLevel = PdfSignatureAppearance.CERTIFIED_FORM_FILLING;
            }
            else
            {
                appearance.CertificationLevel = PdfSignatureAppearance.NOT_CERTIFIED;
            }
        }

        private PdfStamper CreatePdfStamper(PdfReader reader, MemoryStream outputStream, byte[] document)
        {
            return PdfStamper.CreateSignature(reader, outputStream, '\0', null, true);
        }

        private void CreateSignatureField(PdfReader reader, PdfStamper stamper, SigningBlock signingBlock)
        {
            if (signingBlock == null)
            {
                return;
            }

            if (!DoesSignatureFieldExist(reader, signingBlock.Name))
            {
                PdfFormField signatureField = PdfFormField.CreateSignature(stamper.Writer);
                signatureField.SetWidget(new Rectangle(signingBlock.X, signingBlock.Y, signingBlock.X + signingBlock.Width, signingBlock.Y + signingBlock.Height), null);
                signatureField.Flags = PdfAnnotation.FLAGS_PRINT;
                signatureField.FieldName = signingBlock.Name;
                signatureField.Page = signingBlock.Page;
                stamper.AddAnnotation(signatureField, signingBlock.Page);
            }
        }

        private bool DoesSignatureFieldExist(PdfReader reader, string signatureFieldName)
        {
            if (String.IsNullOrWhiteSpace(signatureFieldName))
            {
                return false;
            }

            return reader.AcroFields.DoesSignatureFieldExist(signatureFieldName);
        }

        private byte[] GetSignatureImage(List<MemberItemSignature> signatureImages, string signingBlockName)
        {
            MemberItemSignature signature = signingBlockName.Contains("Initial") ? signatureImages.Where(image => image.Use == SignatureUses.Initial).FirstOrDefault() : signatureImages.Where(image => image.Use == SignatureUses.Signature).FirstOrDefault();
            if (signature != null)
            {
                return signature.Image;
            }
            else
            {
                return null;
            }
        }

        private byte[] SignDocument(Certificate certificate, SigningInformation information, List<SigningBlock> signingBlocks, List<MemberItemSignature> signatureImages, byte[] document, bool certify)
        {
            for (int i = 0; i < signingBlocks.Count; i++)
            {
                using (MemoryStream outputStream = new MemoryStream())
                {
                    using (PdfReader reader = new PdfReader(document))
                    {
                        using (PdfStamper stamper = CreatePdfStamper(reader, outputStream, document))
                        {
                            SigningBlock signingBlock = signingBlocks[i];
                            PdfSignatureAppearance appearance = CreatePdfAppearance(stamper, information, certify && i == 0);

                            SignDocumentSigningBlock(certificate, information, signingBlock, appearance, stamper, GetSignatureImage(signatureImages, signingBlock.Name));
                        }
                    }

                    document = outputStream.ToArray();
                }
            }

            return document;
        }

        private void SignDocumentSigningBlock(Certificate certificate, SigningInformation information, SigningBlock block, PdfSignatureAppearance appearance, PdfStamper stamper, byte[] signatureImage)
        {
            X509Certificate2 x509Certificate = new X509Certificate2(certificate.Bytes, certificate.Password, X509KeyStorageFlags.Exportable);

            appearance.SetVisibleSignature(block.Name);
            SignDocumentSigningBlockWithImage(signatureImage, appearance);
            SignDocumentSigningBlockWithText(appearance, x509Certificate);

            using (RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)x509Certificate.PrivateKey)
            {
                IExternalSignature externalSignature = new PrivateKeySignature(DotNetUtilities.GetRsaKeyPair(rsa).Private, _settingManager["DocumentSigningEncryptionHashAlgorithm"]);
                MakeSignature.SignDetached(appearance, externalSignature, new BouncyCastle::X509.X509Certificate[] { DotNetUtilities.FromX509Certificate(x509Certificate) }, null, null, new TSAClientBouncyCastle(_settingManager["DocumentSigningTimestampingServiceAddress"]), Int32.Parse(_settingManager["DocumentSigningEstimatedTimestampSize"]), CryptoStandard.CMS);
            }
        }

        private void SignDocumentSigningBlockWithImage(byte[] signatureImage, PdfSignatureAppearance appearance)
        {
            if (signatureImage != null && signatureImage.Length > 0)
            {
                Image signatureImageInstance = Image.GetInstance(signatureImage);

                appearance.Image = signatureImageInstance;
                appearance.SignatureGraphic = signatureImageInstance;
            }
        }

        private void SignDocumentSigningBlockWithText(PdfSignatureAppearance appearance, X509Certificate2 x509Certificate)
        {
            if (x509Certificate == null)
            {
                return;
            }

            appearance.Layer2Text = SignDocumentSigningBlockWithTextBuildText(x509Certificate);
            appearance.Layer2Font = new Font(Font.FontFamily.COURIER, 7.0f, Font.NORMAL, BaseColor.LIGHT_GRAY);
            appearance.Acro6Layers = true;
        }

        private string SignDocumentSigningBlockWithTextBuildText(X509Certificate2 x509Certificate)
        {
            Dictionary<string, string> fields = SignDocumentSigningBlockWithTextBuildTextIssuerFields(x509Certificate.IssuerName.Name);

            string organization = fields.Keys.Contains("O") ? fields["O"] : String.Empty;
            string commonName = fields.Keys.Contains("CN") ? fields["CN"] : String.Empty;
            string signDate = _dateTimeProvider.Now.ToString(_datetimeFormat);
            string expirationDate = x509Certificate.NotAfter.ToString(_datetimeFormat);

            return "Digitally signed by " + organization + "\nSignee: " + commonName + "\nSign date: " + signDate + "\n" + "Expiration date: " + expirationDate;
        }

        private Dictionary<string, string> SignDocumentSigningBlockWithTextBuildTextIssuerFields(string issuer)
        {
            Dictionary<string, string> fields = new Dictionary<string, string>();

            string[] issuerFields = issuer.Split(',');
            foreach (string field in issuerFields)
            {
                string[] fieldSplit = field.Split('=');
                string key = fieldSplit[0].Trim();
                string value = fieldSplit[1].Trim();

                if (!fields.Keys.Contains(key))
                {
                    fields.Add(key, value);
                }
                else
                {
                    fields[key] = value;
                }
            }

            return fields;
        }
    }

Values

    _settingManager["DocumentSigningEncryptionHashAlgorithm"] = "SHA-256";
_settingManager["DocumentSigningTimestampingServiceAddress"] = "http://services.globaltrustfinder.com/adss/tsa";
_settingManager["DocumentSigningEstimatedTimestampSize"] = 104000;
Johandre
  • 95
  • 2
  • 13
  • *it does not actually sign the document* - What happens instead? – mkl Sep 28 '15 at 12:41
  • +1 just for saying "not working as expected" instead of saying "doesn't work" like a lot of other people tend to. – Chris Haas Sep 28 '15 at 13:04
  • So the document increases in size as i would expect when it is creating the revisions, but the signature field remains unsigned on the document as well as the signing pane. But when I sign, not using SetVisibleSignature, it creates the invisible signatures as expected, and the existing blocks are left unsigned. The names do match the signature blocks that I want to sign. If I take a completely unsigned document and use the SetVisibleSignature(rect, page, name) override it signs the document as I expect it to do. – Johandre Sep 28 '15 at 13:27
  • I am using IText 5.5.7, before i forget to mention that :) – Johandre Sep 28 '15 at 13:28
  • @user2699460 I just tried to compile your code but there are many unknown references. Furthermore it is not clear from which namespaces the security classes are, especially whether they are Org.BouncyCastle or System.Security classes. Thus, please provide a [sscce](http://sscce.org/), with a focus on *compilable* here. – mkl Oct 01 '15 at 08:01
  • @mkl, i didnt post the entire class :), ill update the question with the entire class for you quick. – Johandre Oct 01 '15 at 08:24
  • *ill update the question with the entire class* - If the entire class has lots of stuff not relevant for the question at hand, you might want to simplify it a bit. – mkl Oct 01 '15 at 08:36
  • @mkl: I try to keep to solid principles as much as possible, so the signing stuff should be separated out as much as possible, there are references to domain objects unfortunately but I hope that the code makes better sense now :) – Johandre Oct 01 '15 at 08:41
  • Hhmmm, while this clarifies the namespace some classes are from, the amount of references to unknown classes if anything grew. I'm trying to delete them, but the result I test may be so different from your code that the issue cannot be reproduced. – mkl Oct 01 '15 at 09:11
  • Yeah was afraid of that, cause all the unknown references remaining are domain objects that i reference in the class. and there are quite a few domain objects that i constructed for the signing process. Think they are too many to post to the thread though :( – Johandre Oct 01 '15 at 09:56
  • I reduced your code enough to compile and run it. And what happens... the signature is there. Can you share a sample broken result file? Maybe something interesting can be seen in it... – mkl Oct 01 '15 at 10:53
  • @mkl: mmm isnt that murphy for you :( Ive uploaded the signed document to google drive, heres the link: https://drive.google.com/file/d/0B9RyqgJoa6W8b1hfZ2NQT0pOeVE/view?usp=sharing – Johandre Oct 01 '15 at 12:13
  • Ah, stop, I could reproduce the issue. I merely had to update, I was still on iTextSharp 5.5.6. This seems to be a regression in iTextSharp 5.5.7.Could you check against 5.5.6, too? – mkl Oct 01 '15 at 12:44
  • @mkl: Awesome im not going crazy :D ok cool ill try 5.5.6 as soon as i can, but will probably only be tomorrow and let you know :) – Johandre Oct 01 '15 at 12:57
  • BTW, the same issue exists in iText/Java since 5.5.7... – mkl Oct 01 '15 at 13:56
  • FYI, we had noticed this issue in release 5.5.7 and have fixed it recently. Implementation is reverted to how it was in 5.5.6. The fix is already available in the develop branch and will be in the 5.5.8 release. – rhens Oct 21 '15 at 19:43

1 Answers1

4

The code provided by the OP references and accesses multiple objects of unknown classes. To make it runnable, therefore, it had to be cut down to be self-contained.

The cut-down version fortunately could still be used to reproduce and analyze the issue, cf. the post scriptum. Any statement from here on is based on the behavior of this cut-down version.

The issue observed by the OP could be reproduced using iTextSharp 5.5.7 (and analogously using iText 5.5.7), and also very interestingly it could not be reproduced using version 5.5.6 of either library. As I'm more into Java, I looked into the changes in iText. They had been ported to iTextSharp in a very faithful fashion.

Indeed, this issue is a regression, signing pre-existing empty signature fields in append mode is broken in iText(Sharp) 5.5.7.

Between 5.5.6 and 5.5.7 a change has been made to PdfSignatureAppearance.preClose. If signing an existing signature field, the code used to manipulate the first widget of the signature field in question (af.getFieldItem(name).getWidget(0)), now it works on the associated merged dictionary (af.getFieldItem(name).getMerged(0)).

Unfortunately, while the former was an object actually existing in the original PDF and, therefore, calling writer.markUsed for it marked its changed contents for writing to the incremental update section, the latter does not correspond to an object in the original PDF (it is a virtual aggregation of multiple objects), so calling writer.markUsed for it does not mark the changes to be written as incremental update anymore.

So, while the actual signature value still is written to the file, it is not connected to the designated signature field anymore.


The change has been done to fix the method behavior.

Before this preClosed worked incorrectly because it retrieved field dictionary as widget annotation. It's incorrect in case when field and widget dicts are not merged. In case they were merged, everything worked as expected. The latter is the most possible case for digital signature fields, but it isn't obligated according to spec.

(DEV-1448)

This is correct, in case of separate field and widget dictionaries certain changes have to be made to the field, not the widget. Merely the implementation does not work as desired in append mode.


PS: This is the cut-down version of the OP's code:

public class DocumentSigner
{
    private const string _datetimeFormat = "dd/MM/yyyy hh:mm:ss";

    public byte[] Sign(ICollection<X509Certificate> chain, ICipherParameters pk, string signingBlock, byte[] document, bool certify, String pattern = null)
    {
        document = AddMetaData(document);
        if (pattern != null)
            File.WriteAllBytes(String.Format(pattern, "1"), document);
        document = AddSignatureFields(signingBlock, document);
        if (pattern != null)
            File.WriteAllBytes(String.Format(pattern, "2"), document);
        return SignDocument(chain, pk, signingBlock, document, certify);
    }

    private byte[] AddMetaData(byte[] document)
    {
        return document;
    }

    private byte[] AddSignatureFields(string signingBlock, byte[] document)
    {
            using (MemoryStream outputStream = new MemoryStream())
            {
                using (PdfReader reader = new PdfReader(document))
                {
                    using (PdfStamper stamper = new PdfStamper(reader, outputStream, '\0', true))
                    {
                        CreateSignatureField(reader, stamper, signingBlock);
                    }
                }

                document = outputStream.ToArray();
            }

        return document;
    }

    private PdfSignatureAppearance CreatePdfAppearance(PdfStamper stamper, bool certify)
    {
        PdfSignatureAppearance appearance = stamper.SignatureAppearance;
        appearance.Location = "information.Location";
        appearance.Reason = "information.Purpose";
        appearance.SignatureRenderingMode = PdfSignatureAppearance.RenderingMode.DESCRIPTION;
        CreatePdfAppearanceCertifyDocument(appearance, certify);

        return appearance;
    }

    private void CreatePdfAppearanceCertifyDocument(PdfSignatureAppearance appearance, bool certify)
    {
        if (certify)
        {
            appearance.CertificationLevel = PdfSignatureAppearance.CERTIFIED_FORM_FILLING;
        }
        else
        {
            appearance.CertificationLevel = PdfSignatureAppearance.NOT_CERTIFIED;
        }
    }

    private PdfStamper CreatePdfStamper(PdfReader reader, MemoryStream outputStream, byte[] document)
    {
        return PdfStamper.CreateSignature(reader, outputStream, '\0', null, true);
    }

    private void CreateSignatureField(PdfReader reader, PdfStamper stamper, string signingBlock)
    {
        if (signingBlock == null)
        {
            return;
        }

        if (!DoesSignatureFieldExist(reader, signingBlock))
        {
            PdfFormField signatureField = PdfFormField.CreateSignature(stamper.Writer);
            signatureField.SetWidget(new Rectangle(100, 100, 200, 200), null);
            signatureField.Flags = PdfAnnotation.FLAGS_PRINT;
            signatureField.FieldName = signingBlock;
            signatureField.Page = 1;
            stamper.AddAnnotation(signatureField, 1);
        }
    }

    private bool DoesSignatureFieldExist(PdfReader reader, string signatureFieldName)
    {
        if (String.IsNullOrWhiteSpace(signatureFieldName))
        {
            return false;
        }

        return reader.AcroFields.DoesSignatureFieldExist(signatureFieldName);
    }

    private byte[] GetSignatureImage(string signingBlockName)
    {
        return null;
    }

    private byte[] SignDocument(ICollection<X509Certificate> chain, ICipherParameters pk, string signingBlock, byte[] document, bool certify)
    {
            using (MemoryStream outputStream = new MemoryStream())
            {
                using (PdfReader reader = new PdfReader(document))
                {
                    using (PdfStamper stamper = CreatePdfStamper(reader, outputStream, document))
                    {
                        PdfSignatureAppearance appearance = CreatePdfAppearance(stamper, certify);

                        SignDocumentSigningBlock(chain, pk, signingBlock, appearance, stamper, GetSignatureImage(signingBlock));
                    }
                }

                document = outputStream.ToArray();
            }

        return document;
    }

    private void SignDocumentSigningBlock(ICollection<X509Certificate> chain, ICipherParameters pk, string block, PdfSignatureAppearance appearance, PdfStamper stamper, byte[] signatureImage)
    {
        appearance.SetVisibleSignature(block);
        SignDocumentSigningBlockWithImage(signatureImage, appearance);
        SignDocumentSigningBlockWithText(appearance, chain.First());

        IExternalSignature externalSignature = new PrivateKeySignature(pk, "SHA-256");
        MakeSignature.SignDetached(appearance, externalSignature, chain, null, null, new TSAClientBouncyCastle("http://services.globaltrustfinder.com/adss/tsa"), 104000, CryptoStandard.CMS);
    }

    private void SignDocumentSigningBlockWithImage(byte[] signatureImage, PdfSignatureAppearance appearance)
    {
        if (signatureImage != null && signatureImage.Length > 0)
        {
            Image signatureImageInstance = Image.GetInstance(signatureImage);

            appearance.Image = signatureImageInstance;
            appearance.SignatureGraphic = signatureImageInstance;
        }
    }

    private void SignDocumentSigningBlockWithText(PdfSignatureAppearance appearance, X509Certificate x509Certificate)
    {
        if (x509Certificate == null)
        {
            return;
        }

        appearance.Layer2Text = SignDocumentSigningBlockWithTextBuildText(x509Certificate);
        appearance.Layer2Font = new Font(Font.FontFamily.COURIER, 7.0f, Font.NORMAL, BaseColor.LIGHT_GRAY);
        appearance.Acro6Layers = true;
    }

    private string SignDocumentSigningBlockWithTextBuildText(X509Certificate x509Certificate)
    {
        Dictionary<string, string> fields = SignDocumentSigningBlockWithTextBuildTextIssuerFields(x509Certificate.IssuerDN.ToString());

        string organization = fields.Keys.Contains("O") ? fields["O"] : String.Empty;
        string commonName = fields.Keys.Contains("CN") ? fields["CN"] : String.Empty;
        string signDate = System.DateTime.Now.ToString(_datetimeFormat);
        string expirationDate = x509Certificate.NotAfter.ToString();

        return "Digitally signed by " + organization + "\nSignee: " + commonName + "\nSign date: " + signDate + "\n" + "Expiration date: " + expirationDate;
    }

    private Dictionary<string, string> SignDocumentSigningBlockWithTextBuildTextIssuerFields(string issuer)
    {
        Dictionary<string, string> fields = new Dictionary<string, string>();

        string[] issuerFields = issuer.Split(',');
        foreach (string field in issuerFields)
        {
            string[] fieldSplit = field.Split('=');
            string key = fieldSplit[0].Trim();
            string value = fieldSplit[1].Trim();

            if (!fields.Keys.Contains(key))
            {
                fields.Add(key, value);
            }
            else
            {
                fields[key] = value;
            }
        }

        return fields;
    }
}

PPS: The Java/iText tests have been done using the signTest_2_user2699460 unit test in ComplexSignatureFields.java which works on test-2-user2699460.pdf, an intermediary output of the C# code above.

PPPS: Meanwhile the changes resulting in the regression have been rolled back:

Returned the use of .getWidget method instead of .getMerged since the case, when signature field dictionary and dictionary its widget annotation are not merged, is rather uncommon if can be encountered at all. Moreover the use of merged dictionary instead of widget requires more efforts since .getMerged method returns not actually the dictionary obtained by merging signature field dict and widget annotation dict, but also AcroForm dict.

(DEV-1579)

Thus, the regression most likely will be resolved in version 5.5.8

Community
  • 1
  • 1
mkl
  • 90,588
  • 15
  • 125
  • 265
  • Thanks for the insight into the problem :) i was getting completely lost on it. – Johandre Oct 01 '15 at 18:28
  • @user2699460 Have you verified that your original code also runs on 5.5.6? – mkl Oct 02 '15 at 07:21
  • Sorry for the slow response had production issues on friday :( but i downgraded to itextsharp 5.5.6 and now it works as expected ;) thank you for all the advice and help :) – Johandre Oct 05 '15 at 04:14
  • Confirming that it wasn't resolved in 5.5.8. Had to find by sheer luck why it failed – misha130 Jan 03 '16 at 14:40