Как заранее создать хеш PDF-файла с помощью iText IExternalSignatureContainer

Я использую iText 7 для добавления подписей к PDF-документам. Я также использую свою собственную реализацию IExternalSignatureContainer для интеграции сертификатов в PKCS7 CMS, поскольку служба подписи возвращает только подпись PKCS1.

Процесс подписи асинхронный (пользователь должен пройти аутентификацию), я хотел бы сделать следующее:

  • Подготовьте документ (PdfReader)
  • Вернуть пользователю хеш-значение документа
  • Выбросьте документ (PdfReader)
  • Разрешить пользователю пройти аутентификацию (не имеет прямого отношения к процессу подписи iText) и создать подпись (PKCS1)
  • Если пользователь аутентифицирован, подготовьте документ еще раз и примените подпись.

Причина в том, что мне не нужно слишком хранить подготовленный документ в памяти, а также для пакетной подписи.

Моя проблема в том, что созданное хеш-значение всегда разное. (Даже если я установил дату / время через pdfSigner.SetSignDate на одно и то же значение) или каждый экземпляр PdfReader / PdfSigner.

            //Create the hash of of the pdf document 
            //Part of my IExternalSignatureContainer Sign method
            //Called from iText pdfSigner.SignExternalContainer
            //The produced hash is always different
            byte[] hash = DigestAlgorithms.Digest(pdfStream, DigestAlgorithms.GetMessageDigest(hashAlgorithm));

Вопрос: есть ли способ

  • Заранее создайте хэш PDF-документа на одном экземпляре PdfReader.
  • Создайте подпись
  • Примените подпись к другому экземпляру PdfReader

Прилагается полный пример процесса (включая создание подписи, которое на самом деле должно выполняться другой службой).

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using iText.Kernel.Pdf;
using iText.Signatures;
using Org.BouncyCastle.X509;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;

namespace SignExternalTestManuel
{
    class Program
    {
        const string filePath = @"c:\temp\pdfsign\";
        public static string pdfToSign = Path.Combine(filePath, @"test.pdf");
        public static string destinationFile = Path.Combine(filePath, "test_signed.pdf");
        public static string LocalUserCertificatePublicKey = Path.Combine(filePath, "BITSignTestManuel5Base64.cer");
        public static string LocalCaCertificatePublicKey = Path.Combine(filePath, "BITRoot5Base64.cer");
        public static string privateKeyFile = Path.Combine(filePath, "BITSignTestManuel5.pfx");
        public static string privateKeyPassword = "test";

        public static void Main(String[] args)
        {
            PdfReader reader = new PdfReader(pdfToSign);
            using (FileStream os = new FileStream(destinationFile, FileMode.OpenOrCreate))
            {
                
                StampingProperties stampingProperties = new StampingProperties();
                stampingProperties.UseAppendMode();
                PdfSigner pdfSigner = new PdfSigner(reader, os, stampingProperties);
                pdfSigner.SetCertificationLevel(PdfSigner.NOT_CERTIFIED);

                IExternalSignatureContainer external = new GsSignatureContainer(
                    PdfName.Adobe_PPKLite,
                    PdfName.Adbe_pkcs7_detached);

                pdfSigner.SetSignDate(new DateTime(2021, 2, 22, 10, 0, 0));

                pdfSigner.SetFieldName("MySignatureField");
                pdfSigner.SignExternalContainer(external, 32000);
            }
        }
    }


    public class GsSignatureContainer : IExternalSignatureContainer
    {
        private PdfDictionary sigDic;


        public GsSignatureContainer(PdfName filter, PdfName subFilter)
        {
            sigDic = new PdfDictionary();
            sigDic.Put(PdfName.Filter, filter);
            sigDic.Put(PdfName.SubFilter, subFilter);
        }

        /// <summary>
        /// Implementation based on https://kb.itextpdf.com/home/it7kb/examples/how-to-use-a-digital-signing-service-dss-such-as-globalsign-with-itext-7#HowtouseaDigitalSigningService(DSS)suchasGlobalSign,withiText7-Examplecode
        /// </summary>
        /// <param name="pdfStream"></param>
        /// <returns></returns>
        public byte[] Sign(Stream pdfStream)
        {
            //Create the certificate chaing since the signature is just a PKCS1, the certificates must be added to the signature
            X509Certificate[] chain = null;


            string cert = System.IO.File.ReadAllText(Program.LocalUserCertificatePublicKey);
            string ca = System.IO.File.ReadAllText(Program.LocalCaCertificatePublicKey);
            chain = CreateChain(cert, ca);

            X509CrlParser p = new X509CrlParser();

            String hashAlgorithm = DigestAlgorithms.SHA256;
            PdfPKCS7 pkcs7Signature = new PdfPKCS7(null, chain, hashAlgorithm, false);

            //Create the hash of of the pdf document 
            //Part of my IExternalSignatureContainer Sign method
            //Called from iText pdfSigner.SignExternalContainer
            //The produced hash is always different
            byte[] hash = DigestAlgorithms.Digest(pdfStream, DigestAlgorithms.GetMessageDigest(hashAlgorithm));

            byte[] signature = null;

            //Create the hash based on the document hash which is suitable for pdf siging with SHA256 and a X509Certificate
            byte[] sh = pkcs7Signature.GetAuthenticatedAttributeBytes(hash, null, null, PdfSigner.CryptoStandard.CMS);
            //Create the signature via own certificate
            signature = CreateSignature(sh, Program.privateKeyFile, Program.privateKeyPassword);
            pkcs7Signature.SetExternalDigest(signature, null, "RSA");
            return pkcs7Signature.GetEncodedPKCS7(hash, null, null, null, PdfSigner.CryptoStandard.CMS);
        }

