Technischer Artikel

DocMDP und FieldMDP: PDF-Revisionen prüfen in Delphi

Eine signierte PDF-Datei, die sich nach der Signatur verändert hat, ist nicht automatisch defekt. ISO 32000-1 erlaubt inkrementelle Updates auf einer Signatur, und nur manche davon verletzen die vom Signierenden festgelegte Policy. Die HotPDF-Komponente für Delphi und C++Builder beantwortet diese Frage mit AnalyzeLoadedSignatureRevisions, das jede Revision nach der Signatur klassifiziert und gegen DocMDP und FieldMDP bewertet. Das Szenario ist jedem vertraut, der Vertragssoftware ausliefert: Der Kunde unterschreibt einen Kaufvertrag, versendet ihn, und bekommt ihn mit einer angehängten Anlageseite zurück. Der Reader zeigt einen gelben Balken, dass die Signatur intakt ist, das Dokument sich seit der Signierung jedoch verändert hat, und niemand im Raum kann sagen, ob das ein normaler Gegenzeichnungs-Workflow ist oder jemand still einen signierten Vertrag bearbeitet hat

Was gilt als legale Änderung nach der Signierung?

Eine Änderung ist legal, wenn ihre semantische Kategorie innerhalb der von der zertifizierenden Signatur erklärten Berechtigung liegt. ISO 32000-1 §12.8.2.2 definiert die DocMDP-Transform mit einem /P-Wert von 1, 2 oder 3: 1 erlaubt überhaupt keine Änderungen, 2 erlaubt Formularausfüllung und Signierung, 3 erlaubt Formularausfüllung, Signierung und Annotationen. HotPDF stellt sie als THPDFDocMDPPermission-Werte dmpNoChanges, dmpFormFillAndSign und dmpFormFillSignAndAnnotate bereit, wobei dmpNone für Prüfergebnisse ohne jede DocMDP-Transform reserviert ist

Die Kategorien sind geordnet, und diese Ordnung ist der Motor der gesamten Prüfung. THPDFRevisionModificationLevel läuft über rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, bewusst so angeordnet, dass eine größere Ordinalzahl nie weniger restriktiv ist. Ein ganzes Dokument reduziert sich auf das über alle Revisionen nach der Signatur beobachtete Maximallevel, und der DocMDP-Vergleich wird zu einem einzigen Integer-Test. Eine Nuance ist früh wichtig: Bei dmpNoChanges akzeptiert die Analyse dennoch rmlLongTermValidation. DSS- und VRI-Validierungsmaterial oder einen Dokument-Zeitstempel zu einer zertifizierten Datei hinzuzufügen, ist Pflege der Signatur, keine Änderung des Dokuments, und dies als Verstoß zu behandeln, würde jeden Langzeitarchivierungs-Workflow zerstören, der existiert

Wie baut HotPDF die Revisionskette wieder auf?

Strukturell, nicht heuristisch. Nach ISO 32000-1 §7.5.6 hängt ein inkrementelles Update einen neuen Querverweis-Abschnitt an, dessen /Prev auf den vorherigen zeigt, sodass HotPDF startxref vom Ende liest, den dortigen Abschnitt parst, /Prev rückwärts folgt und das wiederholt, wobei die Abschnitte in der Reihenfolge vom ältesten zum neuesten zurückgegeben werden. Zwei Sicherheitsgrenzen sitzen in dieser Schleife, und beide sind bei der Fehlersuche einer Datei, die fehlschlägt, wissenswert: Ein /Prev, der auf einen bereits besuchten Offset zeigt, beendet den Durchlauf mit einer expliziten Zyklus-Diagnose statt in eine Endlosschleife zu geraten, und eine Kette mit mehr als tausend Revisionen wird von vornherein zurückgewiesen. Beides erscheint in Analysis.Issue, während die Funktion False zurückgibt, und keines von beidem sollte überdeckt werden, denn ein zyklisches /Prev ist eine fehlerhafte oder feindliche Datei, keine bloß ungewöhnliche

