Technischer Artikel

Ein verschlüsseltes PDF lässt sich in Delphi nicht still nachträglich flicken

Nehmen Sie eine Rechnungs-PDF, die bereits AES-256-Verschlüsselung trägt, und bitten Sie die PDFium-Komponente für Delphi und C++Builder (PDFiumPas), sie über eine inkrementelle Aktualisierung statt eines vollständigen Neuschreibens mit PDF/A für die archivarische Aufbewahrung zu stempeln, oder sie mit PAdES zu signieren. Die Bibliothek wird das nicht durch direktes Flicken der verschlüsselten Bytes erreichen: Ihre sechs Konformitäts-Marker-Injektoren erkennen einen bestehenden /Encrypt-Eintrag und leiten die Quelle byteidentisch unverändert durch, und ihr PAdES-Signierer wirft eine Exception, statt eine Signatur auszugeben, die kein Validator akzeptieren wird

Das ist eine andere Frage als das Auditieren eines PDFs, das man nicht selbst erstellt hat, auf verborgene Risiken, was eine eigene, rein lesende Übung ist. Dieser Artikel handelt von der Schreibseite derselben Vertrauensgrenze: was der eigene Code mit einer Datei tun darf, deren Bytes bereits hinter dem Passwort von jemand anderem verschlossen sind, in dem Moment, in dem dieser Code versucht, nachträglich etwas hinzuzufügen

Was verlangt ISO 32000-1, wenn man ein verschlüsseltes PDF aktualisiert?

ISO 32000-1 §7.5.6 verlangt, dass der Trailer einer inkrementellen Aktualisierung jeden Eintrag aus dem vorherigen Trailer wiederholt, außer /Prev, und Tabelle 15 listet /Encrypt unter den Einträgen auf, die ein Trailer tragen kann. Lässt man ihn aus dem neuen Trailer weg, hat ein konformer Reader keinen Grund, die Auslassung zu bezweifeln: Der neueste Trailer ist maßgeblich, sodass ein Reader, der dort kein /Encrypt findet, entscheidet, die ganze Datei sei unverschlüsselt, und versucht, den älteren, weiterhin verschlüsselten Körper als reine Bytes zu parsen. Behält man /Encrypt im neuen Trailer, schreibt aber die eigenen Objekte der Aktualisierung als Klartext, verschiebt sich der Fehlschlag nur einen Schritt später: Der Reader erkennt Verschlüsselung korrekt, lässt jedes Objekt, das er berührt, durch die Chiffre der Datei laufen, einschließlich der neuen, die nie verschlüsselt waren, und bekommt Rauschen zurück für Inhalt, der vollkommen lesbar war, bevor Entschlüsselung ihn berührte. Beide Fehler erzeugen eine Datei, die auf Byte-Ebene wie eine normale, wohlgeformte inkrementelle Aktualisierung aussieht, genau bis ein konformer Reader sie öffnet

Ein ISO-32000-1-7.5.6-Diagramm für Delphi-inkrementelle Updates an einer verschlüsselten PDF: ein neuer Trailer, der /Encrypt fallen lässt, lässt den Leser Chiffretext als Klartextbytes parsen; Klartext-Updateobjekte werden vom Dateichiffre verdreht; und /Encrypt mit verschlüsselten neuen Objekten zu wiederholen ist der einzige konforme Ausgang
Ein Update-Trailer, der /Encrypt fallen lässt, lässt den Leser Chiffretext als schlichte Bytes parsen, und Klartext-Update-Objekte werden vom Datei-Chiffrierer verdreht — nur /Encrypt zu wiederholen und die neuen Objekte zu verschlüsseln überlebt einen konformen Leser

Sechs Marker-Injektoren, ein Verschlüsselungs-Gate ab v2.14.2

PDFiumPas liefert sechs Byte-Ebenen-Marker-Injektoren aus, einen für jede ISO-PDF-Teilmenge, die sie beschriften kann: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1) und PDF/VT-1 (ISO 16612-2). Jeder nimmt die Bytes, die PDFiums eigenes FPDF_SaveAsCopy bereits geschrieben hat, und schichtet eine zweite, kleinere inkrementelle Aktualisierung darüber: einen neuen XMP-Metadaten-Stream, eine Katalog-Wörterbuch-Bearbeitung, die darauf zeigt, und für die druckorientierten Teilmengen ein OutputIntent und ICC-Profil. Ab v2.14.2 liest jeder von InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers und InjectPdfVTMarkers zuerst den Quell-Trailer, und falls dieser einen bestehenden /Encrypt-Eintrag meldet, kopiert er die Quelle unverändert zum Zielstream durch und kehrt sofort zurück. Kein XMP, kein OutputIntent, keine Katalog-Bearbeitung — der Aufrufer bekommt die Originaldatei zurück, Byte für Byte