        public void ModifySigningDictionary(PdfDictionary signDic)
        {
            signDic.PutAll(sigDic);
        }

        private static X509Certificate[] CreateChain(String cert, String ca)
        {
            //Note: The root certificate could be omitted and it would still work
            X509Certificate[] chainy = new X509Certificate[2];
            X509CertificateParser parser = new X509CertificateParser();
            chainy[0] = new X509Certificate(parser.ReadCertificate(Encoding.UTF8.GetBytes(cert))
                .CertificateStructure);
            chainy[1] = new X509Certificate(parser.ReadCertificate(Encoding.UTF8.GetBytes(ca))
                .CertificateStructure);
            return chainy;
        }

        #region "Create signature, will be done by an actual service"
        private byte[] CreateSignature(byte[] hash, string privateKeyFile, string privateKeyPassword)
        {
            //Sign data directly with a X509Certificate
            X509Certificate2 rootCertificateWithPrivateKey = new X509Certificate2();
            byte[] rawData = System.IO.File.ReadAllBytes(privateKeyFile);
            rootCertificateWithPrivateKey.Import(rawData, privateKeyPassword, X509KeyStorageFlags.Exportable);

            using (var key = rootCertificateWithPrivateKey.GetRSAPrivateKey())
            {
                return key.SignData(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
            }
        }
        #endregion


    }
}


person Manuel    schedule 22.02.2021    source источник


Ответы (1)


Вопрос: есть ли способ

  • Заранее создайте хэш PDF-документа на одном экземпляре PdfReader.
  • Создайте подпись
  • Примените подпись к другому экземпляру PdfReader

Этот вариант использования в настоящее время не поддерживается iText, в частности, на каждом проходе.

  • создается другой идентификатор PDF-файла,
  • используется другое время модификации, и
  • в случае файлов PDF с шифрованием AES случайные числа, используемые для шифрования, отличаются.

Можно исправить iText, чтобы использовать одни и те же значения на каждом проходе, но перед исправлением библиотеки вы должны подумать, можете ли вы адаптировать свою архитектуру, чтобы в исправлении не было необходимости.

В вашем случае, например, если вы не можете сохранить исходный экземпляр PdfSigner, альтернативным подходом может быть сохранение исходного PdfSigner после хеширования файла результатов с фиктивными байтами подписи (например, new byte[0]). Затем, после получения контейнера подписи, вы можете вставить его в сохраненный файл в другой службе, используя PdfSigner.signDeferred, если обе службы могут получить доступ к общему хранилищу (или первая служба может, по крайней мере, перенаправить файл в хранилище второй службы) .

person mkl    schedule 22.02.2021
comment
Просто продолжение: я получил ваше предложение о создании PKCS7 без подписи, а затем заменил подпись в файле. Но это не работает с отметками времени, потому что отметка времени уже включает подпись. Есть идеи по реализации функции пакетной подписи без сохранения всех экземпляров PdfReader в памяти? - person Manuel; 05.03.2021
comment
Я не понимаю разницы. Если я вас правильно понял, в настоящее время вы изначально вставляете контейнер подписи CMS без байтов подписи и без отметки времени. Позже вы замените этот неполный контейнер CMS контейнером CMS с байтами подписи, но все еще без отметки времени. Я не понимаю, почему вы не можете заменить неполный контейнер CMS контейнером CMS с байтами подписи и меткой времени. - person mkl; 05.03.2021
comment
На самом деле я заменил только фрейм массива подписей в двоичном PDF-документе, а не всю CMS. Это работает только для подписи, но не для метки времени. Но теперь у меня есть рабочее решение с заменой всей CMS в конце. Это позволяет мне вычислить все хэши, а затем освободить память до тех пор, пока не будут созданы все подписи, а затем применить новую CMS. - person Manuel; 05.03.2021
comment
Вот как я это себе представлял. ;) - person mkl; 05.03.2021
comment
Да, но для меня это была довольно сложная тема, чтобы осмыслить :) - person Manuel; 08.03.2021