HotPDF krypterer en PDF til specifikke certifikatholdere gennem ISO 32000 public-key security handler: EnablePubKeyEncryption tager et tilfældigt seed på 20 bytes, og hver modtager får sin egen CMS-envelope, bygget af AddPubKeyRecipientCertificate til RSA-nøgler (RSA-OAEP key transport) eller AddPubKeyAgreementRecipientWithSecret til elliptic curve-nøgler (ECDH på P-256, P-384, P-521, X25519 eller X448). Ingen deler en adgangskode; den, der holder en matchende privat nøgle, åbner filen
Use casen er altid en eller anden version af den samme historie. En kvartals-auditpakke går til tre eksterne reviewers, legal vil have, at hver af dem læser den, kun én af dem må printe den, og ingen vil have en adgangskode liggende i en mailtråd ved siden af den vedhæftede fil. Adgangskodekryptering kan ikke udtrykke det. Certifikatkryptering kan, for hver modtager låser dokumentet op med en nøgle, de allerede holder, og hver modtager kan bære et forskelligt tilladelsessæt inde i sin egen envelope
Hvordan adskiller certifikatbaseret PDF-kryptering sig fra en adgangskode?
En public-key-krypteret PDF afleder sin filnøgle af et tilfældigt seed plus de præcise bytes af hver modtager-envelope, ikke af noget, et menneske taster. Handleren er beskrevet i ISO 32000-1 §7.6.4 (§7.6.5 i ISO 32000-2), og envelope-ne er CMS EnvelopedData-strukturer som defineret i RFC 5652. HotPDF skriver /Filter /Adobe.PubSec med /SubFilter /adbe.pkcs7.s5; for AES-256 betyder det /V 5 og en /DefaultCryptFilter-entry under /CF med /CFM /AESV3, og /Recipients-arrayet bor inde i den crypt filter. Hver envelope krypterer 24 bytes: seeden på 20 bytes efterfulgt af den modtagers 32-bit tilladelsesord. /P-værdien i encryption-dictionaryen er kun en pladsholder, for de rigtige tilladelser rejser inde i hver envelope. Ved load pakker en reader én envelope op, gendanner seeden og hasher seeden sammen med hver envelope i /Recipients-rækkefølge (SHA-256 til AES-256, SHA-1 til de ældre ciphers) for at genopbygge filnøglen. Hvis du stadig vælger mellem denne model og almindelige adgangskoder, dækker guiden til AES-256-adgangskodekryptering og tilladelsesflag den anden side af det trade-off
At skrive RSA-modtagere med EnablePubKeyEncryption
For RSA-certifikater: kald EnablePubKeyEncryption med aes256, og kald derefter AddPubKeyRecipientCertificate én gang pr. DER-encoded certifikat før BeginDoc. Helperen bygger en RSAES-OAEP-envelope in-process med THPDFRSAOAEPHash-værdier til OAEP-digesten og MGF1-digesten (rohSHA256, rohSHA384 eller rohSHA512), og den krypterer envelope-indholdet med 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); // præcis 20 bytes, også for 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 type er aes128
// Reviewer A må printe; reviewer B må kun læse og udtrække
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;
Tre detaljer i det listing bærer vægt. For det første er seed-længden fastlåst til 20 bytes for alle nøgletyper, AES-256 inkluderet; EnablePubKeyEncryption rejser ved enhver anden længde. For det andet defaulter EnablePubKeyEncryption til aes128, og begge certifikat-helpers nægter at køre, medmindre nøgletypen er aes256, så at glemme det andet argument giver dig exceptionen "certificate envelopes require aes256". De legacy-ciphers (k40, k128, aes128) virker stadig, men kun gennem AddPubKeyRecipient med en envelope, du har bygget et andet sted. For det tredje er AES-256 public-key-kryptering en PDF 2.0-feature, så HotPDF hæver dokumentversionen til 2.0 automatisk. Med StrictVersionLock sat på en lavere version returnerer EnablePubKeyEncryption uden at aktivere noget, og fejlen viser først på næste linje som "call EnablePubKeyEncryption first". At skifte kryptering under en inkrementel opdatering rejser EInvalidOpException med det samme
Tilføjelse af ECDH-modtagere: P-256, P-384, P-521, X25519 og X448
Til elliptic curve-certifikater skriver AddPubKeyAgreementRecipientWithSecret en CMS key-agreement-modtager (KeyAgreeRecipientInfo, KARI-strukturen fra RFC 5753, med X25519- og X448-profilen fra RFC 8418) og beregner den ECDH-delte secret in-process. Du vælger kurven med en THPDFPubKeyAgreementScheme-værdi: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 eller pkasX448. Schemat skal matche nøglen i certifikatet, ellers rejser kaldet "Certificate key does not match the requested agreement scheme". Under motorhjelmen får hver envelope et frisk tilfældigt UKM på 32 bytes, en key-encryption-nøgle afledt med stdDH KDF (SHA-256 til P-256 og X25519, SHA-384 til P-384, SHA-512 til P-521 og X448) og en AES-256 key wrap som defineret i RFC 3394. Den delte secret kommer selv fra ren Pascal-kurvekode uden nogen platform crypto provider involveret; artiklen om ren Pascal NIST curve-aritmetik forklarer, hvordan det lag blev bygget og verificeret. Til Montgomery-kurverne kan hele det efemære nøglepar genereres lokalt:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Frisk efemær scalar pr. envelope; clamping sker inde i ladderen
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: kun meningsfuldt for NIST-kurverne
finally
HPDFSecureClearBytes(Scalar);
end;
end;
NIST-kurverne kræver mere af calleren. HotPDF leverer kun public-key-helpers til X25519 og X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), så til P-256, P-384 og P-521 genererer du det efemære nøglepar med dit eget værktøj og giver en big-endian scalar af præcis feltstørrelsen (32, 48 eller 66 bytes) plus det matchende ukomprimerede 0x04||X||Y-punkt som OriginatorPublicKey. HotPDF validerer modtager-punktet mod kurveligningen, men den kan ikke tjekke, at din originator public key faktisk hører til din scalar. Mispassende halvdele producerer stadig en helt velformet envelope, som ingen modtager kan åbne, og det er derfor, en round-trip-load hører hjemme i din test-suite, ikke kun et filstørrelse-tjek
Hvorfor betyder rækkefølgen af /Recipients noget?
Rækkefølgen af /Recipients betyder noget, fordi filnøglen er en digest over seeden og hver envelope i array-rækkefølge, så writer og reader skal hashe de samme bytes i samme sekvens. HotPDF beholder envelopes i den rækkefølge, du tilføjer dem, og skriver dem uændret, hvilket betyder, at du kan tilføje modtagere i vilkårlig rækkefølge, men intet downstream må sortere det array om, re-encode det eller "rydde op" i det. De fleste af de rigtige bugs på dette område var en variation over det tema, hvor to sider hashe let forskellige bytes:
- At gemme dynamic arrays i en
TListviaAddbeholder kun en rå pointer, mens reference countet bliver hos den lokale variabel. NæsteSetLengthfrigør bufferen og kan genbruge den, så hver slot endte med at alias'e den sidste envelope, og multi-modtager-filer afledte den forkerte nøgle. Fixet er at gemme en ejet kopi medList.Add(Pointer(System.Copy(Bytes))) - Envelope-udpakning parser DER in place, og key-recovery-passet hashe oprindeligt de samme levende arrays. Readeren tager nu snapshots af upåvirkede kopier af hver envelope, før nogen udpakning rører dem, og digesten løber over snapshotene
- Binær DER, der sendes gennem en Unicode
TStringList, får bytes på eller over$80re-encodet af code siden, så HotPDF gemmer envelopes som hex-tekst internt - Krypterede og binære strenge skal skrives som hex-strenge. En literal streng er underlagt end-of-line-normalisering, hvor CR, LF og CRLF alle bliver til én enkelt LF (ISO 32000-1 §7.3.4.2), og det omskriver ciphertext i stilhed. HotPDF udsender hver
/Recipients-entry som en hex-streng og fritager den for string-kryptering, da enhver reader skal bruge envelopes, før den holder nogen nøgle - Den første byte af en DER
BIT STRINGtæller ubrugte bits og skal være nul for byte-justerede nøgler. At lade den stå uinitialiseret efterSetLengthskrev, hvad der nu lå på stakken, og en strict unwrapper afviste originator-nøglen, så en fil af og til kunne nægte at åbne med netop den nøgle, den var skrevet til - Når samme nøgle stadig ikke kan dekryptere, så sammenlign lag for lag: filnøglen, derefter ciphertext-præfikset (IV), derefter objektnøglen, derefter plaintext. Buggen bor lige efter det første lag, der ikke stemmer
Hvordan åbner du en certifikatkrypteret PDF med en privat nøgle?
For at åbne en certifikatkrypteret PDF registrerer du det private nøglemateriale, før du kalder LoadFromFile, for HotPDF gendanner filnøglen under det strukturelle pass. Tildel en RSA- eller EC-nøgle parset med HPDFParsePFX til PubSecKeyMaterial, tilføj flere RSA-nøgler med AddPubSecKeyMaterial, og registrer rå ECDH-scalars med AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), med konstanterne HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 eller HPDFOIDECP521. NIST-kurverne kræver modtagerens eget ukomprimerede public point; Montgomery-kurverne ignorerer det
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);
// Valgfrit: vælg envelope direkte i stedet for at prøve dem alle
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = prøv hver envelope i rækkefølge
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Uden en callback prøver HotPDF hver envelope mod hver registreret nøgle: primærnøglen først, derefter hver ekstra RSA-nøgle, derefter EC-materialet. PubSecRecipientQuery modtager envelope-antallet og returnerer et 0-baseret indeks eller -1, og et indeks uden for arrayet rejser en exception i stedet for at blive clamped. Bemærk, at AddPubSecKeyMaterial kun accepterer RSA-materiale (den insisterer på en modulus og en privat eksponent), så EC-nøgler hører hjemme i PubSecKeyMaterial eller AddPubSecAgreementKeyMaterial. Når ingen nøgle pakker nogen envelope op, returnerer recovery-trinnet uden en filnøgle i stedet for at rejse, så verificér, at det indhold, du forventer, faktisk blev dekrypteret, i stedet for at stole på, at load-kaldet returnerede
Hvad HotPDF ikke garanterer
HotPDF garanterer, at dens egen writer og reader er enige byte for byte, og den bygger envelopes, der følger de CMS-strukturer, der er citeret ovenfor. Den garanterer ikke, at enhver PDF-viewer åbner enhver kombination. Support for RSA-OAEP key transport og for X25519- eller X448-modtagere varierer mellem readers og versioner, og vi har ikke publiceret kompatibilitetsresultater for de kombinationer. Skal et dokument åbne i en bestemt viewer, så kryptér en testfil til et testcertifikat af samme nøgletype og åbn den dér, før du beslutter dig for et scheme. Tilladelser, der bæres i envelope, forbliver policy, som conforming software ærer, præcis som under adgangskodekryptering. Seed-kvaliteten er også dit ansvar: AESGenerateRandomBytes er der til det job, og HotPDF tørrer sin kopi af seeden, så snart filnøglen er afledt. Skal du også bruge en streng, stream eller vedhæftet fil til at bruge et andet crypt filter, viser crypt filter-policy-guiden for StmF, StrF og EFF, hvilke filternavne public-key-handleren accepterer
Certifikatkryptering, RSA-OAEP- og ECDH-modtager-envelope og indlæsning af private nøgler følger alle med i HotPDF Delphi PDF-komponenten, sammen med adgangskodekryptering, digitale signaturer og resten af ISO 32000-værktøjskassen til Delphi og C++Builder