Vier historische Formen tauchen in echten Dokumenten auf, und alle vier werden behandelt: klassische xref-Tabellen, Zeile für Zeile geparst, Querverweis-Streams, dekomprimiert und über ihre /W- und /Index-Felder dekodiert, Hybrid-Querverweis-Dateien, deren klassischer Trailer einen /XRefStm-Schlüssel trägt, der geparst und in dieselbe Revision zusammengeführt wird (der Office-Producer-Fall, behandelt in dem Artikel zu hybriden Querverweis-Streams), und Objekte, die innerhalb eines ObjStm-Containers leben, was relevant ist, weil ein modernes Update das geänderte Dictionary meist in einen komprimierten Stream statt es direkt zu schreiben legt, wie in dem Beitrag zu Object Streams und inkrementellen Updates beschrieben. Die Signatur bildet die Trennlinie: /ByteRange[2] + /ByteRange[3] wird zu SignedRevisionLength, und jeder Abschnitt bei oder jenseits dieses Offsets liegt nach der Signatur. Ob der Byte-Bereich noch korrekt hasht, ist eine separate Frage, die VerifyLoadedSignature beantwortet, behandelt in dem Artikel zur Verifikation digitaler PDF-Signaturen

Wie jedes geänderte Objekt klassifiziert wird

Die Klassifikation läuft pro Objekt und wird dann entlang von Referenzen weitergegeben. Für jede Objektnummer, die ein Abschnitt nach der Signatur berührt, liest HotPDF den neuen Body und den Body, wie er im signierten Snapshot stand; ein identischer Body ist rmlNone, weil Producer Objekte durchaus neu schreiben, ohne sie zu verändern. Die Erkennungslogik ist bewusst eng. Ein Objekt vom Typ /Type /DocTimeStamp, oder eines, dessen /SubFilter ETSI.RFC3161 ist, ist rmlLongTermValidation, ebenso alles, was über den Catalog-Baum /DSS erreichbar ist; ein Dictionary vom Typ /Type /Sig ist rmlFormFillAndSign. Bei Containern ist der Test, welche Schlüssel sich bewegt haben, nicht was das Objekt ist: Der Catalog darf nur /DSS, /Extensions oder /AcroForm hinzufügen oder ändern; das AcroForm-Dictionary nur /Fields, /SigFlags, /NeedAppearances, /DR, /DA oder /Q; eine Seite nur /Annots; ein Feld oder Widget nur /V, /AP, /AS oder /M. Alles außerhalb dieser Mengen fällt auf rmlOther zurück, und genau so wird die angehängte Anlageseite erwischt: Das Hinzufügen einer Seite ordnet den Seitenbaum auf eine Weise um, die keine Whitelist abdeckt, und keine Menge legitimer Formularausfüllung sieht dem ähnlich

Dann breiten sich die Level aus, wobei jeder Container das Maximallevel der geänderten Kinder erbt, auf die er verweist, iteriert bis die Zuordnung stabil ist. Das macht Appearance-Streams funktionsfähig. Ein ausgefülltes Textfeld schreibt /V neu und verweist auf einen frischen /AP-Stream, und dieser Stream für sich genommen ist ein anonymer Klumpen von Content-Operatoren ohne erkennbaren Typ; weil das Feld, dem er gehört, rmlFormFillAndSign ist, erbt der Stream dasselbe Level, statt auf rmlOther durchzufallen. Dieselbe Ausbreitung trägt DSS-Kontext auf Zertifikats- und Sperrlisten-Streams, die sonst nicht klassifizierbar wären

Warum zählt ein unlesbares Objekt als Verstoß?

Weil die Alternative ein Validator ist, der geschlagen wird, indem man etwas schreibt, das er nicht versteht. Drei Situationen landen in HotPDF ohne Widerspruch bei rmlOther: ein Objekt, dessen Body aus der Revision nicht gelesen werden konnte, ein Objekt, das die Revision als frei markiert, und ein Objekt, das zu keiner der obigen Erkennungsregeln passt. Jede davon protokolliert eine konkrete Diagnose im Issue-Feld der Revision, sodass ein Operator sehen kann, welche Objektnummer das Urteil hervorgebracht hat

Freigabe ist die schärfste der drei. Eine Revision nach der Signatur, die ein zuvor definiertes Objekt als frei markiert, hat Inhalt aus einem signierten Dokument gelöscht, und kein Berechtigungslevel nach §12.8.2.2 erlaubt das; die Objektnummern landen in FreedObjectNumbers, und die Revision wird auf rmlOther angehoben. Unlesbare Objekte folgen derselben Logik aus einem anderen Grund. Ein Validator, der ein Objekt nicht parsen kann, hat keine Grundlage, es für harmlos zu erklären, und die ehrliche Antwort darauf ist nicht Schweigen. Ein ungewöhnliches, aber harmloses Konstrukt als Verstoß zu melden, kostet eine manuelle Prüfung; der umgekehrte Fehler liefert einen signierten Vertrag mit einer unbemerkten Änderung darin aus

Das Urteil in Delphi lesen

