HotPDF versleutelt een PDF voor specifieke certificaathouders via de ISO 32000 public-key security handler: EnablePubKeyEncryption neemt een random seed van 20 bytes, en elke ontvanger krijgt zijn eigen CMS-envelope, gebouwd door AddPubKeyRecipientCertificate voor RSA-sleutels (RSA-OAEP key transport) of AddPubKeyAgreementRecipientWithSecret voor elliptische-curve-sleutels (ECDH op P-256, P-384, P-521, X25519 of X448). Niemand deelt een wachtwoord; wie een passende privésleutel bezit opent het bestand
De use case is altijd wel een variant op hetzelfde verhaal. Een kwartaalauditpakket gaat naar drie externe reviewers, legal wil dat ze het alle drie lezen, slechts één van hen mag het printen, en niemand wil een wachtwoord in een e-mailthread slingeren naast de bijlage. Wachtwoordversleuteling kan dat niet uitdrukken. Certificaatversleuteling wel, want elke ontvanger ontgrendelt het document met een sleutel die hij al bezit, en elke ontvanger kan een eigen permissionset in zijn eigen envelope meedragen
Hoe verschilt certificaatgebaseerde PDF-versleuteling van een wachtwoord?
Een met public key versleutelde PDF leidt zijn file key af uit een random seed plus de exacte bytes van elke ontvanger-envelope, niet uit iets wat een mens intoetst. De handler staat beschreven in ISO 32000-1 §7.6.4 (§7.6.5 in ISO 32000-2), en de envelopes zijn CMS-EnvelopedData-structuren zoals gedefinieerd in RFC 5652. HotPDF schrijft /Filter /Adobe.PubSec met /SubFilter /adbe.pkcs7.s5; bij AES-256 betekent dat /V 5 en een /DefaultCryptFilter-entry onder /CF met /CFM /AESV3, en de /Recipients-array woont in die crypt filter. Elke envelope versleutelt 24 bytes: de seed van 20 bytes gevolgd door het 32-bit-permissionword van die ontvanger. De /P-waarde in de encryption-dictionary is alleen een placeholder, want de echte permissies reizen binnenin elke envelope. Bij het laden pakt een reader één envelope uit, herstelt de seed, en hasht de seed samen met elke envelope in /Recipients-volgorde (SHA-256 voor AES-256, SHA-1 voor de oudere ciphers) om de file key opnieuw op te bouwen. Twijfelt u nog tussen dit model en gewone wachtwoorden, dan behandelt de gids over AES-256-wachtwoordversleuteling en permissionflags de andere kant van die afweging
RSA-ontvangers schrijven met EnablePubKeyEncryption
Voor RSA-certificaten roept u EnablePubKeyEncryption aan met aes256, en daarna AddPubKeyRecipientCertificate één keer per DER-gecodeerd certificaat vóór BeginDoc. De helper bouwt in-process een RSAES-OAEP-envelope met THPDFRSAOAEPHash-waarden voor de OAEP-digest en de MGF1-digest (rohSHA256, rohSHA384 of rohSHA512), en versleutelt de envelope-inhoud met 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); // precies 20 bytes, ook voor AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // default keytype is aes128
// Reviewer A mag printen; reviewer B mag alleen lezen en extraheren
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;
Drie details in die listing zijn dragend. Ten eerste staat de seedlengte vast op 20 bytes voor elk keytype, AES-256 incluis; EnablePubKeyEncryption gooit bij elke andere lengte. Ten tweede staat EnablePubKeyEncryption standaard op aes128, en allebei de certificaathelpers weigeren te draaien tenzij het keytype aes256 is, dus het vergeten van het tweede argument levert de exception "certificate envelopes require aes256" op. De legacy-ciphers (k40, k128, aes128) werken nog, maar alleen via AddPubKeyRecipient met een envelope die u elders zelf bouwde. Ten derde is AES-256-public-key-versleuteling een PDF 2.0-feature, dus HotPDF verhoogt de documentversie automatisch naar 2.0. Met StrictVersionLock gezet op een lagere versie keert EnablePubKeyEncryption terug zonder iets in te schakelen, en het falen duikt pas op de volgende regel op als "call EnablePubKeyEncryption first". Versleuteling wisselen tijdens een incrementele update gooit meteen een EInvalidOpException
ECDH-ontvangers toevoegen: P-256, P-384, P-521, X25519 en X448
Voor elliptische-curve-certificaten schrijft AddPubKeyAgreementRecipientWithSecret een CMS key-agreement-ontvanger (KeyAgreeRecipientInfo, de KARI-structuur uit RFC 5753, met het X25519- en X448-profiel uit RFC 8418) en berekent hij het ECDH shared secret in-process. De curve kiest u met een THPDFPubKeyAgreementScheme-waarde: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 of pkasX448. Het scheme moet matchen met de sleutel in het certificaat, anders gooit de aanroep "Certificate key does not match the requested agreement scheme". Onder de motorkap krijgt elke envelope een verse random UKM van 32 bytes, een met de stdDH-KDF afgeleide key-encryption key (SHA-256 voor P-256 en X25519, SHA-384 voor P-384, SHA-512 voor P-521 en X448), en een AES-256 key wrap zoals gedefinieerd in RFC 3394. Het shared secret zelf komt uit pure Pascal-curvecode, zonder dat er een platform-cryptoprovider aan te pas komt; het artikel over pure Pascal NIST-curve-aritmetiek legt uit hoe die laag is gebouwd en geverifieerd. Voor de Montgomery-curves kan het hele ephemere sleutelpaar lokaal worden gegenereerd:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Verse ephemere scalar per envelope; clamping gebeurt binnen de ladder
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: alleen betekenisvol voor de NIST-curves
finally
HPDFSecureClearBytes(Scalar);
end;
end;
De NIST-curves vragen meer van de aanroeper. HotPDF levert public-key-helpers alleen voor X25519 en X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), dus voor P-256, P-384 en P-521 genereert u het ephemere sleutelpaar met eigen tooling en geeft u een big-endian scalar van precies de veldgrootte (32, 48 of 66 bytes) mee plus het bijpassende ongecomprimeerde 0x04||X||Y-punt als OriginatorPublicKey. HotPDF valideert het ontvangerpunt tegen de curvevergelijking, maar hij kan niet controleren dat uw originator public key werkelijk bij uw scalar hoort. Niet-passende helften leveren nog steeds een keurig gevormde envelope op die geen enkele ontvanger kan openen, en daarom hoort een round-trip-load in uw testsuite, niet alleen een controle op bestandsgrootte
Waarom maakt de volgorde van /Recipients uit?
De volgorde van /Recipients telt omdat de file key een digest is over de seed en elke envelope in arrayvolgorde, dus writer en reader moeten dezelfde bytes in dezelfde volgorde hashen. HotPDF houdt envelopes in de volgorde waarin u ze toevoegt en schrijft ze onveranderd weg, wat betekent dat u ontvangers in elke gewenste volgorde mag toevoegen, maar niets stroomafwaarts die array mag herordenen, herencoderen of "opschonen". De meeste echte bugs op dit terrein waren een variatie op dat thema, waarbij twee kanten iets verschillende bytes hashten:
- Dynamische arrays in een
TListbewaren viaAddhoudt alleen een raw pointer vast terwijl de reference count bij de lokale variabele blijft. De volgendeSetLengthgeeft de buffer vrij en kan hem hergebruiken, dus elke slot begon naar de laatste envelope te aliasen en bestanden met meerdere ontvangers leidden de verkeerde key af. De fix is een owned kopie bewaren metList.Add(Pointer(System.Copy(Bytes))) - Het uitpakken van envelopes parseert de DER in place, en de key-recovery-pas hashte oorspronkelijk diezelfde live arrays. De reader snapshot nu pristine kopieën van elke envelope voordat een uitpakactie ze aanraakt, en de digest loopt over de snapshots
- Binaire DER die door een Unicode-
TStringListgaat krijgt bytes op of boven$80hergecodeerd door de codepage, dus HotPDF bewaart envelopes intern als hex-tekst - Versleutelde en binaire strings moeten als hex-strings worden weggeschreven. Een literal string ondervalt end-of-line-normalisatie, waarbij CR, LF en CRLF allemaal één LF worden (ISO 32000-1 §7.3.4.2), en dat herschrijft ciphertext geruisloos. HotPDF schrijft elke
/Recipients-entry als hex-string en vrijwaart haar van stringversleuteling, want elke reader heeft de envelopes nodig voordat hij enige key bezit - De eerste byte van een DER-
BIT STRINGtelt ongebruikte bits en moet nul zijn voor byte-uitgelijnde sleutels. Haar ongeïnitialiseerd laten naSetLengthschreef wat er toevallig op de stack stond, en een strenge unwrapper weigerde de originator key, dus een bestand kon af en toe weigeren te openen met precies de key waarvoor het was geschreven - Als dezelfde key nog steeds niet kan decoderen, vergelijk dan laag voor laag: de file key, daarna de ciphertext-prefix (de IV), daarna de object key, daarna de plaintext. De bug zit vlak na de eerste laag waar het verschil zit
Hoe opent u een certificaatversleutelde PDF met een privésleutel?
Om een certificaatversleutelde PDF te openen registreert u het privésleutelmateriaal vóór het aanroepen van LoadFromFile, want HotPDF herstelt de file key tijdens de structurele pas. Wijs een met HPDFParsePFX geparsde RSA- of EC-sleutel toe aan PubSecKeyMaterial, voeg extra RSA-sleutels toe met AddPubSecKeyMaterial, en registreer kale ECDH-scalars met AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), met de constanten HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 of HPDFOIDECP521. De NIST-curves vereisen het eigen ongecomprimeerde public point van de ontvanger; de Montgomery-curves negeren het
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);
// Optioneel: pak de envelope rechtstreeks in plaats van ze allemaal te proberen
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = probeer elke envelope op volgorde
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Zonder callback probeert HotPDF elke envelope tegen elke geregistreerde sleutel: eerst de primaire sleutel, daarna elke extra RSA-sleutel, daarna het EC-materiaal. PubSecRecipientQuery ontvangt het aantal envelopes en geeft een 0-based index of -1 terug, en een index buiten de array gooit een exception in plaats van geclampt te worden. Bedenk dat AddPubSecKeyMaterial alleen RSA-materiaal accepteert (hij dringt aan op een modulus en een private exponent), dus EC-sleutels horen thuis in PubSecKeyMaterial of AddPubSecAgreementKeyMaterial. Als geen enkele key een envelope uitpakt keert de recoverystap terug zonder file key in plaats van te gooien, dus verifieer dat de inhoud die u verwacht echt gedecodeerd is in plaats van te vertrouwen op het terugkeren van de load-aanroep
Wat HotPDF niet garandeert
HotPDF garandeert dat zijn eigen writer en reader byte voor byte overeenkomen, en hij bouwt envelopes die de hierboven genoemde CMS-structuren volgen. Hij garandeert niet dat elke PDF-viewer elke combinatie opent. Ondersteuning voor RSA-OAEP key transport en voor X25519- of X448-ontvangers verschilt per reader en per versie, en we hebben geen compatibiliteitsresultaten voor die combinaties gepubliceerd. Moet een document in een specifieke viewer opengaan, versleutel dan een testbestand voor een testcertificaat van hetzelfde keytype en open het daar voordat u aan een scheme vasthoudt. Permissies die in de envelope reizen blijven policy die conformerende software eert, precies zoals bij wachtwoordversleuteling. De kwaliteit van de seed is ook uw verantwoordelijkheid: AESGenerateRandomBytes is er voor die klus, en HotPDF wist zijn kopie van de seed zodra de file key is afgeleid. Heeft u ook nog een string, stream of bijlage nodig die een andere crypt filter gebruikt, dan laat de crypt filter policy-gids voor StmF, StrF en EFF zien welke filternamen de public-key-handler accepteert
Certificaatversleuteling, RSA-OAEP- en ECDH-ontvanger-envelopes en het laden van privésleutels zitten allemaal in de HotPDF Delphi PDF component, naast wachtwoordversleuteling, digitale handtekeningen en de rest van de ISO 32000-toolset voor Delphi en C++Builder