Ga naar inhoud

Incrementele updates en waarom ze ertoe doen

Evidence: Standard-backed

Wanneer een PDF wordt gewijzigd nadat hij is geschreven, is de veilige manier om hem op te slaan niet om het bestand te herschrijven. In plaats daarvan voeg je de gewijzigde objecten en een nieuwe cross-reference-sectie aan het einde toe, zodat elke oorspronkelijke byte precies blijft staan waar hij stond. Deze pagina legt uit hoe dat werkt en waarom een digitale handtekening daardoor een latere bewerking kan overleven.

Een handtekening beschermt een bytebereik. Als bij het opslaan van een wijziging van één woord het bestand zou worden herschreven, zou elke byte-offset verschuiven. Het ondertekende bereik zou niet langer dezelfde inhoud beschrijven. De handtekening zou ongeldig worden, ook al bleef de ondertekende inhoud zelf onaangeroerd.

Incrementele updates zijn er om dit te voorkomen. De oorspronkelijke bytes, inclusief de bytes die een handtekening dekt, blijven op hun plaats. Een validator kan een ondertekend en daarna bewerkt document nemen en de eerste handtekening verifiëren tegen de oorspronkelijke revisie. De validator ziet precies wat er is ondertekend en, los daarvan, wat er daarna is gewijzigd. Als je dit verkeerd doet, maak je ofwel geldige handtekeningen ongeldig of, erger nog, verlies je de mogelijkheid om aan te tonen wat een handtekening daadwerkelijk bevestigde.

  • Een incrementele update voegt toe: nieuwe en gewijzigde objecten, daarna een nieuwe cross-reference-sectie, daarna een nieuwe trailer, alles aan het einde van het bestand.
  • De oorspronkelijke bestandsinhoud blijft intact — niet op zijn oorspronkelijke plek bewerkt.
  • De nieuwe trailer bevat een /Prev-vermelding: de byte-offset van de vorige cross-reference-sectie. De secties vormen een achterwaartse keten.
  • Een lezer bouwt zijn index op door die keten te doorlopen, te beginnen bij de nieuwste sectie. Voor elk objectnummer wint de meest recente vermelding.
  • Omdat er niets is overschreven, is het bytebereik dat een eerdere handtekening dekte nog steeds byte voor byte hetzelfde — dus de handtekening kan nog steeds worden geverifieerd en je kunt het document exact herstellen zoals het was ondertekend.

NextPDF schrijft het basisdocument zoals beschreven op de vorige pagina en stelt vervolgens de drie dingen beschikbaar die een incrementele update nodig heeft.

Na build() bewaart de writer (src/Writer/PdfWriter.php):

  • de uitvoerbuffer, opvraagbaar via getBuffer(), zodat een update precies aan het einde van de bestaande bytes kan worden toegevoegd;
  • de byte-offset van de laatste cross-reference-sectie, via getLastXrefOffset(), die de /Prev-waarde van de nieuwe sectie wordt;
  • de vermeldingen van de catalog-dictionary, via getCatalogEntries(), zodat een update die de catalog opnieuw moet uitschrijven (bijvoorbeeld om een handtekeningverwijzing toe te voegen) bestaande sleutels niet kwijtraakt.

Een toegevoegde revisie wijst nieuwe objectnummers toe (of hergebruikt bestaande voor objecten die zij vervangt) op basis van dezelfde ObjectRegistry, zodat de objectnummering over revisies heen consistent blijft. De nieuwe cross-reference-sectie vermeldt alleen de objecten die deze revisie heeft aangeraakt. De nieuwe trailer herhaalt de vermeldingen van de vorige trailer en voegt /Prev toe, dat terugwijst naar de voorgaande sectie. Dat is de keten die een lezer volgt.

Dit is het duidelijkst bij ondertekenen. De NextPDF-ByteRangeCalculator (src/Security/Signature/ByteRangeCalculator.php) berekent de /ByteRange-array als twee segmenten: alles vóór de handtekeningwaarde en alles erna — zodat de handtekening de hele revisie dekt behalve haar eigen bytes. Omdat een latere bewerking wordt toegevoegd en niet over die bytes heen wordt geschreven, verschuift dat bereik nooit.

  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.
Hoe een ondertekende PDF die later wordt bewerkt verifieerbaar blijft: elke opslag voegt toe, zodat de oorspronkelijk ondertekende bytes nooit worden overschreven en de trailer-keten de geschiedenis vastlegt.

De append-only-regel is normatief. Spec: ISO 32000-2, §7.5.6 bepaalt dat de inhoud van een PDF incrementeel kan worden bijgewerkt zonder het hele bestand te herschrijven, en dat wijzigingen daarbij shall worden toegevoegd aan het einde van het bestand, terwijl de oorspronkelijke inhoud intact blijft. Evidence: Standard-backed

