Articolo tecnico

FieldMDP in Delphi: rilevare modifiche ai campi bloccati

PDFlibPas risponde alla domanda FieldMDP attraverso TPDFlib.GetSignatureModificationVerdict. Dato un PDF firmato, la sua password e il nome di un campo firma, il metodo attraversa ogni aggiornamento incrementale accodato dopo quella firma, risolve il nome completo di ogni campo modulo toccato da quegli aggiornamenti e restituisce 3 quando è stato modificato un campo bloccato da FieldMDP e 2 quando sono stati toccati solo campi non bloccati. GetSignatureModificationDetail indica poi la revisione e il campo

Il flusso di lavoro che ne ha bisogno è il modulo multi-approvatore. Un richiedente compila Order.Customer e Order.Total su una richiesta d'acquisto e firma Approval.Requester, bloccando quei due campi. Un responsabile aggiunge un centro di costo e firma Approval.Manager. Due settimane dopo qualcuno apre il file in un editor desktop e alza Order.Total in un terzo aggiornamento incrementale. Ogni firma continua a verificare, perché ciascuna copre solo il proprio byte range. Le domande che un revisore si pone davvero sono quale campo è cambiato dopo quale firma, e se quel cambiamento era consentito

Perché il solo DocMDP non individua un campo bloccato modificato?

DocMDP non riesce a individuarlo perché classifica tipi di cambiamento, non i singoli campi. I livelli di permesso di certificazione in ISO 32000-1 §12.8.2.2 stabiliscono se gli aggiornamenti successivi possono compilare moduli e firmare (P 2) o anche annotare (P 3), e quel permesso si applica a ogni campo del documento allo stesso modo. Modificare Order.Total è compilazione di un modulo, quindi una certificazione P 2 è soddisfatta proprio dalla modifica che il richiedente intendeva vietare. La classificazione delle modifiche a livello di documento in PDFlibPas copre quel livello, e questo articolo scende a una granularità più fine

FieldMDP è il livello con granularità di campo. Un campo firma può portare un dizionario /Lock (ISO 32000-1 §12.7.4.5), e la firma che contiene può portare una transform FieldMDP nel suo signature reference (ISO 32000-1 §12.8.2.4). Entrambi usano lo stesso vocabolario: /Action /All blocca ogni campo, /Action /Include blocca i nomi elencati in /Fields, e /Action /Exclude blocca tutto tranne i nomi in /Fields. Prima della v3.586.0 GetSignatureModificationVerdict sapeva che una revisione successiva aveva modificato un campo modulo, ma non quale, quindi qualsiasi documento con un blocco di campo i cui aggiornamenti successivi andavano oltre il materiale di validazione tornava come 4, indeterminato. La revision map ora registra un nome di campo risolto per ogni oggetto legato ai campi che classifica, ed è questo che trasforma quel 4 in un vero 2 o 3

Scrivere il blocco di campo quando firma ogni approvatore

TPDFlib.SetSignProcessFieldLock imposta il blocco per la prossima firma prodotta da un processo di firma. L'azione 1 è All, 2 è Include, 3 è Exclude e 0 cancella il blocco; Include ed Exclude prendono un elenco di nomi completi di campo separati da punto e virgola. Il campo firmato finisce con un dizionario /Lock e una transform FieldMDP corrispondente, che è l'abbinamento che i validatori rigorosi si aspettano

Codici azione di PDFlibPas SetSignProcessFieldLock per la firma FieldMDP in Delphi: l'azione 1 All blocca ogni campo modulo incluse le firme successive, l'azione 2 Include blocca i campi dati nominati, l'azione 3 Exclude blocca tutto tranne l'elenco, e l'azione 0 cancella il blocco
Scegliete Include per i campi dati di proprietà di ciascun approvatore oppure Exclude elencando i campi firma successivi; una chiamata rifiutata restituisce 0 e lascia in piedi il blocco precedente, quindi controllate sempre il valore di ritorno
var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('purchase-request.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open the form');
    try
      Pdf.SetSignProcessField(SignId, 'Approval.Requester');
      Pdf.SetSignProcessPFXFromFile(SignId, 'requester.pfx', PfxPassword);
      // 2 = Include: solo questi campi vengono congelati da questa firma
      if Pdf.SetSignProcessFieldLock(SignId, 2,
        'Order.Customer;Order.Total') <> 1 then
        raise Exception.Create('field lock rejected');
      Pdf.EndSignProcessToFile(SignId, 'purchase-request-signed.pdf');
      if Pdf.GetSignProcessResult(SignId) <> 1 then
        raise Exception.CreateFmt('signing failed, code %d',
          [Pdf.GetSignProcessResult(SignId)]);
    finally
      Pdf.ReleaseSignProcess(SignId);
    end;
  finally
    Pdf.Free;
  end;
