Technischer Artikel

FieldMDP in Delphi: Gesperrte Formularfelder prüfen

PDFlibPas beantwortet die FieldMDP-Frage über TPDFlib.GetSignatureModificationVerdict. Angesichts eines signierten PDFs, seines Kennworts und des Namens eines Signaturfelds durchläuft die Methode jedes inkrementelle Update, das nach dieser Signatur angehängt wurde, löst den vollständig qualifizierten Namen jedes Formularfelds auf, das diese Updates berührt haben, und liefert 3 zurück, wenn ein von FieldMDP gesperrtes Feld geändert wurde, und 2, wenn nur ungesperrte Felder betroffen waren. GetSignatureModificationDetail nennt anschließend die Revision und das Feld

Der Workflow, der das braucht, ist das Formular mit mehreren Freigebern. Ein Antragsteller füllt Order.Customer und Order.Total in einer Einkaufsanforderung aus und signiert Approval.Requester, wodurch diese beiden Felder gesperrt werden. Ein Manager ergänzt eine Kostenstelle und signiert Approval.Manager. Zwei Wochen später öffnet jemand die Datei in einem Desktop-Editor und schraubt Order.Total in einem dritten inkrementellen Update nach oben. Jede Signatur verifiziert weiterhin, weil jede nur ihren eigenen Bytebereich abdeckt. Die Fragen, die ein Auditor tatsächlich stellt, lauten: welches Feld sich nach welcher Signatur geändert hat und ob diese Änderung erlaubt war

Warum erkennt DocMDP allein keine bearbeiteten gesperrten Felder?

DocMDP kann das nicht aufdecken, weil DocMDP Arten von Änderungen klassifiziert, nicht einzelne Felder. Die Zertifizierungsberechtigungsstufen in ISO 32000-1 §12.8.2.2 legen fest, ob spätere Updates Formulare ausfüllen und signieren dürfen (P 2) oder auch annotieren (P 3), und diese Berechtigung gilt für jedes Feld im Dokument gleichermaßen. Das Bearbeiten von Order.Total ist Ausfüllen eines Formulars, also wird eine P-2-Zertifizierung exakt durch die Änderung erfüllt, die der Antragsteller eigentlich untersagen wollte. Die dokumentweite Änderungsklassifizierung in PDFlibPas deckt diese Ebene ab, und dieser Artikel geht eine Stufe feiner

FieldMDP ist die Ebene mit Feldgranularität. Ein Signaturfeld kann ein /Lock-Dictionary tragen (ISO 32000-1 §12.7.4.5), und die Signatur, die es enthält, kann in ihrer Signature-Reference eine FieldMDP-Transform mitführen (ISO 32000-1 §12.8.2.4). Beide nutzen dasselbe Vokabular: /Action /All sperrt jedes Feld, /Action /Include sperrt die in /Fields aufgeführten Namen, und /Action /Exclude sperrt alles außer den Namen in /Fields. Vor v3.586.0 wusste GetSignatureModificationVerdict zwar, dass eine spätere Revision ein Formularfeld geändert hatte, aber nicht welches, sodass jedes Dokument mit einer Feldsperre, dessen spätere Updates über Validierungsmaterial hinausgingen, als 4, unbestimmt, zurückkam. Die Revisions-Map verzeichnet jetzt für jedes feldbezogene Objekt, das sie klassifiziert, einen aufgelösten Feldnamen, und genau das macht aus der 4 ein echtes 2 oder 3

Setzen der Feldsperre, wenn jeder Freigeber signiert

TPDFlib.SetSignProcessFieldLock setzt die Sperre für die nächste Signatur, die ein Signaturprozess erzeugt. Aktion 1 ist All, 2 ist Include, 3 ist Exclude, und 0 entfernt die Sperre; Include und Exclude nehmen eine durch Semikolons getrennte Liste vollständig qualifizierter Feldnamen entgegen. Das signierte Feld erhält am Ende ein /Lock-Dictionary und eine passende FieldMDP-Transform — genau die Paarung, die strenge Validatoren erwarten

