Внешняя подпись PDF с iText - PullRequest
       79

Внешняя подпись PDF с iText

0 голосов
/ 06 февраля 2019

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

Я разрабатываю приложение для цифровой подписи PDF, использующее iText5, которое зависит от внешней службы, предоставляющей подписанный хэш после подготовки PDF для подписания.

Как описано в Документация iText , на первом этапе я подготовил PDF-файл (в окончательной реализации все PDF-файлы могут быть мультиподписанными, поэтому я использую режим добавления), например так:

public static byte[] GetBytesToSign(string unsignedPdf, string tempPdf, string signatureFieldName, List<Org.BouncyCastle.X509.X509Certificate> certificateChain) {
        // we create a reader and a stamper
        using (PdfReader reader = new PdfReader(unsignedPdf)) {
            using (FileStream baos = File.OpenWrite(tempPdf)) {

                List<Org.BouncyCastle.X509.X509Certificate> chain = certificateChain;
                PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
                sap                   = pdfStamper.SignatureAppearance;
                sap.Certificate       = certificateChain[0];
                sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
                //sap.SetVisibleSignature(signatureFieldName);
                sap.SignDate          = DateTime.Now;
                PdfSignature dic      = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);  
                dic.Date              = new PdfDate(sap.SignDate);
                dic.Name              = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
                sap.CryptoDictionary  = dic;
                sap.Certificate       = certificateChain[0];
                sap.Acro6Layers       = true;
                sap.Reason            = "test";
                sap.Location          = "test";

                IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
                MakeSignature.SignExternalContainer(sap, external, 8192);
                signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
                byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
                //byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

                return hash;
            }
        }
    }

После этого шага я отправляю хэш внешней службе, которая возвращает подписанный хеш.

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

Затем я завершаю процесс подписи, используя метод, приведенный ниже:

private byte[] Sign(PdfPKCS7 signatureContainer, List<X509Certificate2> chain2, List<Org.BouncyCastle.X509.X509Certificate> chain, byte[] hash, byte[] signedBytes, string tmpPdf, string signedPdf, string signatureFieldName) {
        System.Security.Cryptography.RSACryptoServiceProvider publicCertifiedRSACryptoServiceProvider = chain2[0].PublicKey.Key as System.Security.Cryptography.RSACryptoServiceProvider;
        bool verify = publicCertifiedRSACryptoServiceProvider.VerifyHash(hash, "SHA256", signedBytes); //verify if the computed hash is same as signed hash using the cert public key
        Console.WriteLine("PKey signed computed hash is equal to signed hash: " + verify);

        AsnEncodedData asnEncodedData = new AsnEncodedData(signedBytes);
        Console.WriteLine(asnEncodedData.Format(true));

        //ITEXT5
        try {
            //Console.WriteLine("Signed bytes: " + Encoding.UTF8.GetString(signedBytes));

            using (PdfReader reader = new PdfReader(tmpPdf)) {
                using (FileStream outputStream = File.OpenWrite(signedPdf)) {
                IExternalSignatureContainer external = new Objects.MyExternalSignatureContainer(signedBytes, chain, signatureContainer);
                MakeSignature.SignDeferred(reader, signatureFieldName, outputStream, external);
                }
            }
            return new byte[] { };
        }
        catch(Exception ex) {
            File.Delete(tmpPdf);
            Console.WriteLine("Error signing file: " + ex.Message);
            return new byte[] { };
        }
    }

В начале метода Sign я проверяю,хэш, отправленный во внешнюю службу, подписанный тем же сертификатом, равен ответу внешней службы, что является истинным.

Код MyExternalSignatureContainer:

public class MyExternalSignatureContainer : IExternalSignatureContainer {
        private readonly byte[] signedBytes;
        public List<Org.BouncyCastle.X509.X509Certificate> Chain;
        private PdfPKCS7 sigField;

        public MyExternalSignatureContainer(byte[] signedBytes) {
            this.signedBytes = signedBytes;
        }

