Eine zertifikatsverschlüsselte PDF trägt einen einzigen /P-Wert in ihrem Encryption-Dictionary, ganz gleich, für wen die Datei geschrieben wurde; in den weiter unten gebauten Dateien ist es -64. Drei Empfänger mit drei verschiedenen Permission-Sets teilen sich trotzdem diese eine Zahl. Code, der „darf dieser Nutzer drucken?“ aus /P beantwortet, liest einen Platzhalter, denn unter dem Public-Key-Security-Handler reist das echte Permission-Wort im verschlüsselten Envelope jedes Empfängers und erscheint erst, sobald der Private Key dieses Empfängers ihn geöffnet hat
Dieses Design ist der Grund, warum Zertifikatsverschlüsselung die Mühe wert ist. Ein Quartals-Auditpaket geht an drei externe Reviewer; die Rechtsabteilung will, dass alle es lesen, nur einer darf es drucken, und niemand will ein Passwort im Mail-Thread direkt neben dem Attachment liegen haben. Ein Passwort kann das nicht ausdrücken. Zertifikate können es: Jeder Empfänger schließt die Datei mit einem Key auf, den er ohnehin hält, und jeder Envelope trägt sein eigenes Permission-Wort
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, und nichts, was ein Mensch tippt, geht in die Rechnung ein. Der Handler ist in ISO 32000-1 §7.6.4 spezifiziert (§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, einen /DefaultCryptFilter-Eintrag unter /CF mit /CFM /AESV3 und das /Recipients-Array innerhalb dieses Crypt Filters statt im Dictionary der obersten Ebene
Jeder Envelope verschlüsselt 24 Bytes: den 20-Byte-Seed gefolgt vom 32-Bit-Permission-Wort dieses Empfängers. Zur Ladezeit entpackt ein Reader einen Envelope, holt den Seed zurück und hasht den Seed 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. Reviewer B erfährt nie, was Reviewer A darf, doch Bs Reader hasht trotzdem As Envelope-Bytes, also muss das ganze Array byte für byte überleben. Wägen Sie noch zwischen diesem Modell und gewöhnlichen Passwörtern, 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 ist die Sequenz kurz: EnablePubKeyEncryption mit aes256, dann ein AddPubKeyRecipientCertificate-Aufruf pro DER-kodiertem Zertifikat, alles vor BeginDoc. Der Helfer baut einen RSAES-OAEP-Envelope im Prozess. Seine letzten zwei Parameter wählen den OAEP-Digest und den MGF1-Digest aus THPDFRSAOAEPHash (rohSHA256, rohSHA384 oder rohSHA512), beide mit Vorgabe rohSHA256, und der Envelope-Inhalt wird mit AES-256-CBC verschlüsselt
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 für AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // Default-Key-Typ 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. Der Seed ist für jeden Key-Typ 20 Bytes groß, AES-256 eingeschlossen, und EnablePubKeyEncryption wirft bei jeder anderen Länge. Der Key-Typ hat die Vorgabe aes128, während beide Zertifikats-Helfer auf aes256 bestehen, sodass das Weglassen des zweiten Arguments den ersten AddPubKeyRecipientCertificate-Aufruf mit „certificate envelopes require aes256“ werfen lässt. Die Legacy-Chiffren (k40, k128, aes128) bleiben über AddPubKeyRecipient verfügbar, das einen kompletten DER-Envelope nimmt, den andere Werkzeuge gebaut haben
Das dritte Detail ist die Version. AES-256 unter dem Public-Key-Handler ist ein PDF-2.0-Feature, und HotPDF hebt die Dokumentversion eigenmächtig auf 2.0. Mit gesetztem StrictVersionLock auf einer niedrigeren Version ist dieser Sprung nicht erlaubt: EnablePubKeyEncryption kehrt zurück, ohne irgendetwas einzuschalten, und der Fehler taucht eine Zeile später auf, wenn der Empfänger-Helfer „call EnablePubKeyEncryption first“ meldet. Diese Meldung schickt Leute auf die Jagd nach einem Aufrufreihenfolge-Fehler. Prüfen Sie stattdessen die Versionssperre. Die Verschlüsselung während eines inkrementellen Updates zu wechseln wird rundheraus mit EInvalidOpException verweigert
ECDH-Empfänger auf P-256, P-384, P-521, X25519 und X448 hinzufügen
Elliptic-Curve-Zertifikate laufen über AddPubKeyAgreementRecipientWithSecret, das einen CMS-Key-Agreement-Empfänger schreibt (KeyAgreeRecipientInfo aus RFC 5753, mit dem X25519- und X448-Profil aus RFC 8418) und das ECDH-Shared Secret im Prozess berechnet. Ein THPDFPubKeyAgreementScheme-Wert nennt die Kurve: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 oder pkasX448. Das Schema muss zum Key-Algorithmus, zur Kurve und zur Schlüssellänge des Zertifikats passen, sonst wirft der Aufruf „Certificate key does not match the requested agreement scheme“
Jeder Envelope bekommt einen frischen zufälligen 32-Byte-UKM, einen Key-Encryption-Key aus dem stdDH-KDF (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 gemäß RFC 3394. Die Kurven-Arithmetik ist schlichtes Pascal ohne plattformseitigen Krypto-Provider darunter; der Artikel zur reinen Pascal-NIST-Kurven-Arithmetik führt durch, wie diese Schicht zusammengesetzt ist. Auf den Montgomery-Kurven lässt sich das ganze ephemerale Key-Paar lokal erzeugen:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Frischer ephemerer Skalar pro Envelope; Clamping passiert innerhalb 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 für die NIST-Kurven von Bedeutung
finally
HPDFSecureClearBytes(Scalar);
end;
end;
Die NIST-Kurven verlangen dem Aufrufer mehr ab. HotPDF leitet Originator-Public-Keys nur für X25519 und X448 ab (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), für P-256, P-384 und P-521 kommt das ephemerale Paar also aus Ihrem eigenen Werkzeug. Übergeben Sie einen Big-Endian-Skalar von exakt der Feldgröße (32, 48 oder 66 Bytes) und den passenden unkomprimierten Punkt 0x04||X||Y (65, 97 oder 133 Bytes) als OriginatorPublicKey, und reichen Sie denselben Punkt an OwnPublicPoint, den Parameter, der für die NIST-Schemata reserviert ist. Falsche Längen werfen sofort. Der aus dem Zertifikat genommene Empfänger-Punkt wird gegen die Kurvengleichung geprüft, bevor irgendeine Multiplikation läuft
Was keine Bibliothek prüfen kann, ist, ob Ihr Originator-Public-Key zu Ihrem Skalar gehört. Falsch gepaarte Hälften ergeben trotzdem einen wohlgeformten Envelope. Der Writer leitet seinen Key aus Skalar mal Empfänger-Punkt ab, der Empfänger seinen aus seinem Private Key mal dem falschen Originator-Punkt, und die RFC-3394-Integritätsprüfung auf Empfängerseite ist das Erste, was es bemerkt, bis dahin ist die Datei schon ausgeliefert. Ein Roundtrip-Load gegen einen Test-Key auf derselben Kurve gehört in Ihren Build; ein erfolgreiches EndDoc und eine plausible Dateigröße beweisen hier nichts
Dieser Paarungsschritt ist der Grund, warum die Kurvenwahl in der Praxis zählt. Kontrollieren Sie die Zertifikatsausstellung, ist X25519 die Kurve, die sich aus Delphi leichter richtig hinbekommen lässt: ein 32-Byte-Zufallsskalar, ein Helferaufruf, kein Punkt, der von Hand synchron gehalten werden muss. Die NIST-Kurven verdienen ihren Platz, wenn die Zertifikate der Empfänger aus einer PKI kommen, die nur P-256 oder P-384 ausstellt, und dann hört der Roundtrip-Load auf, optional zu sein
Warum kommt es auf die Reihenfolge von /Recipients an?
Die Reihenfolge von /Recipients zählt, weil der File Key ein Digest über den Seed gefolgt von jedem Envelope in Array-Reihenfolge ist, Writer und Reader müssen also identische Bytes in identischer Sequenz hashen. HotPDF hält die Envelopes in der Reihenfolge, in der man sie hinzufügt, und schreibt sie unverändert, man darf also jede Reihenfolge wählen. Was danach mit dem Array passiert, ist der riskante Teil. Nachbearbeitungs-Werkzeuge, Signier-Pipelines und selbstgestrickte Envelope-Builder sind die Orte, an denen es bricht, und die Fehlschläge teilen eine Form — zwei Seiten hashen minimal unterschiedliche Bytes:
- Delphis untypisierte
TListspeichert einen rohen Pointer. Envelopes mitList.Add(Pointer(Bytes))zu sammeln lässt den Referenzzähler bei der lokalen Variablen, sodass das nächsteSetLengthauf dieser Variable den Puffer freigibt oder wiederverwendet und jeder Slot am Ende den letzten Envelope aliasen kann.TList<TBytes>oder ein schlichtesTArray<TBytes>hält die Referenzen gezählt - DER-Parser, die an Ort und Stelle arbeiten, verändern den Puffer, den sie parsen. Über eine unangetastete Kopie jedes Envelopes hashen, genommen bevor irgendein Entpackversuch sie anfasst
- Eine Unicode-
TStringListist kein Ort für binäres DER: Bytes ab$80werden auf dem Weg hinein von der Codepage neu encodiert. Envelopes inTByteshalten, oder erst hex-encodieren - Chiffretext gehört in Hex-Strings. Innerhalb eines Literal-Strings lesen CR, LF und CRLF alle als ein einziges LF zurück (ISO 32000-1 §7.3.4.2), sodass ein Envelope, der zufällig
$0D $0Aenthält, ein Byte verliert. HotPDF schreibt jeden/Recipients-Eintrag als Hex-String und nimmt ihn von der String-Verschlüsselung aus, denn ein Reader braucht die Envelopes, bevor er irgendeinen Key hält - Das erste Inhalts-Byte eines DER-
BIT STRINGzählt unbenutzte Bits und muss bei byte-alignten Keys$00sein. Handgeschriebenes DER, das dieses Byte weglässt und den0x04||X||Y-Punkt direkt in den Inhalt legt, verschiebt den ganzen Key um ein Byte, und ein strenger Parser weist den Originator-Key rundheraus ab - Wenn ein Key, der funktionieren sollte, weiterhin nicht entschlüsseln kann, Schicht für Schicht vergleichen, in dieser Reihenfolge: File Key, die IV am Kopf des Chiffretexts, pro-Objekt-Key, Plaintext. Der Fehler sitzt direkt hinter der ersten Schicht, die nicht übereinstimmt
Eine zertifikatsverschlüsselte PDF mit Private Keys laden
Private Keys müssen registriert sein, bevor LoadFromFile läuft, denn HotPDF holt den File Key während des strukturellen Durchlaufs zurück. RSA-Identitäten, geparst mit HPDFParsePFX, gehen in PubSecKeyMaterial, und weitere RSA-Keys in AddPubSecKeyMaterial, das Material ohne Modulus und privaten Exponenten zurückweist. Beide Key-Slots werden auf RSA-Modulus und privaten Exponenten gematcht, EC-Keys, PFX-basierte eingeschlossen, nehmen also einen dritten Weg: AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), mit einer der Konstanten HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 oder HPDFOIDECP521. Die NIST-Kurven verlangen dort den eigenen unkomprimierten Public Point des Empfängers; die Montgomery-Kurven ignorieren ihn
Eine EC-Identität, die als PFX-Datei ankommt, passt in denselben Aufruf. HPDFParsePFX meldet sie mit KeyAlgorithm = pfxkaECDSA (X25519- und X448-Identitäten eingeschlossen) und füllt CurveOID, den privaten Skalar in PrivateKey und den Zertifikats-Punkt in PublicKey, was geradlinig auf die drei Parameter mappt
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;
procedure OpenAuditPack(const LegalScalar: TBytes);
var
Reader: THotPDF;
EcKey: THPDFPFXKeyMaterial;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
// RSA-Identität wird der Primär-Key
Reader.PubSecKeyMaterial :=
HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
// EC-Identität aus einem PFX: Kurve, Skalar und Punkt kommen in die Agreement-Liste
EcKey := HPDFParsePFX(TFile.ReadAllBytes('reviewer-c-p256.pfx'), 'pfx-password');
if EcKey.KeyAlgorithm = pfxkaECDSA then
Reader.AddPubSecAgreementKeyMaterial(EcKey.CurveOID,
EcKey.PrivateKey, EcKey.PublicKey);
// Rohr X25519-Skalar bei der Rechtsabteilung; kein Punkt nötig
Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
In Ruhe gelassen, probiert HotPDF jeden Envelope gegen jeden registrierten Key: den primären RSA-Key, dann jeden zusätzlichen RSA-Key, dann das EC-Material. PubSecRecipientQuery schränkt das auf einen einzigen Envelope ein. Der Callback bekommt die Envelope-Anzahl und muss einen nullbasierten Index innerhalb des Arrays zurückgeben; jeder andere Wert, -1 eingeschlossen, wirft während des Ladens. Um jeden Envelope zu probieren, lässt man die Property unzugewiesen. Unter Delphi ist der Typ eine Referenz auf eine Funktion, eine anonyme Methode funktioniert also, während es unter FPC ein schlichter Funktionspointer ist, dessen Context-Argument aus PubSecRecipientQueryContext kommt
Ein Verhalten verdient einen eigenen Test. Wenn kein registrierter Key irgendeinen Envelope entpackt, endet die Key-Recovery ohne File Key und wirft dafür nicht. Auf Inhalte asserten, von denen man weiß, dass das Dokument sie hält, etwa einen Textstring oder einen Metadatenwert; ein LoadFromFile-Aufruf, der normal zurückkehrt, zeigt nur, dass die Dateistruktur geparst wurde
Was sollte man im Ziel-Viewer prüfen?
HotPDFs Writer und Reader stimmen byte für byte überein, und die Envelopes folgen den oben zitierten CMS-Strukturen. Drittanbieter-Viewer sind eine andere Frage: Die Unterstützung für RSA-OAEP-Key-Transport und erst recht für X25519- oder X448-Empfänger variiert zwischen Readern und zwischen Versionen desselben Readers. Muss das Dokument in einem bestimmten Viewer aufgehen, verschlüsselt man eine Testdatei für ein Testzertifikat desselben Key-Typs und öffnet sie dort, bevor man sich auf ein Schema festlegt. Liegt der Viewer außerhalb Ihrer Kontrolle, wählt seine Unterstützungsliste das Schema, und das rangiert über dem X25519-Bequemlichkeitsargument von vorhin
Envelope-Permissions bleiben Policy, die konforme Software ehrt, exakt wie unter Passwortverschlüsselung. Die Seed-Qualität liegt bei Ihnen: AESGenerateRandomBytes existiert für diesen Job, und HotPDF wischt seine eigene Kopie des Seeds weg, sobald der File Key abgeleitet ist. Braucht ein String, Stream oder Attachment ein anderes Crypt Filter, zeigt der Crypt-Filter-Policy-Guide für StmF, StrF und EFF, wie man eines zuweist; der Public-Key-Handler akzeptiert DefaultCryptFilter oder Identity
Per-Empfänger-Envelopes für RSA- und Elliptic-Curve-Zertifikate, zusammen mit dem Key-Laden auf dem Rückweg, sind Teil des HotPDF PDF component für Delphi und C++Builder, neben Passwortverschlüsselung und digitalen Signaturen