Een contract wordt getekend, krijgt daarna nog één update, en een auditscript voegt die update bij de getekende revisie in of laat de herschreven pagina als validatiemateriaal door. Beide gebeuren niet meer zodra een revisie eindigt waar startxref een sectie benoemt die de /Prev-keten bereikte en een herschreven object zijn eerdere klasse houdt — de twee regels achter de PDFlibPas-revisiekaart
Waarom %%EOF de verkeerde plek is om een bestand te knippen
Elke incrementele update wordt afgesloten met zijn eigen end-of-file marker (ISO 32000-1 §7.5.6), dus het bestand bij elke %%EOF knippen lijkt de voor de hand liggende manier om revisies te vinden. De marker is echter maar vijf bytes, en niets houdt die bytes tegen als ze opduiken in een content stream, een embedded file of een fontprogramma. Gelineariseerde bestanden voegen een tweede probleem toe: Annex F (§F.3.4) staat toe dat de trailer van de eerste pagina eindigt met zijn eigen startxref en %%EOF vlak bij het begin van het bestand, ruim vóór de hoofd-xref-tabel. Knippen bij elke marker verzint revisies die geen enkele xref-sectie steunt. Gokken welke markers nep zijn is erger — die gok is precies wat een vervalser mag vormgeven. De PDFlibPas-revisiekaart werkt precies omgekeerd. Hij volgt de /Prev-keten, noteert de byte-offset van elke xref-sectie die de keten echt las en scant pas daarna naar startxref N … %%EOF-reeksen, waarbij hij een grens alleen houdt als N een van die genoteerde offsets is. Een verdwaalde marker in een stream zou een startxref voor zich nodig hebben die een echte sectie benoemt voordat hij ergens voor telt
De bytes tussen die drie tokens zijn rommeliger dan §7.5.5 doet vermoeden. De standaard tekent startxref, de offset en de marker op opeenvolgende regels, maar end-of-line-markers en de andere witruimtetekens van §7.2.2 (Tabel 1) mogen herhaald worden, en geen enkele regel stelt daar een grens aan. Mist u één opgevulde marker, dan versmelten twee revisies. De getekende lijkt nu te eindigen na bytes die niemand heeft getekend. De PDFlibPas-revisiekaart volgt de reeks van keyword naar operand naar marker hoeveel witruimte ze ook scheidt en waar die vulling in het bestand ook valt, en hij telt elk keyword één keer. Het trailer-keyword heeft een vergelijkbaar los slot: << is een delimiter (§7.2.2, Tabel 2) en §7.5.5 eist nooit een regeleinde na trailer, dus trailer<< op één regel is conforme syntaxis. Regel voor regel gelezen ziet die regel eruit als een kop van een xref-subsectie — en de keten stopt halverwege
Over de revisiekaart lopen met PDFlibPas
De revisie-accessors staan gedeclareerd op TPDFlib en zijn geïmplementeerd in de Small Business-editie: maak een TPDFlibSmallBusiness aan vanuit de unit PDFlibSmallBusiness (Enterprise stamt ervan af en draagt dezelfde aanroepen). Op een kale TPDFlib geven ze hun faalwaarde terug (0, 7 of -1) en zetten ze LastErrorCode op PDFLIB_ERROR_SMALLBUSINESS_REQUIRED. Revisies worden genummerd vanaf 0, het originele document. Elke aanroep neemt een bestandsnaam en een wachtwoord en bouwt de kaart uit de bytes op schijf, dus de antwoorden beschrijven het bestand zoals het staat, nooit een document dat u toevallig geladen en bewerkt heeft. GetDocumentRevisionKind meldt de verstrekkende verandering in een revisie. Een update die een OCSP-response toevoegt en ook een pagina herschrijft wordt als de paginaherschrijving gemeld:
| Soort | Betekenis | Typische bron |
|---|---|---|
| 0 | Alleen huishoudelijk werk: objecten die voor een reader niets veranderen | Cross-reference stream, een nieuwe object stream-container |
| 1 | Validatiemateriaal | Document security store, VRI, certificaten, CRL's, OCSP-responses |
| 2 | Document time stamp | Een /DocTimeStamp-signatuur en het veld dat hem draagt |
| 3 | Signatuur | Een bestaand signatuurveld dat zijn waarde krijgt |
| 4 | Formulierveld | Een veldwaarde of zijn appearance |
| 5 | Annotation | Elke annotation anders dan een widget |
| 6 | Documentinhoud | Alles dat kan veranderen wat het document toont |
| 7 | Kon niet worden bepaald | Iets in de revisie was niet leesbaar; behandel hem als onveilig |
uses
System.SysUtils, PDFlibSmallBusiness;
procedure ListRevisions(const FileName, Password: WideString);
const
KindNames: array[0..7] of string = ('housekeeping', 'validation material',
'document time stamp', 'signature', 'form field', 'annotation',
'content', 'undetermined');
var
Pdf: TPDFlibSmallBusiness;
Count, I, J, Kind, Changes: Integer;
begin
Pdf := TPDFlibSmallBusiness.Create;
try
Count := Pdf.GetDocumentRevisionCount(FileName, Password);
if Count = 0 then
raise Exception.Create('the cross-reference chain could not be followed');
if Pdf.GetDocumentRevisionsComplete(FileName, Password) <> 1 then
Writeln('WARNING: history incomplete, every figure below is a lower bound');
for I := 0 to Count - 1 do
begin
Kind := Pdf.GetDocumentRevisionKind(FileName, Password, I);
if (Kind < 0) or (Kind > 7) then
Kind := 7;
Writeln(Format('revision %d: bytes %d..%d, %s', [I,
Pdf.GetDocumentRevisionStart(FileName, Password, I),
Pdf.GetDocumentRevisionEnd(FileName, Password, I),
KindNames[Kind]]));
// één regel 'objnum,change,class,detail' per object
Changes := Pdf.GetDocumentRevisionChanges(FileName, Password, I);
if Changes <> 0 then
try
for J := 1 to Pdf.GetStringListCount(Changes) do // items tellen vanaf 1
Writeln(' ' + Pdf.GetStringListItem(Changes, J));
finally
Pdf.ReleaseStringList(Changes);
end;
end;
finally
Pdf.Free;
end;
end;
GetDocumentRevisionsComplete is de aanroep die u leest voordat u een van de anderen vertrouwt. Hij geeft 1 terug alleen als de keten helemaal terug tot de oorspronkelijke sectie werd gevolgd; een gebroken of cyclische /Prev, of een keten dieper dan 256 schakels, laat de oudste updates onzichtbaar, en elke telling en index wordt dan een ondergrens. Een diepte van 256 biedt ruimte voor de opsla-en-teken-geschiedenis van echte bestanden, en een keten die naar zichzelf wijst eindigt nog steeds vrijwel meteen. Let op de indexering in de lus. GetStringListItem telt vanaf 1 terwijl revisies vanaf 0 tellen, en de binnenlus bij 0 laten beginnen verliest het eerste gewijzigde object van elke revisie zonder enige foutmelding
Hoe een vervalsde update zich voordoet als validatiemateriaal
PAdES long-term validation voegt op legitieme wijze updates toe na de laatste signatuur: een document security store (/Type /DSS), per-signatuur /VRI-dictionaries, certificaat- en intrekkingsstreams, document time stamps. Certificatieniveaus laten ze door, en daarmee is validatiemateriaal het waardevolste label dat een update kan dragen. Het is ook het goedkoopste om te vervalsen. /Type is precies wat de update schreef. Een viewer die een pagina tekent kijkt nooit naar de /Type van een content stream, een resource-dictionary of een font, dus een update kan de content stream van pagina 3 herschrijven, /Type /DSS aan zijn dictionary toevoegen, en elke renderer schildert nog steeds de nieuwe tekst terwijl een classificeerder die /Type gelooft huishoudelijk werk ziet. /VRI, /Sig en /DocTimeStamp werken hetzelfde
De PDFlibPas-revisiekaart negeert het label dat een update op een door hem herschreven object schrijft. Een herschreven object telt alleen als validatiemateriaal, time stamp of signatuur wanneer de eerdere versie van datzelfde object dat al was. Certificaat-, CRL- en OCSP-response-streams dragen geen eigen type, dus ze worden herkend via de document security store die naar hen wijst — en alleen een store die in de beoordelde revisie of een oudere werd weggeschreven telt. Een later toegevoegde store kan niet terugreichen en borg staan voor een object dat de getekende revisie schreef. Veranderingen aan de structuur van het document tellen als inhoud zodra ze veranderen wat een reader zou oplossen:
- het vervangen van een bestaande object stream, want elk gecomprimeerd object erin lost nu op naar de vervanger (een splinternieuwe container blijft huishoudelijk werk, want elk lid krijgt zijn eigen entry en wordt daar beoordeeld)
- het toevoegen van een catalog of een pagina
- een trailer waarvan
/Rooteen andere catalog benoemt, zelfs als de update niets herdefinieert dat de signatuur dekte, want het wisselen van de root vervangt het hele document zonder één getekend object aan te raken - een herschreven stream waarvan de dictionary byte-identiek is aan de oude: een indirecte
/Lengthof een vervangende body opgevuld tot dezelfde grootte houdt de dictionary onveranderd terwijl de tekening verandert, dus alleen objecten zonder stream-body worden op dat bewijs als onveranderd behandeld
Leden van een object stream hebben geen header om te strippen
Sinds PDF 1.5 kan een update herschreven objecten binnen een object stream opslaan, en §7.5.7 is ondubbelzinnig over het formaat: alleen de objectwaarden worden opgeslagen en de keywords obj en endobj mogen niet worden gebruikt. Code die een object op zijn locatie leest en de header verwijdert door naar de string obj te zoeken, vindt daarom aan de voorkant van een lid niets en blijft doorzoeken tot in zijn body. Geef een na het tekenen herschreven pagina-dictionary een onschuldig ogende string als (obj<</Type/DSS>>) en die zoektocht stopt binnen de string. De decoder ontvangt <</Type/DSS>> — een store-dictionary die de revisie nooit schreef. De PDFlibPas-revisiekaart parseert N G obj van de voorkant van een ongecomprimeerd object, controleert dat de nummers het verwachte object benoemen en behandelt een ontbrekende of niet-matchende header als onleesbaar in plaats van verder te speuren
Cross-reference streams dragen een ambiguïteit op veldniveau met zich mee. Onder §7.5.8.3 (Tabel 18) houdt alleen een type 1-entry een byte-offset vast. Het tweede veld van een type 2-entry is het objectnummer van de omvattende object stream en het derde is de index daarin, terwijl het tweede veld van een type 0-entry het volgende vrije objectnummer is. Leest u dat objectnummer als offset, dan belandt een gecomprimeerde entry in welke revisie die kleine byteposities ook beslaat — vrijwel altijd het originele document, wat de herschrijving binnen de getekende bytes verstopt. De revisiekaart plaatst elke entry bij de xref-sectie die hem declareerde. Dezelfde clausule zegt dat een entry-type anders dan 0, 1 of 2 een verwijzing naar het null-object is, dus zulke entries voegen niets toe of vrij en blijven van de kaart af
Wanneer moet een signatuuraudit weigeren te antwoorden?
Zodra het bestand een stuk van zijn eigen geschiedenis achterhoudt. De revisiekaart meldt dat op drie manieren: GetDocumentRevisionsComplete geeft 0 terug, GetDocumentRevisionKind geeft 7 terug voor een of andere revisie na de signatuur, of GetSignatureRevisionIndex geeft -1 terug. Die laatste waarde dekt een ByteRange die niet op een revisiegrens eindigt — bytes zitten binnen de range die de signatuur claimt te dekken — en evenzeer een veld dat niet bestaat, geen signatuurwaarde draagt of in een bestand zit dat niet leesbaar was. Stuur alle drie naar handmatige beoordeling met de revisie-index en de GetDocumentRevisionChanges-regels erbij, zodat wie het bestand opent bij objectnummers kan beginnen
// Toetst de updates na één signatuur aan een DocMDP-achtig beleid.
// Soorten zijn op ernst geordend, dus elk beleidsniveau is een plafond:
// P 1 tolereert huishoudelijk werk, validatiemateriaal en time stamps (0..2),
// P 2 voegt tekenen en formulieren invullen toe (..4), P 3 voegt annotations toe (..5).
function UpdatesWithinPolicy(Pdf: TPDFlibSmallBusiness;
const FileName, Password, FieldName: WideString; Level: Integer): Boolean;
const
Ceiling: array[1..3] of Integer = (2, 4, 5);
var
Signed, Count, I, Kind: Integer;
begin
Result := False;
if (Level < 1) or (Level > 3) then
Exit;
if Pdf.GetDocumentRevisionsComplete(FileName, Password) <> 1 then
Exit; // een deel van de geschiedenis werd nooit gezien
Signed := Pdf.GetSignatureRevisionIndex(FileName, Password, FieldName);
if Signed < 0 then
Exit; // buiten een revisiegrens, geen zo getekend veld,
// of het bestand was niet leesbaar
Count := Pdf.GetDocumentRevisionCount(FileName, Password);
for I := Signed + 1 to Count - 1 do
begin
Kind := Pdf.GetDocumentRevisionKind(FileName, Password, I);
if (Kind = 7) or (Kind > Ceiling[Level]) then
Exit; // 7 ligt boven elk plafond; vermeld voor de lezer
end;
Result := True;
end;
De plafonds volgen ISO 32000-2 §12.8.2.2 (Tabel 257): P 1 staat geen enkele verandering toe, 2 staat invullen en tekenen toe, 3 voegt annotations toe, en incrementele updates die alleen DSS-data of document time stamps toevoegen tellen op geen enkel niveau als verandering. ISO 32000-1 §12.8.2.2 definieert dezelfde drie waarden zonder die vrijstelling. Schrijf de lus zelf wanneer het beleid van u is en niet van de certificeerder, bijvoorbeeld een intake-regel die geannoteerde contracten weigert welke P het bestand ook draagt. Moet het beleid uit het bestand komen, dan leest GetSignatureModificationVerdict uit dezelfde klasse het certificatieniveau, past de per-veld-locks toe die in bewerkingen van FieldMDP-vergrendelde formuliervelden opsporen staan beschreven, en geeft 5 terug voor het off-boundary-geval. Beiden beantwoorden een andere vraag dan veranderingen classificeren tegen een kopie van het document dat u bewaarde: de revisiekaart heeft niets nodig behalve het bestand ervoor
Toevoegen volgt dezelfde keten. De trailer van een incrementele update moet een /Prev-entry dragen met de locatie van de vorige xref-sectie (§7.5.6), en een document dat alleen te openen was door de xref-tabel op te bouwen vanuit een volledige scan heeft zo'n locatie niet te geven. AppendToFile geeft er 0 voor terug en laat het doelebestand onaangeroerd. Sla het volledig op en teken in plaats daarvan de schone kopie — een volledige herschrijving neemt bestaande signaturen niet mee
Voor een archief van getekende contracten draait u UpdatesWithinPolicy één keer per signatuurveld voordat er iets nieuws wordt toegevoegd. Een bestand waarvan de latere revisie als soort 7 leest moet eerst bij een mens terechtkomen, vóór elke PDFlibPas-signing- of long-term-validatiestap die de onleesbare revisie onder nog een update zou begraven