Как на Java c помощью КриптоПро подписать документ PDF

Привет! Я сотрудник Альфа-Банка и занимаюсь разработкой программного обеспечения со встроенными средствами криптографической защиты информации.
В данной статье хочу рассказать о следующих вещах:

-преимуществах формата PDF в качестве документа с электронной подписью;
-платформе Java, библиотеке itextpdf и СКЗИ КриптоПро CSP, как инструментах подписи;
-о том, с какими трудностями пришлось столкнуться, о доработке itextpdf;
-привести пример кода, выполняющего несколько подписей;
-поговорить о целесообразности использования формата PDF в качестве документа с подписью.

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

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

Преимущества PDF

В качестве контейнера для хранения разнообразной информации в дружественном для человека представлении формат PDF по праву завоевал свою популярность. Его получится открыть в любой ОС. Документ PDF может содержать не только текстовые и табличные данные, но и аудио и видеозаписи, инженерную графику, трехмерные модели. Для дополнительной обработки PDF-документов в данный формат включен такой мощный инструмент как поддержка JavaScript для изменения содержимого в формах и полях по наступлению какого-то события или выполнению действия пользователя. 

Инструменты Adobe Systems (разработчика формата PDF) поддерживают использование электронной подписи. В отличие от сообщений с присоединенной усиленной электронной подписью стандарта PKCS#7 и его усовершенствования CAdES, для просмотра документа PDF с подписью не требуется дополнительное специальное ПО. Кроме криптографического провайдера, который требуется во всех случаях. 

Т.е. инструменты Adobe позволяют визуализировать электронную подпись в документе. 

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

При просмотре таких документов, кроме визуализации в теле документа, программы Acrobat и Adobe Reader отображают вкладку «Подписи» с сопутствующей информацией: значок, показывающий статус проверки подписи, сведения о том, был ли изменен документ, результат проверки сертификата открытого ключа, время последней проверки, страницу и поле, содержащие электронную подпись. 

Инструменты подписи

Использовалась версия Java: «1.8.0_111» HotSpot(TM) 64-Bit Server VM (build 25.111-b14).

В качестве сертифицированного средства защиты информации от лицензированного разработчика применяем криптографический провайдер КриптоПро CSP v4.0 и КриптоПро JCP – v.2.0, с установкой модуля КриптоПро Java CSP v.4.0

Почему КриптоПро JCP – v.2.0, с модулем КриптоПро Java CSP v.4.0?

Потому, что провайдер КриптоПро JCP после длительного несертифицированного периода получил сертификат соответствия от регулятора до 31.12.2018, а дальше, по информации от разработчика, с сертификацией может вновь возникнуть неопределенность. Модуль КриптоПро Java CSP v.4.0 не выполняет в себе криптографических преобразований и является по сути API к провайдеру КриптоПро CSP, с очередной сертификацией которого вопросов нет. Здесь нужно сказать, что действующий сертификат на СКЗИ не обязателен при условии использования криптографического провайдера исключительно для внутренних целей.

В соответствии со спецификацией Java Cryptography Architecture (JCA), в своем приложении я указываю и использую функции криптографического провайдера: JCSP. После установки КриптоПро, данный провайдер отображается в списке всех доступных в файле ../java/jdk1.8.x_xxx/jre/lib/security/java.security, где можно настраивать, который из них предпочтительнее для использования по умолчанию, если явно не указано в приложении: 
#
# List of providers and their preference orders (see above):
#
security.provider.1=ru.CryptoPro.JCSP.JCSP

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

Для этого в ../java/jdk1.8.x_xxx/jre/lib/security необходимо заменить файлы local_policy.jar и US_export_policy.jar на предоставленные по адресу: Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 8 Download
и содержащие в default_local.policy и default_US_export.policy:

grant {
// There is no restriction to any algorithms.
permission javax.crypto.CryptoAllPermission;
};

Применение библиотеки iText itextpdf.com для работы с PDF документами на Java было продиктовано провайдером КриптоПро.

В составе КриптоПро JCP поставляется файл Git patch для библиотеки iTextpdf версии 5.1.3 — 
jcp-2.0.xxxxx\Doc\itextpdf\ itextpdf_5.1.3.gost.user.patch

Патч адаптирует itextpdf к работе с провайдером КриптоПро. Необходимо скачать исходный код библиотеки версии 5.1.3, затем с помощью командной строки Bash системы управления версиями Git применить патч: git apply --stat itextpdf_5.1.3.gost.user.patch 

Далее нужно собрать полученную библиотеку из обновленного исходного кода и подключить к приложению.

В составе КриптоПро JCP можно найти много примеров в файле samples-sources.jar. В частности, там есть примеры подписи и проверки ЭП PDF-документов. (\PDF\SignVerifyPDFExample.java).

Проблемы и трудности сборки

