Цифровая подпись iText портит PDF/A 2b

При цифровой подписи документа с помощью itext v5.5.11 документы PDF/A-2b повреждаются - это означает, что они больше не действительны как документы PDF / A. Нарушено следующее правило: https://github.com/veraPDF/veraPDF-validation-profiles/wiki/PDFA-Parts-2-and-3-rules

В приведенной выше ссылке указано, что дайджест недействителен, поэтому я также даю вам фрагмент кода, который занимается вычислением дайджеста при подписании документа pdf с помощью iText:

        // Make the digest
        InputStream data;
        try {

            data = signatureAppearance.getRangeStream();
        } catch (IOException e) {
            String message = "MessageDigest error for signature input, type: IOException";
            signLogger.logError(message, e);
            throw new CustomException(message, e);
        }
        MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance("SHA1");

        } catch (NoSuchAlgorithmException ex) {
            String message = "MessageDigest error for signature input, type: NoSuchAlgorithmException";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        }
        byte[] buf = new byte[8192];
        int n;
        try {
            while ((n = data.read(buf)) > 0) {
                messageDigest.update(buf, 0, n);
            }
        } catch (IOException ex) {
            String message = "MessageDigest update error for signature input, type: IOException";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        }
        byte[] hash = messageDigest.digest();
        // If we add a time stamp:
        // Create the signature
        PdfPKCS7 sgn;
        try {

            sgn = new PdfPKCS7(key, chain, configuration.getSignCertificate().getSignatureHashAlgorithm().value() , null, new BouncyCastleDigest(), false);
        } catch (InvalidKeyException ex) {
            String message = "Certificate PDF sign error for signature input, type: InvalidKeyException";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        } catch (NoSuchProviderException ex) {
            String message = "Certificate PDF sign error for signature input, type: NoSuchProviderException";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        } catch (NoSuchAlgorithmException ex) {
            String message = "Certificate PDF sign error for signature input, type: NoSuchAlgorithmException";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        }catch (Exception ex) {
            String message = "Certificate PDF sign error for signature input, type: Exception";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        }
        byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, null,null, MakeSignature.CryptoStandard.CMS);
        try {
            sgn.update(sh, 0, sh.length);
        } catch (java.security.SignatureException ex) {
            String message = "Certificate PDF sign error for signature input, type: SignatureException";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        }
        byte[] encodedSig = sgn.getEncodedPKCS7(hash);
        if (contentEstimated + 2 < encodedSig.length) {
            String message = "The estimated size for the signature is smaller than the required one. Terminating request..";
            signLogger.log("ERROR", message);
            throw new CustomException(message);
        }
        byte[] paddedSig = new byte[contentEstimated];
        System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length);
        // Replace the contents
        PdfDictionary dic2 = new PdfDictionary();
        dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));
        try {
            signatureAppearance.close(dic2);
        } catch (IOException ex) {
            String message = "PdfSignatureAppearance close error for signature input, type: IOException";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        } catch (DocumentException ex) {
            String message = "PdfSignatureAppearance close error for signature input, type: DocumentException";
            signLogger.logError(message, ex);
            throw new CustomException(message, ex);
        }

Для проверки PDF / A я использую библиотеку VeraPDF.

Также полезно упомянуть, что хотя библиотека VeraPDF сообщает о поврежденной библиотеке PDF / A, инструменты проверки Adobe Reader сообщают, что документ PDF / A не поврежден.

Любая помощь приветствуется.

3 ответа

Решение

При цифровой подписи документа с помощью itext v5.5.11 документы PDF/A-2b повреждаются - это означает, что они больше не действительны как документы PDF / A. Нарушено следующее правило: https://github.com/veraPDF/veraPDF-validation-profiles/wiki/PDFA-Parts-2-and-3-rules

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

Причиной такого неправильного обнаружения нарушения является ошибка в veraPDF.

Как veraPDF определяет допустимость подписанных диапазонов байтов

Как версия VeryPDF (основанная на анализаторе нового поля, так и на основе PDFBox) пытается определить номинальное значение диапазонов байтов и сравнить его с фактическим значением. Вот как это определяет номинальную стоимость:

public long[] getByteRangeBySignatureOffset(long signatureOffset) throws IOException {
    pdfSource.seek(signatureOffset);
    skipID();
    byteRange[0] = 0;
    parseDictionary();
    byteRange[3] = getOffsetOfNextEOF(byteRange[2]) - byteRange[2];
    return byteRange;
}

private long getOffsetOfNextEOF(long currentOffset) throws IOException {
    byte[] buffer = new byte[EOF_STRING.length];
    pdfSource.seek(currentOffset + document.getHeaderOffset());
    readWholeBuffer(pdfSource, buffer);
    pdfSource.rewind(buffer.length - 1);
    while (!Arrays.equals(buffer, EOF_STRING)) {    //TODO: does it need to be optimized?
        readWholeBuffer(pdfSource, buffer);
        if (pdfSource.isEOF()) {
            pdfSource.seek(currentOffset + document.getHeaderOffset());
            return pdfSource.length();
        }
        pdfSource.rewind(buffer.length - 1);
    }
    long result = pdfSource.getPosition() + buffer.length - 1;  // offset of byte after 'F'
    pdfSource.seek(currentOffset + document.getHeaderOffset());
    return result - 1;
}

