Articolo tecnico

DocMDP e FieldMDP: audit delle revisioni PDF in Delphi

Un PDF firmato che è cambiato dopo la firma non è automaticamente rotto. ISO 32000-1 permette aggiornamenti incrementali sopra una firma, e solo alcuni di essi violano la policy stabilita dal firmatario. HotPDF Component per Delphi e C++Builder risponde a questa domanda con AnalyzeLoadedSignatureRevisions, che classifica ogni revisione post-firma e la valuta rispetto a DocMDP e FieldMDP. Lo scenario è familiare a chiunque distribuisca software contrattuale: il tuo cliente firma un contratto di acquisto, lo invia, e lo riceve indietro con una pagina di allegato aggiunta. Il reader mostra una barra gialla che dice che la firma è integra ma il documento è stato modificato dopo la firma, e nessuno nella stanza sa dire se si tratti di un normale flusso di controfirma o di qualcuno che sta silenziosamente modificando un contratto firmato

Cosa conta come modifica legittima dopo la firma?

Una modifica è legittima quando la sua categoria semantica rientra nel permesso dichiarato dalla firma di certificazione. ISO 32000-1 §12.8.2.2 definisce la trasformazione DocMDP con un valore /P di 1, 2 o 3: 1 non permette alcuna modifica, 2 permette la compilazione di moduli e la firma, 3 permette compilazione di moduli, firma e annotazioni. HotPDF espone questi valori come THPDFDocMDPPermission con dmpNoChanges, dmpFormFillAndSign e dmpFormFillSignAndAnnotate, con dmpNone riservato ai risultati di ispezione che non portano alcuna trasformazione DocMDP

Le categorie sono ordinate, e quell'ordinamento è il motore dell'intero controllo. THPDFRevisionModificationLevel esegue rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, disposti deliberatamente in modo che un ordinale maggiore non sia mai meno restrittivo. Un intero documento si riduce al livello massimo osservato in tutte le revisioni successive alla firma, e il confronto DocMDP diventa un singolo test su interi. Una sfumatura conta fin da subito: a dmpNoChanges l'analisi accetta comunque rmlLongTermValidation. Aggiungere materiale di validazione DSS e VRI o una marca temporale del documento a un file certificato è manutenzione della firma, non modifica del documento, e trattarlo come violazione romperebbe ogni flusso di archiviazione a lungo termine esistente

Come ricostruisce HotPDF la catena delle revisioni?

Strutturalmente, non euristicamente. Secondo ISO 32000-1 §7.5.6 un aggiornamento incrementale accoda una nuova sezione di cross-reference il cui /Prev punta alla precedente, quindi HotPDF legge startxref dalla coda, analizza la sezione lì presente, segue /Prev all'indietro e ripete, restituendo le sezioni dalla più vecchia alla più recente. In quel ciclo si trovano due limiti di sicurezza ed entrambi vale la pena conoscerli quando si esamina un file che fallisce: un /Prev che punta a un offset già visitato termina il percorso con una diagnostica esplicita di ciclo invece di andare in loop, e una catena più lunga di mille revisioni viene rifiutata del tutto. Entrambi emergono in Analysis.Issue con la funzione che restituisce False, e nessuno dei due dovrebbe essere ignorato, perché un /Prev ciclico è un file malformato o ostile piuttosto che semplicemente insolito