        public MyExternalSignatureContainer(byte[] signedBytes, List<Org.BouncyCastle.X509.X509Certificate> chain, PdfPKCS7 pdfPKCS7) {
            this.signedBytes = signedBytes;
            this.Chain = chain;
            this.sigField = pdfPKCS7;
        }

        public byte[] Sign(Stream data) {
            try {
                sigField.SetExternalDigest(signedBytes, null, "RSA");
                return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);
            }
            catch (IOException ioe) {
                throw ioe;
            }
        }

        public void ModifySigningDictionary(PdfDictionary signDic) {
        }
    }

Проблема заключается в том, что когда я открываю PDF в Acrobat, он утверждает, что документ был изменен или поврежден с момента применения подписи.

(Если я открою тот же PDF-файл в PDF-XChange, он скажет, что PDF-файл не был изменен).

То, что я пробовал без удачи:

Не будучи полностью уверенным, что внешняя служба использует SHA256, я уже пытался изменить дайджест на SHA1 предварительной подписи, что привело к «ошибке форматирования» в Acrobat Reader.

Как указано в другом посте вВ StackOverlow, касающемся той же проблемы (я не могу найти сообщение, связывающее ее), потенциальная проблема - использование разных потоков для временного файла.Я уже пытался использовать тот же поток без удачи.

Образцы файлов PDF:

Исходный файл

Temp File

Подписанный файл

Хэш Base64, отправленный в сервис:

XYfaS/SisA/tk5hcl035RpBjOczrH9E5rgiAMpqgkjI=

Base64 подписанный хеш, отправленный в ответ:

CnV3WL7skhMCtZG1r1Qi2oyE9WPO3KP4Ieu/Xm4lec+DAbYbhQxCvjMISsG3sTwYY7Lqi4luD60uceViDH848rS9OkTn8szzAnnX2fSYIwqDpG3qjJAb6NOXEv41hy+XYhSBJWS4ji2mM2ReruwPafxB1aM25L5Jyd0V7WecuNFUevUrvd85Y2KBkyBw9zCA8NDAQPPY0UT4GkXZi3Z35+Sf/s2o8zxCOlBDaIJyMvJ9De79nw4jC5L9NesHpFxx3mX1g1N33GHjUNdETgFMhnd8RDUlGLW6bsAyv78gvwE6aXF6COObap/VtlLvMOME68MzLr6izKte6uA35Zwj9Q==


Обновление после MKL-ответа :

Согласно ответу, я изменил код подписи документа только в одну фазу и в итоге использовал следующие методы:

using (PdfReader reader = new PdfReader(fileLocation)) {
    using (FileStream baos = File.OpenWrite(tmpFile)) {

        List<Org.BouncyCastle.X509.X509Certificate> chain = Chain;
        PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
        PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
        sap.Certificate = Chain[0];
        sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
        //sap.SetVisibleSignature(signatureFieldName);
        sap.SignDate = DateTime.Now;
        PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
        dic.Date = new PdfDate(sap.SignDate);
        dic.Name = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
        sap.CryptoDictionary = dic;
        sap.Certificate = Chain[0];
        sap.Acro6Layers = true;
        //sap.CertificationLevel = PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS;
        sap.Reason = "test";
        sap.Location = "test";

        IExternalSignature signature = new Objects.RemoteSignature(client, signatureRequest);
        MakeSignature.SignDetached(sap, signature, Chain, null, null, null, 8192, CryptoStandard.CMS);

    }
}

И реализация IExternalSignature:

public virtual byte[] Sign(byte[] message) {
    IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
    byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
    //
    // Request signature for hash value messageHash
    // and return signature bytes
    //
    signatureRequest.Hash = messageHash;
    SignatureService.SignatureResponse signatureResponse = client.Signature(signatureRequest);

    if (signatureResponse.Status.Code == "00") {
         return signatureResponse.DocumentSignature;
    }
    else {
        throw new Exception("Error signing file: " + signatureResponse.Status.Message);
    }
}

