Technical Article

Анализ цифровых подписей PDF и уровней PAdES в Delphi

Предположим, вы получили подписанный файл PDF и хотите отобразить в окне просмотра, кто его подписал, когда, распространяется ли подпись на весь файл и насколько она соответствует требованиям долгосрочного хранения. Библиотека PDFium Component для Delphi и Lazarus решает эти задачи с помощью набора read-only методов: свойство SignatureCount и семейство функций FPDFSignatureObj_* предоставляют доступ к словарю подписи, а метод TPdf.ValidatePades классифицирует базовый уровень PAdES. Это первая статья из цикла, посвященного подписям PDF в PDFium; последующие статьи расскажут о создании подписей B-B и добавлении штампов времени долгосрочного архивирования. Важная деталь: вся описываемая здесь работа относится к анализу структуры, а чтение заявленных подписью данных отличается от криптографической проверки подлинности и принятия решения о доверии подписанту

Почему словарь подписи PDF — это не просто бинарный массив

Подпись PDF представляет собой структурированный словарь, а не произвольный вложенный файл, и два его наиболее важных элемента показывают, какая часть файла действительно защищена. Спецификация ISO 32000-1 §12.8 определяет словарь подписи с элементами /ByteRange и /Contents. Элемент /Contents содержит hex-строку структуры CMS SignedData (RFC 5652) — криптографический контейнер, несущий сертификат подписанта, подписанные атрибуты и само значение подписи. Параметр /ByteRange — деталь, которую часто недооценивают: это массив пар «смещение-длина», которые вместе покрывают весь файл, за исключением hex-строки /Contents. Этот пустой промежуток — именно то место, где находятся байты подписи, а диапазоны по обе стороны от него представляют собой данные, которые подпись защищает

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

Чтение словаря подписи с помощью read-only API PDFium

Компонент PDFium Component предоставляет доступ к словарю подписи через свойства SignatureCount и записи Signature[Index]. На внутреннем уровне они вызывают функции FPDF_GetSignatureCount, FPDF_GetSignatureObject и методы доступа FPDFSignatureObj_* для чтения параметров /SubFilter, /ByteRange, /Contents, /Reason и времени подписания. Слово «read-only» здесь является ключевым: PDFium может находить и читать подписи, но не предоставляет API для их создания или записи. Именно поэтому запись подписей реализована силами самой библиотеки, а не движка PDFium

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // signer date string, e.g. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = no certification, 1..3 = MDP level
    end;
  finally
    Pdf.Free;
  end;
end;

Каждая запись TPdfSignature проецируется на соответствующий словарь подписи. Свойство Encoding отражает параметр /SubFilter — наиболее важное диагностическое поле, поскольку оно указывает обработчик подписи и сразу отделяет современную подпись ETSI.CAdES.detached от устаревших или небезопасных форматов. Свойство Time возвращает заявленное автором время подписания в виде даты PDF, которое является лишь утверждением подписанта и не может рассматриваться как доверенное время. Свойство Content содержит исходные байты CMS SignedData, а Permission указывает уровень сертификации DocMDP (0 для обычной утверждающей подписи, от 1 до 3 для удостоверяющей подписи, которая блокирует внесение последующих изменений). Запись не возвращает разобранный массив ByteRange в чистом виде, так как метод ValidatePades выполняет все расчеты покрытия ByteRange самостоятельно

В чем разница между PAdES B-B, B-T, B-LT и B-LTA?

Четыре базовых уровня PAdES образуют цепочку от минимально корректной подписи до структуры, рассчитанной на десятилетия архивного хранения, причем каждый последующий уровень включает в себя предыдущий. Стандарт ETSI EN 319 142-1 определяет уровни B-B, B-T, B-LT и B-LTA. Уровень B-B (Basic) представляет собой подпись вместе с обязательными подписанными атрибутами и ничего более. Уровень B-T (Timestamp) добавляет доверенный штамп времени RFC 3161 поверх подписи, благодаря чему момент подписания подтверждается независимым сервером штампов времени, а не часами компьютера пользователя. Уровень B-LT (Long-Term) внедряет все данные проверки (цепочку сертификатов и дополнительно ответы OCSP или списки отзывов CRL) внутрь файла, что позволяет проверять подпись спустя годы, когда выпускающая инфраструктура перестанет существовать. Уровень B-LTA (Long-Term with Archive timestamp) накладывает архивный штамп времени документа поверх этих данных проверки, защищая сам архивный контейнер и обеспечивая возможность обновления штампов до того, как устареют используемые криптографические алгоритмы

На практике разница заключается во временном горизонте. Подпись B-B отвечает на вопрос «подписал ли кто-то этот документ». Уровень B-T отвечает «и когда именно, с гарантией». Уровень B-LT отвечает «и смогу ли я проверить подпись после истечения срока действия сертификатов». Уровень B-LTA отвечает «и останется ли эта проверка корректной через двадцать лет». Регуляторные требования выбирают уровень: в рамках электронного документооборота и eIDAS требуется как минимум уровень B-T, а для архивного хранения — B-LT или B-LTA. Определение реального уровня подписи до ее обработки — ключевая задача этапа проверки