PDFlibPas SetSignProcessFieldLock-Aktionscodes für FieldMDP-Signierung in Delphi: Aktion 1 All sperrt jedes Formularfeld einschließlich späterer Signaturfelder, Aktion 2 Include sperrt die genannten Datenfelder, Aktion 3 Exclude sperrt alles außer der Liste, und Aktion 0 entfernt die Sperre
Wählen Sie Include für die Datenfelder, die jeder Freigeber besitzt, oder Exclude mit den späteren Signaturfeldern in der Liste; ein abgelehnter Aufruf gibt 0 zurück und lässt die bisherige Sperre stehen, prüfen Sie also stets den Rückgabewert
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: nur diese Felder friert diese Signatur ein
      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 gibt 0 zurück, wenn ein Include- oder Exclude-Aufruf keine Namen enthält, und ebenso bei einer Liste mit leerem Eintrag wie 'Order.Customer;;Order.Total'; ein abgelehnter Aufruf lässt die zuvor konfigurierte Sperre unverändert, prüfen Sie den Rückgabewert also, statt anzunehmen, der letzte Aufruf habe gewonnen. Den Rest der Signatur-Einrichtung — Wahl des Digests, /Contents-Reservierung und Ergebniscodes — behandelt der PAdES-Leitfaden für Signierung und Validierung mit PDFlibPas. Ein Designpunkt ist speziell für Freigabeketten relevant: Eine /All-Sperre auf der Signatur des Antragstellers friert auch Approval.Manager ein, denn ein Signaturfeld ist ebenfalls ein Formularfeld. Verwenden Sie Include für die Datenfelder, die jeder Freigeber besitzt, oder Exclude mit den späteren Signaturfeldern in der Liste

Prüfergebnisse entlang einer Freigabekette ermitteln

GetSignatureModificationVerdict beurteilt eine Signatur nach der anderen, eine Freigabekette wird also geprüft, indem die Methode pro Signaturfeld einmal aufgerufen wird. Der Rückgabewert ist eine kleine, geschlossene Menge, und jeder Wert bedeutet für einen Prüfer etwas anderes:

  • 0: Die Datei oder das genannte Signaturfeld ließ sich nicht lesen
  • 1: Nach der Signatur wurde nichts angehängt
  • 2: Es wurden nur Änderungen angehängt, die die effektiven Berechtigungen zulassen, und kein gesperrtes Feld wurde berührt
  • 3: Eine spätere Revision hat eine nicht erlaubte Änderung vorgenommen, einschließlich einer Bearbeitung eines von FieldMDP gesperrten Felds
  • 4: Die Antwort ließ sich nicht ermitteln, was niemals als Freigabe gelesen werden darf
  • 5: Die Signatur endet nicht auf einer Revisionsgrenze, sodass Bytes innerhalb des Bereichs liegen, den sie für sich beansprucht
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]);
      // beide Getter beschreiben den gerade gemachten Aufruf, also lesen Sie sie vor dem nächsten
      Log.Add(Format('%s: verdict %d, MDP level %d, %s',
        [Approvals[I], Verdict, Pdf.GetSignatureEffectiveMDPLevel,
         Pdf.GetSignatureModificationDetail]));
      case Verdict of
        1, 2: ;                                 // akzeptiert
        3: RejectForm(Approvals[I]);            // Detail nennt Feld und Revision
      else
        QueueForManualReview(Approvals[I]);     // 0, 4, 5
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Nehmen wir die manipulierte Einkaufsanforderung, bei der Revision 0 das leere Formular ist, die Revisionen 1 und 2 die beiden Signaturen sind und Revision 3 die Bearbeitung. Weil die Sperrdefinitionen aus allen signierten Feldern zusammengeführt werden, setzt das Prüfergebnis für Approval.Manager bereits die Sperre des Antragstellers durch, und sein einziges späteres Update ist die Bearbeitung selbst. Wenn dieses Update nur das Feld-Dictionary umgeschrieben hat, lautet das Ergebnis 3, und die Detailmeldung liest sich Revision 3 changed locked form field "Order.Total", which FieldMDP does not allow. Diese eine Zeile beantwortet beide Auditfragen. Das Ergebnis für Approval.Requester muss erst an der signierten Manager-Revision vorbeikommen, und aus den Gründen im Einschränkungsteil unten meldet es oft 4, daher liefert die jüngste Signatur meist die schärfste Antwort. GetSignatureEffectiveMDPLevel meldet den niedrigsten DocMDP-P-Wert, der unter den Signaturen gefunden wurde, oder 3, wenn nichts das Dokument zertifiziert hat, und beide Getter bleiben nur bis zum nächsten Prüfergebnis-Aufruf gültig

Zeitstrahl der inkrementellen Updates einer von PDFlibPas GetSignatureModificationVerdict gelesenen, manipulierten Delphi-PDF: Revision 1 trägt eine Include-Sperre für Order.Customer und Order.Total, Revision 2 signiert Approval.Manager, und die Neuschreibung des gesperrten Felds Order.Total in Revision 3 liefert Ergebnis 3 für die Manager-Signatur und 4 für die Antragsteller-Signatur
Sperren werden aus jedem signierten Feld zusammengeführt, daher setzt das Ergebnis für Approval.Manager bereits die Antragsteller-Sperre durch und der Detailstring nennt das betroffene Feld und die Revision, während die früheste Signatur oft bei 4 landet