(На основе PDFBox SignatureParser учебный класс)

public long[] getByteRangeBySignatureOffset(long signatureOffset) throws IOException {
    source.seek(signatureOffset);
    skipID();
    byteRange[0] = 0;
    parseDictionary();
    byteRange[3] = getOffsetOfNextEOF(byteRange[2]) - byteRange[2];
    return byteRange;
}

private long getOffsetOfNextEOF(long currentOffset) throws IOException {
    byte[] buffer = new byte[EOF_STRING.length];
    source.seek(currentOffset + document.getHeader().getHeaderOffset());
    source.read(buffer);
    source.unread(buffer.length - 1);
    while (!Arrays.equals(buffer, EOF_STRING)) {    //TODO: does it need to be optimized?
        source.read(buffer);
        if (source.isEOF()) {
            source.seek(currentOffset + document.getHeader().getHeaderOffset());
            return source.getStreamLength();
        }
        source.unread(buffer.length - 1);
    }
    long result = source.getOffset() - 1 + buffer.length;   // byte right after 'F'
    source.seek(currentOffset + document.getHeader().getHeaderOffset());
    return result - 1;
}

(основанный на синтаксическом анализаторе SignatureParser )

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

Почему это неправильно

Существует несколько причин, по которым этот способ определения номинального значения диапазонов байтов со знаком является неправильным:

  1. Согласно спецификации PDF / A,

    Никакие данные не могут следовать за последним маркером конца файла, за исключением одного необязательного маркера конца строки, как описано в ISO 32000-1:2008, 7.5.5.

    Таким образом, смещение непосредственно после следующего маркера конца файла %%EOF это не обязательно уже конец подписанной ревизии, правильное смещение может быть тем, которое следует за следующим маркером конца строки! А так как маркер конца строки PDF может быть либо одним CR, либо одной LF или комбинацией CRLF, это означает, что veraPDF выбирает одно из трех возможных смещений и заявляет, что оно является номинальным концом ревизии и, следовательно,, номинальный конец подписанных диапазонов байтов.

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

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

  3. Маркер конца файла %%EOF синтаксически на самом деле это просто комментарий со специальным значением в конце PDF / ревизии, и комментарии разрешены почти везде в PDF за пределами строк PDF, данных потока PDF и таблиц перекрестных ссылок PDF. Таким образом, последовательность байтов %%EOF может встречаться как обычный комментарий или как некомментированное содержимое строки или потока любое количество раз между словарем значения подписи и фактическим концом подписанной ревизии.

    Если это происходит, veraPDF выбирает последовательность байтов в качестве маркера конца файла, который никогда не рассматривался как конец чего-либо.

Кроме того, если фактический конец файла не достигнут в цикле (и pdfSource.length() / source.getStreamLength() возвращается) результат - 1 в return result - 1 не соответствует использованию результата.

Версии veraPDF

Я проверил текущие версии veraPDF 1.5.0-SNAPSHOT с тегами:

  • veraPDF-pdfbox-validation 1.5.4
  • veraPDF-валидация 1.5.2
  • veraPDF-парсер 1.5.1

Образец документа ОП

Образец документа, предоставленный OP, имеет LF после маркера конца файла. Из-за этого и проблемы, упомянутой выше, veraPDF определяет номинальный конец диапазона байтов со знаком, который составляет два байта.

Как уже говорилось выше, мы только что выпустили исправление для veraPDF 1.4, которое устраняет проблемы в этом обсуждении. Новая версия доступна для скачивания: http://downloads.verapdf.org/rel/1.4/verapdf-1.4.5-installer.zip

В частности, документы PDF/A-2 с подписью iText, похоже, проходят проверку veraPDF просто отлично.

Я согласен с анализом того, как veraPDF проверяет ByteRange в данный момент. Действительно, предполагается, что файл заканчивается точно на маркере%EOF сразу после поля подписи.

Причина довольно проста. Документ может быть подписан последовательно несколькими людьми и все еще может быть действительным документом PDF/A-2B. Когда вторая подпись генерируется, она будет постепенно обновлять файл, содержащий первую подпись.

Итак, если мы интерпретируем файл терминов в требованиях PDF/A-2B буквально:

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

Мы никогда не сможем создать действительный файл PDF / A с несколькими подписями. Это явно не было целью стандарта PDF / A-2.

PDF-файл обычно понимается как диапазон байтов от начального%PDF до конечного%EOF, что позволяет, например, для PDF-файлов составлять часть большего байтового потока (например, почтовых вложений). Это то, на чем основана реализация veraPDF.

Однако я согласен с тем, что этот подход не учитывает необязательную последовательность конца строки после%EOF. Я создал соответствующую проблему для veraPDF: https://github.com/veraPDF/veraPDF-validation/issues/166

Это оставляет интересный вопрос: что является действительным ByteRange первой подписи, если документ имеет больше подписей? Я считаю, все случаи:

  • ByteRange покрывает файл до следующего следующего маркера%EOF
  • ByteRange покрывает файл до следующего следующего маркера%EOF + один символ CR
  • ByteRange покрывает файл до следующего следующего маркера%EOF + один символ LF
  • ByteRange охватывает файл до следующего следующего маркера%EOF + двухбайтовой последовательности CR+LF

должно быть разрешено.

Другие вопросы по тегам