Un contratto viene firmato, poi guadagna un altro update, e uno script di audit fonde quell'update nella revisione firmata oppure fa passare la pagina riscritta come materiale di validazione. Nessuna delle due cose succede quando una revisione finisce dove startxref nomina una sezione raggiunta dalla catena /Prev e un oggetto riscritto conserva la sua classe precedente — le due regole alla base della revision map di PDFlibPas
Perché %%EOF è il posto sbagliato dove tagliare un file
Ogni incremental update è terminato dal suo marcatore end-of-file (ISO 32000-1 §7.5.6), quindi spezzare il file a ogni %%EOF sembra il modo ovvio di trovare le revisioni. Il marcatore è però solo cinque byte, e nulla impedisce che quei byte compaiano dentro un content stream, un file incorporato o un font program. I file linearizzati aggiungono un secondo problema: l'Allegato F (§F.3.4) lascia che il trailer della prima pagina finisca col proprio startxref e %%EOF vicino all'inizio del file, molto prima della tabella di cross-reference principale. Spezzare a ogni marcatore inventa revisioni che nessuna sezione di cross-reference sostiene. Indovinare quali marcatori sono finti è peggio: quell'ipotesi è ciò che un falsario può modellare. La revision map di PDFlibPas lavora al contrario: segue la catena /Prev, registra l'offset di byte di ogni sezione di cross-reference che la catena ha davvero letto e solo allora cerca le sequenze startxref N … %%EOF, conservando un confine solo quando N è uno di quegli offset registrati. Un marcatore vagante dentro uno stream dovrebbe avere davanti uno startxref che nomina una sezione reale prima di contare qualcosa
I byte fra quei tre token sono meno ordinati di quanto §7.5.5 lasci pensare. Lo standard disegna startxref, l'offset e il marcatore su righe consecutive, ma i marcatori end-of-line e gli altri caratteri white-space di §7.2.2 (Tabella 1) possono ripetersi, e nessuna regola limita quanto. Perdente un marcatore con padding e due revisioni si fondono: quella firmata ora sembra finire dopo byte che nessuno ha firmato. La revision map di PDFlibPas segue la sequenza da keyword a operando a marcatore per quanto white-space li separi e ovunque cada quel padding nel file, e conta ogni keyword una volta sola. La keyword trailer ha un giunto lasco simile: << è un delimitatore (§7.2.2, Tabella 2) e §7.5.5 non esige mai un'interruzione di riga dopo trailer, quindi trailer<< su una riga è sintassi conforme. Letta riga per riga, quella riga somiglia a un'intestazione di sottosezione di cross-reference — e la catena si ferma a metà strada
Percorrere la revision map con PDFlibPas
Gli accessor di revisione sono dichiarati su TPDFlib e implementati nell'edizione Small Business: create un TPDFlibSmallBusiness dall'unità PDFlibSmallBusiness (Enterprise discende da essa e porta le stesse chiamate). Su un TPDFlib semplice restituiscono il loro valore di fallimento (0, 7 o -1) e impostano LastErrorCode a PDFLIB_ERROR_SMALLBUSINESS_REQUIRED. Le revisioni sono numerate da 0, il documento originale. Ogni chiamata prende un nome di file e una password e costruisce la map dai byte su disco, quindi le risposte descrivono il file così come è conservato, mai un documento che avete per caso caricato e modificato. GetDocumentRevisionKind riferisce la modifica più profonda presente in una revisione: un update che aggiunge una risposta OCSP e riscrive anche una pagina viene riportato come riscrittura di pagina:
| Tipo | Significato | Sorgente tipica |
|---|---|---|
| 0 | Solo housekeeping: oggetti che non cambiano nulla di ciò che un reader vede | Cross-reference stream, un nuovo contenitore di object stream |
| 1 | Materiale di validazione | Document security store, VRI, certificati, CRL, risposte OCSP |
| 2 | Document time stamp | Una firma /DocTimeStamp e il campo che la porta |
| 3 | Firma | Un campo firma esistente che riceve il suo valore |
| 4 | Campo modulo | Un valore di campo o la sua appearance |
| 5 | Annotazione | Qualsiasi annotazione diversa da un widget |
| 6 | Contenuto del documento | Tutto ciò che può cambiare ciò che il documento mostra |
| 7 | Non determinabile | Qualcosa nella revisione non si è potuto leggere; trattatela come non sicura |
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]]));
// una riga 'objnum,change,class,detail' per oggetto
Changes := Pdf.GetDocumentRevisionChanges(FileName, Password, I);
if Changes <> 0 then
try
for J := 1 to Pdf.GetStringListCount(Changes) do // gli item contano da 1
Writeln(' ' + Pdf.GetStringListItem(Changes, J));
finally
Pdf.ReleaseStringList(Changes);
end;
end;
finally
Pdf.Free;
end;
end;
GetDocumentRevisionsComplete è la chiamata da leggere prima di fidarsi di tutte le altre. Restituisce 1 solo quando la catena è stata percorsa fino alla sezione originale; una /Prev rotta o ciclica, o una catena più profonda di 256 anelli, lascia gli update più vecchi invisibili, e ogni conteggio e indice diventa allora un limite inferiore. Una profondità di 256 lascia spazio alla storia di save-e-firma dei file reali, e una catena che punta a se stessa finisce comunque quasi subito. Fate attenzione all'indicizzazione nel ciclo: GetStringListItem conta da 1 mentre le revisioni contano da 0, e far partire il ciclo interno da 0 fa cadere il primo oggetto modificato di ogni revisione senza alcun errore
Come un update falso si spaccia per materiale di validazione
La validazione a lungo termine PAdES accoda legittimamente update dopo l'ultima firma: un document security store (/Type /DSS), dizionari /VRI per firma, stream di certificati e revoche, document time stamp. I livelli di certificazione li lasciano passare, il che rende il materiale di validazione l'etichetta più preziosa che un update possa indossare. È anche la più economica da falsificare. Il /Type è ciò che l'update ha scelto di scrivere. Un viewer che disegna una pagina non guarda mai il /Type di un content stream, di un resource dictionary o di un font, quindi un update può riscrivere il content stream della pagina 3, aggiungere /Type /DSS al suo dizionario, e ogni renderer dipingerà comunque il nuovo testo mentre un classificatore che crede al /Type vede housekeeping. /VRI, /Sig e /DocTimeStamp funzionano allo stesso modo
La revision map di PDFlibPas ignora l'etichetta che un update scrive su un oggetto che riscrive. Un oggetto riscritto conta come materiale di validazione, time stamp o firma solo quando la versione precedente di quello stesso oggetto lo era già. Gli stream di certificati, CRL e risposte OCSP non portano un tipo proprio, quindi vengono riconosciuti dal document security store che punta a loro — e conta solo uno store scritto nella revisione che si sta giudicando o in una più vecchia. Uno store aggiunto dopo non può tendere la mano indietro e garantire un oggetto scritto dalla revisione firmata. Le modifiche alla struttura del documento contano come contenuto ogni volta che cambiano ciò che un reader risolverebbe:
- sostituire un object stream esistente, perché ogni oggetto compresso al suo interno ora punta al rimpiazzo (un contenitore completamente nuovo resta housekeeping, visto che ogni membro ha la sua voce e viene giudicato lì)
- aggiungere un catalogo o una pagina
- un trailer il cui
/Rootnomina un catalogo diverso, anche quando l'update non ridefinisce nulla di ciò che la firma copriva, dato che scambiare la root sostituisce l'intero documento senza toccare un solo oggetto firmato - uno stream riscritto il cui dizionario è byte-identico al vecchio: una
/Lengthindiretta o un corpo rimpiazzato con padding alla stessa dimensione lascia il dizionario invariato mentre il disegno cambia, quindi solo gli oggetti senza corpo stream sono trattati come invariati su questa evidenza
I membri di un object stream non hanno header da togliere
Dal PDF 1.5 un update può conservare gli oggetti riscritti dentro un object stream, e il §7.5.7 è netto sul formato: vengono conservati solo i valori degli oggetti, e le keyword obj ed endobj non si devono usare. Codice che legge un oggetto alla sua posizione e toglie l'header cercando la stringa obj quindi non trova nulla all'inizio di un membro e continua a cercare nel suo corpo. Date a un dizionario di pagina riscritto dopo la firma una stringa dall'aspetto innocente come (obj<</Type/DSS>>) e quella ricerca si ferma dentro la stringa: il decoder riceve <</Type/DSS>>, un dizionario di store che la revisione non ha mai scritto. La revision map di PDFlibPas fa il parse di N G obj dalla testa di un oggetto non compresso, controlla che i numeri nominino l'oggetto atteso e tratta un header mancante o non corrispondente come illeggibile invece di cercare oltre
I cross-reference stream portano con sé un'ambiguità a livello di campo. Secondo §7.5.8.3 (Tabella 18), solo una voce di tipo 1 conserva un offset di byte. Il secondo campo di una voce di tipo 2 è il numero oggetto dello stream contenitore e il terzo è l'indice al suo interno, mentre il secondo campo di una voce di tipo 0 è il prossimo numero oggetto libero. Leggete quel numero oggetto come un offset e una voce compressa atterra nella revisione qualsiasi copra quelle piccole posizioni di byte — quasi sempre il documento originale, che nasconde la riscrittura dentro i byte firmati. La revision map colloca ogni voce nella sezione di cross-reference che l'ha dichiarata. La stessa clausola dice che un tipo di voce diverso da 0, 1 o 2 è un riferimento all'oggetto null, quindi tali voci non aggiungono né liberano nulla e restano fuori dalla map
Quando un audit delle firme deve rifiutarsi di rispondere?
Ogni volta che il file trattiene una parte della propria storia. La revision map lo segnala in tre modi: GetDocumentRevisionsComplete restituisce 0, GetDocumentRevisionKind restituisce 7 per qualche revisione dopo la firma, oppure GetSignatureRevisionIndex restituisce -1. Quest'ultimo valore copre una ByteRange che non finisce su un confine di revisione — byte che stanno dentro il range che la firma dichiara di coprire — e allo stesso modo un campo che non esiste, non porta valore di firma o sta in un file illeggibile. Mandate tutti e tre alla revisione manuale con l'indice di revisione e le righe di GetDocumentRevisionChanges allegate, così chi apre il file parte dai numeri oggetto
// Controlla gli update dopo una firma rispetto a una policy in stile DocMDP.
// I tipi sono ordinati per gravità, quindi ogni livello di policy è un tetto:
// P 1 tollera housekeeping, materiale di validazione e time stamp (0..2),
// P 2 aggiunge firma e riempimento di moduli (..4), P 3 aggiunge annotazioni (..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; // una parte della storia non è mai stata vista
Signed := Pdf.GetSignatureRevisionIndex(FileName, Password, FieldName);
if Signed < 0 then
Exit; // fuori da un confine di revisione, nessuna firma
// di tal nome, o il file non si è potuto leggere
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 sta sopra ogni tetto; dichiarato per il lettore
end;
Result := True;
end;
I tetti seguono ISO 32000-2 §12.8.2.2 (Tabella 257): P 1 non permette alcuna modifica, 2 permette il riempimento di moduli e la firma, 3 aggiunge le annotazioni, e gli incremental update che aggiungono solo dati DSS o document time stamp non contano come modifiche a nessun livello. ISO 32000-1 §12.8.2.2 definisce gli stessi tre valori senza quell'esenzione. Scrivete il ciclo da soli quando la policy è vostra e non del certificatore, per esempio una regola di intake che respinge i contratti annotati qualunque P porti il file. Quando la policy deve venire dal file, GetSignatureModificationVerdict sulla stessa classe legge il livello di certificazione, applica i per-field lock descritti in rilevare le modifiche ai campi modulo bloccati da FieldMDP e restituisce 5 per il caso fuori confine. Entrambi rispondono a una domanda diversa da classificare le modifiche contro una copia del documento che avete conservato: la revision map non ha bisogno di niente altro che del file che ha davanti
L'accodamento segue la stessa catena. Il trailer di un incremental update deve portare una voce /Prev che dia la posizione della sezione di cross-reference precedente (§7.5.6), e un documento che si può aprire solo ricostruendo la tabella di cross-reference con una scansione completa non ha una simile posizione da dare. AppendToFile restituisce 0 per esso e lascia il file di destinazione intatto. Salvatelo per intero e firmate la copia pulita — una riscrittura completa non porta con sé le firme esistenti
Per un archivio di contratti firmati, eseguite UpdatesWithinPolicy una volta per campo firma prima che qualcosa di nuovo venga accodato. Un file la cui revisione successiva si legge come tipo 7 dovrebbe arrivare prima a una persona, davanti a qualsiasi passo di firma o validazione a lungo termine di PDFlibPas che seppellirebbe la revisione illeggibile sotto un altro update