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
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 is byte-identical to the source: still encrypted,
// no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
// nothing was corrupted either
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'; // needed to open the source at all
Pdf.FileName := 'signed-encrypted-invoice.pdf';
Pdf.LoadDocument;
Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
// invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
// ran first inside SaveAsPdfA: the output opens without a password
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"
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
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