Przejdź do głównej zawartości

Aktualizacje przyrostowe i dlaczego są ważne

Evidence: Standard-backed

Gdy plik PDF zmienia się po zapisaniu, bezpieczny sposób zapisu nie polega na przepisaniu całego pliku. Zamiast tego dołącza się zmienione obiekty i nową sekcję odsyłaczy na końcu, pozostawiając każdy oryginalny bajt dokładnie tam, gdzie był. Ta strona wyjaśnia, jak działa ten mechanizm i dlaczego dzięki niemu podpis cyfrowy może przetrwać późniejszą edycję.

Podpis chroni zakres bajtów. Gdyby zapisanie jednowyrazowej zmiany przepisywało plik, przesunąłby się każdy offset bajtowy. Podpisany zakres nie odnosiłby się już do tej samej treści. Podpis przestałby być prawidłowy, mimo że sama podpisana treść pozostałaby nietknięta.

Aktualizacje przyrostowe istnieją właśnie po to, aby tak się nie działo. Oryginalne bajty, w tym bajty objęte podpisem, pozostają na miejscu. Weryfikator może sprawdzić pierwszy podpis w dokumencie, który po podpisaniu został edytowany, względem oryginalnej wersji. Widzi dokładnie to, co zostało podpisane, oraz – osobno – to, co zmieniło się później. Pomyłka w tym miejscu oznacza albo unieważnienie prawidłowych podpisów, albo – co gorsza – utratę możliwości udowodnienia, co faktycznie poświadczał dany podpis.

  • Aktualizacja przyrostowa dołącza nowe i zmienione obiekty, następnie nową sekcję odsyłaczy, a potem nowy zwiastun (trailer) – wszystko na końcu pliku.
  • Oryginalna zawartość pliku pozostaje nienaruszona – nie jest edytowana w miejscu.
  • Nowy zwiastun zawiera wpis /Prev: offset bajtowy poprzedniej sekcji odsyłaczy. Sekcje tworzą łańcuch wsteczny.
  • Czytnik buduje swój indeks, przechodząc ten łańcuch od najnowszej sekcji. Dla każdego numeru obiektu wygrywa najnowszy wpis.
  • Ponieważ nic nie zostało nadpisane, zakres bajtów objęty wcześniejszym podpisem nadal jest identyczny bajt po bajcie – dzięki temu podpis wciąż przechodzi weryfikację, a dokument można odtworzyć dokładnie w postaci, w jakiej został podpisany.

NextPDF zapisuje dokument bazowy w sposób opisany na poprzedniej stronie, a następnie udostępnia trzy elementy potrzebne do przygotowania aktualizacji przyrostowej.

Po wywołaniu build() moduł zapisujący (src/Writer/PdfWriter.php) zachowuje:

  • bufor wyjściowy, udostępniany przez getBuffer(), aby aktualizację można było dołączyć dokładnie na końcu istniejących bajtów;
  • offset bajtowy ostatniej sekcji odsyłaczy, udostępniany przez getLastXrefOffset(), który staje się wartością /Prev nowej sekcji;
  • wpisy słownika katalogu, udostępniane przez getCatalogEntries(), aby aktualizacja, która musi ponownie wyemitować katalog (na przykład w celu dołączenia odwołania do podpisu), nie utraciła żadnego z wcześniejszych kluczy.

Dołączana wersja przydziela nowe numery obiektów (lub ponownie używa istniejących dla obiektów, które zastępuje) w ramach tego samego ObjectRegistry, dzięki czemu numeracja obiektów pozostaje spójna między wersjami. Nowa sekcja odsyłaczy wymienia tylko obiekty, których dotyczy ta wersja. Nowy zwiastun powtarza wpisy poprzedniego zwiastuna i dodaje /Prev, wskazujący wstecz na wcześniejszą sekcję. To właśnie ten łańcuch przechodzi czytnik.

Najlepiej widać to przy podpisywaniu. Klasa NextPDF ByteRangeCalculator (src/Security/Signature/ByteRangeCalculator.php) oblicza tablicę /ByteRange jako dwa segmenty: wszystko przed wartością podpisu oraz wszystko po niej – dzięki czemu podpis obejmuje całą wersję z wyjątkiem własnych bajtów. Ponieważ późniejsza edycja jest dołączana, a nie zapisywana na tych bajtach, ten zakres nigdy się nie przesuwa.

  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.
Jak podpisany plik PDF, który jest później edytowany, pozostaje weryfikowalny: każdy zapis dołącza dane, dzięki czemu pierwotnie podpisane bajty nigdy nie są nadpisywane, a łańcuch zwiastunów zapisuje historię.

Zasada wyłącznego dołączania ma charakter normatywny. Spec: ISO 32000-2, §7.5.6 stanowi, że zawartość pliku PDF można aktualizować przyrostowo bez przepisywania całego pliku oraz że w ramach takiej operacji zmiany muszą być dołączane na końcu pliku, pozostawiając oryginalną zawartość nienaruszoną. Evidence: Standard-backed