end;

SetSignProcessFieldLock restituisce 0 per una chiamata Include o Exclude senza nomi, e anche per un elenco che contiene una voce vuota come 'Order.Customer;;Order.Total'; una chiamata rifiutata lascia al suo posto il blocco configurato in precedenza, quindi controllate il valore di ritorno invece di dare per vinta l'ultima chiamata. Il resto della configurazione della firma, scelta del digest, riservazione del /Contents e codici di risultato, è trattato nella guida a firma e validazione PAdES per PDFlibPas. Un punto di progettazione è specifico delle catene di approvazione: un blocco /All sulla firma del richiedente congela anche Approval.Manager, perché un campo firma è a sua volta un campo modulo. Usate Include per i campi dati di proprietà di ciascun approvatore, oppure Exclude elencando i campi firma successivi

Eseguire il verdetto lungo una catena di approvazione

GetSignatureModificationVerdict giudica una firma alla volta, quindi una catena di approvazione si controlla chiamandolo una volta per campo firma. Il valore di ritorno è un insieme piccolo e chiuso, e ogni valore significa qualcosa di diverso per chi revisiona:

  • 0: non è stato possibile leggere il file o il campo firma indicato
  • 1: dopo la firma non è stato accodato nulla
  • 2: sono stati accodati solo cambiamenti consentiti dai permessi effettivi, e nessun campo bloccato è stato toccato
  • 3: una revisione successiva ha introdotto un cambiamento non consentito, compresa una modifica a un campo bloccato da FieldMDP
  • 4: non è stato possibile determinare la risposta, e non va mai letto come approvazione
  • 5: la firma non termina su un confine di revisione, quindi ci sono byte dentro l'intervallo che dichiara di coprire
const
  Approvals: array[0..2] of WideString =
    ('Approval.Requester', 'Approval.Manager', 'Approval.Finance');
var
  Pdf: TPDFlib;
  I, Verdict: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    for I := Low(Approvals) to High(Approvals) do
    begin
      Verdict := Pdf.GetSignatureModificationVerdict(
        'purchase-request-final.pdf', FormPassword, Approvals[I]);
      // entrambi i getter descrivono la chiamata appena fatta, quindi leggeteli prima della successiva
      Log.Add(Format('%s: verdict %d, MDP level %d, %s',
        [Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
         Pdf.GetSignatureModificationDetail]));
      case Verdict of
        1, 2: ;                                 // accettata
        3: RejectForm(Approvals[I]);            // il dettaglio indica campo e revisione
      else
        QueueForManualReview(Approvals[I]);     // 0, 4, 5
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Prendete la richiesta d'acquisto manomessa, dove la revisione 0 è il modulo in bianco, le revisioni 1 e 2 sono le due firme e la revisione 3 è la modifica. Poiché le definizioni di blocco vengono messe in comune da ogni campo firmato, il verdetto per Approval.Manager fa rispettare già il blocco del richiedente, e il suo unico aggiornamento successivo è la modifica stessa. Quando quell'aggiornamento ha riscritto solo il dizionario del campo, il verdetto è 3 e il dettaglio recita Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Quella riga risponde a entrambe le domande della revisione contabile. Il verdetto per Approval.Requester deve prima superare la revisione della firma del responsabile, e per i motivi nella sezione limiti qui sotto spesso riporta 4, quindi la firma più recente di solito dà la risposta più netta. GetSignatureEffectiveMDPLevel riporta il valore P DocMDP più basso trovato tra le firme, o 3 quando nessuno ha certificato il documento, e entrambi i getter restano validi solo fino alla successiva chiamata di verdetto

