Tehnički članak

Šifrovanje PDF-a sertifikatima u Delphi-ju: RSA-OAEP i ECDH

HotPDF šifruje PDF za konkretne nosioce sertifikata kroz ISO 32000 public-key security handler: EnablePubKeyEncryption uzima 20-bajtni nasumični seed, a svaki primalac dobija svoj CMS envelope, koji gradi AddPubKeyRecipientCertificate za RSA ključeve (RSA-OAEP key transport) ili AddPubKeyAgreementRecipientWithSecret za eliptične krive (ECDH na P-256, P-384, P-521, X25519 ili X448). Niko ne deli lozinku; ko drži odgovarajući privatni ključ, taj otvara fajl

Slučaj upotrebe je uvek neka verzija iste priče. Tromesečni audit paket ide trojici spoljnih recenzenta, pravno odeljenje želi da ga svaki od njih pročita, samo jedan od njih sme da ga štampa, i niko neće lozinku koja sedi u mejl lancu pored priloga. Šifrovanje lozinkom to ne može da izrazi. Šifrovanje sertifikatom može, jer svaki primalac otključava dokument ključem koji već drži, i svaki primalac može nositi drugačiji skup dozvola unutar sopstvenog envelope-a

Po čemu se šifrovanje PDF-a sertifikatom razlikuje od lozinke?

PDF šifrovan javnim ključem izvodi svoj file key iz nasumičnog seed-a plus tačnih bajtova svakog envelope-a primalaca, a ne iz ičega što čovek otkuca. Handler je opisan u ISO 32000-1 §7.6.4 (§7.6.5 u ISO 32000-2), a envelope-i su CMS EnvelopedData strukture kako ih definiše RFC 5652. HotPDF upisuje /Filter /Adobe.PubSec sa /SubFilter /adbe.pkcs7.s5; za AES-256 to znači /V 5 i /DefaultCryptFilter unos pod /CF sa /CFM /AESV3, a /Recipients niz živi unutar tog crypt filtera. Svaki envelope šifruje 24 bajta: 20-bajtni seed pa 32-bitni permission word tog primalca. /P vrednost u encryption rečniku je samo rezervisano mesto, jer prave dozvole putuju unutar svakog envelope-a. Pri učitavanju čitač odmotava jedan envelope, povrati seed, i hešuje seed zajedno sa svakim envelope-om u redosledu /Recipients (SHA-256 za AES-256, SHA-1 za starije šifre) da ponovo izgradi file key. Ako još uvek birate između ovog modela i običnih lozinki, vodič o AES-256 šifrovanju lozinkom i permission flagovima pokriva drugu stranu tog kompromisa

Dijagram public-key šifrovanja u HotPDF-u: EnablePubKeyEncryption fiksira 20-bajtni seed, svaki CMS EnvelopedData envelope šifruje tih 20 bajtova plus jedan 32-bitni permission word unutar /Filter /Adobe.PubSec sa /SubFilter /adbe.pkcs7.s5 i /CFM /AESV3, a čitač odmotava jedan envelope, povrati seed i hešuje ga sa svakim /Recipients unosom po redosledu niza da ponovo izgradi file key
/P vrednost u encryption rečniku je samo rezervisano mesto jer prave dozvole putuju unutar svakog envelope-a, i ništa nizvodno ne sme da preuredi ili ponovo enkoduje niz preko koga digest prolazi

Upisivanje RSA primalaca pomoću EnablePubKeyEncryption

Za RSA sertifikate, pozovite EnablePubKeyEncryption sa aes256, pa pozovite AddPubKeyRecipientCertificate jednom po DER-enkodovanom sertifikatu pre BeginDoc. Helper gradi RSAES-OAEP envelope u samom procesu sa THPDFRSAOAEPHash vrednostima za OAEP digest i MGF1 digest (rohSHA256, rohSHA384 ili rohSHA512), i šifruje sadržaj envelope-a sa 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);                      // tačno 20 bajtova, čak i za AES-256
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // podrazumevani tip ključa je aes128
    // Recenzent A sme da štampa; recenzent B sme samo da čita i izvlači
    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;

Tri detalja u tom ispisu nose teret. Prvo, dužina seed-a je fiksirana na 20 bajtova za svaki tip ključa, pa i AES-256; EnablePubKeyEncryption podiže izuzetak na bilo koju drugu dužinu. Drugo, EnablePubKeyEncryption po podrazumevanju ide na aes128, i oba sertifikat helpera odbijaju da rade osim ako je tip ključa aes256, pa zaboravljeni drugi argument donosi izuzetak „certificate envelopes require aes256”. Legacy šifre (k40, k128, aes128) i dalje rade, ali samo kroz AddPubKeyRecipient sa envelope-om koji ste izgradili negde drugde. Treće, AES-256 public-key šifrovanje je mogućnost PDF 2.0, pa HotPDF automatski podiže verziju dokumenta na 2.0. Sa postavljenim StrictVersionLock-om na nižoj verziji, EnablePubKeyEncryption se vrati bez uključivanja ičega, a kvar se pojavi tek u sledećoj liniji kao „call EnablePubKeyEncryption first”. Prelazak na drugu šifru tokom inkrementalnog ažuriranja odmah podiže EInvalidOpException

