Ga naar inhoud

Hoe handtekeningen in een PDF zijn ingebed

Spec: ETSI EN 319 142-1 Spec: RFC 5652 Evidence: Standard-backed

Een PDF-handtekening wordt niet als een envelop om het bestand gevouwen. Ze zit in het bestand zelf: een dictionary die de handtekening beschrijft, plus een digest die wordt berekend over een opgegeven reeks bytes en de handtekeningwaarde zelf bewust overslaat. Deze pagina legt dat mechanisme uit en, minstens zo belangrijk, wat het niet belooft.

“Het document is ondertekend” is een zin waarop mensen beslissingen baseren. Ze verbinden er een betaling, een goedkeuring of een juridische verplichting aan. Als je niet precies weet welke bytes een handtekening dekt, kun je niet zeggen wat een geldig resultaat eigenlijk aantoont. Een PDF kan een volledig geldige handtekening bevatten en toch aan de lezer inhoud tonen die de ondertekenaar nooit heeft gezien, omdat die inhoud na het ondertekenen is toegevoegd in een gebied dat de handtekening nooit heeft gedekt. Weten waar de bewijskracht van de handtekening begint en eindigt, maakt het verschil tussen een verdedigbare beslissing en een beslissing op hoop van zegen.

  • Een PDF-handtekening bevindt zich in een signature dictionary en een signature field binnen het document, niet in een externe envelop.
  • De ondertekende bytes worden opgegeven door een ByteRange-array: twee (offset, length)-segmenten die samen het hele bestand dekken behalve de hexadecimale handtekeningwaarde in de Contents-entry.
  • De digest van die twee aaneengeschakelde segmenten is wat de cryptografische handtekening daadwerkelijk beschermt.
  • Alles wat later in een nieuwe revisie wordt toegevoegd, valt buiten de oorspronkelijke byte range. De oorspronkelijke handtekening blijft geldig; die heeft nooit een uitspraak gedaan over de nieuwe bytes.
  • Een approval-handtekening en een certification-handtekening verschillen in reikwijdte: certificering (DocMDP) beperkt welke latere wijzigingen zijn toegestaan; goedkeuring doet dat niet.

NextPDF bouwt de handtekening op volgens het model van het PDF-formaat, in een vaste volgorde, zodat de byte range exact is en geen benadering.

Bij het schrijven van een handtekening reserveert de engine eerst een slot van vaste grootte voor de Contents-waarde en schrijft hij een ByteRange-placeholder van vaste breedte. De engine wacht tot het volledige document is geschreven, inclusief de cross-reference table en de end-of-file-marker. Pas dan berekent hij de twee werkelijke offsets, schrijft die terug in de placeholder zonder ook maar één byte te verschuiven, hasht hij de twee segmenten en plaatst hij het resulterende CMS-object in het gereserveerde slot. De placeholder wordt met nullen aangevuld tot een constante lengte, juist zodat het invullen van de werkelijke getallen de bytes die worden gehasht niet kan verschuiven. Dit is de enige volgorde die een intern consistente handtekening oplevert. De engine behandelt elke fout in deze reeks als een harde fout in plaats van een stille fallback.

Voor het PDF 2.0-profiel is het handtekeningobject zelf een losgekoppelde (detached) CMS SignedData-structuur. De PDF-dictionary zegt waar en hoe; het CMS-object bevat het wie en het cryptografische bewijs.

  1. Step 1 of 4: ISO 32000-2 §12.8.1 — ByteRange digest & signature dictionary
  2. Step 2 of 4: ISO 32000-2 §12.8.3.3 — ETSI.CAdES.detached SubFilter
  3. Step 3 of 4: ETSI EN 319 142-1 PAdES baseline profile
  4. Step 4 of 4: RFC 5652 CMS SignedData in Contents
Waar een PDF-handtekening wordt gedefinieerd, van het containerformaat tot aan het cryptografische object: ISO 32000-2 specificeert de dictionary en het byte-range-mechanisme, ETSI EN 319 142-1 profileert het voor PAdES, en RFC 5652 definieert het CMS SignedData-object dat in Contents wordt geplaatst.

Evidence: Standard-backed Het mechanisme wordt gedefinieerd door Spec: ISO 32000-2, §12.8.1 . Een byte-range-digest wordt berekend over een reeks bytes die wordt aangeduid door de ByteRange-entry. Die reeks moet het hele bestand omvatten, inclusief de signature dictionary maar exclusief de handtekeningwaarde: de Contents-entry. ByteRange is een array van integerparen — beginoffset en lengte. Niet-aaneengesloten reeksen worden hier juist gebruikt zodat de digest de handtekeningwaarde zelf kan weglaten.

Voor het PDF 2.0-profiel specificeert Spec: ISO 32000-2, §12.8.3.3 dat, wanneer de SubFilter ETSI.CAdES.detached is, de Contents-waarde een DER-gecodeerd CMS SignedData-object is: dezelfde structuur die Spec: RFC 5652 definieert. Voor dat object geldt het PAdES-profiel dat Spec: ETSI EN 319 142-1 beschrijft.

