Techninis straipsnis

PDF šifravimas sertifikatais Delphi: RSA-OAEP ir ECDH

HotPDF užšifruoja PDF konkretiems sertifikatų savininkams per ISO 32000 viešojo rakto saugos apdorojimo mechanizmą: EnablePubKeyEncryption paima 20 baitų atsitiktinę seed, ir kiekvienas gavėjas gauna savą CMS voke, kurį RSA raktams sukonstruoja AddPubKeyRecipientCertificate (RSA-OAEP rakto perdavimas), o elipsinės kreivės raktams – AddPubKeyAgreementRecipientWithSecret (ECDH ant P-256, P-384, P-521, X25519 arba X448). Niekas nesidalina slaptažodžiu; failą atveria tas, kas turi atitinkantį privatų raktą

Naudojimo atvejis visada yra kažkuri tos pačios istorijos versija. Ketvirtinis audito paketas keliauja trims išoriniams peržiūrėtojams, juristai nori, kad kiekvienas jų jį perskaitytų, spausdinti leidžiama tik vienam, o niekam nereikia slaptažodžio, gulinčio el. pašto gijoje šalia priedo. Slaptažodžio šifravimas to neišreiškia. Sertifikatų šifravimas – gali, nes kiekvienas gavėjas dokumentą atrakina raktu, kurį jau turi, ir kiekvienas gavėjas savo voke gali nešti kitokį leidimų rinkinį

Kuo sertifikatų pagrįstas PDF šifravimas skiriasi nuo slaptažodžio?

Viešuoju raktu užšifruotas PDF savo failo raktą išveda iš atsitiktinės seed plius tikslių kiekvieno gavėjo voko baitų, o ne iš nieko, ką žmogus surinka. Apdorojimo mechanizmas aprašytas ISO 32000-1 §7.6.4 (§7.6.5 ISO 32000-2), o vokai yra RFC 5652 apibrėžtos CMS EnvelopedData struktūros. HotPDF rašo /Filter /Adobe.PubSec su /SubFilter /adbe.pkcs7.s5; AES-256 atveju tai reiškia /V 5 ir /DefaultCryptFilter įrašą po /CF su /CFM /AESV3, o /Recipients masyvas gyvena tame crypt filtre. Kiekvienas voke užšifruoja 24 baitus: 20 baitų seed, po kurio seka to gavėjo 32 bitų leidimų žodis. /P reikšmė šifravimo žodyne yra tik vietos ženklas, nes tikri leidimai keliauja kiekvieno voko viduje. Įkėlimo metu skaitytuvas išvynioja vieną voką, atgauna seed ir hašuoja seed kartu su kiekvienu voku /Recipients tvarka (SHA-256 AES-256, SHA-1 senesniems šiframs), kad atstatytų failo raktą. Jei vis dar svėrinate šį modelį ir paprastus slaptažodžius, AES-256 slaptažodžio šifravimo ir leidimų vėliavėlių vadovas aprėpia kitą to kompromiso pusę

HotPDF viešojo rakto šifravimo diagrama: EnablePubKeyEncryption užkalė 20 baitų seed, kiekvienas CMS EnvelopedData voke užšifruoja tuos 20 baitų plius vieną 32 bitų leidimų žodį viduje /Filter /Adobe.PubSec su /SubFilter /adbe.pkcs7.s5 ir /CFM /AESV3, o skaitytuvas išvynioja vieną voką, atgauna seed ir hašuoja jį su kiekvienu /Recipients įrašu masyvo tvarka, kad atstatytų failo raktą
/P reikšmė šifravimo žodyne yra tik vietos ženklas, nes tikri leidimai keliauja kiekvieno voko viduje, ir niekas toliau grandinėje negali supertvarkyti ar pakartotinai užkoduoti masyvo, per kurį bėga hašas

RSA gavėjų rašymas su EnablePubKeyEncryption