Dezelfde clausule beschrijft hoe dit werkt. Een cross-reference-sectie voor een incrementele update bevat alleen vermeldingen voor objecten die zijn gewijzigd, vervangen of verwijderd. Verwijderde objecten blijven in het bestand staan, maar worden via hun cross-reference-vermeldingen als verwijderd gemarkeerd. De toegevoegde trailer shall een /Prev-vermelding bevatten die de locatie van de vorige cross-reference-sectie aangeeft. De updatevermelding voor een gewijzigd object bevat de byte-offset van de nieuwe kopie, die de oude offset vervangt. Een lezer bouwt zijn cross-reference-informatie zo op dat de meest recente kopie van elk object degene is die wordt benaderd.

Het gevolg voor de handtekening wordt rechtstreeks vermeld door Spec: ISO 32000-2, §12.8.1 : een byte-range-digest wordt berekend over een bereik van het bestand — normaal gesproken het hele bestand, met uitzondering van de handtekeningwaarde (de /Contents-vermelding). De standaard merkt vervolgens op dat wanneer een ondertekend document wordt gewijzigd en via een incrementele update wordt opgeslagen, de gegevens die overeenkomen met het bytebereik van de oorspronkelijke handtekening behouden blijven. Als de handtekening geldig is, kan de toestand van het document op het moment van ondertekenen daardoor worden gereconstrueerd. Append-only is geen luxe. Het is de eigenschap waarvan het handtekeningmodel afhangt.

Een ondertekende-en-vervolgens-bewerkte PDF, structureel bekeken. De oorspronkelijke revisie eindigt bij haar eigen %%EOF. De tweede revisie wordt eronder toegevoegd.

%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

Een validator leest de laatste trailer, ziet /Prev 920 en heeft daarmee de hele keten. Hij kan de handtekening verifiëren tegen de bytes tot aan de eerste %%EOF, die ongewijzigd zijn. Vervolgens kan hij apart rapporteren dat revisie 2 een annotatie heeft toegevoegd. De geschiedenis zit in het bestand. Niets is verborgen door overschrijven.

De valkuil is “incrementele update betekent dat de wijziging klein is, dus onschadelijk.” Bij toevoegen gaat het om bytebehoud, niet om grootte. Een incrementele update kan zeer veel inhoud toevoegen. Wat het een incrementele update maakt, is dat hij de bytes die er al stonden niet aanraakt. Het gevolg verrast mensen ook: een tool die een ondertekende PDF “optimaliseert” of “lineariseert” door deze vanaf nul te herschrijven, levert een kleiner, schoner bestand op én een ongeldige handtekening, omdat het ondertekende bytebereik niet langer bestaat. Een ondertekende PDF opslaan en opnieuw opslaan zijn niet dezelfde bewerking.

Append-only beschermt de bytes. Het zegt op zichzelf niets over de vraag of de toegevoegde wijzigingen geautoriseerd waren. Een tweede revisie kan rechtmatig een tweede handtekening toevoegen, maar kan ook inhoud toevoegen die de eerste ondertekenaar nooit heeft beoogd. Bepalen welke van beide het geval is, is de taak van handtekeningvalidatie en wijzigingsdetectiebeleid (DocMDP). Toevoegen is de onderlaag die die analyse mogelijk maakt, niet de analyse zelf.

Deze pagina behandelt ook niet hoe de twee bytebereiken van een handtekening worden berekend en samengevoegd, of wat een volledige validatie controleert. Dat zijn afzonderlijke onderwerpen. De garantie hier gaat over bestanden die door een conforme writer zijn geschreven en bijgewerkt: een bestand waarvan eerdere revisies al misvormd waren, wordt niet goed gevormd doordat er iets aan wordt toegevoegd.

Hoe weet ik hoeveel revisies een PDF heeft? Tel de %%EOF-markeringen en volg de /Prev-keten vanaf de laatste trailer. Elke cross-reference-sectie die je bereikt is één opgeslagen revisie.

Verwijdert het wissen van een object het uit het bestand? Nee. Een incrementele update markeert het object als verwijderd in zijn cross-reference-vermelding, maar de bytes van het object blijven in eerdere revisies staan. “Verwijderd” betekent “niet gerefereerd door de huidige revisie”, niet “gewist.”

Kan een incrementele update de PDF-versie wijzigen? Ja, door de /Version-vermelding in de catalog in de toegevoegde revisie in te stellen. De header blijft zoals geschreven. De /Version in de catalog heeft voorrang wanneer deze een latere versie noemt.

  • Incrementele update — een wijziging opslaan door de gewijzigde objecten, een nieuwe cross-reference-sectie en een nieuwe trailer aan het einde van het bestand toe te voegen, zonder bestaande bytes te wijzigen.
  • /Prev — de vermelding in de trailer (of cross-reference-stream) die de byte-offset van de vorige cross-reference-sectie bevat. Die koppelt revisies tot een achterwaartse keten.
  • Revisie — de bestandstoestand die wordt vastgelegd door één cross-reference-sectie en de bijbehorende trailer. Een bestand met N cross-reference-secties heeft N revisies.
  • /ByteRange — de array in een handtekening-dictionary die de twee bytesegmenten aangeeft die de handtekening-digest dekt (alles behalve de handtekeningwaarde zelf).
  • Ondertekend bytebereik — de exacte bytes waarover de digest van een handtekening is berekend. Incrementele updates zijn er zodat deze bytes nooit worden verplaatst of overschreven.