Timeline degli aggiornamenti incrementali di un modulo PDF Delphi manomesso letta da PDFlibPas GetSignatureModificationVerdict: la revisione 1 porta un blocco Include su Order.Customer e Order.Total, la revisione 2 firma Approval.Manager, e la riscrittura in revisione 3 del campo bloccato Order.Total produce verdetto 3 per la firma del responsabile e 4 per quella del richiedente
I blocchi vengono messi in comune da ogni campo firmato, quindi il verdetto per Approval.Manager fa rispettare già il blocco del richiedente e la stringa di dettaglio nomina campo e revisione colpevoli, mentre la firma più vecchia spesso finisce su 4

Come fa PDFlibPas a sapere quale campo ha modificato una revisione?

PDFlibPas ricostruisce il nome del campo dal grafo di oggetti di ogni revisione, perché un oggetto modificato raramente porta con sé il nome completo. Un dizionario di campo con /FT o /V, o un'annotazione widget, contiene solo il proprio nome parziale in /T. Il nome completo descritto in ISO 32000-1 §12.7.3.2 è la catena di nomi parziali dalla radice, uniti da punti, quindi la revision map risale i collegamenti /Parent e antepone ogni /T che trova. Ogni padre viene letto dalla sua definizione più recente alla revisione da classificare o precedente, il che impedisce a una rinomina successiva di un nodo padre di riscrivere la storia delle modifiche precedenti

Risoluzione del nome completo FieldMDP in PDFlibPas per Delphi: la revision map risale i collegamenti /Parent dal widget modificato, antepone ogni nome parziale /T unito da punti per costruire Order.Total, e lo confronta con le voci /Fields per uguaglianza esatta o prefisso con punto, così un blocco su Order copre ogni discendente
Un oggetto modificato raramente porta il proprio nome completo: i padri si leggono dalla definizione più recente alla revisione classificata o precedente, i nomi si confrontano distinguendo maiuscole e minuscole, e la risalita si ferma dopo otto livelli

Il confronto del nome risolto con i blocchi è volutamente semplice. Un nome corrisponde a una voce /Fields quando è identico alla voce o comincia con la voce seguita da un punto, quindi bloccare Order blocca anche Order.Total e ogni altro discendente. All blocca ogni nome, Include blocca i nomi corrispondenti, e Exclude blocca ogni nome che non corrisponde. Le definizioni di blocco provengono da ogni campo firma firmato presente nel file; quando un campo ha sia un dizionario /Lock sia una transform FieldMDP, si usano i valori del /Lock. Per vedere esattamente cosa ha bloccato ogni firma, la classe di livello più basso TPDFlibSignDoc dell'unità PDFlibDigitalSign espone il rapporto sui permessi che il verdetto legge:

uses
  PDFlibDigitalSign;

var
  SignDoc: TPDFlibSignDoc;
  Report: TPDFlibSignaturePermissionReport;
  Names: TStringList;
  I, J: Integer;
  Line: string;
begin
  SignDoc := TPDFlibSignDoc.Create;
  Names := TStringList.Create;
  try
    if not SignDoc.Open('purchase-request-final.pdf', FormPassword, False) then
      Exit;
    try
      SignDoc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
      begin
        SignDoc.GetSignaturePermissionInfoByName(Names[I], Report);
        if not (Report.HasFieldLock or Report.HasFieldMDP) then
          Continue;
        Line := Names[I] + ': ' + string(Report.FieldLockAction);
        for J := 0 to High(Report.FieldNames) do
          Line := Line + ' ' + string(Report.FieldNames[J]);
        if Report.HasFieldLock and Report.HasFieldMDP and
          not Report.FieldMDPMatches then
          Line := Line + ' (lock and FieldMDP transform disagree)';
        Log.Add(Line);
      end;
    finally
      SignDoc.Close;
    end;
  finally
    Names.Free;
    SignDoc.Free;
  end;
end;

Moduli cifrati: passate la password o i nomi sono illeggibili

Un modulo cifrato va decifrato prima di poter risolvere qualunque nome di campo, quindi GetSignatureModificationVerdict ha bisogno dell'argomento Password per qualcosa di più dell'apertura del file. Stringhe come /T sono cifrate per oggetto, e i campi scritti in object stream compressi non si possono leggere affatto finché lo stream che li contiene non è decifrato e decompresso. Dalla v3.586.0 la revision map interna accetta la password, aggancia un contesto di decifratura mentre analizza object stream e dizionari di revisione, e decifra ogni oggetto che decodifica, comprese le versioni precedenti con cui fa il diff e i padri che risale

