Инкрементные обновления и зачем они нужны
ISO 32000-2 §7.5.6 Evidence: Standard-backed
Когда PDF изменяется после записи, безопасный способ сохранить его — не перезаписывать файл. Вместо этого вы добавляете изменённые объекты и новый раздел перекрёстных ссылок в конец, оставляя каждый исходный байт на прежнем месте. На этой странице объясняется, как это работает и почему благодаря этому цифровая подпись может пережить последующее редактирование.
Почему это важно
Заголовок раздела «Почему это важно»Подпись защищает диапазон байтов. Если бы при сохранении изменения в одно слово файл перезаписывался, смещение каждого байта изменилось бы. Подписанный диапазон больше не описывал бы то же самое содержимое. Подпись стала бы недействительной, хотя само подписанное содержимое осталось нетронутым.
Инкрементные обновления нужны, чтобы этого не происходило. Исходные байты, в том числе байты, которые охватывает подпись, остаются на месте. Проверяющий может взять документ, который сначала подписали, а затем отредактировали, и проверить первую подпись по исходной редакции. Он видит ровно то, что было подписано, и отдельно — что изменилось позже. Ошибётесь здесь — и вы либо сделаете действительные подписи недействительными, либо, что хуже, потеряете возможность доказать, что именно подпись на самом деле удостоверяла.
Если коротко
Заголовок раздела «Если коротко»- Инкрементное обновление добавляет: новые и изменённые объекты, затем новый раздел перекрёстных ссылок, затем новый трейлер — всё в конец файла.
- Исходное содержимое файла остаётся нетронутым — оно не изменяется на месте.
- Новый трейлер несёт запись
/Prev: смещение в байтах предыдущего раздела перекрёстных ссылок. Разделы образуют обратную цепочку. - Программа чтения строит свой индекс, проходя по этой цепочке от самой новой записи к старым. Для любого номера объекта побеждает самая последняя запись.
- Поскольку ничего не было перезаписано, диапазон байтов, который охватывала более ранняя подпись, по-прежнему совпадает байт в байт — поэтому подпись всё ещё проходит проверку, и вы можете восстановить документ ровно в том виде, в каком он был подписан.
Как это реализовано в NextPDF
Заголовок раздела «Как это реализовано в NextPDF»NextPDF записывает базовый документ так, как описано на предыдущей странице, а затем предоставляет три вещи, нужные для инкрементного обновления.
После build() модуль записи (src/Writer/PdfWriter.php) сохраняет:
- буфер вывода, доступный через
getBuffer(), чтобы обновление можно было добавить точно в конец существующих байтов; - смещение в байтах последнего раздела перекрёстных ссылок, доступное через
getLastXrefOffset(); оно становится значением/Prevнового раздела; - записи словаря каталога, доступные через
getCatalogEntries(), чтобы обновление, которому нужно заново записать каталог (например, чтобы прикрепить ссылку на подпись), не потеряло ни одного прежнего ключа.
Добавленная редакция выделяет новые номера объектов (или переиспользует существующие для объектов, которые заменяет) в рамках того же ObjectRegistry, поэтому нумерация объектов остаётся согласованной между редакциями. Новый раздел перекрёстных ссылок перечисляет только объекты, затронутые этой редакцией. Новый трейлер повторяет записи предыдущего трейлера и добавляет /Prev, который указывает назад на предыдущий раздел. Именно по этой цепочке следует программа чтения.
Нагляднее всего это видно при подписании. Компонент ByteRangeCalculator (src/Security/Signature/ByteRangeCalculator.php) в NextPDF вычисляет массив /ByteRange как два сегмента: всё, что идёт до значения подписи, и всё, что идёт после него, — так что подпись охватывает всю редакцию, кроме собственных байтов. Поскольку последующее редактирование добавляется, а не записывается поверх этих байтов, этот диапазон никогда не смещается.
- Write base revision Header, body, xref section, trailer — the original bytes.
- Sign A /ByteRange digest covers the whole revision except the signature value itself.
- Edit and save Changed objects + a new xref section are appended; originals are untouched.
- New trailer chains back The appended trailer carries /Prev = offset of the previous xref section.
- Verify The first signature still covers the same unchanged bytes; the chain shows what came after.
Что говорят источники
Заголовок раздела «Что говорят источники»Правило «только добавление» — нормативное. Spec: ISO 32000-2, §7.5.6 ISO 32000-2 §7.5.6 устанавливает, что содержимое PDF может обновляться инкрементно без перезаписи всего файла и что при этом изменения должны добавляться в конец файла, оставляя исходное содержимое нетронутым. Evidence: Standard-backed
Тот же пункт описывает механику процесса. Раздел перекрёстных ссылок для инкрементного обновления содержит записи только для объектов, которые были изменены, заменены или удалены. Удалённые объекты остаются в файле, но помечаются как удалённые в своих записях перекрёстных ссылок. Добавленный трейлер должен содержать запись /Prev, указывающую расположение предыдущего раздела перекрёстных ссылок. Запись обновления для изменённого объекта несёт смещение в байтах новой копии, переопределяя старое смещение. Программа чтения строит свои данные перекрёстных ссылок так, чтобы обращаться к самой последней копии каждого объекта.
Следствие для подписи прямо изложено в
Spec: ISO 32000-2, §12.8.1 ISO 32000-2 §12.8.1 : дайджест диапазона байтов
вычисляется по диапазону файла — обычно по всему файлу, исключая
значение подписи (запись /Contents). Далее стандарт отмечает, что
если подписанный документ изменяется и сохраняется через инкрементное обновление, данные,
соответствующие диапазону байтов исходной подписи, сохраняются, поэтому, если
подпись действительна, состояние документа на момент подписания может быть
воссоздано. Принцип «только добавление» — не приятная мелочь, а свойство, от которого зависит модель
подписи.
Практический пример
Заголовок раздела «Практический пример»Так выглядит структура PDF, который был подписан, а затем отредактирован. Исходная редакция завершается собственным %%EOF. Вторая редакция добавляется под ней.
%PDF-2.0... original objects, including the signature dictionary ...xref0 8... entries for the original revision ...trailer<< /Size 8 /Root 1 0 R >>startxref920%%EOF <-- end of revision 1: the signed bytes stop here9 0 obj <-- revision 2, appended<< /Type /Annot /Subtype /Text /Contents (added after signing) >>endobjxref0 19 0 obj-entry...8 90000001740 00000 ntrailer<< /Size 10 /Root 1 0 R /Prev 920 >>startxref1980%%EOFПрограмма проверки читает последний трейлер, видит /Prev 920 и получает всю цепочку. Она может проверить подпись по байтам вплоть до первого %%EOF, которые не изменились. Затем она может отдельно сообщить, что редакция 2 добавила аннотацию. История хранится в самом файле. Ничто не скрыто за счёт перезаписи.
Распространённое заблуждение
Заголовок раздела «Распространённое заблуждение»Ловушка — мысль: «инкрементное обновление означает, что изменение небольшое, а значит, безвредное». Добавление — это про сохранение байтов, а не про размер. Инкрементное обновление может добавить очень много содержимого. Инкрементным обновлением его делает то, что оно не трогает байты, которые уже были на месте. Следствие тоже сбивает людей с толку: инструмент, который «оптимизирует» или «линеаризует» подписанный PDF, переписывая его с нуля, выдаст файл поменьше и почище и сломанную подпись, потому что подписанного диапазона байтов больше не существует. Сохранить подписанный PDF и пересохранить его — не одна и та же операция.
Ограничения и границы применимости
Заголовок раздела «Ограничения и границы применимости»Принцип «только добавление» защищает байты. Сам по себе он не говорит вам, были ли добавленные изменения авторизованы. Вторая редакция может законно добавить вторую подпись, а может добавить содержимое, которого первый подписавший вовсе не имел в виду. Определять, какой случай имеет место, — задача проверки подписи и политики обнаружения изменений (DocMDP). Добавление — это основа, которая делает такой анализ возможным, а не сам анализ.
Эта страница также не раскрывает, как вычисляются и сшиваются два диапазона байтов подписи и что проверяет полная проверка. Это отдельные темы. Гарантия здесь касается файлов, записанных и обновлённых модулем записи, который соответствует стандарту: файл, более ранние редакции которого уже были некорректными, не становится корректным от того, что к нему что-то добавили.
Мини-FAQ
Заголовок раздела «Мини-FAQ»Как узнать, сколько редакций у PDF? Подсчитайте маркеры %%EOF и пройдите по цепочке /Prev от последнего трейлера. Каждый достигнутый раздел перекрёстных ссылок — это одна сохранённая редакция.
Удаляет ли удаление объекта его из файла? Нет. Инкрементное обновление помечает объект удалённым в его записи перекрёстных ссылок, но байты объекта остаются в более ранних редакциях. «Удалён» означает «не упоминается в текущей редакции», а не «стёрт».
Может ли инкрементное обновление изменить версию PDF? Да, путём установки записи /Version в каталоге в добавленной редакции. Заголовок остаётся таким, каким был записан. Запись /Version в каталоге имеет приоритет, когда указывает более позднюю версию.
Связанная документация
Заголовок раздела «Связанная документация»- Что на самом деле представляет собой PDF — объектная модель и единственный раздел перекрёстных ссылок, который расширяет инкрементное обновление.
- Как подписи располагаются в PDF — механизм диапазона байтов, ради защиты которого существуют инкрементные обновления.
- Как правильно проверять подпись — что проверяет корректная проверка по всей истории редакций файла.
Глоссарий
Заголовок раздела «Глоссарий»- Инкрементное обновление — сохранение изменения путём добавления изменённых объектов, нового раздела перекрёстных ссылок и нового трейлера в конец файла без изменения существующих байтов.
/Prev— запись трейлера (или потока перекрёстных ссылок), содержащая смещение в байтах предыдущего раздела перекрёстных ссылок. Она связывает редакции в обратную цепочку.- Редакция — состояние файла, зафиксированное одним разделом перекрёстных ссылок и его трейлером. Файл с N разделами перекрёстных ссылок имеет N редакций.
/ByteRange— массив в словаре подписи, задающий два сегмента байтов, которые охватывает дайджест подписи (всё, кроме самого значения подписи).- Подписанный диапазон байтов — точные байты, по которым был вычислен дайджест подписи. Инкрементные обновления существуют для того, чтобы эти байты никогда не перемещались и не перезаписывались.