In PDFium Component Builds vor v3.126.2 konnte TPdf.AnalyzeSignatureRevisions einen echten Seiteninhalts-Edit als erlaubte Annotations-Änderung unter DocMDP P=3 werten, weil sein Revisions-Rollengraph den /P-Rückverweis des Signature-Widgets auf seine Seite als Ownership behandelte. Seit v3.126.2 trennt PDFium Component Navigations-Kanten von owned-payload-Kanten, Seiteninhalt bleibt also Seiteninhalt. Der Bug-Report hinter diesem Fix sieht auf dem Papier harmlos aus. Ein zertifizierter Vertrag erlaubt Annotationen, die Gegenseite hängt einen Incremental Save an, und der Analyzer sagt, jede spätere Änderung sei erlaubt. Dann diffed jemand die gerenderten Seiten, und der Zahlungsbetrag auf Seite 2 ist ein anderer
Dieser Artikel ist die Fortsetzung aus Angreifersicht zur Übersicht über die Post-Signature-Revisions-Änderungsanalyse, überspringt also die Grundlagen von Rebuild und DocMDP-Bewertung und geht direkt in den Objektgraphen: wie Ownership modelliert war, warum die Richtung einer Kante über ein Sicherheitsurteil entscheidet, was sich in v3.126.2 geändert hat, und wie Sie Ihre eigene Annahme-Logik auditieren
Warum ging ein Seiten-Edit als Annotations-Änderung unter DocMDP P=3 durch?
Der Seiten-Edit ging durch, weil der alte Rollengraph jedem indirekten Verweis in einem Dictionary folgte, als gehöre das referenzierte Objekt zum verweisenden, und das Signature-Widget zeigt zurück auf seine Seite. Ein Annotation-Dictionary trägt /P, einen indirekten Verweis auf das Seitenobjekt, auf dem es sitzt (ISO 32000-1 §12.5.2). Dieser Eintrag ist ein Navigations-Hinweis. Das Widget besitzt die Seite nicht; die Seite besitzt das Widget über ihr /Annots-Array
Der Analyzer weist jedem Objekt vor der Bewertung späterer Änderungen eine Menge von Rollen-Bits zu: Seite, Annotation, Form und Validierungsmaterial. Root-Objekte bekommen ihre Rolle aus ihrem eigenen Dictionary, und die Rolle breitet sich dann auf alles aus, was sie referenzieren. In der alten Propagation lief die Kette so:
- Das Signature-Widget ist ein
/Subtype /Widgetmit/FT /Sig, bekommt also die Annotations-Rolle - Das
/Pdes Widgets drückt die Annotations-Rolle auf das Seiten-Dictionary, das die Seiten-Rolle bereits hat - Die Seite drückt beide Rollen in
/Contents,/Resourcesund über/Parenthinauf in den Pages-Baum und hinüber zu jeder Geschwisterseite - Ein Content-Stream-Dictionary wie
<< /Length 812 >>hat kein/Type, der Klassifizierer griff also auf die Rollen-Bits zurück und prüfte die Annotations-Rolle vor der Seiten-Rolle
Der modifizierte Content-Stream kam also als prckAnnotation heraus. Unter ISO 32000-1 §12.8.2.2 erlaubt DocMDP P=3 Annotations-Änderungen, das Urteil war also prdAllowed, und der Report rollte zu prasAllowed auf. Dieselbe Datei unter P=2 wurde abgewiesen, aber nur durch Zufall: P=2 verbietet Annotations-Änderungen, der fehlbeschriftete Stream wurde also aus dem falschen Grund abgelehnt. Eine feste Vier-Durchläufe-Propagations-Schleife legte eine zweite Schwäche nach. Payload, die über ein indirektes Array erreicht wurde oder über eine lange Kette, deren Objektnummern rückwärts laufen, bekam unter Umständen nie irgendeine Rolle
Warum muss ein Signature-Validator fragen, wem ein Objekt gehört?
Ein Signature-Validator muss fragen, wem ein Objekt gehört, weil PDF-Incremental-Updates (ISO 32000-1 §7.5.6) jedem erlauben, eine Revision anzuhängen, die eine bestehende Objektnummer neu definiert, und der neu definierte Body verrät nicht, was er ist. Die Signatur verifiziert weiterhin, sie deckt ja nur die Bytes ihrer eigenen Revision. Jede Verteidigung gegen Manipulation nach dem Signieren hängt deshalb davon ab, jedes geänderte Objekt der Struktur zuzuordnen, die es benutzt, und dann zu fragen, ob der Unterzeichner erlaubt hat, dass sich diese Struktur ändert
Mehrere veröffentlichte Angriffsklassen arbeiten exakt in dieser Lücke. Incremental-Saving-Angriffe hängen eine Revision an, die Seiteninhalt austauscht, und setzen darauf, dass der Verifier nur die signierte Byte-Range prüft. Shadow-Angriffe pflanzen versteckten Inhalt vor dem Signieren ein und aktivieren ihn danach mit einer kleinen, unverfänglichen Änderung. Angriffe auf zertifizierte Dokumente missbrauchen die Tatsache, dass P=2 und P=3 manche späteren Edits ausdrücklich erlauben, und kleiden dann einen verbotenen Edit als erlaubten. Ein Verifier, der Objekte nach Labels wie /Type /Annot klassifiziert oder nach irgendeinem Referenzpfad, der zufällig zu ihnen führt, ist der dritten Klasse ausgesetzt: Der Angreifer braucht nur eine erlaubte Struktur, die die verbotene erreichen kann
Deshalb lautet die Frage nicht, welche Objekte sich geändert haben, sondern wem sie gehören. Ein Content-Stream, den eine Seite über /Contents erreicht, ist Seiteninhalt, egal was sonst noch auf ihn zeigt. Eine Annotation, die über /P zurück auf die Seite zeigt, sagt, wo die Annotation lebt, nicht, was sie besitzt
Wie modelliert PDFium Component v3.126.2 Ownership?
PDFium Component v3.126.2 behandelt Rückverweise als Navigation, hält sie aus der Rollen-Propagation heraus und entscheidet anhand der strukturellen Rolle des besitzenden Dictionarys, welche Keys als Navigation zählen, nicht allein am Key-Namen. Die Tabelle fasst die Navigations-Keys zusammen, die keine Ownership mehr tragen
| Besitzer-Dictionary | Als Navigation behandelte Keys | Spezifikations-Referenz |
|---|---|---|
| Page- oder Pages-Knoten | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotation oder Widget | /P | ISO 32000-1 §12.5.2 |
| Widget- oder Field-Dictionary | /Parent | ISO 32000-1 §12.7.3 |
Global nach Key-Namen zu filtern hätte ein neues Loch gerissen. Eine Font- oder XObject-Ressource darf legitim /P, /Parent oder /Annots heißen, und ein /Resources-Dictionary, das seinen /P-Eintrag aus der Propagation nimmt, ließe einen Angreifer ein seitenbesessenes XObject hinter einem unverfänglichen Ressourcennamen verstecken. In v3.126.2 greift der Navigationsfilter nur, wenn das besitzende Dictionary tatsächlich eine Seite, ein Pages-Knoten, eine Annotation, ein Widget oder ein Field ist. Trägt eines dieser Dictionarys einen doppelt gesetzten Navigations-Key, etwa zwei /P-Einträge in einem Widget, rät der Analyzer nicht, welche Kopie ein Viewer benutzen würde; der Rollenaufbau schlägt fehl, und die Signatur wird Indeterminate
Mehrere weitere Regeln schließen die verbleibenden Umfirmierungs-Wege:
- Pages-Knoten sind in eigenem Recht Seiten-Rollen-Roots, Ressourcen, die aus dem Pages-Baum geerbt werden (ISO 32000-1 §7.7.3.4), gelangen also über echte Ownership in den Seiten-Kontext, nicht über einen
/Parent-Spaziergang von einer Kindseite - Eine Annotations- oder Form-Rolle, die bei einem Catalog, Pages-Knoten, einer Seite, einer Annotation oder einem Field-Dictionary ankommt, endet dort, denn diese strukturellen Objekte begründen ihre eigenen Rollen, und eine hereinkommende Payload-Rolle darf sie nicht überstimmen
- Die Seiten-Rolle ist bei der Klassifizierung autoritativ: Ein seitenbesessenes Objekt ist
prckPageContent, selbst wenn eine spätere Revision es mit einem gefälschten/FT, einem/Type /Annot-Label umschreibt oder es mit einem Appearance-Stream teilt - Ein Form XObject, das nur als Field- oder Annotation-Appearance dient, behält seine Form- oder Annotations-Kategorie, die gewöhnliche Appearance-Regenerierung nach einem Form-Fill wird also weiterhin nach den normalen Erlaubnis-Regeln bewertet
- Ein Widget ohne eigenes
/FTlöst den geerbten Feldtyp über die/Parent-Kette auf, und eine nicht auflösbare Kette lässt den Rollenaufbau scheitern, statt auf Annotation zu defaulten - Die Rollen-Bits jeder späteren Revision werden in die Rollen der abgedeckten Revision verschmolzen, ein späteres Update kann also eine frühere Seiten-Ownership-Beziehung nicht auslöschen, indem es zuerst einen Stream abkoppelt und ihn danach editiert
Fixpunkt statt fester Durchlaufanzahl
Die Rollen-Erreichbarkeit läuft in v3.126.2 als Work-Queue, die iteriert, bis kein Objekt mehr ein neues Rollen-Bit gewinnt, ein echter Fixpunkt, egal wie tief die Kette oder wie die Objekte nummeriert sind. Indirekte Arrays, etwa ein als eigenes Objekt gespeichertes /Contents-Array, werden mit abgelaufen. Jedes Objekt kann höchstens vier verschiedene Rollen-Bits gewinnen, die Queue ist also auf vier Einträge pro Objektnummer begrenzt; ein Überschreiten dieses Budgets wirft prrResourceLimitExceeded. Ein Verweis auf ein freies Objekt, ein Generation-Mismatch oder ein kaputter Objekt-Header wirft prrMalformedRevisionChain, Payload innerhalb eines komprimierten Object-Streams wirft prrCompressedObjectUnresolved. Jeder dieser Fehlschläge endet mit prasIndeterminate, nie mit einem erlaubten Urteil, und passiert der Fehlschlag beim Aufbau der Rollen der abgedeckten Revision, meldet die Signatur gar keine Changes
Die folgende Routine listet die Seiteninhalts-Edits auf, die diese Analyse überleben. Eine prckPageContent-Änderung wird nie als prdAllowed bewertet: DocMDP P=1, 2 oder 3 macht sie zu prdDisallowed, und eine Signatur ohne DocMDP bewertet sie als prdSuspicious
uses
SysUtils, TypInfo, PDFium, FPdfPades;
procedure ListPageContentEdits(const FileName: string);
const
ShadowTag: array[Boolean] of string = (' (unreferenced shadow)', '');
var
Pdf: TPdf;
Report: TPadesRevisionAnalysisReport;
Sig: TPadesSignatureRevisionAnalysis;
Change: TPadesRevisionObjectChange;
i, j: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Report := Pdf.AnalyzeSignatureRevisions; // Record, nichts freizugeben
for i := 0 to High(Report.Signatures) do
begin
Sig := Report.Signatures[i];
if not (prrPageContentChanged in Sig.Risks) then
Continue;
Writeln(Format('Signature %d (DocMDP P=%d): page content changed',
[Sig.SignatureIndex, Sig.DocMdpPermission]));
for j := 0 to High(Sig.Changes) do
begin
Change := Sig.Changes[j];
if Change.Kind <> prckPageContent then
Continue;
Writeln(Format(' revision %d object %d %d R %s%s',
[Change.RevisionIndex, Change.ObjectNumber, Change.Generation,
GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Change.Decision)),
ShadowTag[Change.IsAuthoritative]]));
end;
end;
finally
Pdf.Free;
end;
end;
Was passiert, wenn sich FieldMDP und eine Annotation ein Objekt teilen?
Wenn das /V eines Felds und das /Contents einer Annotation auf dasselbe indirekte Objekt zeigen, hält v3.126.2 die FieldMDP-Sperre in Kraft, selbst wenn die Änderung als Annotations-Edit klassifiziert ist. Das Szenario lässt sich von Hand leicht bauen: Ein Unterzeichner sperrt das Total-Feld mit FieldMDP (ISO 32000-1 §12.8.2.4), und der Angreifer lässt das /Contents einer Text-Annotation dasselbe String-Objekt referenzieren, das den Feldwert hält. Unter P=3 ist der Annotations-Edit erlaubt, das Umschreiben dieses geteilten Strings änderte vor dem Fix also einen gesperrten Feldwert mit einem erlaubten Urteil
Das Objekt trägt jetzt beide Rollen, Annotation und Form, und die Annotations-Entscheidung prüft die Form-Seite nach, sobald die Signatur eine FieldMDP-Transform hat:
- Unter P=2 ist die Annotations-Änderung rundherum unzulässig, exakt wie zuvor
- Mit FieldMDP
Allist jedes Feld gesperrt, die geteilte Änderung ist alsoprdDisallowed - Mit FieldMDP
IncludeoderExcludekann der Analyzer einen geteilten Skalar nicht auf einen Feldnamen zurückverfolgen, das Urteil ist alsoprdIndeterminatestatt einer Vermutung - Ohne FieldMDP greift die P=3-Annotations-Regel, und die Änderung bleibt erlaubt
Ein Reporting-Detail zählt für Gate-Code. Der geteilte Fall wird als Kind = prckAnnotation mit Decision = prdIndeterminate gemeldet, und prrFieldMdpUnresolved kommt nur für als Formfelder klassifizierte Änderungen in die Risiko-Menge. Ein Gate, das nach prrFieldMdpUnresolved sucht und Status ignoriert, verpasst diesen Fall komplett
Wie sollte Delphi-Code bei der Revisions-Analyse fail-closed reagieren?
Delphi-Code sollte ein signiertes Dokument nur akzeptieren, wenn der Analyse-Status prasNoLaterChanges oder prasAllowed ist und kein strukturelles Risiko vorliegt, und er sollte prasIndeterminate und prasSuspicious als nicht vertrauenswürdig behandeln, nicht als Warnung, die man loggt und durchlässt. Indeterminate heißt, der Analyzer konnte nicht beweisen, dass die späteren Revisionen erlaubt waren; für einen Angreifer ist eine Eingabe, die zuverlässig Indeterminate produziert, genauso nützlich wie eine, die Allowed produziert, wenn Ihr Code sie durchlässt. Die globale AnalyzePadesSignatureRevisions nimmt ein beliebiges TStream und liest es ab Position 0, passend für Upload-Handler, die das Dokument nie rendern müssen
uses
Classes, SysUtils, TypInfo, FPdfPades;
const
// Doppelte Definitionen werden erfasst, ohne Status abzustufen
BlockingRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrCompressedObjectUnresolved,
prrResourceLimitExceeded];
function SignedRevisionsAcceptable(const FileName: string;
out Reason: string): Boolean;
var
Source: TFileStream;
Report: TPadesRevisionAnalysisReport;
begin
Result := False;
Reason := '';
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Report := AnalyzePadesSignatureRevisions(Source);
finally
Source.Free;
end;
if Report.SignatureCount = 0 then
begin
Reason := 'no signature anchors the analysis';
Exit;
end;
if Report.Risks * BlockingRisks <> [] then
begin
Reason := 'structural risk in the revision chain';
Exit;
end;
case Report.Status of
prasNoLaterChanges, prasAllowed:
Result := True;
else
// prasIndeterminate und prasSuspicious sind Zurückweisungen, keine Warnungen
Reason := 'revision status ' +
GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
end;
end;
Zwei Grenzen sind es wert, klar benannt zu werden. TPadesRevisionAnalysisReport sagt nichts über CMS-Integrität oder Zertifikatsvertrauen, dieses Gate sitzt also neben der kryptografischen und Trust-Validierung, nicht an ihrer Stelle. Und ein korrekter Ownership-Graph macht P=3 nicht für jeden Workflow sicher. P=3 erlaubt Annotationen wirklich, und eine Annotation mit opakem Appearance kann signierten Text überdecken, ohne einen einzigen Content-Stream anzufassen. Sind Ihre zertifizierten Dokumente Verträge statt Review-Kopien, dann zertifizieren Sie entweder mit P=2 oder routen erlaubte Annotations-Änderungen an einen Menschen, wie in diesem Helfer:
function AllowedAnnotationEditsUnderP3(
const Report: TPadesRevisionAnalysisReport): Integer;
var
i, j: Integer;
begin
Result := 0;
for i := 0 to High(Report.Signatures) do
if Report.Signatures[i].DocMdpPermission = 3 then
for j := 0 to High(Report.Signatures[i].Changes) do
if (Report.Signatures[i].Changes[j].Kind = prckAnnotation) and
(Report.Signatures[i].Changes[j].Decision = prdAllowed) then
Inc(Result);
end;
Checkliste für das Signature-Revisions-Audit
Nehmen Sie diese Liste, um zu prüfen, ob Ihre Verifikations-Pipeline exponiert war und ob sie jetzt fail-closed reagiert:
- PDFium-Component-Builds vor v3.126.2 konnten für Seiteninhalts-Edits in DocMDP-P=3-Dokumenten
prasAllowedmelden; fahren SieTPdf.AnalyzeSignatureRevisionsüber zertifizierte P=3-Dateien, die ältere Builds akzeptiert haben - Prüfen Sie P=3-Dokumente mit FieldMDP-Sperren erneut, wo ein Feldwert und eine Annotation sich ein indirektes Objekt teilen könnten
- Akzeptieren Sie nur
prasNoLaterChangesundprasAllowed; behandeln SieprasIndeterminateundprasSuspiciousals nicht vertrauenswürdig - Testen Sie
Report.Risksgenauso wieReport.Status, dennprrDuplicateObjectDefinitionändert den Status für sich genommen nicht - Lesen Sie ein leeres
Changes-Array nicht als sauberes Ergebnis, wenn der Signatur-Status Indeterminate ist; ein gescheiterter Rollenaufbau meldet keine Änderungen - Verlassen Sie sich nicht allein auf
prrFieldMdpUnresolved, um FieldMDP-Probleme zu erwischen, der geteilte Annotations-Fall zeigt sich nur über Entscheidung und Status - Entscheiden Sie, ob erlaubte Annotations-Änderungen unter P=3 in Ihrem Workflow eine menschliche Prüfung brauchen
- Analysieren Sie die Original-Bytes der Datei; ein mit
SaveAsumgeschriebenes Dokument enthält die Revisions-Kette nicht mehr
Revisions-Analyse ist eine Schicht einer Signaturprüfung. Kombinieren Sie sie mit der Inspektion von PDF-Digitalsignaturen und PAdES-Leveln für Dictionary und Baseline-Level, und mit einem breiteren PDF-Sicherheitsrisiko-Audit für JavaScript, Launch-Actions und eingebettete Dateien. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions und der hier beschriebene Ownership-bewusste Rollengraph kommen mit der PDFium Component für Delphi, C++Builder und Lazarus