コンテンツにスキップ

増分更新とその重要性

Evidence: Standard-backed

PDF が書き込まれた後に変更された場合、適切な保存方法はファイルを書き換えないことです。代わりに、変更されたオブジェクトと新しいクロスリファレンスセクションを末尾に追記し、元のバイトをすべて元の位置に残します。このページでは、その仕組みと、それがデジタル署名を後からの編集に耐えさせる唯一の理由であることを説明します。

署名はバイトの範囲を保護します。一語の変更を保存するためにファイルを書き換えると、すべてのバイトオフセットが移動してしまいます。署名された範囲は、もはや同じ内容を表さなくなります。署名された内容そのものは手つかずであっても、署名は壊れてしまいます。

増分更新は、そうした事態を防ぐために存在します。署名が対象とするバイトを含め、元のバイトはそのまま動きません。レビュー担当者は、署名後に編集された文書を受け取り、最初の署名を元の改訂に照らして検証できます。レビュー担当者は、何が署名されたかを正確に把握し、それとは別に、その後の変更内容も確認できます。この扱いを誤ると、正当な署名を無効にしてしまうか、さらに悪い場合には、署名が実際に何を証明していたのかを示す能力を失ってしまいます。

  • 増分更新は追記します。新規および変更されたオブジェクト、新しいクロスリファレンスセクション、新しいトレーラーを、この順にすべてファイルの末尾に追記します。
  • 元のファイル内容はそのまま残され、その場で編集されることはありません。
  • 新しいトレーラーは**/Prev**エントリを持ちます。これは前のクロスリファレンスセクションのバイトオフセットです。これらのセクションは後方に連鎖します。
  • リーダーは、新しいものから順にその連鎖をたどってインデックスを構築します。どのオブジェクト番号についても、最新のエントリが優先されます。
  • 何も上書きされていないため、以前の署名が対象としていたバイト範囲は引き続きバイト単位で同一です。したがって署名は引き続き検証でき、署名された時点とまったく同じ状態で文書を復元できます。

NextPDF は、前のページで説明したとおりにベース文書を書き込み、そのうえで、増分更新に必要な 3 つのものを公開します。

build()の後、ライター(src/Writer/PdfWriter.php)は次のものを保持します。

  • 出力バッファ。getBuffer()で取得でき、既存バイトの正確な末尾に更新を追記できます。
  • 最後のクロスリファレンスセクションのバイトオフセット。getLastXrefOffset()で取得でき、これが新しいセクションの/Prev値になります。
  • カタログ辞書のエントリ。getCatalogEntries()で取得でき、カタログを再出力しなければならない更新(たとえば署名参照を付加する場合)でも、以前のキーを失いません。

追記された改訂は、同じObjectRegistryに対して新しいオブジェクト番号を割り当てます(または置き換えるオブジェクトについては既存の番号を再利用します)。これにより、改訂をまたいでオブジェクト番号付けの一貫性が保たれます。新しいクロスリファレンスセクションは、この改訂が触れたオブジェクトのみを列挙します。新しいトレーラーは、前のトレーラーのエントリを繰り返し、前のセクションを指す/Prevを追加します。リーダーがたどるのはこの連鎖です。

この仕組みが最も明確に効いてくるのは署名の場面です。NextPDF のByteRangeCalculatorsrc/Security/Signature/ByteRangeCalculator.php)は、/ByteRange配列を 2 つのセグメントとして計算します。署名値より前のすべてと、署名値より後のすべてです。これにより署名は、自身のバイトを除く改訂全体を対象とします。後からの編集は、それらのバイトを上書きせずに追記されるため、その範囲は決して移動しません。

  1. Write base revision Header, body, xref section, trailer — the original bytes.
  2. Sign A /ByteRange digest covers the whole revision except the signature value itself.
  3. Edit and save Changed objects + a new xref section are appended; originals are untouched.
  4. New trailer chains back The appended trailer carries /Prev = offset of the previous xref section.
  5. Verify The first signature still covers the same unchanged bytes; the chain shows what came after.
後から編集された署名済み PDF が検証可能であり続ける仕組み。各保存が追記されるため、元々署名されたバイトが上書きされることはなく、トレーラーの連鎖が履歴を記録します。

追記のみという規則は規定要件です。 Spec: ISO 32000-2, §7.5.6 は、PDF の内容をファイル全体を書き換えることなく増分的に更新できること、そしてその際、変更はファイルの末尾に追記されなければならず、元の内容はそのまま残されることを定めています。 Evidence: Standard-backed

同じ箇条は、その仕組みも定義しています。増分更新のクロスリファレンスセクションには、変更、置換、または削除されたオブジェクトのエントリのみが含まれます。削除されたオブジェクトはファイル内に残されますが、クロスリファレンスエントリを通じて削除済みとしてマークされます。追記されるトレーラーには、前のクロスリファレンスセクションの位置を示す/Prevエントリが含まれなければなりません。変更されたオブジェクトの更新エントリは、新しいコピーのバイトオフセットを保持し、古いオフセットを上書きします。リーダーは、各オブジェクトの最新コピーにアクセスできるようにクロスリファレンス情報を構築します。