RSA sertifikatams iškvieskite EnablePubKeyEncryption su aes256, tada AddPubKeyRecipientCertificate po kartą kiekvienam DER koduotam sertifikatui prieš BeginDoc. Pagalbinė funkcija RSAES-OAEP voke sukonstruoja procese su THPDFRSAOAEPHash reikšmėmis OAEP hašui ir MGF1 hašui (rohSHA256, rohSHA384 ar rohSHA512), o voko turinį užšifruoja 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);                      // lygiai 20 baitų, netgi AES-256
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // numatytasis rakto tipas aes128
    // Peržiūrėtojas A gali spausdinti; peržiūrėtojas B gali tik skaityti ir ištraukti
    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;

Trys to sąrašo detalės yra laikančiosios. Pirma, seed ilgis fiksuotas – 20 baitų kiekvienam rakto tipui, įskaitant AES-256; EnablePubKeyEncryption kelia išimtį bet kokiam kitam ilgiui. Antra, EnablePubKeyEncryption pagal numatymą naudoja aes128, ir abu sertifikatų pagalbininkai atsisako veikti, kol rakto tipas nėra aes256, tad užmiršus antrąjį argumentą gaunama išimtis „certificate envelopes require aes256“. Senieji šifrai (k40, k128, aes128) vis tiek veikia, bet tik pro AddPubKeyRecipient su kitur sukurtu voke. Trečia, AES-256 viešojo rakto šifravimas yra PDF 2.0 funkcija, tad HotPDF automatiškai pakelia dokumento versiją iki 2.0. Nustačius StrictVersionLock žemesnei versijai, EnablePubKeyEncryption grįžta nieko neįjungęs, o gedimas pasirodo tik kitoje eilutėje kaip „call EnablePubKeyEncryption first“. Šifravimo keitimas inkrementinio atnaujinimo metu iš karto kelia EInvalidOpException

ECDH gavėjų pridėjimas: P-256, P-384, P-521, X25519 ir X448

Elipsinės kreivės sertifikatams AddPubKeyAgreementRecipientWithSecret rašo CMS rakto sutarimo gavėją (KeyAgreeRecipientInfo, KARI struktūrą iš RFC 5753, su X25519 ir X448 profiliu iš RFC 8418) ir ECDH bendrą paslaptį apskaičiuoja procese. Kreivę pasirenkate THPDFPubKeyAgreementScheme reikšme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ar pkasX448. Schema turi sutapti su sertifikato raktu, kitaip iškvietimas kelia „Certificate key does not match the requested agreement scheme“. Po gaubtu kiekvienas voke gauna šviežią atsitiktinę 32 baitų UKM, rakto šifravimo raktą, išvestą stdDH KDF (SHA-256 P-256 ir X25519, SHA-384 P-384, SHA-512 P-521 ir X448), ir RFC 3394 apibrėžtą AES-256 rakto apvyniojimą. Pati bendra paslaptis ateina iš gryno Pascal kreivių kodo, be jokio platformos crypto tiekėjo; gryno Pascal NIST kreivių aritmetikos straipsnis paaiškina, kaip tas sluoksnis buvo pastatytas ir patikrintas. Montgomery kreivėms visą efemerinę rakto porą galima sugeneruoti lokaliai:

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

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Šviežias efemerinis skaliaras kiekvienam vokui; clamping vyksta kopėčiose
  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: prasminga tik NIST kreivėms
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST kreivės iš kviečiančiojo reikalauja daugiau. HotPDF viešojo rakto pagalbininkus tiekia tik X25519 ir X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), tad P-256, P-384 ir P-521 atveju efemerinę rakto porą sugeneruojate savais įrankiais ir perduodate big-endian skaliarą, lygiai lauko dydžio (32, 48 ar 66 baitai), plius atitinkantį nesuspaustą 0x04||X||Y tašką kaip OriginatorPublicKey. HotPDF gavėjo tašką patikrina pagal kreivės lygtį, bet negali patikrinti, ar jūsų originaučio viešasis raktas iš tikrųjų priklauso jūsų skaliarui. Nesutampantys pusės vis tiek duoda visiškai tvarkingą voką, kurio negali atverti nė vienas gavėjas – todėl apsukimas įkėlimu pirmyn ir atgal priklauso jūsų testų rinkiniui, o ne vien failo dydžio patikrinimui

