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

Классификация изменений в PDF после подписания

Подпись над PDF не запрещает последующие изменения. Она фиксирует диапазон байтов, а инкрементальное обновление дописывает новые байты после него, поэтому подпись остаётся математически верной, пока документ обрастает новым содержимым. Приемлемо ли это содержимое — вопрос политики, и DocMDP — то место, где автор её заявляет: никаких изменений вовсе, только заполнение форм и подписание или всё это плюс аннотации. Принудительное применение означает классификацию того, что действительно изменилось, — этим и занимается AnalyzeModifications. Направьте её на более раннюю ревизию, затем прочтите GetModificationLevel для общего вердикта, а методы доступа по находкам — для уровня, номера объекта и описания каждой разницы

Когда это на месте, применение DocMDP сжимается до одного сравнения: вычисленный уровень не выше уровня, который разрешает политика

Схема лестницы уровней модификаций PDFlibPas от mlNone до mlUnclassified, показывающая применение политики DocMDP как одно сравнение в Delphi
Лестница TPLModificationLevel идёт от mlNone до mlUnclassified, а применение DocMDP сводится к сравнению вычисленного уровня с политикой

Почему подписанный PDF и должен меняться

Три легитимных случая, и они покрывают большую часть того, что вы увидите. Второй подписант добавляет свою подпись. Получатель заполняет поля форм, оставленные автором открытыми. И дописывается материал долгосрочной проверки: ответы OCSP и CRL записываются в хранилище безопасности документа, чтобы подпись оставалась проверяемой после исчезновения ответчиков. Последний случай не просто разрешён — так управляемый архив намеренно поступает с подписанными документами

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

Классифицируйте по форме объекта, а не по пути, который его создал

Классификатор смотрит, чем объект является после изменения, а не какой вызов библиотеки его создал. Это сознательно: анализ работает с файлами, порождёнными чужим программным обеспечением, где нет пути вызовов, который можно инспектировать

Распознаются четыре формы. Словари информации хранилища безопасности документа и связанной с проверкой, объекты потоков перекрёстных ссылок, запись metadata каталога и словари подписей с диапазоном байтов — материал долгосрочного архива. Объект, несущий одновременно тип поля и значение поля, — заполнение форм. Объект с типом annotation или с подтипом из перечисленных в таблице 168 ISO 32000-2 — изменение аннотаций. Всё остальное — неклассифицировано

Дерево решений, которое PDFlibPas применяет к каждому изменённому объекту PDF, распределяя обновления по уровням: архив, заполнение форм, аннотации или неклассифицировано
Каждый изменённый объект классифицируется по тому, чем он является, — хранилище безопасности, поток xref, поле, аннотация, — а не по вызвавшему его вызову

Удаления трактуются строже добавлений. Удалённый объект попадает в белый список, только если объект на старой стороне сам был архивным материалом, что покрывает обычный случай замены хранилища безопасности более новым. Всякое иное удаление — неклассифицировано, ведь удаление содержимого из подписанного документа не входит в то, что разрешает уровень прав. Различия уровня документа строже ещё: изменение числа страниц сразу уходит в неклассифицированные без осмотра отдельных объектов, поскольку ни один уровень DocMDP не разрешает добавлять или убирать страницы

Белый список ошибается в сторону отказа

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

Отсюда практическое следствие, которое стоит предвидеть: файлы от необычных производителей иногда сообщают неклассифицированные изменения, которые при осмотре оказываются безобидными. Правильный ответ — смотреть на детали находки и номер объекта, а не расширять белый список, ведь белый список, разрастающийся, чтобы заглушить отдельные отчёты, перестаёт быть средством контроля безопасности

uses
  PDFlibrary, PDFlibCompare;

var
  Pdf: TPDFlib;
  I, Level: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('contract-countersigned.pdf', '');
    if Pdf.AnalyzeModifications('contract-as-signed.pdf', '') < 0 then
      raise Exception.Create('the earlier revision could not be loaded');

    // TPLModificationLevel упорядочен: mlNone, mlLTAUpdates, mlFormFilling,
    // mlAnnotations, mlUnclassified; геттер возвращает его порядковый номер
    Level := Pdf.GetModificationLevel;
    // Применение DocMDP теперь одно сравнение с политикой
    if Level > Ord(mlFormFilling) then
      for I := 0 to Pdf.GetModificationFindingCount - 1 do
        Report.Add(Format('object %d, level %d: %s',
          [Pdf.GetModificationFindingObjNum(I),
           Pdf.GetModificationFindingLevel(I),
           Pdf.GetModificationFindingDetail(I)]));
  finally
    Pdf.Free;
  end;