Der Aufruf ist kurz. Dokument laden, einen Signaturindex wählen, den Datensatz lesen; die parameterlose Überladung öffnet die Datei, aus der das Dokument geladen wurde, erneut, und die TStream-Überladung nimmt vom Aufrufer bereitgestellte Bytes entgegen und stellt die Stream-Position wieder her, bevor sie zurückkehrt. PolicyCompliant ist der einzelne Boolean, den die meisten Aufrufer wollen, und kombiniert drei unabhängige Entscheidungen: die strukturelle Gültigkeit der Berechtigungs-Dictionaries, DocMDPCompliant und FieldMDPCompliant. Man sollte die Komponenten in der eigenen UI sichtbar halten, statt sie zusammenzufalten, und beachten, dass ein Dokument ohne DocMDP-Transform DocMDPCompliant auf True belässt, da eine gewöhnliche Genehmigungssignatur keine Policy erklärt, die verletzt werden könnte, und das aggregierte ModificationLevel dann beschreibend ist statt ein Urteil

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;

Für die Triage will man meist die Aufschlüsselung pro Revision statt der Zusammenfassung, weil sie zeigt, wann in der Dokumentgeschichte etwas schiefging. Jeder Eintrag in Analysis.Revisions trägt seinen Index in der Kette, den Querverweis-Offset, an dem er geschrieben wurde, sein eigenes Modifikationslevel und die beteiligten Objektnummern

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 wird separat beurteilt, und das ist Absicht

Ein Dokument kann DocMDP erfüllen und trotzdem nicht legitim sein, weshalb FieldMDPCompliant ein eigenständiger Boolean ist statt in den Level-Vergleich eingefaltet zu werden. ISO 32000-1 §12.8.2.4 definiert die FieldMDP-Transform, und §12.7.5.5 den zugehörigen Eintrag /SigFieldLock, um benannte Formularfelder im Moment der Signierung einzufrieren, selbst wenn das Dokument als Ganzes noch Formularausfüllung erlaubt. Ein Feld auszufüllen ist eine Level-2-Aktion; ein vom Signierenden gesperrtes Feld auszufüllen ist ein Verstoß, unabhängig vom Level. HotPDF liest den Geltungsbereich in THPDFFieldLockAction als flaAll, flaInclude oder flaExclude, mit flaNone für Ergebnisse ohne Sperrpolicy, und die Namen in Permissions.FieldNames: flaAll sperrt alles, flaInclude sperrt die aufgeführten Namen, flaExclude sperrt alles außer ihnen. Ein Detail ist beim Lesen der Ergebnisse wichtig: In ChangedFieldNames werden nur Felder gemeldet, die bereits im signierten Snapshot vorhanden waren, weil ein erst nach der Signierung erstelltes Feld keinen signierten Zustand hat, dem es widersprechen könnte, und stattdessen vom DocMDP-Pfad erfasst wird

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;

Was diese Analyse nicht verrät

Sie verifiziert keine Signatur. AnalyzeLoadedSignatureRevisions urteilt über Struktur und Berechtigungen; ob der signierte Byte-Bereich noch auf den Wert im CMS-Blob hasht und ob sich das Signaturzertifikat auf etwas Vertrauenswürdiges zurückführen lässt, beantworten VerifyLoadedSignature und VerifyLoadedSignatureWithTrust. Eine Datei kann policy-konform und dennoch kryptographisch wertlos sein, daher gehören die beiden Prüfungen in jedem echten Abnahme-Gate nebeneinander. Sie liest auch keine Absicht innerhalb von Content-Streams: Eine Seite, deren Content-Stream vollständig ersetzt wurde, wird als Änderung außerhalb der Whitelist erkannt, aber die Analyse verrät nicht, dass der Ersatz eine Zahlungssumme ausgetauscht hat. Ein Urteil rmlOther bedeutet, dass ein Mensch hinsehen sollte, nicht dass Betrug vorliegt, und ein konformes Urteil bedeutet, dass die Änderung in eine erlaubte Kategorie passt, nicht dass die Änderung erwünscht war. Wer nur wissen will, was der Signierende erklärt hat, ohne den Revisionsdurchlauf, bekommt mit GetLoadedSignaturePermissions die Berechtigungs-Dictionaries für sich allein zurück

Alles hier Beschriebene läuft nativ in Delphi und C++Builder, ohne externen Signaturdienst in der Kette, was es praktikabel macht, es auf jedem eingehenden Dokument laufen zu lassen statt nur auf jenen, die bereits verdächtig wirkten. Die vollständige Signatur- und Revisions-API, einschließlich der Berechtigungs- und Verifikationsmethoden, mit denen sie zusammenspielt, ist Teil der HotPDF-Komponente für Delphi und C++Builder