HotPDF ECDH sutarimo diagrama: AddPubKeyAgreementRecipientWithSecret bendrą paslaptį išveda grynu Pascal kreivių kodu, maišo šviežią 32 baitų UKM pro stdDH KDF su SHA-256 P-256 ir X25519, SHA-384 P-384, SHA-512 P-521 ir X448, tada turinio raktą apvynioja RFC 3394 AES-256 key wrap, kad sukurtų KeyAgreeRecipientInfo voką
Schema nuo pkasECDHP256 iki pkasX448 turi sutapti su sertifikato raktu, o nesutampantys skaliaro ir viešojo taško pusės vis tiek duoda tvarkingą voką, kurio negali atverti nė vienas gavėjas

Kodėl /Recipients tvarka svarbi?

/Recipients tvarka svarbi, nes failo raktas yra hašas per seed ir kiekvieną voką masyvo tvarka, tad rašytojas ir skaitytuvas turi hašuoti tuos pačius baitus ta pačia seka. HotPDF vokus laiko jų pridėjimo tvarka ir rašo nepakeistus, tad gavėjus galite pridėti bet kuria jums patinkančia tvarka, bet niekas toliau grandinėje negali to masyvo supertvarkyti, pakartotinai užkoduoti ar „sutvarkyti“. Dauguma tikrų šios srities bugų buvo tos temos variacijos, kur dvi pusės hašavo kiek kitokius baitus:

  • Dinaminių masyvų saugojimas TList per Add palieka tik žalią rodyklę, o nuorodų skaitiklis lieka pas vietinį kintamąjį. Kitas SetLength atlaisvina buferį ir gali jį panaudoti pakartotinai, tad kiekvienas langas gale slėpė paskutinįjį voką, ir daugelio gavėjų failai išvedė neteisingą raktą. Pataisa – saugoti nuosavą kopiją su List.Add(Pointer(System.Copy(Bytes)))
  • Vokų išvyniojimas DER išanalizuoja vietoje, o rakto atkūrimo perėjimas iš pradžių hašavo tuos pačius gyvus masyvus. Skaitytuvas dabar padaro pirmines kiekvieno voko kopijas, dar prieš bet kuriam išvyniojimui juos lietiant, o hašas bėga per kopijas
  • Dvejetainis DER, praėjęs pro Unicode TStringList, baitus nuo $80 ir aukščiau perkoduoja pagal kodų puslapį, tad HotPDF vokus viduje saugo kaip hex tekstą
  • Užšifruotos ir dvejetainės eilutės turi būti rašomos kaip hex eilutės. Literalinė eilutė paklūsta eilutės pabaigos normalizacijai, kur CR, LF ir CRLF visi tampa vienu LF (ISO 32000-1 §7.3.4.2), ir tai tyliai perrašo šifratą. HotPDF kiekvieną /Recipients įrašą išveda kaip hex eilutę ir atleidžia jį nuo eilučių šifravimo, nes kiekvienam skaitytuvui vokai reikalingi dar prieš turint bet kokį raktą
  • Pirmasis DER BIT STRING baitas skaičiuoja nepanaudotus bitus ir turi būti nulis baitais lygiuotiems raktams. Palikus jo neinicializuotą po SetLength, ten atsidurdavo tai, kas gulėjo stoke, ir griežtas išvyniotojas atmesdavo originaučio raktą, tad failas retkarčiais galėdavo neatverti su pačiu raktu, kuriam jis buvo rašytas
  • Kai tas pats raktas vis tiek nesugeba iššifruoti, lyginkite sluoksnis po sluoksnio: failo raktą, tada šifrato priešdėlį (IV), tada objekto raktą, tada griežtąjį tekstą. Bugas gyvena iškart po pirmojo sluoksnio, kuris nesutampa

Kaip atverti sertifikatu užšifruotą PDF su privatuoju raktu?

