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
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
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
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
/AcroFormoder/Permsbegrenzt 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