После успешного обновления исходного кода itextpdf в нем появляются зависимости на пакеты ru.CryptoPro.JCP и ru.CryptoPro.reprov.x509. 

Без них проект с исходным кодом itextpdf_5.1.3.gost не соберется.

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project itextpdf: Compilation failure: Compilation failure:
[ERROR] \github\iTextpdf_5.1.3_patched_cryptopro_bc1.50\src\main\java\com\itextpdf\text\pdf\PdfPKCS7.java:[138,23] error: package ru.CryptoPro.JCP does not exist
[ERROR] \github\iTextpdf_5.1.3_patched_cryptopro_bc1.50\src\main\java\com\itextpdf\text\pdf\PdfPKCS7.java:[139,31] error: package ru.CryptoPro.reprov.x509 does not exist

Нужно взять из поставки КриптоПро 2.0 файлы JCP.jar и JCPRevTools.jar и поместить в каталог JRE, которую использует Maven: Java\jdk1.8.0_111\jre\lib\ext. Само собой, они должны быть и в classPath приложения. 

Итак, библиотека собрана, подключаем ее в приложение. И тут возникает основная проблема. iTextpdf_5.1.3 содержит зависимость на Bouncy Castle версии 1.46 – библиотеку с открытым кодом, реализующую криптографический провайдер и поддержку ASN.1 структур.

                
            org.bouncycastle
            bctsp-jdk15
            1.46
            jar
            compile
            true
        

Поставка КриптоПро JCP 2.0 в свою очередь имеет зависимости на Bouncy Castle версии 1.50 bcpkix-jdk15on-1.50 и bcprov-jdk15on-1.5, соответственно, они помещаются в jre/lib/ext при установке КриптоПро. 

В итоге при запуске своего приложения и метода подписания PDF мы получаем ошибку:

Exception in thread "main" java.lang.NoClassDefFoundError: org/bouncycastle/asn1/DEREncodable
at com.itextpdf.text.pdf.PdfSigGenericPKCS.setSignInfo(PdfSigGenericPKCS.java:97)
at com.itextpdf.text.pdf.PdfSignatureAppearance.preClose(PdfSignatureAppearance.java:1003)
at com.itextpdf.text.pdf.PdfSignatureAppearance.preClose(PdfSignatureAppearance.java:904)
at com.itextpdf.text.pdf.PdfStamper.close(PdfStamper.java:194)
at ru.alfabank.ccjava.trustcore.logic.SignatureProcessor.pdfSignature(SignatureProcessor.java:965)
at ru.alfabank.ccjava.trustcore.logic.SignatureProcessor.main(SignatureProcessor.java:1363)
Caused by: java.lang.ClassNotFoundException: org.bouncycastle.asn1.DEREncodable
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 6 more

Каждый разработчик Java сталкивался с таким исключением в потоке main и знает, что можно потратить много времени на разбор проблемы, и как все исправить. Суть исключения NoClassDefFoundError следующая – оно выбрасывается, когда виртуальная машина Java во время исполнения приложения не может найти конкретный класс, который был доступен на этапе компиляции.

Что получается – библиотека iTextpdf_5.1.3 имеет зависимость от более старого провайдера Bouncy Castle, а для новых версий iTextpdf нет патча от КриптоПро. 

Конкретно в поставке КриптоПро JCP 2.0 зависимости на новую версию Bouncy Castle имеет библиотека CAdES.jar. Если удалить из JRE эту библиотеку или вовсе отказаться от поддержки формирования CAdES подписей при установке КриптоПро JCP 2.0, то проблема будет решена.

Но что если поддержка CAdES должна остаться? 

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

  • заменяем в исходном коде библиотеки iTextpdf_5.1.3_patched_cryptopro зависимость org.bouncycastle 1.46 на версию 1.50.
                       
                    org.bouncycastle
                    bcprov-ext-jdk15on
                    1.50
                
                
                    org.bouncycastle
                    bcprov-jdk15on
                    1.50
                
                
                    org.bouncycastle
                    bcmail-jdk15on
                    1.50
                
        
  • вносим исправления в проект iTextpdf_5.1.3_patched_cryptopro, руководствуясь документом Bouncy Castle «Porting from earlier BC releases to 1.47 and later»
  • чтобы поправить те места iTextpdf_5.1.3_patched_cryptopro, где дело не ограничивалось новыми названиями классов и методов Bouncy Castle, а явно были изменены конструкции в методах, скачиваем исходный код itextpdf 5.5.5, который имеет зависимость на Bouncy Castle 1.50, вдумчиво переносим реализацию методов оттуда.

Как итог, проект iTextpdf_5.1.3_patched_cryptopro_bc1.50 начинает собираться. Конфликт разрешен, КриптоПро и itextpdf ссылаются на одну версию org.bouncycastle 1.50. 

Исходный код iTextpdf_5.1.3_patched_cryptopro_bc1.50 выложен в GitHub: iTextpdf_5.1.3_patched_cryptopro_bc1.50

