Tehnični članak

Šifriranje PDF s certifikati v Delphiju: RSA-OAEP in ECDH

HotPDF šifrira PDF za določene imetnike certifikatov skozi upravljalnik varnosti z javnimi ključi ISO 32000: EnablePubKeyEncryption vzame 20-bajtno naključno seme, vsak prejemnik pa dobi svojo ovojnico CMS, zgrajeno s AddPubKeyRecipientCertificate za ključe RSA (prenos ključev RSA-OAEP) ali AddPubKeyAgreementRecipientWithSecret za ključe eliptičnih krivulj (ECDH na P-256, P-384, P-521, X25519 ali X448). Nihče ne deli gesla; kdor drži ujemajoč se zasebni ključ, odpre datoteko

Primer uporabe je vedno nekaka različica iste zgodbe. Četrtletni paket revizije gre trem zunanjim recenzentom, pravna služba želi, da ga vsak prebere, samo enemu je dovoljeno tiskati, in nikomur ni do gesla, ki bi sedelo v niti e-pošte poleg priloge. Šifriranje z geslom tega ne zna izraziti. Šifriranje s certifikatom zna, ker vsak prejemnik odklene dokument s ključem, ki ga že ima, vsak prejemnik pa lahko v svoji ovojnici nosi drugačen nabor dovoljenj

Kako se šifriranje PDF s certifikatom razlikuje od gesla?

Šifriran PDF z javnimi ključi izpelje svoj ključ datoteke iz naključnega semena plus točnih bajtov vsake prejemniške ovojnice in ne iz česar koli, kar bi vtipkal človek. Upravljalnik je opisan v ISO 32000-1 §7.6.4 (§7.6.5 v ISO 32000-2), ovojnice pa so strukture CMS EnvelopedData, kot jih definira RFC 5652. HotPDF zapiše /Filter /Adobe.PubSec s /SubFilter /adbe.pkcs7.s5; za AES-256 to pomeni /V 5 in vnos /DefaultCryptFilter pod /CF z /CFM /AESV3, polje /Recipients pa živi znotraj tega kripto filtra. Vsaka ovojnica šifrira 24 bajtov: 20-bajtno seme, ki mu sledi 32-bitna beseda dovoljenj tega prejemnika. Vrednost /P v šifrirnem slovarju je le ogradek, ker prava dovoljenja potujejo znotraj vsake ovojnice. Ob nalaganju bralnik odvije eno ovojnico, povrne seme in seme hasha skupaj z vsako ovojnico v vrstnem redu /Recipients (SHA-256 za AES-256, SHA-1 za starejše šifre), da znova zgradi ključ datoteke. Če se še odločate med tem modelom in navadnimi gesli, vodnik o šifriranju z geslom AES-256 in zastavicami dovoljenj pokriva drugo stran tega kompromisa

Diagram šifriranja z javnimi ključi v HotPDF: EnablePubKeyEncryption pripne 20-bajtno seme, vsaka ovojnica CMS EnvelopedData šifrira teh 20 bajtov plus eno 32-bitno besedo dovoljenj znotraj /Filter /Adobe.PubSec s /SubFilter /adbe.pkcs7.s5 in /CFM /AESV3, bralnik pa odvije eno ovojnico, povrne seme in ga hashira z vsakim vnosom /Recipients v vrstnem redu polja, da znova zgradi ključ datoteke
Vrednost /P v šifrirnem slovarju je le ogradek, ker prava dovoljenja potujejo znotraj vsake ovojnice, nič nadalje pa ne sme prerazporediti ali znova kodirati polja, po katerem teče povzetek

Zapisovanje prejemnikov RSA z EnablePubKeyEncryption