署名に関する帰結は、次の箇条で直接述べられています Spec: ISO 32000-2, §12.8.1 。バイト範囲のダイジェストは、通常はファイル全体を対象に、ファイル内の一定範囲にわたって計算されます。ただし、署名値(/Contentsエントリ)は除外されます。続いて規格は、署名済みの文書が変更され、増分更新によって保存された場合、元の署名のバイト範囲に対応するデータは保持されると述べています。したがって、署名が有効であれば、署名時点の文書の状態を再現できます。追記のみという方式は、あれば望ましいといった程度のものではありません。それは、署名モデルそのものが依拠する性質です。

署名後に編集された PDF を、構造面から見たものです。元の改訂は、それ自身の%%EOFで終わります。2 番目の改訂はその下に追記されます。

%PDF-2.0
... original objects, including the signature dictionary ...
xref
0 8
... entries for the original revision ...
trailer
<< /Size 8 /Root 1 0 R >>
startxref
920
%%EOF
<-- end of revision 1: the signed bytes stop here
9 0 obj <-- revision 2, appended
<< /Type /Annot /Subtype /Text /Contents (added after signing) >>
endobj
xref
0 1
9 0 obj-entry...
8 9
0000001740 00000 n
trailer
<< /Size 10 /Root 1 0 R /Prev 920 >>
startxref
1980
%%EOF

バリデーターは最後のトレーラーを読み、/Prev 920を確認することで、連鎖全体を把握します。最初の%%EOFまでのバイト(変更されていない部分)に照らして署名を検証できます。その後、改訂 2 が注釈を追加したことを別途報告できます。履歴はファイル内にあります。上書きによって隠されたものは何もありません。

よくある落とし穴は、「増分更新は変更が小さいことを意味するので、無害だ」という考え方です。追記が関わるのはバイトの保存であって、サイズではありません。増分更新は、大量の内容を追加することもできます。それを増分更新にしているのは、すでにそこにあったバイトに触れないという点です。その帰結もまた誤解されがちです。署名済み PDF を一から書き換えて「最適化」または「線形化」するツールは、より小さく整ったファイルを生成しますが、同時に壊れた署名も生み出します。署名されたバイト範囲がもはや存在しなくなるためです。署名済み PDF を保存することと、それを保存し直すことは、同じ操作ではありません。

追記のみという方式はバイトを保護します。それ自体は、追記された変更が許可されたものかどうかを教えてくれるわけではありません。2 番目の改訂は、正当に 2 番目の署名を追加することもできれば、最初の署名者が意図しなかった内容を追加することもできます。どちらなのかを判断するのは、署名検証と変更検出ポリシー(DocMDP)の役割です。追記は、その分析を可能にする基盤であって、分析そのものではありません。

このページは、署名の 2 つのバイト範囲がどのように計算され、つなぎ合わされるか、また完全な検証が何を確認するかも扱いません。これらは別のトピックです。そしてここでの保証は、準拠したライターによって書き込まれ、更新されるファイルに関するものです。以前の改訂がすでに不正な形式であったファイルは、追記されたからといって正しい形式になるわけではありません。

PDF の改訂が何回あるかはどうやって分かりますか。 %%EOFマーカーを数え、最後のトレーラーから/Prevの連鎖をたどります。到達した各クロスリファレンスセクションが、保存された 1 つの改訂です。

オブジェクトを削除すると、ファイルから取り除かれるのですか。 いいえ。増分更新はクロスリファレンスエントリでオブジェクトを削除済みとしてマークしますが、そのオブジェクトのバイトは以前の改訂に残ります。「削除済み」とは「現在の改訂から参照されていない」という意味であって、「消去された」という意味ではありません。

増分更新で PDF バージョンを変更できますか。 はい、追記された改訂のカタログで/Versionエントリを設定することで可能です。ヘッダーは書き込まれた状態のままです。カタログの/Versionは、より新しいバージョンを指定している場合に優先されます。

  • 増分更新 — 既存のバイトを変更せずに、変更されたオブジェクト、新しいクロスリファレンスセクション、新しいトレーラーをファイルの末尾に追記して変更を保存すること。
  • /Prev — 前のクロスリファレンスセクションのバイトオフセットを保持する、トレーラー(またはクロスリファレンスストリーム)のエントリ。これは改訂を後方に連鎖させます。
  • 改訂 — 1 つのクロスリファレンスセクションとそのトレーラーによって表されるファイルの状態。N 個のクロスリファレンスセクションを持つファイルには、N 個の改訂があります。
  • /ByteRange — 署名辞書の中にある配列で、署名ダイジェストが対象とする 2 つのバイトセグメント(署名値そのものを除くすべて)を示すもの。
  • 署名されたバイト範囲 — 署名のダイジェストの計算対象になった正確なバイト。増分更新は、これらのバイトが決して移動したり上書きされたりしないようにするために存在します。