HotPDF verschlüsselt eine PDF für konkrete Zertifikatsinhaber über den ISO-32000-Public-Key-Security-Handler: EnablePubKeyEncryption nimmt einen 20-Byte-Zufalls-Seed, und jeder Empfänger bekommt seinen eigenen CMS-Envelope, gebaut von AddPubKeyRecipientCertificate für RSA-Keys (RSA-OAEP-Key-Transport) oder AddPubKeyAgreementRecipientWithSecret für Elliptic-Curve-Keys (ECDH auf P-256, P-384, P-521, X25519 oder X448). Niemand teilt ein Passwort; wer den passenden Private Key hält, öffnet die Datei
Der Anwendungsfall ist immer eine Variante derselben Geschichte. Ein Quartals-Auditpaket geht an drei externe Reviewer, die Rechtsabteilung will, dass jeder es liest, nur einer darf es drucken, und niemand will ein Passwort in einem Mail-Thread direkt neben dem Attachment liegen haben. Passwort-Verschlüsselung kann das nicht ausdrücken. Zertifikatsverschlüsselung kann es, denn jeder Empfänger schließt das Dokument mit einem Key auf, den er ohnehin hat, und jeder Empfänger kann sein eigenes Permission-Set in seinem eigenen Envelope tragen
Wie unterscheidet sich zertifikatsbasierte PDF-Verschlüsselung von einem Passwort?
Eine Public-Key-verschlüsselte PDF leitet ihren File Key aus einem Zufalls-Seed plus den exakten Bytes jedes Empfänger-Envelopes ab, nicht aus irgendetwas, das ein Mensch tippt. Der Handler ist in ISO 32000-1 §7.6.4 beschrieben (§7.6.5 in ISO 32000-2), und die Envelopes sind CMS-EnvelopedData-Strukturen nach RFC 5652. HotPDF schreibt /Filter /Adobe.PubSec mit /SubFilter /adbe.pkcs7.s5; bei AES-256 heißt das /V 5 und ein /DefaultCryptFilter-Eintrag unter /CF mit /CFM /AESV3, und das /Recipients-Array wohnt in diesem Crypt Filter. Jeder Envelope verschlüsselt 24 Bytes: den 20-Byte-Seed gefolgt vom 32-Bit-Permission-Wort dieses Empfängers. Der /P-Wert im Encryption-Dictionary ist nur ein Platzhalter, denn die echten Permissions reisen in jedem Envelope. Zur Ladezeit entpackt ein Reader einen Envelope, holt den Seed zurück und hasht ihn zusammen mit jedem Envelope in /Recipients-Reihenfolge (SHA-256 für AES-256, SHA-1 für die älteren Chiffren), um den File Key zu rekonstruieren. Wenn Sie noch zwischen diesem Modell und gewöhnlichen Passwörtern abwägen, behandelt der AES-256-Passwortverschlüsselungs- und Permission-Flags-Guide die andere Seite dieses Trade-offs
RSA-Empfänger mit EnablePubKeyEncryption schreiben
Für RSA-Zertifikate rufen Sie EnablePubKeyEncryption mit aes256 auf und dann AddPubKeyRecipientCertificate einmal pro DER-kodiertem Zertifikat vor BeginDoc. Der Helfer baut einen RSAES-OAEP-Envelope im Prozess mit THPDFRSAOAEPHash-Werten für den OAEP-Digest und den MGF1-Digest (rohSHA256, rohSHA384 oder rohSHA512) und verschlüsselt den Envelope-Inhalt mit AES-256-CBC
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFRSA;
procedure WriteAuditPack(const OutFile: string);
var
Pdf: THotPDF;
Seed: AnsiString;
begin
SetLength(Seed, 20); // exakt 20 Bytes, auch bei AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // der Default-Key-Type ist aes128
// Reviewer A darf drucken; Reviewer B darf nur lesen und extrahieren
Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-a.cer'),
[prPrint, prPrint12bit, prExtractContent], rohSHA256, rohSHA256);
Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-b.cer'),
[prExtractContent]);
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 720, 0, 'Q3 audit pack');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Drei Details in diesem Listing tragen Last. Erstens ist die Seed-Länge für jeden Key-Typ auf 20 Bytes festgenagelt, AES-256 eingeschlossen; EnablePubKeyEncryption wirft bei jeder anderen Länge. Zweitens ist der Default von EnablePubKeyEncryption aes128, und beide Zertifikats-Helfer weigern sich zu laufen, sofern der Key-Typ nicht aes256 ist, das Vergessen des zweiten Arguments liefert Ihnen also die Exception „certificate envelopes require aes256“. Die Legacy-Chiffren (k40, k128, aes128) funktionieren weiterhin, aber nur über AddPubKeyRecipient mit einem Envelope, den Sie woanders gebaut haben. Drittens ist Public-Key-Verschlüsselung mit AES-256 ein PDF-2.0-Feature, HotPDF hebt die Dokumentversion also automatisch auf 2.0. Mit gesetztem StrictVersionLock auf einer niedrigeren Version kehrt EnablePubKeyEncryption zurück, ohne irgendetwas einzuschalten, und der Fehler erscheint erst in der nächsten Zeile als „call EnablePubKeyEncryption first“. Die Verschlüsselung während eines inkrementellen Updates zu wechseln, wirft sofort ein EInvalidOpException
ECDH-Empfänger hinzufügen: P-256, P-384, P-521, X25519 und X448
Für Elliptic-Curve-Zertifikate schreibt AddPubKeyAgreementRecipientWithSecret einen CMS-Key-Agreement-Empfänger (KeyAgreeRecipientInfo, die KARI-Struktur aus RFC 5753, mit dem X25519- und X448-Profil aus RFC 8418) und berechnet das ECDH-Shared Secret im Prozess. Die Kurve wählen Sie mit einem THPDFPubKeyAgreementScheme-Wert: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 oder pkasX448. Das Schema muss zum Key im Zertifikat passen, sonst wirft der Aufruf „Certificate key does not match the requested agreement scheme“. Unter der Haube bekommt jeder Envelope einen frischen zufälligen 32-Byte-UKM, einen mit dem stdDH-KDF abgeleiteten Key-Encryption-Key (SHA-256 für P-256 und X25519, SHA-384 für P-384, SHA-512 für P-521 und X448) und einen AES-256-Key-Wrap nach RFC 3394. Das Shared Secret selbst stammt aus reinem Pascal-Kurven-Code, ohne jeden Plattform-Krypto-Provider; der Artikel zur reinen Pascal-NIST-Kurven-Arithmetik erklärt, wie diese Schicht gebaut und verifiziert wurde. Für die Montgomery-Kurven lässt sich das komplette ephemerale Key-Paar lokal erzeugen:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Frischer ephemeraler Skalar pro Envelope; das Clamping passiert in der Leiter
SetLength(Scalar, 32);
AESGenerateRandomBytes(@Scalar[0], Length(Scalar));
try
OriginatorPublic := HPDFX25519PublicFromScalar(Scalar);
Pdf.AddPubKeyAgreementRecipientWithSecret(
TFile.ReadAllBytes('legal-x25519.cer'),
[prPrint, prExtractContent], pkasX25519,
OriginatorPublic, Scalar,
[]); // OwnPublicPoint: nur bei den NIST-Kurven von Bedeutung
finally
HPDFSecureClearBytes(Scalar);
end;
end;
Die NIST-Kurven verlangen dem Aufrufer mehr ab. HotPDF liefert Public-Key-Helfer nur für X25519 und X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), für P-256, P-384 und P-521 erzeugen Sie das ephemerale Key-Paar also mit Ihrem eigenen Werkzeug und übergeben einen Big-Endian-Skalar von exakt der Feldgröße (32, 48 oder 66 Bytes) plus dem passenden unkomprimierten Punkt 0x04||X||Y als OriginatorPublicKey. HotPDF validiert den Empfänger-Punkt gegen die Kurvengleichung, aber es kann nicht prüfen, dass Ihr Originator-Public-Key tatsächlich zu Ihrem Skalar gehört. Falsch gepaarte Hälften ergeben trotzdem einen tadellos geformten Envelope, den kein Empfänger öffnen kann – deshalb gehört ein Round-Trip-Load in Ihre Test-Suite, nicht nur ein Dateigrößen-Check
Warum kommt es auf die Reihenfolge von /Recipients an?
Die Reihenfolge von /Recipients zählt, weil der File Key ein Digest über den Seed und jeden Envelope in Array-Reihenfolge ist, Writer und Reader müssen also dieselben Bytes in derselben Sequenz hashen. HotPDF hält die Envelopes in der Reihenfolge, in der Sie sie hinzufügen, und schreibt sie unverändert – Sie dürfen Empfänger in beliebiger Reihenfolge hinzufügen, aber nichts downstream darf dieses Array umordnen, neu kodieren oder „aufräumen“. Die meisten echten Bugs in diesem Bereich waren eine Variante dieses Themas, bei der zwei Seiten minimal unterschiedliche Bytes hashten:
- Dynamische Arrays über
Addin einerTListzu speichern hält nur einen rohen Pointer fest, während der Referenzzähler bei der lokalen Variablen bleibt. Das nächsteSetLengthgibt den Buffer frei und kann ihn wiederverwenden, jeder Slot aliaste also den letzten Envelope, und Multi-Empfänger-Dateien leiteten den falschen Key ab. Der Fix: eine eigene Kopie mitList.Add(Pointer(System.Copy(Bytes)))speichern - Das Envelope-Entpacken parst das DER an Ort und Stelle, und der Key-Recovery-Durchlauf hashte ursprünglich ebendiese lebenden Arrays. Der Reader snapshotet jetzt saubere Kopien jedes Envelopes, bevor irgendein Entpacken sie anfasst, und der Digest läuft über die Snapshots
- Binäres DER durch eine Unicode-
TStringListbekommt Bytes ab$80über die Codepage neu kodiert, HotPDF speichert Envelopes deshalb intern als Hex-Text - Verschlüsselte und binäre Strings müssen als Hex-Strings geschrieben werden. Ein Literal-String unterliegt der End-of-Line-Normalisierung, bei der CR, LF und CRLF alle zu einem einzelnen LF werden (ISO 32000-1 §7.3.4.2), und das schreibt Ciphertext still um. HotPDF gibt jeden
/Recipients-Eintrag als Hex-String aus und nimmt ihn von der String-Verschlüsselung aus, denn jeder Reader braucht die Envelopes, bevor er irgendeinen Key hält - Das erste Byte eines DER-
BIT STRINGzählt unbenutzte Bits und muss bei byte-alignten Keys null sein. NachSetLengthuninitialisiert gelassen, schrieb es, was gerade auf dem Stack lag, und ein strenger Entpacker wies den Originator-Key zurück, sodass eine Datei gelegentlich mit genau dem Key scheiterte, für den sie geschrieben war - Wenn derselbe Key weiterhin nicht entschlüsseln kann, vergleichen Sie Schicht für Schicht: den File Key, dann das Ciphertext-Präfix (die IV), dann den Objekt-Key, dann den Plaintext. Der Bug wohnt direkt hinter der ersten Schicht, die nicht übereinstimmt
Wie öffnen Sie eine zertifikatsverschlüsselte PDF mit einem Private Key?
Um eine zertifikatsverschlüsselte PDF zu öffnen, registrieren Sie das Private-Key-Material, bevor Sie LoadFromFile aufrufen, denn HotPDF holt den File Key schon im strukturellen Durchlauf zurück. Weisen Sie einen mit HPDFParsePFX geparsten RSA- oder EC-Key an PubSecKeyMaterial zu, fügen Sie weitere RSA-Keys mit AddPubSecKeyMaterial hinzu und registrieren Sie rohe ECDH-Skalare mit AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), unter Verwendung der Konstanten HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 oder HPDFOIDECP521. Die NIST-Kurven verlangen den eigenen unkomprimierten Public Point des Empfängers; die Montgomery-Kurven ignorieren ihn
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;
procedure OpenAuditPack(const LegalScalar: TBytes);
var
Reader: THotPDF;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
Reader.PubSecKeyMaterial :=
HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
// Optional: den Envelope direkt wählen, statt alle durchzuprobieren
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = jeden Envelope der Reihe nach versuchen
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Ohne Callback probiert HotPDF jeden Envelope gegen jeden registrierten Key: zuerst den Primär-Key, dann jeden zusätzlichen RSA-Key, dann das EC-Material. PubSecRecipientQuery bekommt die Envelope-Anzahl und gibt einen 0-basierten Index oder -1 zurück, ein Index außerhalb des Arrays wirft eine Exception, statt geklemmt zu werden. Beachten Sie, dass AddPubSecKeyMaterial nur RSA-Material akzeptiert (es besteht auf einem Modulus und einem privaten Exponenten), EC-Keys gehören also in PubSecKeyMaterial oder AddPubSecAgreementKeyMaterial. Wenn kein Key irgendeinen Envelope entpackt, kehrt der Recovery-Schritt ohne File Key zurück, statt zu werfen – prüfen Sie also, ob der erwartete Inhalt tatsächlich entschlüsselt wurde, statt darauf zu vertrauen, dass der Load-Aufruf zurückkehrte
Was HotPDF nicht garantiert
HotPDF garantiert, dass sein eigener Writer und Reader byte für byte übereinstimmen, und es baut Envelopes, die den zitierten CMS-Strukturen folgen. Es garantiert nicht, dass jeder PDF-Viewer jede Kombination öffnet. Der Support für RSA-OAEP-Key-Transport und für X25519- oder X448-Empfänger variiert zwischen Readern und Versionen, und wir haben keine Kompatibilitätsergebnisse für diese Kombinationen veröffentlicht. Muss ein Dokument in einem konkreten Viewer aufgehen, verschlüsseln Sie eine Testdatei für ein Testzertifikat desselben Key-Typs und öffnen Sie sie dort, bevor Sie sich auf ein Schema festlegen. Permissions, die im Envelope reisen, bleiben Policy, die konforme Software ehrt, exakt wie unter Passwortverschlüsselung. Die Seed-Qualität ist ebenfalls Ihre Verantwortung: AESGenerateRandomBytes ist genau für diesen Job da, und HotPDF wischt seine Kopie des Seeds weg, sobald der File Key abgeleitet ist. Wenn Sie auch einen String, Stream oder ein Attachment über ein anderes Crypt Filter laufen lassen wollen, zeigt der Crypt-Filter-Policy-Guide für StmF, StrF und EFF, welche Filternamen der Public-Key-Handler akzeptiert
Zertifikatsverschlüsselung, RSA-OAEP- und ECDH-Empfänger-Envelopes und Private-Key-Laden erscheinen alle in der HotPDF Delphi PDF component, neben Passwortverschlüsselung, digitalen Signaturen und dem Rest des ISO-32000-Werkzeugkastens für Delphi und C++Builder