La decifratura per oggetto nella revision map si attiva solo quando Password non è vuota. Un verdetto calcolato su un file cifrato con password vuota va quindi trattato come non verificato, anche se il documento si è aperto senza chiedere nulla. Conservate la password del modulo accanto al record del flusso di lavoro e passatela a ogni chiamata

Cosa non copre il verdetto FieldMDP

Il controllo dei campi è una risposta strutturale a una domanda ristretta, e diversi casi restano fuori dal suo perimetro. Conoscerli in anticipo è ciò che impedisce a un 4 di trasformarsi in una escalation al supporto:

  • Il confronto dei campi parte solo dopo che il controllo a livello di documento è uscito pulito e almeno una revisione successiva ha cambiato più che materiale di validazione o timestamp; una violazione DocMDP viene riportata come 3 prima di guardare qualunque campo
  • Gli oggetti classificati come legati alla firma ma senza nome di campo, come il dizionario del valore di firma, un dizionario AcroForm riscritto, o un cambiamento di catalogo limitato a /AcroForm o /Perms, fanno restituire 4 al verdetto invece di tirare a indovinare, così una revisione che aggiunge una firma di approvazione successiva sotto un blocco attivo spesso finisce su 4
  • Solo i dizionari di campo e le annotazioni widget ricevono un nome di campo; la revision map non ha una classe dedicata agli appearance stream, quindi un editor che rigenera gli aspetti insieme al valore può essere fermato dal controllo a livello di documento come cambiamento di contenuto prima che parta il controllo dei campi, e la stringa di dettaglio distingue i due casi
  • La scansione si ferma al primo campo bloccato o alla prima voce senza nome che incontra, quindi un 3 nomina un campo colpevole, non tutti
  • I blocchi vengono messi in comune da ogni campo firma firmato senza ordinarli nel tempo, quindi un blocco scritto da un firmatario successivo si applica anche agli aggiornamenti venuti prima; questo può solo spingere un risultato verso 3, mai verso 2
  • I nomi si confrontano in modo esatto e distinguendo le maiuscole, e la risalita dei /Parent si ferma dopo otto livelli, quindi una gerarchia insolitamente profonda si risolve in un nome troncato
  • Una voce FieldMDP la cui struttura non risulta valida e la cui azione non è leggibile fa restituire 4 al verdetto

Quando un verdetto torna 4, GetSignatureRevisionIndex dà la revisione a cui la firma termina e GetDocumentRevisionChanges elenca ogni oggetto che ogni revisione successiva ha aggiunto, modificato o liberato, con la sua classe di cambiamento. Basta a una persona per decidere in un minuto. La meccanica con cui quelle revisioni vengono accodate è trattata nella guida agli aggiornamenti incrementali per PDFlibPas

FAQ

Un verdetto 2 significa che le firme sono valide?

No. GetSignatureModificationVerdict esamina cosa è stato accodato dopo una firma e non controlla mai il valore della firma CMS, la catena di certificati o la revoca. Un 2 dice che gli aggiornamenti successivi sono rimasti dentro i permessi; la validazione crittografica è un passo a parte e servono entrambi

Perché la firma di un secondo approvatore produce 4 invece di 2?

Firmare accoda un dizionario del valore di firma e di solito riscrive la contabilità dei moduli, come il dizionario AcroForm. Quegli oggetti non appartengono a nessun campo nominato, e quando un blocco di campo è attivo il verdetto rifiuta di dare il via libera a un cambiamento che non sa attribuire a un campo. Usate GetDocumentRevisionChanges per confermare che la revisione contiene solo gli oggetti di firma attesi

Un solo nome può bloccare un intero gruppo di campi?

Sì. Una voce di blocco corrisponde al campo nominato e a ogni discendente il cui nome completo comincia con esso seguito da un punto, quindi elencare Order in un blocco Include congela Order.Customer, Order.Total e qualunque altro campo sotto quel padre

Verdetti FieldMDP, firma con blocco di campo e ispezione delle revisioni sono tutti presenti nella PDFlibPas Delphi PDF Library, insieme alle funzionalità di certificazione DocMDP e di validazione PAdES di lungo termine su cui si appoggia il flusso di approvazione descritto sopra