Norėdami atverti sertifikatu užšifruotą PDF, užregistruokite privataus rakto medžiagą dar prieš kviesdami LoadFromFile, nes HotPDF failo raktą atgauna struktūrinės perėjimo metu. RSA arba EC raktą, išanalizuotą HPDFParsePFX, priskirkite PubSecKeyMaterial, papildomus RSA raktus pridėkite AddPubSecKeyMaterial, o žalius ECDH skaliarus užregistruokite AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), naudodami HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ar HPDFOIDECP521 konstantas. NIST kreivės reikalauja gavėjo paties nesuspausto viešojo taško; Montgomery kreivės jo nepaiso

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);
    // Pasirinktinai: imkite voką tiesiai, vietoj to, kad bandytumėte visus
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = bandyti kiekvieną voką eilės tvarka
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Be callback HotPDF bandys kiekvieną voką prieš kiekvieną užregistruotą raktą: pirmiausia pirminį raktą, tada kiekvieną papildomą RSA raktą, tada EC medžiagą. PubSecRecipientQuery gauna vokų skaičių ir grąžina 0-based indeksą arba -1, o indeksas už masyvo ribų kelia išimtį, o ne būna nukirptas. Atkreipkite dėmesį, kad AddPubSecKeyMaterial priima tik RSA medžiagą (reikalauja modulio ir privataus eksponento), tad EC raktai priklauso PubSecKeyMaterial arba AddPubSecAgreementKeyMaterial. Kai nė vienas raktas neišvynioja nė vieno voko, atkūrimo žingsnis grįžta be failo rakto, o ne keldamas išimtį, tad patikrinkite, ar tikrai iššifruotas jūsų laukiamas turinys, o ne pasitikėkite tuo, kad įkėlimo iškvietimas grįžo

HotPDF privataus rakto įkėlimo diagrama: PubSecKeyMaterial neša pirminį RSA arba EC raktą iš HPDFParsePFX, AddPubSecKeyMaterial prideda tik RSA raktus, AddPubSecAgreementKeyMaterial užregistruoja žalius ECDH skaliarus po HPDFOIDX25519 iki HPDFOIDP521 kreivių OID, o LoadFromFile metu tiekėjas bandys pirminį raktą, tada kiekvieną papildomą RSA raktą, tada EC medžiagą prieš kiekvieną voką
Kai nė vienas raktas neišvynioja nė vieno voko, atkūrimo žingsnis grįžta be failo rakto, o ne keldamas išimtį, tad patikrinkite, ar turinys tikrai iššifruotas, arba užfiksuokite voką per PubSecRecipientQuery

Ko HotPDF negarantuoja

HotPDF garantuoja, kad jo paties rašytojas ir skaitytuvas sutampa baitas į baitą, ir sukonstruoja vokus, laikančius anksčiau cituotas CMS struktūras. Jis negarantuoja, kad kiekviena PDF peržiūros programa atvers kiekvieną kombinaciją. RSA-OAEP rakto perdavimo ir X25519 arba X448 gavėjų palaikymas skiriasi tarp skaitytuvų ir versijų, o mes neskylijome suderinamumo rezultatų tiems kombinacijoms. Jei dokumentas privalo atsiverti konkrečioje peržiūros programoje, užšifruokite testinį failą testiniam to paties rakto tipo sertifikatui ir ten jį atverkite, dar prieš apsispręsdami dėl schemos. Leidimai, nešami voko viduje, lieka politika, kurią atitinkanti programinė įranga gerbia – lygiai taip pat, kaip ir slaptažodžio šifravime. Seed kokybė taip pat jūsų atsakomybė: AESGenerateRandomBytes tam ir yra, o HotPDF savo seed kopiją ištrina, kai tik failo raktas išvestas. Jei eilutei, srautui ar priedui taip pat reikia kito crypt filtro, crypt filtrų politikos vadovas StmF, StrF ir EFF parodo, kuriuos filtrų vardus priima viešojo rakto apdorojimo mechanizmas

Sertifikatų šifravimas, RSA-OAEP ir ECDH gavėjų vokai bei privataus rakto įkėlimas visi atkeliauja su HotPDF Delphi PDF komponentu, greta slaptažodžio šifravimo, skaitmeninių parašų ir likusios ISO 32000 priemonių dalies Delphi ir C++Builder aplinkose