Aller au contenu

Mises à jour incrémentielles : pourquoi elles comptent

Evidence: Standard-backed

Quand un PDF est modifié après sa création, la bonne manière de l’enregistrer n’est pas de réécrire le fichier. Il faut plutôt ajouter à la fin les objets modifiés et une nouvelle section de références croisées, en laissant chaque octet d’origine exactement à sa place. Cette page explique le mécanisme et pourquoi c’est lui qui permet à une signature numérique de survivre à une modification ultérieure.

Une signature protège une plage d’octets. Si l’enregistrement d’une modification portant sur un seul mot réécrivait le fichier, tous les décalages d’octets changeraient. La plage signée ne décrirait plus le même contenu. La signature serait invalidée, même si le contenu signé n’avait pas lui-même été touché.

Les mises à jour incrémentielles empêchent ce scénario. Les octets d’origine, y compris ceux couverts par une signature, ne bougent pas. Un validateur peut prendre un document signé puis modifié et vérifier la première signature par rapport à la révision d’origine. Il voit précisément ce qui a été signé et, séparément, ce qui a changé ensuite. Si tu te trompes sur ce point, soit tu invalides des signatures valides, soit, pire, tu perds la capacité de prouver ce qu’une signature attestait réellement.

  • Une mise à jour incrémentielle ajoute à la fin : les objets nouveaux et modifiés, puis une nouvelle section de références croisées, puis une nouvelle bande-annonce, le tout à la fin du fichier.
  • Le contenu d’origine du fichier est laissé intact — il n’est pas modifié sur place.
  • La nouvelle bande-annonce porte une entrée /Prev : le décalage d’octet de la section de références croisées précédente. Les sections forment une chaîne remontant vers l’arrière.
  • Un lecteur construit son index en parcourant cette chaîne du plus récent au plus ancien. Pour un numéro d’objet donné, l’entrée la plus récente l’emporte.
  • Parce que rien n’a été écrasé, la plage d’octets couverte par une signature antérieure reste identique, octet pour octet — la signature se vérifie donc toujours, et tu peux récupérer le document exactement tel qu’il a été signé.

NextPDF écrit le document de base de la manière décrite dans la page précédente, puis expose les trois éléments nécessaires à une mise à jour incrémentielle.

Après build(), le rédacteur (src/Writer/PdfWriter.php) conserve :

  • le tampon de sortie, récupérable via getBuffer(), afin qu’une mise à jour puisse être ajoutée exactement à la suite des octets existants ;
  • le décalage d’octet de la dernière section de références croisées, via getLastXrefOffset(), qui devient la valeur /Prev de la nouvelle section ;
  • les entrées du dictionnaire de catalogue, via getCatalogEntries(), afin qu’une mise à jour devant réémettre le catalogue (par exemple pour y joindre une référence de signature) ne perde aucune clé antérieure.

Une révision ajoutée à la fin attribue de nouveaux numéros d’objet (ou réutilise les numéros existants pour les objets qu’elle remplace) dans le même ObjectRegistry, de sorte que la numérotation des objets reste cohérente d’une révision à l’autre. La nouvelle section de références croisées ne répertorie que les objets que cette révision a touchés. La nouvelle bande-annonce reprend les entrées de la bande-annonce précédente et ajoute /Prev, qui pointe vers la section antérieure. C’est cette chaîne qu’un lecteur suit.

C’est pour la signature que ce point est le plus évident. Le ByteRangeCalculator de NextPDF (src/Security/Signature/ByteRangeCalculator.php) calcule le tableau /ByteRange sous forme de deux segments : tout ce qui précède la valeur de la signature, puis tout ce qui la suit — de sorte que la signature couvre toute la révision sauf ses propres octets. Comme une modification ultérieure est ajoutée à la fin au lieu d’être écrite par-dessus ces octets, cette plage ne bouge jamais.

  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.
Comment un PDF signé puis modifié reste vérifiable : chaque enregistrement ajoute à la fin, de sorte que les octets signés à l'origine ne sont jamais écrasés et que la chaîne de bandes-annonces enregistre l'historique.

La règle selon laquelle on ajoute uniquement en fin de fichier est normative. Spec: ISO 32000-2, §7.5.6 indique que le contenu d’un PDF peut être mis à jour de manière incrémentielle sans réécrire tout le fichier et que, dans ce cas, les modifications doivent être ajoutées à la fin du fichier, en laissant intact le contenu d’origine. Evidence: Standard-backed

La même clause définit les mécanismes. Pour une mise à jour incrémentielle, une section de références croisées ne contient d’entrées que pour les objets modifiés, remplacés ou supprimés. Les objets supprimés restent dans le fichier, mais sont marqués comme supprimés au moyen de leurs entrées de références croisées. La bande-annonce ajoutée doit contenir une entrée /Prev donnant l’emplacement de la section de références croisées précédente. L’entrée de mise à jour d’un objet modifié porte le décalage d’octet de la nouvelle copie et remplace l’ancien décalage. Un lecteur construit ses informations de références croisées de sorte que la copie la plus récente de chaque objet soit celle à laquelle il accède.