Woher weiß PDFlibPas, welches Feld eine Revision geändert hat?

PDFlibPas rekonstruiert den Feldnamen aus dem Objektgraphen jeder Revision, denn ein geändertes Objekt trägt selten seinen vollständigen Namen. Ein Feld-Dictionary mit /FT oder /V oder eine Widget-Annotation enthält nur seinen Teilnamen in /T. Der in ISO 32000-1 §12.7.3.2 beschriebene vollständig qualifizierte Name ist die Kette der Teilnamen von der Wurzel, verbunden mit Punkten; daher läuft die Revisions-Map die /Parent-Verweise nach oben ab und stellt jeden gefundenen /T voran. Jedes übergeordnete Element wird aus seiner neuesten Definition zum oder vor dem Zeitpunkt der zu klassifizierenden Revision gelesen, was verhindert, dass eine spätere Umbenennung eines Elternknotens die Geschichte früherer Bearbeitungen umschreibt

Auflösung vollständig qualifizierter FieldMDP-Namen in PDFlibPas für Delphi: die Revisions-Map läuft die /Parent-Verweise vom geänderten Widget nach oben ab, stellt jeden /T-Teilnamen, verbunden mit Punkten, voran, um Order.Total zu bilden, und gleicht ihn mit den /Fields-Einträgen über exakte Gleichheit oder Punkt-Präfix ab, sodass eine Sperre auf Order jeden Nachfahren abdeckt
Ein geändertes Objekt trägt selten seinen vollständigen Namen: Eltern werden aus ihrer neuesten Definition zur oder vor der klassifizierten Revision gelesen, Namen werden case-sensitiv verglichen, und der Lauf endet nach acht Ebenen

Der Abgleich des aufgelösten Namens mit den Sperren ist bewusst einfach gehalten. Ein Name passt zu einem /Fields-Eintrag, wenn er dem Eintrag exakt entspricht oder mit dem Eintrag gefolgt von einem Punkt beginnt, sodass das Sperren von Order auch Order.Total und jeden anderen Nachfahren sperrt. All sperrt jeden Namen, Include sperrt die passenden Namen, und Exclude sperrt jeden Namen, der nicht passt. Die Sperrdefinitionen stammen aus jedem signierten Signaturfeld der Datei; wo ein Feld sowohl ein /Lock-Dictionary als auch eine FieldMDP-Transform besitzt, werden die /Lock-Werte verwendet. Um exakt zu sehen, was jede Signatur gesperrt hat, legt die tiefer angesiedelte Klasse TPDFlibSignDoc aus der Unit PDFlibDigitalSign den Berechtigungsbericht offen, aus dem das Prüfergebnis liest:

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;

Verschlüsselte Formulare: Kennwort mitgeben, sonst bleiben die Namen unlesbar

Ein verschlüsseltes Formular muss erst entschlüsselt werden, bevor sich ein Feldname auflösen lässt, daher braucht GetSignatureModificationVerdict das Argument Password zu mehr als nur zum Öffnen der Datei. Zeichenketten wie /T werden objektweise verschlüsselt, und Felder, die in komprimierte Objekt-Streams geschrieben wurden, lassen sich überhaupt nicht lesen, bevor der umgebende Stream entschlüsselt und entpackt wurde. Seit v3.586.0 akzeptiert die interne Revisions-Map das Kennwort, hängt beim Parsen von Objekt-Streams und Revisions-Dictionarys einen Entschlüsselungskontext an und entschlüsselt jedes Objekt, das sie dekodiert, einschließlich der früheren Versionen, mit denen sie abgleicht, und der Elternknoten, die sie durchläuft

Die objektweise Entschlüsselung in der Revisions-Map schaltet sich nur ein, wenn Password nicht leer ist. Ein Prüfergebnis, das auf einer verschlüsselten Datei mit leerem Kennwort berechnet wurde, sollte daher als unbestätigt behandelt werden, selbst wenn sich das Dokument selbst ohne Nachfrage öffnen ließ. Legen Sie das Formular-Kennwort neben dem Workflow-Datensatz ab und geben es bei jedem Aufruf mit

Was das FieldMDP-Prüfergebnis nicht abdeckt