Dodavanje ECDH primalaca: P-256, P-384, P-521, X25519 i X448

Za sertifikate sa eliptičnim krivama, AddPubKeyAgreementRecipientWithSecret upisuje CMS key-agreement primalca (KeyAgreeRecipientInfo, KARI struktura iz RFC 5753, sa X25519 i X448 profilom iz RFC 8418) i računa ECDH deljeni tajni u samom procesu. Krivu birate THPDFPubKeyAgreementScheme vrednošću: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ili pkasX448. Šema mora da se poklopi sa ključem u sertifikatu, ili poziv podiže „Certificate key does not match the requested agreement scheme”. Ispod haube, svaki envelope dobija svež nasumični 32-bajtni UKM, key-encryption key izveden stdDH KDF-om (SHA-256 za P-256 i X25519, SHA-384 za P-384, SHA-512 za P-521 i X448), i AES-256 key wrap kako ga definiše RFC 3394. Sam deljeni tajni dolazi iz čistog Pascal koda za krive, bez ikakvog platformskog crypto provajdera; članak o čisto Pascal aritmetici NIST krivih objašnjava kako je taj sloj izgrađen i verifikovan. Za Montgomery krive ceo efemerni par ključeva može da se generiše lokalno:

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
  HPDFKeyAgreement;

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Svež efemerni skalar po envelope-u; klamping radi unutar ladder-a
  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: značajan samo za NIST krive
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST krive traže više od pozivaoca. HotPDF isporučuje public-key helper-e samo za X25519 i X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), pa za P-256, P-384 i P-521 sami generišete efemerni par ključeva svojim alatima i predajete big-endian skalar tačno veličine polja (32, 48 ili 66 bajtova) plus odgovarajuću nekompresovanu 0x04||X||Y tačku kao OriginatorPublicKey. HotPDF validira tačku primalca naspram jednačine krive, ali ne može da proveri da vaš originator javni ključ zaista pripada vašem skalaru. Nepoklopljene polovine i dalje proizvode savršeno dobro oblikovan envelope koji nijedan primalac ne može da otvori, pa round-trip učitavanje spada u vaš test suite, a ne samo provera veličine fajla

Dijagram ECDH dogovora u HotPDF-u: AddPubKeyAgreementRecipientWithSecret izvodi deljeni tajni čisto Pascal kodom za krive, meša svež 32-bajtni UKM kroz stdDH KDF sa SHA-256 za P-256 i X25519, SHA-384 za P-384, SHA-512 za P-521 i X448, pa umotava content key RFC 3394 AES-256 key wrap-om da izgradi KeyAgreeRecipientInfo envelope
Vrednost šeme od pkasECDHP256 do pkasX448 mora da se poklopi sa ključem sertifikata, a nepoklopljene polovine skalara i javne tačke i dalje proizvode dobro oblikovan envelope koji nijedan primalac ne može da otvori

Zašto je redosled /Recipients važan?

Redosled /Recipients je važan jer je file key digest preko seed-a i svakog envelope-a po redosledu niza, pa pisac i čitač moraju da hešuju iste bajtove u istom nizu. HotPDF čuva envelope-e u redosledu kojem ih dodajete i upisuje ih nepromenjene, što znači da primalace možete dodavati u bilo kom redosledu, ali ništa nizvodno ne sme da preuredi, ponovo enkoduje ili „sredi” taj niz. Većina pravih bugova u ovoj oblasti bila je varijanta na tu temu, gde su dve strane hešovale malo drugačije bajtove:

  • Čuvanje dinamičkih nizova u TList preko Add zadržava samo sirovi pokazivač dok brojač referenci ostaje uz lokalnu promenljivu. Sledeći SetLength oslobađa bafer i može ga iskoristiti ponovo, pa je svaki slot završio kao alias poslednjeg envelope-a i fajlovi sa više primalaca izvodili su pogrešan ključ. Popravka je da se čuva vlasnička kopija sa List.Add(Pointer(System.Copy(Bytes)))
  • Odmotavanje envelope-a parsira DER na mestu, i key-recovery prolaz je prvobitno hešovao te iste žive nizove. Čitač sada snima netaknute kopije svakog envelope-a pre nego što bilo koje odmotavanje dotakne, i digest ide preko snimaka
  • Binarni DER proveden kroz Unicode TStringList dobija bajtove od $80 naviše ponovo enkodirane po code page, pa HotPDF čuva envelope-e interno kao heks tekst
  • Šifrovani i binarni stringovi moraju da se upišu kao heks stringovi. Literalan string podleže end-of-line normalizaciji gde CR, LF i CRLF svi postaju jedan LF (ISO 32000-1 §7.3.4.2), i to tiho prepisuje ciphertext. HotPDF emituje svaki /Recipients unos kao heks string i izuzima ga iz string šifrovanja, jer svakom čitaču trebaju envelope-i pre nego što drži ijedan ključ
  • Prvi bajt DER BIT STRING-a broji nekorišćene bitove i mora biti nula za bajtno poravnate ključeve. Neinicijalizovan posle SetLength upisivao je šta god je bilo na steku, i strog odmotavač je odbijao originator ključ, pa fajl povremeno nije mogao da se otvori baš ključem za koji je napisan
  • Kad isti ključ i dalje ne može da dešifruje, poredajte sloj po sloj: file key, pa ciphertext prefiks (IV), pa object key, pa čist tekst. Bug sedi odmah iza prvog sloja koji se ne složi