Ta sama klauzula definiuje mechanizm działania. Sekcja odsyłaczy dla aktualizacji przyrostowej zawiera wpisy wyłącznie dla obiektów, które zostały zmienione, zastąpione lub usunięte. Usunięte obiekty pozostają w pliku, ale są oznaczone jako usunięte przez odpowiadające im wpisy odsyłaczy. Dodany zwiastun musi zawierać wpis /Prev podający położenie poprzedniej sekcji odsyłaczy. Wpis aktualizacji dla zmienionego obiektu zawiera offset bajtowy nowej kopii zamiast starego offsetu. Czytnik buduje swoje informacje o odsyłaczach tak, aby udostępnić najnowszą kopię każdego obiektu.

Konsekwencję dla podpisu wskazuje wprost Spec: ISO 32000-2, §12.8.1 : skrót zakresu bajtów jest obliczany dla zakresu pliku – zwykle całego pliku, z wyłączeniem wartości podpisu (wpisu /Contents). Następnie norma wskazuje, że jeśli podpisany dokument zostaje zmodyfikowany i zapisany z użyciem aktualizacji przyrostowej, dane odpowiadające zakresowi bajtów oryginalnego podpisu zostają zachowane, więc jeśli podpis jest prawidłowy, stan dokumentu z chwili podpisania można odtworzyć. Wyłączne dołączanie nie jest udogodnieniem dodatkowym. To właśnie na tej własności opiera się model podpisu.

Plik PDF najpierw podpisany, a następnie edytowany, wygląda strukturalnie tak. Oryginalna wersja kończy się własnym znacznikiem %%EOF. Druga wersja jest dołączana poniżej.

%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

Walidator czyta ostatni zwiastun, widzi /Prev 920 i ma dostęp do całego łańcucha. Może zweryfikować podpis względem niezmienionych bajtów aż do pierwszego %%EOF. Może następnie osobno zgłosić, że wersja 2 dodała adnotację. Historia pozostaje w pliku. Nic nie zostało ukryte przez nadpisanie.

Pułapką jest przekonanie, że „aktualizacja przyrostowa” oznacza zmianę niewielką, a więc nieszkodliwą. W dołączaniu chodzi o zachowanie bajtów, a nie o rozmiar. Aktualizacja przyrostowa może dodać bardzo dużo treści. O jej przyrostowym charakterze decyduje to, że nie narusza bajtów, które już tam były. Zaskakujący bywa też wniosek: narzędzie, które „optymalizuje” lub „linearyzuje” podpisany plik PDF, przepisując go od nowa, wytworzy mniejszy, czystszy plik oraz uszkodzony podpis, ponieważ podpisany zakres bajtów już nie istnieje. Zapis przyrostowy podpisanego pliku PDF i zapisanie go od nowa to nie ta sama operacja.

Wyłączne dołączanie chroni bajty. Samo w sobie nie mówi, czy dołączone zmiany były autoryzowane. Druga wersja może w sposób uprawniony dodać drugi podpis albo może dodać treść, której pierwszy podpisujący nigdy nie zamierzał. Rozstrzygnięcie, która sytuacja zachodzi, jest zadaniem walidacji podpisu i polityki wykrywania modyfikacji (DocMDP). Dołączanie jest podstawą, która czyni tę analizę możliwą, a nie samą analizą.

Ta strona nie omawia również, w jaki sposób obliczane i łączone są dwa zakresy bajtów podpisu ani co sprawdza pełna walidacja. To odrębne tematy. Gwarancja dotyczy tu plików zapisanych i zaktualizowanych przez zgodny ze specyfikacją moduł zapisujący: plik, którego wcześniejsze wersje były już zniekształcone, nie staje się poprawny tylko dlatego, że coś do niego dołączono.

Skąd wiadomo, ile wersji ma plik PDF? Policz znaczniki %%EOF i prześledź łańcuch /Prev od ostatniego zwiastuna. Każda osiągnięta sekcja odsyłaczy to jedna zapisana wersja.

Czy usunięcie obiektu usuwa go z pliku? Nie. Aktualizacja przyrostowa oznacza obiekt jako usunięty w jego wpisie odsyłaczy, ale bajty obiektu pozostają we wcześniejszych wersjach. „Usunięty” oznacza „nieodwoływany przez bieżącą wersję”, a nie „wymazany”.

Czy aktualizacja przyrostowa może zmienić wersję PDF? Tak, przez ustawienie wpisu /Version w katalogu w dołączonej wersji. Nagłówek pozostaje w postaci, w jakiej został zapisany. Wpis /Version w katalogu ma pierwszeństwo, jeśli wskazuje późniejszą wersję.

  • Aktualizacja przyrostowa – zapisanie zmiany przez dołączenie zmienionych obiektów, nowej sekcji odsyłaczy i nowego zwiastuna na końcu pliku, bez zmiany istniejących bajtów.
  • /Prev – wpis zwiastuna (lub strumienia odsyłaczy) przechowujący offset bajtowy poprzedniej sekcji odsyłaczy. Łączy wersje w łańcuch wsteczny.
  • Wersja – stan pliku opisany przez jedną sekcję odsyłaczy i jej zwiastun. Plik z N sekcjami odsyłaczy ma N wersji.
  • /ByteRange – tablica w słowniku podpisu podająca dwa segmenty bajtów objęte skrótem podpisu (wszystko z wyjątkiem samej wartości podpisu).
  • Podpisany zakres bajtów – dokładne bajty, dla których obliczono skrót podpisu. Aktualizacje przyrostowe istnieją po to, aby te bajty nigdy nie były przenoszone ani nadpisywane.