Пример кода, несколько подписей PDF

Пример использования КриптоПро и iTextpdf_5.1.3_patched_cryptopro_bc1.50 выглядит следующим образом:

 /**     *      * @param aliases     *            - имена контейнеров с ключами ЭП     * @param data     *            - массив байтов с документом PDF     * @param pdfVersion     *            - номер версии формата PDF     * @return     * @throws SignatureProcessorException     */    public static byte[] samplePDFSignature(String[] aliases, byte[] data, char pdfVersion) throws SignatureProcessorException {        ByteArrayOutputStream bais = new ByteArrayOutputStream();        HashMap currSignAttrMap = new HashMap();        for (String alias : aliases) {            X509Certificate certificate = (X509Certificate) signAttributesMap1.get(alias)[0];            PrivateKey privateKey = (PrivateKey) signAttributesMap1.get(alias)[1];                        currSignAttrMap.put(certificate, privateKey);            if (certificate == null) {                throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + CERTIFICATE_NOT_FOUND_BY_ALIAS);            }                        if (privateKey == null) {                throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + PRIVATE_KEY_NOT_FOUND_BY_ALIAS);            }        }        try {            FileInputStream fis = new FileInputStream(new File(FILE_PATH));            ByteArrayOutputStream baos = new ByteArrayOutputStream();            byte[] buf = new byte[1024];            int n = 0;            while ((n = fis.read(buf, 0, buf.length)) != -1) {                baos.write(buf, 0, n);            }            fis.close();            byte[] im = baos.toByteArray();                        X509Certificate innerCA = obtainCertFromTrustStoreJKS(false, INNER_CA);            PdfStamper stp = null;            PdfReader reader = null;            int pageNumber = 1;            for (Entry entry : currSignAttrMap.entrySet()) {                if (bais.toByteArray().length == 0) {                    reader = new PdfReader(data);                } else {                    reader = new PdfReader(bais.toByteArray());                    bais = new ByteArrayOutputStream();                }                stp = PdfStamper.createSignature(reader, bais, pdfVersion); //'\0'                Certificate[] certPath = new Certificate[] {entry.getKey(), innerCA};                PdfSignatureAppearance sap = stp.getSignatureAppearance();                sap.setProvider("JCSP"); //JCP                sap.setCrypto(entry.getValue(), certPath, null,                        PdfSignatureAppearance.CRYPTOPRO_SIGNED);                Image image = Image.getInstance(im);                sap.setImage(image);                sap.setVisibleSignature(new Rectangle(150, 150), pageNumber, null);                pageNumber++;                stp.close();                bais.close();                reader.close();            }        } catch (RuntimeException e) {            throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));        } catch (IOException e) {            throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));        } catch (DocumentException e) {            throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));        } catch (CertificateEncodingException e) {            throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));        } catch (Exception e) {            throw new SignatureProcessorException(PDF_SIGNATURE_ERROR + ExceptionUtils.getFullStackTrace(e));        }        return bais.toByteArray();    }

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

Создается объект с корневым сертификатом для формирования пути сертификации, который требуется методу setCrypto класса com.itextpdf.text.pdf.PdfSignatureAppearance. 

В цикле выполняется подпись страниц PDF-документа ключами, имена которых были переданы в метод.

Для демонстрации выполнены две подписи — валидная и невалидная с недействительным сертификатом.

Вкладка «Подписи» выглядит следующим образом:

Применение электронной подписи PDF-документов

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

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

Практичнее формировать электронную подпись в виде отдельного файла или оборачивать файл и подпись в один контейнер стандарта PKCS#7 (CAdES). Этот стандарт с присоединенной или отсоединенной подписью отлично подойдет для большого документооборота между информационными системами.

Тот же документ PDF можно подписать по стандарту CAdES отсоединенной подписью, в итоге будет два файла — сам PDF и контейнер с подписью. 

Вспомним проблемы, с которыми пришлось столкнуться при подписании на Java. Вывод — формат PDF сейчас плохо поддерживается КриптоПро в части прикладного программного интерфейса для Java. Существующую библиотеку itextpdf пришлось править самостоятельно. 

В сфере деятельности, связанной с разработкой коммерческого программного обеспечения со встроенными СКЗИ, такой подход недопустим, патчи, обновления и доработки должны выполняться организацией-лицензиатом. Поэтому приведенный в посте способ подписи PDF-документа можно применять с целью демонстрации возможностей и для внутреннего использования. 

Оригинал статьи и автор.

А также, полезные ссылки:

  1. Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 8
  2. Сайт компании КриптоПро
  3. Разработчик iTextpdf
  4. Bouncy Castle «Porting from earlier BC releases to 1.47 and later»
  5. Исходный код библиотеки iTextpdf_5.1.3_patched_cryptopro_bc1.50 в GitHub

20 сентября 2017

Поделиться
Отправить