Die Feldprüfung ist eine strukturelle Antwort auf eine einzige enge Frage, und etliche Fälle fallen heraus. Wer sie im Voraus kennt, verhindert, dass aus einer 4 ein Support-Eskalationsfall wird:

  • Der Feldvergleich läuft erst, nachdem die Prüfung auf Dokumentebene sauber durch ist und mindestens eine spätere Revision mehr geändert hat als Validierungsmaterial oder Zeitstempel; eine DocMDP-Verletzung wird als 3 gemeldet, bevor überhaupt ein Feld angesehen wird
  • Objekte, die als signaturbezogen klassifiziert werden, aber keinen Feldnamen tragen — etwa das Signature-Value-Dictionary, ein umgeschriebenes AcroForm-Dictionary oder eine Katalogänderung, auf /AcroForm oder /Perms begrenzt ist — bringen das Prüfergebnis dazu, 4 zurückzugeben, statt zu raten; eine Revision, die unter einer aktiven Sperre eine spätere Freigabesignatur ergänzt, landet deshalb oft bei 4
  • Nur Feld-Dictionarys und Widget-Annotationen erhalten einen Feldnamen; die Revisions-Map hat keine eigene Klasse für Appearance-Streams, sodass ein Editor, der Darstellungen zusammen mit dem Wert neu generiert, schon vom Check auf Dokumentebene als Inhaltsänderung gestoppt werden kann, bevor die Feldprüfung läuft, und der Detailstring unterscheidet die beiden Fälle
  • Der Scan stoppt am ersten gesperrten Feld oder unbenannten Eintrag, dem er begegnet, sodass eine 3 ein betroffenes Feld nennt, nicht alle
  • Sperren werden aus jedem signierten Signaturfeld zusammengeführt, ohne sie zeitlich zu ordnen, sodass eine Sperre, die ein späterer Unterzeichner schreibt, auch auf Updates angewendet wird, die vor ihr kamen; das kann ein Ergebnis nur Richtung 3 schieben, nie Richtung 2
  • Namen werden exakt und case-sensitiv verglichen, und der /Parent-Lauf endet nach acht Ebenen, sodass eine ungewöhnlich tiefe Hierarchie zu einem abgeschnittenen Namen aufgelöst wird
  • Ein FieldMDP-Eintrag, dessen Struktur nicht validiert und dessen Aktion sich nicht lesen lässt, bringt das Prüfergebnis dazu, 4 zurückzugeben

Wenn ein Prüfergebnis als 4 zurückkommt, nennt GetSignatureRevisionIndex die Revision, bei der die Signatur endet, und GetDocumentRevisionChanges listet jedes Objekt auf, das jede spätere Revision hinzugefügt, geändert oder freigegeben hat, samt seiner Änderungsklasse. Das reicht einer Person, um in einer Minute zu entscheiden. Die Mechanik, wie diese Revisionen angehängt werden, behandelt der Leitfaden zu inkrementellen Updates für PDFlibPas

FAQ

Bedeutet ein Ergebnis von 2, dass die Signaturen gültig sind?

Nein. GetSignatureModificationVerdict untersucht, was nach einer Signatur angehängt wurde, und prüft nie den CMS-Signaturwert, die Zertifikatkette oder die Sperrung. Eine 2 sagt aus, dass die späteren Updates innerhalb der Berechtigungen blieben; die kryptografische Validierung ist ein separater Schritt, und beides wird gebraucht

Warum liefert die Signatur eines zweiten Freigebers 4 statt 2?

Beim Signieren wird ein Signature-Value-Dictionary angehängt und meist Formular-Buchhaltung wie das AcroForm-Dictionary umgeschrieben. Diese Objekte gehören zu keinem benannten Feld, und solange eine Feldsperre aktiv ist, weigert sich das Prüfergebnis, eine Änderung freizugeben, die sich keinem Feld zuordnen lässt. Verwenden Sie GetDocumentRevisionChanges, um zu bestätigen, dass die Revision nur die erwarteten Signaturobjekte enthält

Kann ein einzelner Name eine ganze Gruppe von Feldern sperren?

Ja. Ein Sperreintrag passt zum genannten Feld und zu jedem Nachfahren, dessen vollständig qualifizierter Name mit ihm gefolgt von einem Punkt beginnt, sodass Order in einer Include-Sperre Order.Customer, Order.Total und jedes andere Feld unter diesem Elternelement einfriert

FieldMDP-Prüfergebnisse, das Signieren von Feldsperren und die Revisionsinspektion sind alle in der PDFlibPas Delphi PDF Library enthalten, zusammen mit den DocMDP-Zertifizierungs- und PAdES-Long-Term-Validation-Funktionen, auf die der obige Freigabe-Workflow baut