Подписать PDF-хэш с помощью Java и iText - PullRequest
0 голосов
/ 07 марта 2019

У меня есть приложение, которое генерирует PDF, и которое должно быть подписано.

У нас нет сертификатов для подписи документа, потому что они находятся в HSM, и единственный способ, которым мы могли бы сделатьиспользование сертификатов использует веб-сервис.

    PdfReader reader = new PdfReader(src);
    reader.setAppendable(true);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    FileOutputStream fout = new FileOutputStream(dest);
    PdfStamper stamper = PdfStamper.createSignature(reader, fout, '\0');
    PdfSignatureAppearance appearance  = stamper.getSignatureAppearance();

    appearance.setReason("Test");
    appearance.setLocation("footer");
    appearance.setVisibleSignature(new Rectangle(100, 100, 200, 200), 1, null);
    appearance.setCertificate(certChain[0]);

    PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
    dic.setReason(appearance.getReason());
    dic.setLocation(appearance.getLocation());
    dic.setContact(appearance.getContact());
    dic.setDate(new PdfDate(appearance.getSignDate()));
    appearance.setCryptoDictionary(dic);

    HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
    exc.put(PdfName.CONTENTS, new Integer(8192 * 2 + 2));
    appearance.preClose(exc);

    ExternalDigest externalDigest = new ExternalDigest()
    {
        public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException
        {
            return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
        }
    };

    PdfPKCS7 sgn = new PdfPKCS7(null, certChain, "SHA256", null, externalDigest, false);
    InputStream data = appearance.getRangeStream();
    byte[] hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));
    Calendar cal = Calendar.getInstance();

    byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
    sh = MessageDigest.getInstance("SHA256", "BC").digest(sh);

    String hashPdf = new String(Base64.encodeBytes(sh));

    String hashSignat = hashPdf;

Это наш код, во-первых, мы получаем вид подписи и вычисляем хеш

В этом пункте у нас есть хэш-коддокумента.Затем мы отправляем хеш в веб-сервис и получаем подписанный хеш-код.

Наконец, мы помещаем подписанный хеш в PDF:

    sgn.setExternalDigest(Base64.decode(hashSignat), null, "RSA");
    byte[] encodedSign = sgn.getEncodedPKCS7(hash, cal, null, null, 
    null, CryptoStandard.CMS);
    byte[] paddedSig = new byte[8192];
    System.arraycopy(encodedSign, 0, paddedSig, 0, 
    encodedSign.length);

    PdfDictionary dic2 = new PdfDictionary();
    dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));

    appearance.close(dic2);

В этом пункте мы получаемPDF подписан, но с неверной подписью.Adobe говорит, что «документ был изменен или поврежден с момента его подписания».

Я прошел через Подпись PDF, используя внешний сервис, и iText , Подпись PDF itext pkcs7 multi sign и Можно ли подписать документ PDF с помощью хэша и подписанного хэша? , но не повезло.

1 Ответ

0 голосов
/ 11 марта 2019

Файл примера

Вы предоставили файл примера , подписанный вашим кодом в комментарии к вашему вопросу.

Анализ этого файла (выполняется с использованием AnalyzeSignatures test testPriyankaSignatureSampleinformedconsent_Signed) показывает, что фактически подписанный хэш равен

1134ED96F42AC7352E546BE0E906C0BF5D44229AEAFAC39145DB40A0BB7E817B

, который должен быть хешем аутентифицированных атрибутов, но их хеш равен

E7101D9770ABF5E58D43670AAC6E9418265AE80F74B3BDFB67911C0CC5D5D949.

.хэш диапазонов байтов со знаком равен

D7917CF3BA9FE5D5B626ED825965D699F7C54DBBF9F18DECE18EF8DD36DC4C28,

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

В итоге оказалось, что

утилита кодирования в веб-сервисе отличалась из-за того, что декодированное хеш-значение было неправильным иMessageDigest(sh = MessageDigest.getInstance("SHA256", "BC").digest(sh);) не нужно.эта строка давала неверный хеш.

Подписание ранее подписанных файлов

После этого возникла новая проблема:

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

Для подписания уже подписанных PDF-файлов вы должны использовать режим добавления .Для этого вам понадобится другая перегрузка PdfStamper.createSignature с еще одним параметром, boolean, для которого вы установите true.

