Техническая статья

Верификация цифровых подписей PDF в Delphi с помощью HotPDF

HotPDF проверяет цифровые подписи в загруженных документах PDF с помощью трех методов THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature, и VerifyLoadedSignatureEx, представленных в версии v2.259.0. Компонент повторно хэширует сегменты /ByteRange исходного файла, проверяет атрибут CMS messageDigest и выполняет верификацию RSA PKCS#1 v1.5 по встроенному сертификату подписанта, возвращая svValid при сохранении целостности байтов документа

Сценарий вполне обычен, чего не сканей об уровнях ответственности. Контрагент возвращает подписанный договор, вашей системе нужно его зарегистрировать, и кто-то задает единственный важный вопрос: является ли этот документ байт в байт тем, который мы отправили, и подписан ли он указанным сертификатом? Ответ на этот вопрос в коде — это сторона верификации; сторона подписания (создание и встраивание подписей PAdES) подробно описана в сопутствующей статье о создании цифровых подписей PAdES с помощью HotPDF. В этой же статье речь пойдет о противоположном направлении: PDF-файл поступает уже подписанным, и вам требуется получить программный вердикт, а не просто скриншот зеленой галочки из Acrobat

Как подписанный PDF доказывает отсутствие изменений?

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

Эта особенность архитектуры формирует весь API верификации: для проверки необходимо хэшировать исходные сериализованные байты в том виде, в каком они хранятся на диске. Разобранная объектная модель для этого бесполезна, поскольку повторная сериализация даже неизмененного документа приводит к изменению байтов. Поэтому HotPDF выполняет проверку по исходному файлу, из которого был загружен документ, или по предоставленному вами потоку TStream необработанных байтов, но никогда по его представлению в оперативной памяти

Чтение метаданных подписи перед началом проверки

