Search code examples
c#.netitextpdf-generation

Howto keep PDF-A when signing a document using IText SignDeferred


I do apply a signature to a pdf document via delayed signing(SignDeferred) using IText. The process contains the following steps:

  • Prepare the pdf document for siging
    • Reserve space for the signature in the pdf document
  • Create the hash value of the pdf document
  • Create the signature based on the hash value
    • Using a self signed certificate
  • Apply the signature to the pdf document

The whole process works and i end with a pdf document where the signature is set and is valid.

enter image description here

The original pdf is a PDF-A1a but the resulting pdf is not a valid PDF-A1a anymore. I am aware that there is a documentation about IText PDF-A support (https://kb.itextpdf.com/home/it7kb/ebooks/itext-7-jump-start-tutorial-for-java/chapter-7-creating-pdf-ua-and-pdf-a-documents), but this seems to not apply since i don't change the content of the document.

enter image description here

My question: How can i apply a signature using deferred signing and keep the PDF-A1a in the resulting document?

Note: If i do apply a signature directly (Without SignDeferred), the resulting pdf is still a PDF-A1a, but i do have to use SignDeferred Note: I do use https://www.pdfen.com/pdf-a-validator for checking pdf-A

Code sample

  • Component used for signing:
    • itext.sign 7.1.5.0
    • itext.kernel 7.1.5.0
  • Component used for creating hash
    • BouncyCastle.Crypto 1.8.1.0

The following is a complete code sample sample with everything required in one file. It only requires the references to itext and BouncyCastle and the path to a self signed certificate

using iText.Kernel.Pdf;
using iText.Signatures;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using System;
using System.Collections.Generic;
using System.IO;

namespace DeferredSigningTestConsole
{
    class Program
    {
        static string SignatureAttributeName = "DeferredSignature";
        static string CertificatePath = @"C:\temp\PDFA\PdfATestCert.2pfx.pfx";
        static string CertificatePassword = "test";

        static void Main(string[] args)
        {
            var signedPdf = SignPdf(System.IO.File.ReadAllBytes(@"C:\temp\PDFA\PDF_A1a.pdf"));
            System.IO.File.WriteAllBytes(@"C:\temp\PDFA\signed.pdf", signedPdf);
        }

        public static byte[] SignPdf(byte[] pdfToSign)
        {
            byte[] hash = null;
            byte[] tmpPdf = null;
            //Step #1 >> prepare pdf for signing (Allocate space for the signature and calculate hash)
            using (MemoryStream input = new MemoryStream(pdfToSign))
            {
                using (var reader = new PdfReader(input))
                {
                    StampingProperties sp = new StampingProperties();
                    sp.UseAppendMode();
                    using (MemoryStream baos = new MemoryStream())
                    {
                        var signer = new PdfSigner(reader, baos, sp);
                        signer.SetCertificationLevel(PdfSigner.NOT_CERTIFIED);

                        signer.SetFieldName(SignatureAttributeName);
                        DigestCalcBlankSigner external = new DigestCalcBlankSigner(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);

                        signer.SignExternalContainer(external, 121743);
                        hash = external.GetDocBytesHash();
                        tmpPdf = baos.ToArray();
                    }
                }

                //Step #2 >> Create the signature based on the document hash
                byte[] signature = GetSignatureFromHash(hash);

                //Step #3 >> Apply the signature to the document
                ReadySignatureSigner extSigContainer = new ReadySignatureSigner(signature);
                using (MemoryStream preparedPdfStream = new MemoryStream(tmpPdf))
                {
                    using (var pdfReader = new PdfReader(preparedPdfStream))
                    {
                        using (PdfDocument docToSign = new PdfDocument(pdfReader))
                        {
                            using (MemoryStream outStream = new MemoryStream())
                            {
                                PdfSigner.SignDeferred(docToSign, SignatureAttributeName, outStream, extSigContainer);
                                return outStream.ToArray();
                            }
                        }
                    }
                }

            }
        }

        public static byte[] GetSignatureFromHash(byte[] hash)
        {
            FileStream fs = new FileStream(CertificatePath, FileMode.Open);
            Pkcs12Store store = new Pkcs12Store(fs, CertificatePassword.ToCharArray());
            String alias = "";
            foreach (string al in store.Aliases)
                if (store.IsKeyEntry(al) && store.GetKey(al).Key.IsPrivate)
                {
                    alias = al;
                    break;
                }
            AsymmetricKeyEntry pk = store.GetKey(alias);
            X509CertificateEntry[] chain = store.GetCertificateChain(alias);

            List<Org.BouncyCastle.X509.X509Certificate> c = new List<Org.BouncyCastle.X509.X509Certificate>();
            foreach (X509CertificateEntry en in chain)
            {
                c.Add(en.Certificate);
            }
            PrivateKeySignature signature = new PrivateKeySignature(pk.Key, "SHA256");
            String hashAlgorithm = signature.GetHashAlgorithm();
            PdfPKCS7 sgn = new PdfPKCS7(null, c.ToArray(), hashAlgorithm, false);
            DateTime signingTime = DateTime.Now;
            byte[] sh = sgn.GetAuthenticatedAttributeBytes(hash, null, null, PdfSigner.CryptoStandard.CMS);
            byte[] extSignature = signature.Sign(sh);
            sgn.SetExternalDigest(extSignature, null, signature.GetEncryptionAlgorithm());
            return sgn.GetEncodedPKCS7(hash, null, null, null, PdfSigner.CryptoStandard.CMS);

        }
    }

    internal class DigestCalcBlankSigner : IExternalSignatureContainer
    {
        private readonly PdfName _filter;

        private readonly PdfName _subFilter;

        private byte[] _docBytesHash;

        internal DigestCalcBlankSigner(PdfName filter, PdfName subFilter)
        {
            _filter = filter;
            _subFilter = subFilter;
        }

        internal virtual byte[] GetDocBytesHash()
        {
            return _docBytesHash;
        }

        public virtual byte[] Sign(Stream docBytes)
        {

            _docBytesHash = CalcDocBytesHash(docBytes);
            //If we retun the signature bytes, GetAuthenticatedAttributeBytes will throw an exception
            //Not clear how this should be done
            return new byte[0];
        }

        public virtual void ModifySigningDictionary(PdfDictionary signDic)
        {
            signDic.Put(PdfName.Filter, _filter);
            signDic.Put(PdfName.SubFilter, _subFilter);
        }

        internal static byte[] CalcDocBytesHash(Stream docBytes)
        {
            byte[] docBytesHash = null;
            docBytesHash = DigestAlgorithms.Digest(docBytes, DigestUtilities.GetDigest(DigestAlgorithms.SHA256));
            return docBytesHash;
        }
    }


    internal class ReadySignatureSigner : IExternalSignatureContainer
    {
        private byte[] cmsSignatureContents;

        internal ReadySignatureSigner(byte[] cmsSignatureContents)
        {
            this.cmsSignatureContents = cmsSignatureContents;
        }

        public virtual byte[] Sign(Stream docBytes)
        {
            return cmsSignatureContents;
        }

        public virtual void ModifySigningDictionary(PdfDictionary signDic)
        {
        }
    }
}


Solution

  • It seems the reason why the signed pdf is no longer a valid PDF-A1a is the estimated size for the signature. I have used a value of about 120kb for the signature.

    //doesn't work
    signer.SignExternalContainer(external, 121743);
    
    //does work
    signer.SignExternalContainer(external, 65000);
    

    The concept of this is documented in the ebook "Digital Signatures for PDF documents" from iText.

    It seems in order to get a valid pdf-A1a the maximum size is limited to 65kb.

    I now will have to test whether this works when i add a visual representation (signature image) since this was the reason i did choose such a large estimated size.

    Edit: I did some more testing and i am now able to produce valid pdf-A document with signatures: The pdf are now valid pdf-A with the changed estimated size:

    • Valid with estimate size 32'000/65'000
      • A1a
      • A1b
    • Valid with estimate size 32'000
      • A2a
      • A2b
      • A2u
      • A3a
      • A3b
      • A3u

    When adding a visual representation (image), pdf-A1a and pdf-A1b are no longer valid.

    A transparent soft mask is present. Beginning with PDF 1.4 transparency is supported. Some PDF-based ISO standards prohibit the use of transparency.

    But this is another problem which i now try to figure out.