Причина в том, что обычно (т.е. без активации режима добавления )1044 *) iText реорганизует внутренние структуры PDF и удаляет неиспользуемые объекты.В уже подписанных PDF-файлах это обычно перемещает позицию (уже существующей) подписи, которая делает недействительной структуру подписи.Используя режим добавления , iText сохраняет исходные байты PDF такими, какими они были, и только добавляет новые данные.

Изменение хешей

Я просто запутался, почемузначение байта "sh" меняется каждый раз для одного и того же файла?на самом деле, я хочу сохранить хеш-значение файла.это из-за "cal"?

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

Могу ли я получить хэш за раз, сохранить его в db, подписать хэш позже и добавить в pdf?

Вы должны либо

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

1-й вариант для меня невозможен.Я не получил 2-й 3-й вариант.Можете ли вы уточнить оба или дать какую-либо ссылку, на которую я могу сослаться?

Исправление iText для принудительного применения фиксированных хэшей для каждого файла

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

OpenPdf (более старая ветка iText) содержит патч, добавляющий свойства EnforcedModificationDate, OverrideFileId и IncludeFileID к PdfStamper.(PdfSignatureAppearance уже имеет свойство SignDate.) Этот патч был применен, чтобы позволить eSignature DSS использовать OpenPdf в процессе подписи (который также включает создание подписанного PDF дважды и поэтому требует фиксированных значений хеша).

Возможно, вы не захотите переключаться на эту старую вилку iText, поскольку она пропускает множество исправлений и новых опций в новых версиях iText.

Сохранение уже подготовленного файла

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

По сути, это пример iText C4_09_DeferredSigning - это все, сначала создается промежуточный PDF со всем, в нем отсутствуют только подписывающие байты:

public void emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
    PdfReader reader = new PdfReader(src);
    FileOutputStream os = new FileOutputStream(dest);
    PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0');
    PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
    appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, fieldname);
    appearance.setCertificate(chain[0]);
    ExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
    MakeSignature.signExternalContainer(appearance, external, 8192);
}

Только на втором шаге вводится фактическая подпись:

public void createSignature(String src, String dest, String fieldname, PrivateKey pk, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {

    PdfReader reader = new PdfReader(src);
    FileOutputStream os = new FileOutputStream(dest);
    ExternalSignatureContainer external = new MyExternalSignatureContainer(pk, chain);
    MakeSignature.signDeferred(reader, fieldname, os, external);
}

с использованием

class MyExternalSignatureContainer implements ExternalSignatureContainer {

    protected PrivateKey pk;
    protected Certificate[] chain;

    public MyExternalSignatureContainer(PrivateKey pk, Certificate[] chain) {
        this.pk = pk;
        this.chain = chain;
    }

    public byte[] sign(InputStream is) throws GeneralSecurityException {
        try {
            PrivateKeySignature signature = new PrivateKeySignature(pk, "SHA256", "BC");
            String hashAlgorithm = signature.getHashAlgorithm();
            BouncyCastleDigest digest = new BouncyCastleDigest();
            PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);
            byte hash[] = DigestAlgorithms.digest(is, digest.getMessageDigest(hashAlgorithm));
            Calendar cal = Calendar.getInstance();
            byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
            byte[] extSignature = signature.sign(sh);
            sgn.setExternalDigest(extSignature, null, signature.getEncryptionAlgorithm());
            return sgn.getEncodedPKCS7(hash, cal, null, null, null, CryptoStandard.CMS);
        }
        catch (IOException ioe) {
            throw new ExceptionConverter(ioe);
        }
    }

    public void modifySigningDictionary(PdfDictionary signDic) {
    }

}

Здесь значение has вычисляется на втором шаге, но вам не нужно этого делать, вы можете

  • уже вычисляет его на первом шаге, не используя исходный ExternalBlankSignatureContainer, как в emptySignature() выше, а вместо этого расширяя его для вычисления хеша, как MyExternalSignatureContainer,
  • сохраняя это значение в вашей базе данных, и
  • (как только вы извлекаете подпись), внедряя эту подпись, используя вариант MyExternalSignatureContainer, который не вычисляет хеш, а вместо этого вводит точно возвращенную подпись.
...