De reikwijdte is niet voor alle handtekeningen gelijk. Spec: ISO 32000-2, §12.7.4.5 definieert de MDP-machtiging: een waarde van 0 maakt van de handtekening een approval-handtekening, terwijl de waarden 13 er een certification-handtekening van maken die beperkt welke latere wijzigingen het document conform houden. Het byte-range-mechanisme blijft hetzelfde; de belofte over latere wijzigingen is anders.

De engine van NextPDF implementeert precies dit: een ByteRange-placeholder van vaste breedte, de aaneengeschakelde digest van twee segmenten en een losgekoppeld CMS-object in een gereserveerd Contents-slot, dat pas wordt afgerond nadat het bestand compleet is.

Je bouwt zelden handmatig een ByteRange. Het doel van het voorbeeld is om de vorm van het resultaat te tonen, zodat je die herkent wanneer je een ondertekend bestand inspecteert.

<?php
declare(strict_types=1);
use NextPDF\Security\Signature\ByteRangeCalculator;
// Offsets the engine knows only after the whole PDF is written:
// $contentsStart — byte just before the '<' of the hex signature
// $contentsEnd — byte just after the '>' that closes it
// $fileLength — total file size in bytes
$range = ByteRangeCalculator::calculate(
contentsStart: $contentsStart,
contentsEnd: $contentsEnd,
fileLength: $fileLength,
);
// $range === [0, $contentsStart, $contentsEnd, $fileLength - $contentsEnd]
// Segment 1: file start → just before the signature value
// Segment 2: just after the signature value → end of file
// The signature value itself is the gap. It is never hashed.
$signedMessage = ByteRangeCalculator::extractSignedData($pdfBytes, $range);
// $signedMessage is segment 1 concatenated with segment 2 — exactly the
// bytes the cryptographic digest is computed over.

De ruimte tussen de twee segmenten is de handtekeningwaarde. Die waarde kan geen deel uitmaken van haar eigen digest, en daarom bestaat de reeks uit twee delen en niet uit één.

De valkuil is dat je denkt dat een geldige handtekening betekent dat het hele bestand dat je voor je hebt is ondertekend. Dat is niet zo. Het betekent dat de bytes binnen de opgegeven reeks intact zijn. Een latere revisie kan legitiem inhoud toevoegen — een tweede handtekening, formuliergegevens, validatiemateriaal — buiten die reeks. De eerste handtekening blijft geldig en zegt niets over de toevoeging. Een correcte viewer laat zien dat een handtekening “het document zoals het bij ondertekening bestond” dekt, niet “elke byte op het scherm”. Als je die twee gelijkstelt, kan een ondertekend document niet-ondertekende inhoud bevatten die ondertekend lijkt.

Deze pagina legt de structuur uit, niet het vertrouwen. Een correct gevormde ByteRange en een CMS-object vertellen je dat de bytes intact zijn en welke sleutel ze heeft ondertekend. Op zichzelf vertellen ze je niet of die sleutel toebehoort aan wie je denkt, of het bijbehorende certificaat geldig was bij ondertekening, of dat het later is ingetrokken. Dat hoort bij certificaatpad- en intrekkingsvalidatie, die aan de orde komt in Een handtekening correct valideren. Deze pagina behandelt evenmin wanneer het ondertekenen volgens een onafhankelijke autoriteit plaatsvond. Een zelfopgegeven ondertekeningstijd is geen vertrouwde tijd — zie Tijdstempels en vertrouwde tijd. NextPDF bouwt de hier beschreven structuur; de certificaten, trust anchors en de tijdstempelautoriteit komen uit je deployment, niet uit de engine.

Wat de engine per niveau levert, is de mogelijkheid om de structuur op te bouwen:

PAdES signature structure (byte range, signature dictionary, detached CMS) — edition availability
Edition Availability
Core

PAdES B-B: de signature dictionary, de ByteRange van vaste breedte en het losgekoppelde CMS SignedData-object dat op deze pagina wordt beschreven.

Pro

Voegt PAdES B-T toe — een vertrouwde tijdstempel op de handtekeningwaarde — bovenop dezelfde structuur.

Enterprise

Voegt de langetermijnprofielen toe (B-LT, B-LTA): ingebed validatiemateriaal en documenttijdstempels, gelaagd op hetzelfde byte-range-fundament.

  • Signature dictionary — de PDF-dictionary die de signature handler, de SubFilter, de ByteRange en de Contents-waarde benoemt.
  • ByteRange — een array van (offset, length)-integerparen die exact aangeven welke bytes de handtekeningdigest dekt.
  • Contents — de hexadecimale entry die de handtekeningwaarde bevat (voor PDF 2.0 een losgekoppeld CMS SignedData-object); die is uitgesloten van haar eigen digest.
  • CMS SignedData — Cryptographic Message Syntax (RFC 5652)-structuur die het certificaat van de ondertekenaar en de handtekeningbytes bevat.
  • PAdES — PDF Advanced Electronic Signatures: het ETSI-profiel voor CMS-handtekeningen in PDF, gedefinieerd in de ETSI EN 319 142-serie.
  • Approval signature — een handtekening met MDP-machtiging 0; die de inhoud bevestigt zonder latere wijzigingen te beperken.
  • Certification signature — een handtekening met een DocMDP-machtiging (MDP 13) die beperkt welke latere wijzigingen het document conform houden.