Метод GetLoadedSignatureInfo разбирает словарь подписи и её контейнер CMS, не затрагивая ни одного байта самого документа, что делает его оптимальным первым вызовом, если вам требуется только отобразить информацию о подписанте и времени подписания. Поля подписей индексируются от 0 в порядке следования полей формы, а GetLoadedSignatureFieldCount сообщает их общее количество. Возвращаемая запись THPDFSignatureInfo содержит имя поля, /SubFilter, имя (Common Name) сертификата подписанта, DN (Distinguished Names) субъекта и издателя, серийный номер, даты действия, время подписания (из подписанного атрибута при его наличии, иначе из записи /M словаря), имя алгоритма хэширования, а также строки /Reason, /Location и /ContactInfo. Поле Status принимает значение svNotVerified — честный статус «разобрано, но не проверено»

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Поле:     ', Info.FieldName);
      Writeln('Подписант:    ', Info.SignerName);
      Writeln('Издатель:    ', Info.IssuerDN);
      Writeln('Алгоритм: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Выполнение криптографической проверки

Метод VerifyLoadedSignatureEx выполняет полную проверку для документа, загруженного из файла, и возвращает заполненную запись о подписи за один вызов: он повторно открывает исходный файл, хэширует сегменты /ByteRange с помощью алгоритма хэширования SignerInfo, сравнивает результат с подписанным атрибутом messageDigest (RFC 5652 §5.4) и затем проверяет подпись RSA по представлению DER SET подписанных атрибутов. Если подпись не содержит подписанных атрибутов, проверка RSA выполняется непосредственно по хэшу документа. Поддерживаются подписи RSA PKCS#1 v1.5 с хэшами SHA-1, SHA-256, SHA-384 или SHA-512, что охватывает субфильтры adbe.pkcs7.detached и ETSI.CAdES.detached, создаваемые стандартными инструментами подписания

var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Подпись верна; подпись покрывает весь файл')
      else
        Writeln('Подпись верна; файл был дополнен после подписания');
    svDigestMismatch:
      Writeln('Байты документа были изменены после подписания');
    svSignatureInvalid:
      Writeln('Проверка RSA для подписанных атрибутов завершилась сбоем');
    svUnsupportedAlgorithm:
      Writeln('Ключ отличен от RSA или неизвестный алгоритм хэширования');
    svMalformed:
      Writeln('Не удалось разобрать контейнер CMS');
    svSourceUnavailable:
      Writeln('Нет исходных байтов; используйте перегрузку TStream');
  end;
end;

Стоит знать две детали реализации, объясняющие сбои, которые извне кажутся загадочными. Во-первых, проверка подписанных атрибутов требовательна к кодированию: внутри файла атрибуты помечены тегом [0] IMPLICIT, но подпись вычислялась по их форме DER SET OF, поэтому верификатор заменяет тег перед хэшированием в точном соответствии с RFC 5652 §5.4. Самодельный верификатор, хэширующий байты в том виде, в каком они записаны в файле, отклонит любой корректно подписанный документ. Во-вторых, поле /Contents по соглашению дополняется нулями до зарезервированного размера, поэтому верификатор перед анализом обрезает блок DER до фактической длины его внешней структуры SEQUENCE; лишние нули в конце являются нормой, а не повреждением данных. Аналогичные проблемы разбора ASN.1 на стороне импорта сертификатов рассматриваются в статье об усилении безопасности PKCS#12 и ASN.1 в HotPDF

Что на самом деле гарантирует валидная подпись?

Статус svValid означает следующее: байты, указанные в /ByteRange, хэшируются в значение, которое подписал автор, и подпись подтверждается открытым ключом сертификата, встроенного в контейнер CMS. Это целостность байтов плюс привязка к ключу, и ничего более. Проверка цепочки сертификатов и доверия намеренно выходит за рамки верификатора HotPDF: он не строит цепочку до корневого центра, не проверяет отзывы и не обращается к хранилищам доверия. Самоподписанный сертификат злоумышленника, который повторно подписал измененный документ, вернет статус svValid, так как математически подпись корректна. Определение подлинности подписанта и уровня доверия к нему является задачей отдельного уровня (например, белого списка сертификатов вашей организации, хранилища сертификатов Windows или центра сертификации)

Флаг CoversWholeDocument указывает на более тонкую деталь. Подпись PDF всегда покрывает только свой диапазон /ByteRange, а механизм инкрементных обновлений PDF позволяет добавлять контент после подписи без её нарушения, что предусмотрено стандартом для работы с несколькими подписями. Этот флаг вычисляется во время проверки и равен true только тогда, когда два сегмента и область /Contents охватывают весь файл целиком. Если возвращается svValid с флагом CoversWholeDocument в значении false, это означает, что подписанная редакция не изменена, но файл содержит более поздние дополнения. Решение о допустимости таких дополнений должен принимать ваш рабочий процесс

Документам, загруженным из потока или зашифрованным, требуются собственные исходные байты

Методы VerifyLoadedSignature и VerifyLoadedSignatureEx без параметров зависят от того, помнит ли компонент, из какого файла был загружен документ. Если загрузить документ из потока, имя файла отсутствует; то же самое относится к пути перезагрузки по паролю для зашифрованных документов, описанному в статье об шифровании AES-256 PDF в HotPDF. В обоих случаях методы, ориентированные на файл, возвращают svSourceUnavailable. Решением является перегрузка с TStream, которая позволяет передать оригинальные байты из любого места их хранения: из потока, буфера памяти или объекта базы данных

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Документ загружен из потока: компонент не хранит имя исходного
  // файла, поэтому передайте оригинальные байты самостоятельно.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Вывод информации о том, что невозможно проверить

Верификатор, знающий только статусы «верен» и «неверен», будет давать некорректные отчеты по документам, логику которых он просто не поддерживает. Поэтому перечисление статусов разделяет ситуации, которые интерфейс должен различать. svDigestMismatch означает изменение байтов документа после подписания — классический признак подделки. Status svSignatureInvalid указывает, что байты хэшируются корректно, но проверка RSA завершилась сбоем, что говорит о повреждении или фальсификации значения подписи. Статус svUnsupportedAlgorithm — честный ответ для ключей ECDSA и неподдерживаемых алгоритмов хэширования: подпись может быть корректной, но HotPDF не может её проверить, и выдача вердикта «неверна» была бы ошибкой. Статус svMalformed указывает на невозможность разбора самого контейнера CMS. Для быстрой проверки метод VerifyAllLoadedSignatures возвращает true, только если в документе есть хотя бы одно поле подписи и все они имеют статус svValid — удобный единый флаг для конвейера импорта архивов

Проверка подписей, подписание PAdES, шифрование AES-256 и API редактирования загруженных документов поставляются в составе единой нативной библиотеки VCL для Delphi и C++Builder без внешних DLL-зависимостей. Полный список возможностей и поддерживаемых версий IDE представлен на странице продукта HotPDF Component