signatureResponse.DocumentSignature представляет подписанные байты, возвращенные службой.

В результате PDF теперь я получаю ошибку декодирования BER.

Анализируя ваш пример PDF, вы, кажется, объявляете неверный сертификат в качестве сертификата подписавшего

Хотя я знаю, что текущий сертификат недействителен, он предоставляется службой и в предыдущей реализации службы, куда я отправлял весь PDF для подписания, подписанный PDF также былподписанный этим сертификатом.

Вопрос: Зная, что при двухэтапной подписи мне удалось подписать PDF-файл этим сертификатом (кроме измененного или поврежденного документа после ошибки подписи)разве этот метод не должен работать с тем же сертификатом?

В настоящее время происходит следующее:

Signature in Acrobat Reader

Проверка подписи:

Signature properties

Опять же, если я открою тот же PDF-файл в PDF-XChange, подпись действительна, и документ не был изменен.Требование, чтобы PDF был действительным в Acrobat, но я озадачен этой разницей между читателями.

Результат PDF


Обновление 2

Т.е. вам нужно только префикс вашего хэша с последовательностью байтов 30 31 30 0d06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

После добавления этого префикса SHA256 в дайджест сообщения полученный PDF-файл теперь правильно подписан.

Примет ли Adobe Reader фиксированную подпись?

Я сомневаюсь в этом.Использование ключа сертификата подписавшего содержит только значение для подписи других сертификатов.

Текущий сертификат используется только для тестирования.В производственной среде я считаю, что сертификат, предоставленный внешней службой, будет действительным.

У меня есть еще два вопроса по этой проблеме:

Для вашего кодаэто означает, что вы должны упаковать хеш в структуру DigestInfo перед отправкой в ​​сервис.

В: Как вы проверили контейнер для подписи, чтобы сделать вывод, что он неправильный?

В: В моем исходном коде у меня была двухфазная подпись.Будет ли тот же принципал, примененный в методе единого знака, все еще действительным, то есть, применяя префикс SHA256, делать предварительно подписанные байты и после установки дайджеста с результирующими подписанными байтами?

1 Ответ

0 голосов
/ 07 февраля 2019

В вашем коде есть ряд проблем.

Прежде всего, ваш код смешивает разные поколения API подписи iText.Существует старое поколение API, которое требует, чтобы вы работали очень близко к внутренним элементам PDF, и есть более новый (начиная с версии 5.3.x) API, который реализован в виде слоя поверх старого API и не требует, чтобы вы знали эти внутренние компоненты..

В техническом документе «Цифровые подписи для документов PDF» основное внимание уделяется отображению более нового API, только в разделе 4.3.3 «Подписание документа на сервере с использованием подписи, созданной на клиенте» используется старый API, посколькуВариант использования не позволяет использовать более новый API.

Ваш сценарий использования допускает использование более нового API, поэтому вам следует попробовать использовать только его.

(В определенных ситуациях можно смешивать эти API, но тогда вы должны действительно знать, что делаете, и все равно можете сделать это совершенно неправильно ...)

Но теперь некоторые более конкретные проблемы:

Работа с закрытыми объектами

Методы MakeSignature.Sign* неявно закрывают нижележащие объекты PdfStamper и SignatureAppearance, поэтому в дальнейшем работа с этими объектами будет завершена.Нельзя допускать, чтобы это приводило к разумной информации.

Но в GetBytesToSign вы делаете

MakeSignature.SignExternalContainer(sap, external, 8192);
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");

Таким образом, sap.GetRangeStream(), вероятно, возвращает что-то неправильное.(Возможно, он все еще возвращает правильные данные, но вы не должны на это рассчитывать.)

Подпись неправильных байтов

GetBytesToSign возвращает хэш диапазонов подписанного документа PDF:

signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
//byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

return hash;

