Articolo tecnico

Classificare cosa cambia in un PDF dopo la firma

Una firma su un PDF non vieta le modifiche successive. Fissa un intervallo di byte, e un aggiornamento incrementale aggiunge nuovi byte dopo di esso, così la firma resta matematicamente valida mentre il documento acquisisce nuovo contenuto. Se quel contenuto sia accettabile è una questione di policy, e DocMDP è il punto in cui l'autore dichiara la policy: nessuna modifica, solo compilazione di moduli e firma, oppure queste più annotazioni. Farla rispettare significa classificare cosa sia realmente cambiato, che è ciò che fa AnalyzeModifications. Si punta a una revisione precedente, poi si legge GetModificationLevel per il verdetto complessivo e le accessors per singolo riscontro per livello, numero oggetto e descrizione di ogni differenza

Con questo in posto, l'applicazione di DocMDP si riduce a un confronto: il livello calcolato è al di sotto o uguale al livello che la policy consente

Diagramma della scala dei livelli di modifica di PDFlibPas da mlNone a mlUnclassified che mostra l'applicazione della policy DocMDP come un solo confronto in Delphi
La scala TPLModificationLevel va da mlNone a mlUnclassified, e l'applicazione di DocMDP si riduce a confrontare il livello calcolato con la policy

Perché un PDF firmato può legittimamente cambiare

Tre casi legittimi, e coprono la maggior parte di ciò che si vedrà. Un secondo firmatario aggiunge la propria firma. Un destinatario compila i campi modulo che l'autore ha lasciato aperti. E il materiale di validazione a lungo termine viene aggiunto: risposte OCSP e CRL scritte nel document security store del documento perché la firma resti verificabile quando i responder non ci sono più. Quest'ultimo caso non è semplicemente permesso: è ciò che un archivio ben gestito fa di proposito ai documenti firmati

Quindi "il file è cresciuto dopo la firma" non porta alcuna informazione. La domanda è sempre cosa sia stato aggiunto, e la risposta deve venire dal confronto degli stati del documento piuttosto che dall'osservazione dei byte. La meccanica di append in sé è trattata nell'articolo sugli aggiornamenti incrementali

Classificare per la forma dell'oggetto, non per il percorso che l'ha prodotto

Il classificatore guarda ciò che un oggetto è dopo la modifica, non quale chiamata di libreria l'ha creato. È deliberato, perché l'analisi gira su file prodotti da altro software, dove non c'è alcun percorso di chiamata da ispezionare

Quattro forme sono riconosciute. I dizionari di informazione del document security store e legati alla validazione, gli oggetti cross-reference stream, la voce metadata del catalogo e i dizionari di firma che trasportano un byte range sono materiale di archiviazione a lungo termine. Un oggetto che trasporta sia un tipo di campo sia un valore di campo è compilazione di moduli. Un oggetto il cui tipo è annotation, o il cui subtype è uno di quelli elencati nella Tabella 168 di ISO 32000-2, è una modifica di annotazione. Tutto il resto è unclassified

Albero decisionale che PDFlibPas applica a ogni oggetto PDF modificato, ordinando gli aggiornamenti nei livelli archivio, compilazione moduli, annotazione o unclassified
Ogni oggetto modificato è classificato per ciò che è — security store, xref stream, campo, annotazione — mai per la chiamata che l'ha prodotto

Le rimozioni sono trattate più severamente delle aggiunte. Un oggetto rimosso è inserito in whitelist solo quando l'oggetto sul lato vecchio era esso stesso materiale di archivio, il che copre il caso normale di un security store sostituito da uno più recente. Ogni altra rimozione è unclassified, perché cancellare contenuto da un documento firmato non è qualcosa che un livello di permessi autorizza. Le differenze a livello di documento sono più severe ancora: una modifica nel numero di pagine va dritta a unclassified senza esaminare i singoli oggetti, poiché nessun livello DocMDP consente di aggiungere o togliere pagine

La whitelist sbaglia verso il rifiuto

Questa è la regola di progetto che governa ogni decisione di confine. Una modifica classificata per errore come permessa è una firma che valida su contenuto che l'autore non ha mai autorizzato. Una modifica classificata per errore come unclassified è un documento che viene segnalato e revisionato da una persona. Quei due errori non sono simmetrici, quindi la whitelist resta stretta e le forme non riconosciute ricadono in unclassified invece di essere indovinate

C'è una conseguenza pratica da prevedere: i file di produttori insoliti a volte segnaleranno modifiche unclassified che, a un esame, sono benigne. La risposta giusta è guardare il dettaglio del riscontro e il numero oggetto piuttosto che allargare la whitelist, perché una whitelist che cresce per silenziare singoli report smette di essere un controllo di sicurezza

uses
  PDFlibrary, PDFlibCompare;