Определение базового уровня с помощью TPdf.ValidatePades

Компонент PDFium Component сводит задачу проверки уровня к одному вызову. Метод TPdf.ValidatePades возвращает запись TPadesValidationResult, поле Level которой содержит значение перечисления TPadesLevel (plNone, plUnknown, plB_B, plB_T, plB_LT или plB_LTA) наряду со списком обнаруженных несоответствий, количеством подписей и штампов времени. Уровень определяется последовательно: валидатор сначала проверяет требования B-B, затем повышает уровень до B-T при наличии штампа времени подписи или документа, до уровня B-LT при наличии в каталоге структуры /DSS с сертификатами и маркера /Extensions /ESIC Level 1, и до уровня B-LTA при наличии архивного штампа времени и маркера ESIC Level 2. Два вспомогательных метода делают результат применимым: свойство IsCompliant возвращает True, если уровень равен как минимум B-B и список замечаний пуст, а метод IsCompliantAt проверяет соответствие заданному порогу требований (например, plB_T)

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B   (basic)');
      plB_T:     Writeln('PAdES B-T   (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT  (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

Почему adbe.pkcs7.sha1 является запрещенным SubFilter?

Потому что алгоритм SHA-1 устарел и скомпрометирован, а обработчик adbe.pkcs7.sha1 использует его на внутреннем уровне. Этот SubFilter хэширует документ с помощью SHA-1 перед упаковкой в PKCS#7, а уязвимости SHA-1 к коллизиям известны уже много лет. Поэтому стандарт EN 319 142-1 (раздел 6.3) прямо запрещает его использование для базовых подписей. Метод ValidatePades фиксирует замечание ppeiForbiddenSubFilter при обнаружении adbe.pkcs7.sha1 или adbe.x509.rsa_sha1, а также выдает ppeiBadDigestAlgorithm, если сама структура CMS использует MD5 или SHA-1 для расчета хэша сообщения (раздел 6.2.1). Это две независимые проверки, выявляющие одинаковые криптографические уязвимости на разных уровнях структуры подписи

Всего коллекция замечаний содержит 26 возможных несоответствий, и наиболее частые из них связаны со структурой файла и покрытием данных. Замечание ppeiByteRangeNotCoveringFile указывает на несоответствие ByteRange, описанное выше. Ошибка ppeiForbiddenCertKey возникает при наличии в словаре подписи элемента /Cert, запрещенного стандартом PAdES (цепочка должна храниться исключительно внутри структуры CMS SignedData.certificates). Ошибки ppeiMissingSigningCertificate, ppeiMissingContentType и ppeiMissingMessageDigest указывают на отсутствие обязательных подписанных атрибутов, а ppeiDetachedContentViolation фиксирует подписи, которые ошибочно внедряют подписанные данные внутрь контейнера вместо их отделения. Анализ коллекции замечаний позволяет получить точные технические причины отклонения подписи

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

Что не проверяет ValidatePades

Метод ValidatePades проверяет структуру подписи, но не уровень доверия к ней, и путаница между этими понятиями опасна. Результат plB_LTA означает, что документ содержит корректную структуру подписи B-LTA со всеми обязательными атрибутами, штампами и данными проверки на нужных позициях — но это не гарантирует криптографическую подлинность подписи, доверие к корневому сертификату или отсутствие отозванных сертификатов в цепочке. Валидатор намеренно не выполняет криптографическую проверку: он не пересчитывает хэш документа по диапазону ByteRange, не строит цепочку доверия и не опрашивает OCSP/CRL серверы на предмет отзывов. Это разделение осознанно, так как структурный анализ выполняется быстро, полностью детерминирован, не требует ключей или сетевой активности и работает на чистом коде Pascal одинаково под Windows, Linux и macOS. Проверка цепочки доверия неразрывно связана с политиками безопасности (какие корневые сертификаты считать доверенными, как запрашивать статусы отзывов, каковы допустимые погрешности времени), поэтому используйте метод ValidatePades как первичный фильтр корректности структуры подписи, а затем передавайте проверенные файлы реальным средствам криптографического контроля

Структурная проверка является базовым шагом и отлично дополняет другие методы контроля, описанные в статьях об аудите угроз безопасности PDF с помощью PDFium Component и проверках соответствия форматам вроде валидации готовых к печати файлов PDF/X. После того как вы научитесь читать и анализировать подписи, следующим шагом станет их создание: вторая статья цикла расскажет о подписании PDF-файлов с уровнем PAdES B-B, а третья — о добавлении доверенных штампов времени и создании долгосрочных подписей B-LT и B-LTA. Анализ структуры подписей и классификатор ValidatePades входят в состав компонента PDFium Component для Delphi, C++Builder и Lazarus