Za certifikate RSA pokličite EnablePubKeyEncryption z aes256, nato pa pokličite AddPubKeyRecipientCertificate enkrat na certifikat, kodiran v DER, pred BeginDoc. Pomožnik zgradi ovojnico RSAES-OAEP v procesu z vrednostmi THPDFRSAOAEPHash za povzetek OAEP in povzetek MGF1 (rohSHA256, rohSHA384 ali rohSHA512), vsebino ovojnice pa šifrira z 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);                      // točno 20 bajtov, tudi za AES-256
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // privzeti tip ključa je aes128
    // Recenzent A sme tiskati; recenzent B sme le brati in izvleč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 podrobnosti v tem seznamu nosijo obremenitev. Prvič, dolžina semena je fiksirana na 20 bajtov za vsak tip ključa, vključno z AES-256; EnablePubKeyEncryption sproži izjemo ob kateri koli drugi dolžini. Drugič, EnablePubKeyEncryption privzame aes128, oba pomožnika za certifikate pa odklonita delo, razen če je tip ključa aes256, zato pozabljen drugi argument prinese izjemo »certificate envelopes require aes256«. Zapuščinske šifre (k40, k128, aes128) še vedno delujejo, a le skozi AddPubKeyRecipient z ovojnico, ki ste jo zgradili drugje. Tretjič, šifriranje z javnimi ključi AES-256 je zmožnost PDF 2.0, zato HotPDF samodejno dvigne različico dokumenta na 2.0. Z nastavljenim StrictVersionLock na nižji različici se EnablePubKeyEncryption vrne, brez da bi kaj omogočil, odpoved pa se pokaže šele v naslednji vrstici kot »call EnablePubKeyEncryption first«. Preklop šifriranja med priraščajočo posodobitvijo takoj sproži EInvalidOpException

Dodajanje prejemnikov ECDH: P-256, P-384, P-521, X25519 in X448

Za certifikate eliptičnih krivulj AddPubKeyAgreementRecipientWithSecret zapiše prejemnika sporazuma o ključih CMS (KeyAgreeRecipientInfo, struktura KARI iz RFC 5753, s profilom X25519 in X448 iz RFC 8418) in izračuna skupno skrivnost ECDH v procesu. Krivuljo izberete z vrednostjo THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ali pkasX448. Shema se mora ujemati s ključem v certifikatu, sicer klic sproži »Certificate key does not match the requested agreement scheme«. Pod kapoto vsaka ovojnica dobi svež naključni 32-bajtni UKM, ključ za šifriranje ključev, izpeljan s stdDH KDF (SHA-256 za P-256 in X25519, SHA-384 za P-384, SHA-512 za P-521 in X448), in ovijanje ključev AES-256, kot ga definira RFC 3394. Sama skupna skrivnost pride iz čisto pascalovske kode krivulj, brez vpletenega platformnega kripto ponudnika; članek o čisto pascalovski aritmetiki krivulj NIST razloži, kako je bila ta plast zgrajena in preverjena. Za Montgomeryjeve krivulje se celoten kratkotrajni par ključev lahko ustvari krajevno:

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

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Svež kratkotrajni skalar na ovojnico; prijem se zgodi znotraj 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: smiselno le za krivulje NIST
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

Krivulje NIST zahtevajo od klicatelja več. HotPDF dobavlja pomožnike z javnimi ključi le za X25519 in X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), zato za P-256, P-384 in P-521 kratkotrajni par ključev ustvarite s svojim orodjem in podate skalar big-endian točno velikosti polja (32, 48 ali 66 bajtov) plus pripadajočo nestisnjeno točko 0x04||X||Y kot OriginatorPublicKey. HotPDF preveri prejemnikovo točko proti enačbi krivulje, ne more pa preveriti, da vaš izvorni javni ključ dejansko pripada vašemu skalarju. Neujemajoči se polovici še vedno izdelata povsem lepo oblikovano ovojnico, ki je noben prejemnik ne more odpreti, zato povratno nalaganje pripada vaši testni zbirki in ne samo preizkusu velikosti datoteke