var
  Pdf: TPDFlib;
  I, Level: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('contract-countersigned.pdf', '');
    if Pdf.AnalyzeModifications('contract-as-signed.pdf', '') < 0 then
      raise Exception.Create('the earlier revision could not be loaded');

    // TPLModificationLevel ordinato mlNone, mlLTAUpdates, mlFormFilling,
    // mlAnnotations, mlUnclassified; il getter restituisce il suo ordinale
    Level := Pdf.GetModificationLevel;
    // l'applicazione di DocMDP è ora un solo confronto con la policy
    if Level > Ord(mlFormFilling) then
      for I := 0 to Pdf.GetModificationFindingCount - 1 do
        Report.Add(Format('object %d, level %d: %s',
          [Pdf.GetModificationFindingObjNum(I),
           Pdf.GetModificationFindingLevel(I),
           Pdf.GetModificationFindingDetail(I)]));
  finally
    Pdf.Free;
  end;
end;

Il livello complessivo è il massimo su tutti i riscontri, che è l'unica aggregazione difendibile: un documento che contiene novantanove aggiunte di archivio e una modifica unclassified è una modifica unclassified

Sotto il cofano: fingerprint, non hash crittografici

Il motore di confronto che CompareWith espone, e su cui l'analisi delle modifiche è costruita, identifica gli oggetti tramite un fingerprint dei loro corpi normalizzati usando un hash a 64 bit non crittografico anziché SHA-256. È una scelta meditata. Ciò che serve al confronto strutturale è il determinismo: lo stesso corpo di oggetto deve produrre sempre lo stesso fingerprint all'interno di un'esecuzione. Non serve la resistenza alle collisioni, perché un attaccante che controlla entrambi i lati del confronto ha già vinto con altri mezzi, e pagare un hash crittografico completo su ogni oggetto in un documento da un milione di oggetti è un costo reale senza beneficio

Due regole di normalizzazione contano più della scelta dell'hash. I riferimenti indiretti si ripiegano in un token segnaposto invece di essere espansi nel contenuto referenziato: espanderli copierebbe il corpo di un oggetto condiviso in ogni referenziante, così una piccola modifica a un font descriptor condiviso invaliderebbe il fingerprint di ogni oggetto che lo raggiunge, e il report sarebbe illeggibile. E i numeri di oggetto stessi sono esclusi dal fingerprint, perché una riscrittura può rinumerare gli oggetti senza cambiare nulla di semantico

L'accoppiamento poi gira in due passate, allineando prima per fingerprint e accoppiando il resto per numero oggetto per identificare modifiche piuttosto che un'aggiunta più una rimozione. I controlli economici vengono sempre per primi: una differenza nel numero di pagine viene riportata prima che inizi qualsiasi attraversamento di oggetti

Diff a due passate delle revisioni PDF in PDFlibPas: controllo del numero di pagine per primo, fingerprint a 64 bit, allineamento per fingerprint poi accoppiamento per numero oggetto
Il motore di confronto calcola fingerprint dei corpi oggetto normalizzati, riporta prima le differenze nel numero di pagine, poi accoppia per fingerprint e numero oggetto

Una trappola: l'autoconfronto non è garantito identico

Il primo test naturale per un motore di diff è confrontare un file con se stesso e asserire che il risultato sia identico. Quell'asserzione qui non vale, e la ragione è istruttiva. Il percorso di caricamento pubblico e il percorso di caricamento documenti di livello più basso non configurano la decodifica allo stesso modo, così lo stesso file caricato attraverso le due vie può produrre fingerprint diversi per alcuni oggetti. Il motore non sbaglia; i due caricamenti hanno davvero prodotto stati in memoria diversi

Invece di forzare le due vie insieme, la semantica del confronto è dichiarata in modo ristretto: l'analisi confronta lo stato corrente del documento con una revisione precedente, e riporta identico solo quando i due insiemi di fingerprint coincidono esattamente. Questa è la domanda che gli utenti pongono davvero, e non richiede che i due loader siano intercambiabili. Quando si progetta una funzione di confronto, definire cosa significhi "lo stesso" è più lavoro che calcolarlo

Dove usarlo

Due posti. In un report di validazione, accanto alla verifica delle firme, così un revisore vede non solo se la firma è crittograficamente intatta ma cosa sia accaduto al documento dopo; il lato firme è trattato in firma e validazione PAdES. E in una porta di ingresso, dove un documento che arriva dall'esterno viene confrontato con la copia inviata, così un contratto restituito con un'annotazione aggiunta è trattato diversamente da uno con una pagina modificata

Un avvertimento sull'ambito. Questa analisi dice cosa sia cambiato tra due revisioni della stessa linea di documenti. Non dice se il contenuto visibile sia fuorviante, se lo appearance stream di un campo modulo corrisponda al suo valore, o se testo nascosto sotto un overlay sia ancora presente nel content stream. Quei casi richiedono un trattamento separato, e il lato rimozione del contenuto è trattato nell'articolo sulla redaction vera. I punti di ingresso di analisi e confronto sono documentati nella pagina di prodotto di losLab PDF Developer Library