Подпись над PDF не запрещает последующие изменения. Она фиксирует диапазон байтов, а инкрементальное обновление дописывает новые байты после него, поэтому подпись остаётся математически верной, пока документ обрастает новым содержимым. Приемлемо ли это содержимое — вопрос политики, и DocMDP — то место, где автор её заявляет: никаких изменений вовсе, только заполнение форм и подписание или всё это плюс аннотации. Принудительное применение означает классификацию того, что действительно изменилось, — этим и занимается AnalyzeModifications. Направьте её на более раннюю ревизию, затем прочтите GetModificationLevel для общего вердикта, а методы доступа по находкам — для уровня, номера объекта и описания каждой разницы
Когда это на месте, применение DocMDP сжимается до одного сравнения: вычисленный уровень не выше уровня, который разрешает политика
Почему подписанный PDF и должен меняться
Три легитимных случая, и они покрывают большую часть того, что вы увидите. Второй подписант добавляет свою подпись. Получатель заполняет поля форм, оставленные автором открытыми. И дописывается материал долгосрочной проверки: ответы OCSP и CRL записываются в хранилище безопасности документа, чтобы подпись оставалась проверяемой после исчезновения ответчиков. Последний случай не просто разрешён — так управляемый архив намеренно поступает с подписанными документами
Поэтому «файл вырос после подписания» не несёт информации. Вопрос всегда в том, что добавлено, а ответ должен приходить из сравнения состояний документа, а не из наблюдения за байтами. Механика дозаписи сама по себе рассмотрена в статье об инкрементальном обновлении
Классифицируйте по форме объекта, а не по пути, который его создал
Классификатор смотрит, чем объект является после изменения, а не какой вызов библиотеки его создал. Это сознательно: анализ работает с файлами, порождёнными чужим программным обеспечением, где нет пути вызовов, который можно инспектировать
Распознаются четыре формы. Словари информации хранилища безопасности документа и связанной с проверкой, объекты потоков перекрёстных ссылок, запись metadata каталога и словари подписей с диапазоном байтов — материал долгосрочного архива. Объект, несущий одновременно тип поля и значение поля, — заполнение форм. Объект с типом annotation или с подтипом из перечисленных в таблице 168 ISO 32000-2 — изменение аннотаций. Всё остальное — неклассифицировано
Удаления трактуются строже добавлений. Удалённый объект попадает в белый список, только если объект на старой стороне сам был архивным материалом, что покрывает обычный случай замены хранилища безопасности более новым. Всякое иное удаление — неклассифицировано, ведь удаление содержимого из подписанного документа не входит в то, что разрешает уровень прав. Различия уровня документа строже ещё: изменение числа страниц сразу уходит в неклассифицированные без осмотра отдельных объектов, поскольку ни один уровень 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. Это взвешенный выбор. Структурному сравнению нужна детерминированность: одно и то же тело объекта должно всегда давать один и тот же отпечаток в пределах прогона. Устойчивость к коллизиям не нужна, ведь атакующий, контролирующий обе стороны сравнения, уже победил другими средствами, а платить полный криптографический хеш за каждый объект в документе на миллион объектов — реальная цена без выгоды
Два правила нормализации важнее выбора хеша. Косвенные ссылки сворачиваются в токен-заполнитель вместо разворачивания в содержимое по ссылке: разворачивание скопировало бы тело разделяемого объекта каждому ссылающемуся, поэтому одна мелкая правка общего дескриптора шрифта обесценила бы отпечаток каждого объекта, который до него добирается, а отчёт стал бы нечитаемым. И сами номера объектов исключены из отпечатка, ведь перезапись может перенумеровать объекты, не меняя ничего семантически
Затем сопоставление идёт в два прохода: сначала выравнивание по отпечаткам, остаток — спаривание по номерам объектов, чтобы опознать изменения, а не добавление плюс удаление. Дешёвые проверки всюду идут первыми: разница в числе страниц сообщается до того, как начнётся какой-либо обход объектов
Ловушка: сравнение с самим собой не гарантированно даёт совпадение
Естественный первый тест для движка сравнения — сравнить файл с самим собой и потребовать идентичного результата. Здесь это утверждение не выполняется, и причина поучительна. Публичный путь загрузки и низкоуровневый путь загрузки документа настраивают декодирование по-разному, поэтому один и тот же файл, загруженный двумя маршрутами, может дать различающиеся отпечатки для некоторых объектов. Движок не ошибается: две загрузки действительно породили разные состояния в памяти
Вместо того чтобы силой сводить два пути, семантика сравнения заявлена узко: анализ сопоставляет текущее состояние документа с более ранней ревизией и сообщает о совпадении, только если два набора отпечатков совпадают точно. Именно этот вопрос пользователи и задают, и он не требует взаимозаменяемости двух загрузчиков. Проектируя возможность сравнения, определить, что значит «одинаково», — большая часть работы, чем вычислить это
Где это применять
В двух местах. В отчёте проверки рядом с проверкой подписи, чтобы рецензент видел не только криптографическую целостность подписи, но и что произошло с документом после; сторона подписи рассмотрена в материале о подписании и проверке PAdES. И во входном шлюзе, где прибывающий извне документ сверяется с отправленной вами копией, чтобы возвращённый контракт с добавленной аннотацией трактовался иначе, чем контракт с отредактированной страницей
Одно предостережение о границах. Этот анализ сообщает, что изменилось между двумя ревизиями одной линии документа. Он не сообщит, вводит ли видимое содержимое в заблуждение, соответствует ли поток внешнего вида поля формы его значению или присутствует ли ещё в потоке содержимого текст, спрятанный под наложением. Это требует отдельной работы, и сторона удаления содержимого рассмотрена в статье о настоящем редактировании. Точки входа анализа и сравнения задокументированы на странице продукта losLab PDF Developer Library