La conséquence pour la signature est énoncée directement par Spec: ISO 32000-2, §12.8.1  : le condensé d’une plage d’octets est calculé sur une plage du fichier — normalement le fichier entier, à l’exclusion de la valeur de la signature (l’entrée /Contents). La norme précise ensuite que, si un document signé est modifié et enregistré au moyen d’une mise à jour incrémentielle, les données correspondant à la plage d’octets de la signature d’origine sont préservées ; donc, si la signature est valide, l’état du document au moment de la signature peut être recréé. L’ajout uniquement en fin de fichier n’est pas un raffinement accessoire : c’est la propriété dont dépend le modèle de signature.

Un PDF signé puis modifié, vu sous l’angle structurel. La révision d’origine se termine par son propre %%EOF. La seconde révision est ajoutée ensuite, à la suite.

%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

Un validateur lit la dernière bande-annonce, voit /Prev 920 et dispose alors de toute la chaîne. Il peut vérifier la signature par rapport aux octets allant jusqu’au premier %%EOF, qui n’ont pas changé. Il peut ensuite signaler séparément que la révision 2 a ajouté une annotation. L’historique est dans le fichier. Rien n’a été dissimulé par écrasement.

Le piège, c’est de croire que « mise à jour incrémentielle » signifie « changement petit, donc inoffensif ». L’ajout à la fin concerne la préservation des octets, pas la taille. Une mise à jour incrémentielle peut ajouter une grande quantité de contenu. Ce qui en fait une mise à jour incrémentielle, c’est qu’elle ne touche pas aux octets déjà présents. Le corollaire surprend aussi : un outil qui « optimise » ou « linéarise » un PDF signé en le réécrivant à partir de zéro produira un fichier plus petit et plus propre, mais aussi une signature invalidée, parce que la plage d’octets signée n’existe plus. Enregistrer un PDF signé et le réenregistrer ne reviennent pas à la même opération.

L’ajout uniquement en fin de fichier protège les octets. À lui seul, il ne te dit pas si les modifications ajoutées étaient autorisées. Une seconde révision peut légitimement ajouter une seconde signature, ou ajouter du contenu que le premier signataire n’avait jamais voulu. Déterminer de quel cas il s’agit relève de la validation de signature et de la politique de détection des modifications (DocMDP). L’ajout à la fin est le socle qui rend cette analyse possible, pas l’analyse elle-même.

Cette page ne couvre pas non plus la façon dont les deux plages d’octets d’une signature sont calculées et raccordées, ni ce que vérifie une validation complète. Ce sont des sujets distincts. La garantie présentée ici porte sur les fichiers écrits et mis à jour par un rédacteur conforme : un fichier dont les révisions antérieures étaient déjà mal formées ne devient pas bien formé du seul fait qu’on y ajoute des données à la fin.

Comment savoir combien de révisions un PDF comporte ? Compte les marqueurs %%EOF et suis la chaîne /Prev à partir de la dernière bande-annonce. Chaque section de références croisées atteinte correspond à une révision enregistrée.

Supprimer un objet le retire-t-il du fichier ? Non. Une mise à jour incrémentielle marque l’objet comme supprimé dans son entrée de références croisées, mais les octets de l’objet restent dans les révisions antérieures. « Supprimé » signifie « non référencé par la révision actuelle », pas « effacé ».

Une mise à jour incrémentielle peut-elle changer la version du PDF ? Oui, en définissant l’entrée /Version dans le catalogue de la révision ajoutée. L’en-tête reste tel qu’il a été écrit. Le /Version du catalogue l’emporte lorsqu’il nomme une version ultérieure.

  • Mise à jour incrémentielle — enregistrer une modification en ajoutant à la fin du fichier les objets modifiés, une nouvelle section de références croisées et une nouvelle bande-annonce, sans altérer les octets existants.
  • /Prev — l’entrée de la bande-annonce (ou du flux de références croisées) qui contient le décalage d’octet de la section de références croisées précédente. Elle relie les révisions en une chaîne remontant vers l’arrière.
  • Révision — l’état du fichier capturé par une section de références croisées et sa bande-annonce. Un fichier avec N sections de références croisées a N révisions.
  • /ByteRange — le tableau, dans un dictionnaire de signature, qui indique les deux segments d’octets que le condensé de la signature couvre (tout sauf la valeur de la signature elle-même).
  • Plage d’octets signée — les octets exacts sur lesquels le condensé d’une signature a été calculé. Les mises à jour incrémentielles existent pour que ces octets ne soient jamais déplacés ni écrasés.