var
  Src, Dst: TFileStream;
  Opts: TPdfXSaveOptions;
begin
  Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
  Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
  try
    Opts.Conformance := pxc4;
    InjectPdfXMarkers(Src, Dst, Opts);
    // pdfx-attempt.pdf ist byte-identisch zur Quelle: weiterhin verschlüsselt,
    // kein /GTS_PDFXVersion, kein OutputIntent. Nichts wurde geschrieben und
    // nichts wurde beschädigt
  finally
    Dst.Free;
    Src.Free;
  end;
end;

Erlaubt, verschlüsselt zu sein, ist nicht dasselbe wie sicher, hineinzuinjizieren

PDF/E-1 und PDF/R-1 erlauben beide ausdrücklich auf Spezifikationsebene, dass ihr Host-Dokument verschlüsselt sein darf, was sich wie eine Ausnahme liest, bis man sich ansieht, was tatsächlich auf der Festplatte passieren muss. ISO 24517-1 §6.3 erlaubt Verschlüsselung für PDF/E-1, und ISO 23504-1 §6.2.3 erlaubt sie für PDF/R-1, vorausgesetzt der Header deklariert %PDF-2.0. Keine dieser Klauseln sagt irgendetwas darüber, ob ein Byte-Ebenen-Postprozessor sicher ein Klartext-Objekt zu diesem verschlüsselten Container hinzufügen kann, und das kann er nicht, aus denselben §7.5.6-Gründen, die für jede andere Teilmenge gelten. PDFiumPas' eigene Konformitäts-Validatoren für diese zwei Profile, ValidatePdfECompliance und ValidatePdfRCompliance, protokollieren die Anwesenheit von /Encrypt absichtlich ohne sie als Defekt zu markieren, was für einen rein lesenden Validator, der nie ein Byte schreibt, korrekt ist. Es ist auch ein Muster, an dem man leicht vorbeiliest und annimmt, der Geschwister-Injektor bräuchte keine separate Absicherung, obwohl der Injektor die eine Funktion des Paars ist, die sich tatsächlich weigern muss

Entschlüsselt SaveAsPdfX Ihr Dokument still?

Ja, wann immer Sie über die öffentlichen Komfortmethoden gehen, statt einen Injektor direkt aufzurufen. Jede von TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR und SaveAsPdfVT rendert das aktuelle Dokument mit SaveAs(Tmp, saRemoveSecurity) in einen temporären Stream, bevor sie diese Bytes ihrem passenden Injektor übergibt. saRemoveSecurity bildet auf PDFiums eigenes FPDF_REMOVE_SECURITY-Flag ab, sodass die temporäre Kopie, die der Injektor empfängt, überhaupt nie verschlüsselt war, und die /Encrypt-Absicherung des Injektors nie einen Grund hat auszulösen. Die Ausgabe trägt Ihre PDF/A-, PDF/X-, PDF/UA-, PDF/E-1-, PDF/R-1- oder PDF/VT-1-Marker, ist aber nicht mehr durch welches Passwort auch immer geschützt, das die Quelle geöffnet hat

Dieser Kompromiss ist unsichtbar, bis jemand nachgelagert die "geschützte" archivarische Kopie ohne Passwort öffnet und bemerkt, dass es einfach funktioniert. Die Lösung ist kein anderer Methodenaufruf; PDFiumPas hat kein saAddSecurity-Gegenstück, das mit saRemoveSecurity gepaart werden könnte, weil die zugrunde liegende PDFium-Engine nie gebaut wurde, um neue Verschlüsselung zu schreiben, nur um sie zu entfernen. Falls beide Eigenschaften für eine Datei zählen, muss Verschlüsselung ein separater Schritt sein, den Sie selbst besitzen, angewendet nach den Konformitäts-Markern, nicht in denselben SaveAsPdfA-Aufruf gefaltet

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';           // nötig, um die Quelle überhaupt zu öffnen
    Pdf.FileName := 'signed-encrypted-invoice.pdf';
    Pdf.LoadDocument;
    Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
    // invoice-pdfa.pdf deklariert nun PDF/A-2b, aber SaveAs(saRemoveSecurity)
    // lief zuerst innerhalb von SaveAsPdfA: die Ausgabe öffnet ohne Passwort
  finally
    Pdf.Free;
  end;
end;

Was passiert, wenn Sie ein verschlüsseltes PDF mit PAdES signieren?