Diagram sporazuma ECDH v HotPDF: AddPubKeyAgreementRecipientWithSecret izpelje skupno skrivnost s čisto pascalovsko kodo krivulj, zmeša svež 32-bajtni UKM skozi stdDH KDF s SHA-256 za P-256 in X25519, SHA-384 za P-384, SHA-512 za P-521 in X448, nato pa ovije ključ vsebine z ovijanjem ključev AES-256 po RFC 3394, da zgradi ovojnico KeyAgreeRecipientInfo
Vrednost sheme od pkasECDHP256 do pkasX448 se mora ujemati s ključem certifikata, neujemajoči se polovici skalra in javne točke pa še vedno izdelata lepo oblikovano ovojnico, ki je noben prejemnik ne more odpreti

Zakaj je vrstni red /Recipients pomemben?

Vrstni red /Recipients je pomemben, ker je ključ datoteke povzetek nad semenom in vsako ovojnico v vrstnem redu polja, zato morata zapisovalec in bralnik hashirati iste bajte v istem zaporedju. HotPDF ovojnice hrani v vrstnem redu, v katerem jih dodate, in jih zapiše nespremenjene, kar pomeni, da lahko prejemnike dodate v kakršnem koli vrstnem redu, nič nadalje pa ne sme prerazporediti, znova kodirati ali »počistiti« tega polja. Večina pravih napak na tem področju je bila variacija na to temo, kjer sta dve strani hashirali malenkostno različne bajte:

  • Shranjevanje dinamičnih polj v TList prek Add obdrži le surovi kazalec, medtem ko števec referenc ostane pri krajevni spremenljivki. Naslednji SetLength sprosti medpomnilnik in ga lahko ponovno uporabi, zato je vsako mesto nazadnje zasedalo vzdevek zadnje ovojnice in večprejemniške datoteke so izpeljale napačen ključ. Popravek je shraniti lastnjeno kopijo s List.Add(Pointer(System.Copy(Bytes)))
  • Odvijanje ovojnic razčleni DER na mestu, prehod za povrnitev ključa pa je prvotno hashiral ista živa polja. Bralnik si zdaj vzame posnetke nedotaknjenih kopij vsake ovojnice, preden katero koli odvijanje nanje poseže, povzetek pa teče čez posnetke
  • Binarni DER, prehojen skozi Unicode TStringList, dobi bajte od $80 navzgor znova kodirane po kodni strani, zato HotPDF ovojnice hrani interno kot šestnajstiško besedilo
  • Šifrirani in binarni nizi se morajo zapisati kot šestnajstiški nizi. Dobesedni niz je podvržen normalizaciji koncev vrstic, kjer CR, LF in CRLF vsi postanejo en sam LF (ISO 32000-1 §7.3.4.2), in to tiho prepiše šifrirno besedilo. HotPDF odda vsak vnos /Recipients kot šestnajstiški niz in ga izvzame iz šifriranja nizov, ker vsak bralnik potrebuje ovojnice, preden drži kakršen koli ključ
  • Prvi bajt DER BIT STRING šteje neuporabljene bite in mora biti nič za bajtno poravnane ključe. Pustiti ga neinicializiranega po SetLength je zapisalo, kar je bilo na skladu, strog odvijalnik pa je zavrnil izvorni ključ, zato je datoteka lahko občasno odpovedala odprtje s prav tistim ključem, za katerega je bila zapisana
  • Ko isti ključ še vedno ne more dešifrirati, primerjajte plast za plastjo: ključ datoteke, nato predpono šifrirnega besedila (IV), nato ključ objekta, nato odprto besedilo. Napaka živi takoj za prvo plastjo, ki se ne strinja

Kako odprete šifriran PDF s certifikatom z zasebnim ključem?