end;

Общий уровень — максимум по всем находкам, и это единственная защищаемая агрегация: документ с девяноста девятью архивными добавлениями и одной неклассифицированной правкой — документ с неклассифицированной правкой

Под капотом: отпечатки, а не криптографические хеши

Движок сравнения, который открывает CompareWith и на котором построен анализ модификаций, опознаёт объекты по отпечатку их нормализованного тела, используя некриптографический 64-битный хеш, а не SHA-256. Это взвешенный выбор. Структурному сравнению нужна детерминированность: одно и то же тело объекта должно всегда давать один и тот же отпечаток в пределах прогона. Устойчивость к коллизиям не нужна, ведь атакующий, контролирующий обе стороны сравнения, уже победил другими средствами, а платить полный криптографический хеш за каждый объект в документе на миллион объектов — реальная цена без выгоды

Два правила нормализации важнее выбора хеша. Косвенные ссылки сворачиваются в токен-заполнитель вместо разворачивания в содержимое по ссылке: разворачивание скопировало бы тело разделяемого объекта каждому ссылающемуся, поэтому одна мелкая правка общего дескриптора шрифта обесценила бы отпечаток каждого объекта, который до него добирается, а отчёт стал бы нечитаемым. И сами номера объектов исключены из отпечатка, ведь перезапись может перенумеровать объекты, не меняя ничего семантически

Затем сопоставление идёт в два прохода: сначала выравнивание по отпечаткам, остаток — спаривание по номерам объектов, чтобы опознать изменения, а не добавление плюс удаление. Дешёвые проверки всюду идут первыми: разница в числе страниц сообщается до того, как начнётся какой-либо обход объектов

Двухпроходное сравнение ревизий PDF в PDFlibPas: сначала проверка числа страниц, 64-битные отпечатки, выравнивание по отпечаткам, затем спаривание по номерам объектов
Движок сравнения снимает отпечатки нормализованных тел объектов, первым делом сообщает о разнице числа страниц, затем сопоставляет по отпечаткам и номерам объектов

Ловушка: сравнение с самим собой не гарантированно даёт совпадение

Естественный первый тест для движка сравнения — сравнить файл с самим собой и потребовать идентичного результата. Здесь это утверждение не выполняется, и причина поучительна. Публичный путь загрузки и низкоуровневый путь загрузки документа настраивают декодирование по-разному, поэтому один и тот же файл, загруженный двумя маршрутами, может дать различающиеся отпечатки для некоторых объектов. Движок не ошибается: две загрузки действительно породили разные состояния в памяти

Вместо того чтобы силой сводить два пути, семантика сравнения заявлена узко: анализ сопоставляет текущее состояние документа с более ранней ревизией и сообщает о совпадении, только если два набора отпечатков совпадают точно. Именно этот вопрос пользователи и задают, и он не требует взаимозаменяемости двух загрузчиков. Проектируя возможность сравнения, определить, что значит «одинаково», — большая часть работы, чем вычислить это

Где это применять

В двух местах. В отчёте проверки рядом с проверкой подписи, чтобы рецензент видел не только криптографическую целостность подписи, но и что произошло с документом после; сторона подписи рассмотрена в материале о подписании и проверке PAdES. И во входном шлюзе, где прибывающий извне документ сверяется с отправленной вами копией, чтобы возвращённый контракт с добавленной аннотацией трактовался иначе, чем контракт с отредактированной страницей

Одно предостережение о границах. Этот анализ сообщает, что изменилось между двумя ревизиями одной линии документа. Он не сообщит, вводит ли видимое содержимое в заблуждение, соответствует ли поток внешнего вида поля формы его значению или присутствует ли ещё в потоке содержимого текст, спрятанный под наложением. Это требует отдельной работы, и сторона удаления содержимого рассмотрена в статье о настоящем редактировании. Точки входа анализа и сравнения задокументированы на странице продукта losLab PDF Developer Library