Позже, однако, ваш код принимает это возвращаемое значение, подписывает его и пытается вставить возвращенные байты сигнатуры в контейнер PdfPKCS7 подписи.Это неправильно, байты подписи должны быть созданы для аутентифицированных атрибутов информации подписавшего о контейнере подписи, а не хеша документа.

(Кстати, здесь вы используете более старый API подписи, не понимая егои, следовательно, используйте его неправильно.)

Помещение подписанных байтов в неправильную позицию

В MyExternalSignatureContainer вы используете подписанные байты в двух вызовах:

sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);

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

(Здесь вы снова используете старый API подписи, не понимая его, и снова используете его неправильно.)

Предоставление неверного сертификата

Анализируя ваш пример PDF, вы, как представляется, объявляете неверный сертификат в качестве сертификата подписавшего.Я так думаю, потому что

  • его открытый ключ не может правильно расшифровать байты подписи, а
  • сертификат является сертификатом CA, а не сертификатом конечной сущности, с неподходящим использованием ключа для подписи документов PDF.

Как улучшить свой код

Прежде всего, если я вас правильно понимаю, вы запрашиваете подпись с какого-то другого сервера, и этот другой сервер реагирует быстро, поэтому нетНужно освободить все ресурсы в ожидании подписи.В такой ситуации нет необходимости в двухэтапном процессе подписания, вы должны сделать это за один шаг.Все, что вам нужно, это пользовательская реализация IExternalSignature, что-то вроде

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        //
        // Request signature for hash value messageHash
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_HASH(messageHash);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

, и используйте ее для подписи следующим образом:

PdfReader reader = new PdfReader(...);
PdfStamper pdfStamper = PdfStamper.CreateSignature(...);
PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
// set sap properties for signing
IExternalSignature signature = new RemoteSignature();
MakeSignature.SignDetached(sap, signature, chain, null, null, null, 0, CryptoStandard.CMS);

Обновление IExternalSignature реализации

В обновлении вашего вопроса вы добавили PDF-файл, подписанный с внесенными выше изменениями.Анализируя байты подписи в контейнере подписи, выяснилось, что ваша служба подписи разработана очень глупо, она использует заполнение PKCS1 v1.5 и шифрование RSA, но предполагает, что ее входные данные уже упакованы в структуру DigestInfo.По моему опыту, это необычное предположение, вы должны сказать своему провайдеру подписи правильно документировать это.

Для вашего кода это означает, что вы должны упаковать хэш в структуру DigestInfoперед отправкой в ​​службу.

Простой способ сделать это объясняется в RFC 8017, раздел 9.2, примечание 1 :

Для девяти хеш-функций, упомянутых в Приложении B.1, кодировка МЭД T значения DigestInfo равна следующему:

    ...
    SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H.
    ...

Т.е. вам нужно только поставить префиксхеш с байтовой последовательностью 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

Таким образом, вариант класса RemoteSignature для служб, которым требуется, чтобы вызывающая сторона упаковывала дайджест в структуру DigestInfo, мог бы выглядеть так:

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        byte[] sha256Prefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
        byte[] digestInfo = new byte[sha256Prefix.Length + messageHash.Length];
        sha256Prefix.CopyTo(digestInfo, 0);
        messageHash.CopyTo(digestInfo, sha256Prefix.Length);
        //
        // Request signature for DigestInfo value digestInfo
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_DIGEST_INFO(digestInfo);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

Примет ли Adobe Reader фиксированную подпись?

Я сомневаюсь в этом.Использование ключа сертификата подписавшего содержит только значение для подписи других сертификатов.

Если вы посмотрите Руководство по цифровым сигнатурам Adobe для ИТ , вы увидите, что допустимые расширения использования ключа

  • отсутствует, т. Е. Расширение использования ключа вообще отсутствует, или
  • присутствует с одним или несколькими из следующих значений:
    • nonRepudiation
    • signTransaction (только 11.0.09)
    • digitalSignature (11.0.10 и более поздние версии)

Таким образом, значение signCertificate вашегосертификат может быть проблемой.

...