PDFiumPas weigert sich rundweg, statt die Anfrage still fallen zu lassen, wie es ein Marker-Injektor tut. TPdf.SignPades und SignPadesToStream laufen beide über ein internes SignPadesBytes, und das Erste, was es nach dem Parsen des Quell-Trailers tut, ist auf /Encrypt zu prüfen. Ist der Eintrag vorhanden, wirft es EPadesCrypto mit der Meldung "SignPadesBytes: the source document is encrypted; remove encryption before signing", statt weiter fortzufahren. InjectPadesDssMarkers, die Funktion, die Zertifikate, OCSP-Antworten und CRLs für Langzeit-Validierung einbettet, wendet dieselbe Prüfung aus demselben Grund an, mit ihrer eigenen Meldung: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

Das PDFium-Component-Verschlüsselungstor in Delphi: alle sechs Marker-Injektoren lesen den Quelltrailer; ein /Encrypt-Eintrag reicht die Bytes unverändert durch, während eine unverschlüsselte Datei XMP-, Catalog- und OutputIntent-Marker erhält; und der PAdES-Signer wirft EPadesCrypto, statt durchzulassen
Alle sechs Injektoren und der PAdES-Signer lesen zuerst den Quelltrailer — verschlüsselte Eingabe passiert unangetastet, unverschlüsselte Eingabe erhält ihre Marker, und Signieren verweigert mit EPadesCrypto, statt still durchzulassen

Die Begründung hier ist strenger als der Durchlass der Marker-Injektoren, und das absichtlich. Ein stiller Durchlass ist für einen PDF/A-Stempel sicher, weil das Überspringen Sie mit demselben gültigen PDF zurücklässt, mit dem Sie begonnen haben, nur unbeschriftet. Signieren kann nicht so still scheitern: Eine Signatur, die still nie hinzugefügt wurde, sieht für jeden aufrufenden Code, der nur ein boolesches Ergebnis prüft, exakt wie eine erfolgreich hinzugefügte Signatur aus. EPadesCrypto stammt von der gewöhnlichen Exception-Klasse ab, sodass das Abfangen normale Exception-Behandlung ist, keine besondere Kontrollfluss-Konvention, die man erst lernen müsste

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';
    Pdf.FileName := 'encrypted-contract.pdf';
    Pdf.LoadDocument;
    try
      Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
    except
      on E: EPadesCrypto do
        // E.Message: 'SignPadesBytes: the source document is encrypted;
        // remove encryption before signing'
        raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
    end;
  finally
    Pdf.Free;
  end;
end;

Reihenfolge von Konformitäts-Stempeln, Signaturen und Verschlüsselung

Die praktische Lösung ist Reihenfolge, keine andere Bibliothek. Wenden Sie zuerst PDF/A-, PDF/X-, PDF/UA-, PDF/E-1-, PDF/R-1- oder PDF/VT-1-Marker an, fügen Sie als Nächstes jede PAdES-Signatur hinzu, und lassen Sie erst dann welchen Schritt in Ihrer Pipeline auch immer laufen, der tatsächlich Verschlüsselung besitzt, sei es ein dedizierter PDF-Writer, eine Signier-Appliance oder Ihre eigene AES-Implementierung. PDFiumPas' Schicht für inkrementelle Aktualisierungen passt natürlich in die Mitte dieser Sequenz, hängt kleine, gezielte Objekte an eine ansonsten fertige Datei an, und Verschlüsselung gehört ans Ende, genau weil sie die eine Operation in der Kette ist, die PDFiumPas selbst weder ausführen noch rückgängig machen kann

Eine Delphi-Pipeline-Reihenfolge für PDFiumPas bei verschlüsselten PDFs: Konformitätsmarker werden zuerst injiziert, solange die Datei unverschlüsselt ist, eine PAdES-Signatur folgt als Nächstes, und die Verschlüsselung kommt zuletzt als eigener Schritt, den die Pipeline besitzt, weil PDFiumPas sie nicht anwenden oder entfernen kann
PDFiumPas hängt Konformitätsmarker und PAdES-Signaturen an, solange die Datei noch lesbar ist, und die Verschlüsselung kommt zuletzt als separater Schritt, den die Pipeline besitzt

Nichts davon ändert, wie PDFiumPas den Trailer und die Kreuzreferenzdaten liest, von denen jede inkrementelle Aktualisierung abhängt, was seine eigene Quelle der Feinheit ist, sobald Xref-Streams ins Bild kommen; die Validierung der Objekt- und Xref-Streams eines PDFs behandelt, wie derselbe Trailer-Lese-Pfad PDF-1.5+-komprimierte Strukturen handhabt. Und sobald ein Dokument bereit für etwas Stärkeres als einen Konformitäts-Stempel ist, übernimmt das Signieren eines PDFs mit einer PAdES-B-B-Signatur in Delphi genau dort, wo dieser Artikel aufhört, mit SignPades

Die hier beschriebenen Marker-Injektoren und SignPades-Methoden sind Teil der PDFium-Komponente für Delphi und C++Builder, zusammen mit dem Rendering und der rein lesenden Inspektion, die PDFium nativ bereitstellt