Za odprtje šifriranega PDF s certifikatom registrirajte material zasebnega ključa, preden pokličete LoadFromFile, ker HotPDF povrne ključ datoteke med strukturnim prehodom. Ključ RSA ali EC, razčlenjen s HPDFParsePFX, dodelite PubSecKeyMaterial, dodatne ključe RSA dodajte s AddPubSecKeyMaterial, surove skalre ECDH pa registrirajte s AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint) s konstantami HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ali HPDFOIDECP521. Krivulje NIST zahtevajo prejemnikovo lastno nestisnjeno javno točko; Montgomeryjeve krivulje jo prezrejo

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);
    // Neobvezno: izberite ovojnico neposredno, namesto da bi poskusili vse
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = poskusi vsako ovojnico po vrsti
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Brez povratnega klica HotPDF poskusi vsako ovojnico proti vsakemu registriranemu ključu: najprej primarni ključ, nato vsak dodatni ključ RSA, nato material EC. PubSecRecipientQuery prejme število ovojnic in vrne kazalo od 0 ali -1, kazalo zunaj polja pa sproži izjemo, namesto da bi bilo pritegnjeno. Upoštevajte, da AddPubSecKeyMaterial sprejme le material RSA (insistira na modulu in zasebnem eksponentu), zato ključi EC pripadajo v PubSecKeyMaterial ali AddPubSecAgreementKeyMaterial. Ko noben ključ ne odvije nobene ovojnice, korak za povrnitev vrne brez ključa datoteke, namesto da bi sprožil izjemo, zato preverite, da se je vsebina, ki jo pričakujete, dejansko dešifrirala, namesto da bi zaupali, da se je klic nalaganja vrnil

Diagram nalaganja zasebnega ključa v HotPDF: PubSecKeyMaterial nosi primarni ključ RSA ali EC iz HPDFParsePFX, AddPubSecKeyMaterial dodaja le ključe RSA, AddPubSecAgreementKeyMaterial registrira surove skalre ECDH pod OIDs krivulj od HPDFOIDX25519 do HPDFOIDP521, pri LoadFromFile pa ponudnik poskusi primarni ključ, nato vsak dodatni ključ RSA, nato material EC proti vsaki ovojnici
Ko noben ključ ne odvije nobene ovojnice, korak za povrnitev vrne brez ključa datoteke, namesto da bi sprožil izjemo, zato preverite, da se je vsebina dejansko dešifrirala, ali pa pripnite ovojnico prek PubSecRecipientQuery

Česa HotPDF ne zagotavlja

HotPDF zagotavlja, da se njegov lasten zapisovalec in bralnik strinjata bajt za bajtom, in gradi ovojnice, ki sledijo navedenim strukturam CMS. Ne zagotavlja, da vsak pregledovalnik PDF odpre vsako kombinacijo. Podpora za prenos ključev RSA-OAEP in za prejemnike X25519 ali X448 se razlikuje med bralniki in različicami, rezultatov združljivosti za te kombinacije pa nismo objavili. Če se mora dokument odpreti v določenem pregledovalniku, šifrirajte preskusno datoteko za preskusni certifikat istega tipa ključa in jo tam odprite, preden se zavežete na shemo. Dovoljenja, ki jih nosi ovojnica, ostanejo politika, ki jo skladna programska oprema spoštuje, točno tako kot pri šifriranju z geslom. Tudi kakovost semena je vaša odgovornost: AESGenerateRandomBytes je tam za to delo, HotPDF pa izbriše svojo kopijo semena, ko je ključ datoteke izpeljan. Če potrebujete, da niz, tok ali priloga uporabi drugačen kripto filter, vodnik o politiki kripto filtrov za StmF, StrF in EFF pokaže, katera imena filtrov sprejema upravljalnik z javnimi ključi

Šifriranje s certifikati, ovojnice prejemnikov RSA-OAEP in ECDH ter nalaganje zasebnih ključev vsi prihajajo v komponento HotPDF Delphi PDF, skupaj s šifriranjem z gesli, digitalnimi podpisi in preostankom orodjarni ISO 32000 za Delphi in C++Builder