Quattro forme storiche compaiono in documenti reali e tutte e quattro sono gestite: le tabelle xref tradizionali analizzate riga per riga, gli stream cross-reference decompressi e decodificati tramite i loro campi /W e /Index, i file a riferimento ibrido il cui trailer tradizionale porta una chiave /XRefStm che viene analizzata e unita nella stessa revisione (il caso dei producer Office, trattato in l'articolo sugli stream cross-reference ibridi), e gli oggetti che vivono dentro un contenitore ObjStm, che contano perché un aggiornamento moderno di solito mette il dizionario modificato in uno stream compresso invece di scriverlo direttamente, come descritto in l'articolo su object stream e aggiornamenti incrementali. La firma ancora la suddivisione: /ByteRange[2] + /ByteRange[3] diventa SignedRevisionLength, e ogni sezione a quell'offset o oltre è post-firma. Se l'intervallo di byte produca ancora un hash corretto è una domanda separata, a cui risponde VerifyLoadedSignature ed è trattata in l'articolo sulla verifica delle firme digitali PDF

Come viene classificato ogni oggetto modificato

La classificazione avviene per oggetto, poi si propaga lungo i riferimenti. Per ogni numero oggetto che una sezione post-firma tocca, HotPDF legge il nuovo corpo e il corpo così com'era nello snapshot firmato; un corpo identico è rmlNone, perché i producer riscrivono effettivamente oggetti senza cambiarli. I riconoscitori sono deliberatamente ristretti. Un oggetto /Type /DocTimeStamp, o uno il cui /SubFilter è ETSI.RFC3161, è rmlLongTermValidation, così come qualsiasi cosa raggiungibile dall'albero /DSS del catalogo; un dizionario /Type /Sig è rmlFormFillAndSign. Per i contenitori il test riguarda quali chiavi si sono spostate, non cosa sia l'oggetto: il catalogo può solo ottenere o alterare /DSS, /Extensions o /AcroForm; il dizionario AcroForm solo /Fields, /SigFlags, /NeedAppearances, /DR, /DA o /Q; una pagina solo /Annots; un campo o widget solo /V, /AP, /AS o /M. Qualsiasi cosa fuori da questi insiemi scende a rmlOther, che è esattamente come viene intercettata la pagina di allegato aggiunta: aggiungere una pagina riorganizza l'albero delle pagine in modi che nessuna whitelist copre, e nessuna quantità di compilazione legittima di moduli le somiglia

Poi i livelli si propagano, con ogni contenitore che eredita il livello massimo dei figli modificati a cui punta, iterato finché l'assegnazione si stabilizza. Questo è ciò che fa funzionare gli appearance stream. Un campo di testo compilato riscrive /V e punta a un nuovo /AP stream, e quello stream da solo è un blob anonimo di operatori di contenuto senza alcun tipo da riconoscere; poiché il campo che lo possiede è rmlFormFillAndSign, lo stream eredita lo stesso livello invece di ricadere in rmlOther. La stessa propagazione porta il contesto DSS su stream di certificato e revoca che altrimenti sarebbero non classificabili

Perché un oggetto illeggibile conta come violazione?

Perché l'alternativa è un validatore sconfitto scrivendo qualcosa che non capisce. Tre situazioni finiscono a rmlOther senza appello in HotPDF: un oggetto il cui corpo non ha potuto essere letto dalla revisione, un oggetto che la revisione marca come libero, e un oggetto che non corrisponde a nessuno dei riconoscitori sopra. Ognuna registra una diagnostica specifica nel campo Issue della revisione, così un operatore può vedere quale numero oggetto ha prodotto il verdetto

Liberare è la più tagliente delle tre. Una revisione post-firma che marca come libero un oggetto precedentemente definito ha eliminato contenuto da un documento firmato, e nessun livello di permesso secondo §12.8.2.2 lo consente; i numeri oggetto atterrano in FreedObjectNumbers e la revisione viene elevata a rmlOther. Gli oggetti illeggibili seguono la stessa logica per un motivo diverso. Un validatore che non può analizzare un oggetto non ha basi per dichiararlo innocuo, e la risposta onesta a questo non è il silenzio. Segnalare come violazione un costrutto insolito ma benigno costa una revisione umana; l'errore opposto spedisce un contratto firmato con una modifica non notata al suo interno

Leggere il verdetto in Delphi

La chiamata è breve. Carica il documento, scegli un indice di firma, leggi il record; l'overload senza parametri riapre il file da cui il documento è stato caricato, e l'overload TStream prende byte forniti dal chiamante e ripristina la posizione dello stream prima di restituire il controllo. PolicyCompliant è il singolo booleano che la maggior parte dei chiamanti vuole, combinando tre decisioni indipendenti: la validità strutturale dei dizionari di permesso, DocMDPCompliant, e FieldMDPCompliant. Tieni i componenti visibili nella tua UI invece di comprimerli, e nota che un documento senza trasformazione DocMDP lascia DocMDPCompliant a True, poiché una comune firma di approvazione non dichiara alcuna policy da violare e l'aggregato ModificationLevel è allora descrittivo piuttosto che un verdetto

var
  Pdf: THotPDF;
  Analysis: THPDFSignatureRevisionAnalysis;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
    begin
      if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
      begin
        if Analysis.PolicyCompliant then
          Writeln('Post-signature changes stay inside the signing policy')
        else
          Writeln('Policy violation: ', string(Analysis.Issue));
      end
      else
        Writeln('Analysis could not run: ', string(Analysis.Issue));
    end;
  finally
    Pdf.Free;
  end;
end;

Per il triage di solito vuoi la scomposizione per revisione invece del riepilogo, perché dice quando nella storia del documento le cose sono andate storte. Ogni voce in Analysis.Revisions porta il proprio indice nella catena, l'offset cross-reference a cui è stata scritta, il proprio livello di modifica, e i numeri oggetto coinvolti

const
  LevelNames: array[THPDFRevisionModificationLevel] of string =
    ('none', 'long-term validation', 'form fill and sign',
     'annotations', 'other');
var
  I: Integer;
begin
  Writeln(Format('%d revisions in chain, signature sits at index %d',
    [Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
  for I := 0 to High(Analysis.Revisions) do
    Writeln(Format('  rev %d at offset %d: %s (%d changed, %d freed) %s',
      [Analysis.Revisions[I].RevisionIndex,
       Analysis.Revisions[I].XRefOffset,
       LevelNames[Analysis.Revisions[I].ModificationLevel],
       Length(Analysis.Revisions[I].ChangedObjectNumbers),
       Length(Analysis.Revisions[I].FreedObjectNumbers),
       string(Analysis.Revisions[I].Issue)]));
end;

FieldMDP viene giudicato separatamente, ed è deliberato

Un documento può soddisfare DocMDP ed essere comunque illegittimo, motivo per cui FieldMDPCompliant è un booleano distinto piuttosto che ripiegato nel confronto di livello. ISO 32000-1 §12.8.2.4 definisce la trasformazione FieldMDP, e §12.7.5.5 la relativa voce /SigFieldLock, per congelare campi modulo nominati al momento della firma anche dove il documento nel suo insieme permette ancora la compilazione di moduli. Compilare un campo è un'azione di livello 2; compilare un campo che il firmatario ha bloccato è una violazione indipendentemente dal livello. HotPDF legge l'ambito in THPDFFieldLockAction come flaAll, flaInclude o flaExclude, con flaNone per i risultati che non portano alcuna policy di blocco, e i nomi in Permissions.FieldNames: flaAll blocca tutto, flaInclude blocca i nomi elencati, flaExclude blocca tutto tranne loro. Un dettaglio conta quando si leggono i risultati, ovvero che solo i campi già presenti nello snapshot firmato vengono riportati in ChangedFieldNames, perché un campo creato interamente dopo la firma non ha alcuno stato firmato da contraddire e viene intercettato invece dal percorso DocMDP

var
  Source: TFileStream;
  Analysis: THPDFSignatureRevisionAnalysis;
  I: Integer;
begin
  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  try
    if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
      if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
        for I := 0 to High(Analysis.ChangedFieldNames) do
          Writeln('modified after locking: ',
            string(Analysis.ChangedFieldNames[I]));
  finally
    Source.Free;  // stream position was restored before the call returned
  end;
end;

Cosa non ti dirà questa analisi

Non verifica una firma. AnalyzeLoadedSignatureRevisions ragiona su struttura e permessi; se l'intervallo di byte firmato produca ancora l'hash corrispondente al valore nel blob CMS, e se il certificato del firmatario si concateni a qualcosa di cui ti fidi, sono domande a cui rispondono VerifyLoadedSignature e VerifyLoadedSignatureWithTrust. Un file può essere perfettamente conforme alla policy e crittograficamente privo di valore, quindi i due controlli appartengono fianco a fianco in ogni vero gate di accettazione. Non legge nemmeno l'intento dentro i content stream: una pagina il cui content stream è stato sostituito interamente viene intercettata come modifica fuori dalla whitelist, ma l'analisi non ti dirà che la sostituzione ha scambiato una cifra di pagamento. Un verdetto rmlOther significa che un umano dovrebbe guardare, non che sia avvenuta una frode, e un verdetto conforme significa che la modifica rientra in una categoria consentita, non che la modifica fosse voluta. Quando serve solo ciò che il firmatario ha dichiarato, senza il percorso delle revisioni, GetLoadedSignaturePermissions restituisce da solo i dizionari di policy

Tutto ciò che è descritto qui gira nativamente in Delphi e C++Builder senza alcun servizio di firma esterno nel ciclo, il che è ciò che lo rende pratico da eseguire su ogni documento in ingresso invece che solo su quelli che qualcuno già sospettava. L'API completa di firma e revisione, incluso i metodi di permesso e verifica con cui si accoppia, fa parte di HotPDF Component per Delphi e C++Builder