Kako otvoriti sertifikatom šifrovan PDF privatnim ključem?

Da otvorite PDF šifrovan sertifikatom, registrujte materijal privatnog ključa pre poziva LoadFromFile, jer HotPDF povrati file key tokom strukturnog prolaza. RSA ili EC ključ parsiran sa HPDFParsePFX dodelite PubSecKeyMaterial-u, dodatne RSA ključeve dodajte sa AddPubSecKeyMaterial, a sirove ECDH skalare registrujte sa AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), koristeći konstante HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ili HPDFOIDECP521. NIST krive zahtevaju sopstvenu nekompresovanu javnu tačku primalca; Montgomery krive je ignorišu

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);
    // Opciono: izaberite envelope direktno umesto da isprobavate sve
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = isprobaj svaki envelope po redu
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Bez callback-a, HotPDF isprobava svaki envelope naspram svakog registrovanog ključa: prvo primarni ključ, pa svaki dodatni RSA ključ, pa EC materijal. PubSecRecipientQuery dobija broj envelope-a i vraća indeks sa bazom nula ili -1, a indeks van niza podiže izuzetak umesto da bude prikovan na granicu. Imajte na umu da AddPubSecKeyMaterial prihvata samo RSA materijal (inistiše na modulu i privatnom eksponentu), pa EC ključevi pripadaju PubSecKeyMaterial-u ili AddPubSecAgreementKeyMaterial-u. Kada nijedan ključ ne odmotava nijedan envelope, korak oporavka se vrati bez file key-a umesto da podigne izuzetak, pa proverite da se sadržaj koji očekujete zaista dešifrovao, umesto da verujete tome što je poziv učitavanja vratio

Dijagram učitavanja privatnog ključa u HotPDF-u: PubSecKeyMaterial nosi primarni RSA ili EC ključ iz HPDFParsePFX, AddPubSecKeyMaterial dodaje samo RSA ključeve, AddPubSecAgreementKeyMaterial registruje sirove ECDH skalare pod HPDFOIDX25519 do HPDFOIDP521 krive OID-ovima, a pri LoadFromFile provajder isprobava primarni ključ, pa svaki dodatni RSA ključ, pa EC materijal naspram svakog envelope-a
Kada nijedan ključ ne odmotava nijedan envelope, korak oporavka se vrati bez file key-a umesto da podigne izuzetak, pa proverite da se sadržaj zaista dešifrovao ili pribijte envelope kroz PubSecRecipientQuery

Šta HotPDF ne garantuje

HotPDF garantuje da se njegov pisac i čitač slažu bajt po bajt, i gradi envelope-e koji prate gore navedene CMS strukture. Ne garantuje da će svaki PDF pregledač otvoriti svaku kombinaciju. Podrška za RSA-OAEP key transport i za X25519 ili X448 primalce varira između čitača i verzija, i nismo objavili rezultate kompatibilnosti za te kombinacije. Ako dokument mora da se otvori u konkretnom pregledaču, šifrujte test fajl za test sertifikat istog tipa ključa i otvorite ga tamo pre nego što se opredelite za šemu. Dozvole koje envelope nosi ostaju politika koju usaglašen softver poštuje, tačno kao kod šifrovanja lozinkom. Kvalitet seed-a je i vaša odgovornost: AESGenerateRandomBytes postoji baš za taj posao, a HotPDF briše svoju kopiju seed-a čim se file key izvede. Ako stringu, streamu ili prilogu treba drugačiji crypt filter, vodič o crypt filter politikama za StmF, StrF i EFF pokazuje koja imena filtera public-key handler prihvata

Šifrovanje sertifikatom, RSA-OAEP i ECDH envelope-i primalaca i učitavanje privatnog ključa isporučuju se u HotPDF Delphi PDF component-i, uz šifrovanje lozinkom, digitalne potpise i ostatak ISO 32000 alata za Delphi i C++Builder