Odborný článok

Šifrovanie PDF certifikátmi v Delphi: RSA-OAEP a ECDH

HotPDF zašifruje PDF pre konkrétnych držiteľov certifikátov cez public-key security handler z ISO 32000: EnablePubKeyEncryption berie 20-bajtové náhodné semienko a každý príjemca dostane vlastnú CMS obálku, postavenú cez AddPubKeyRecipientCertificate pre RSA kľúče (key transport RSA-OAEP) alebo AddPubKeyAgreementRecipientWithSecret pre elipticko-krivkové kľúče (ECDH na P-256, P-384, P-521, X25519 alebo X448). Nikto nezdieľa heslo; kto drží zodpovedajúci privátny kľúč, otvorí súbor

Use case je vždy nejaká verzia toho istého príbehu. Štvrťročný audit balík ide trom externým recenzentom, právne oddelenie chce, aby si ho každý prečítal, tlačiť ho smie len jeden z nich a heslo ležať v mailovej konverzácii vedľa prílohy nechce nikto. Heslové šifrovanie to vyjadriť nedokáže. Certifikátové áno, lebo každý príjemca odomkne dokument kľúčom, ktorý už drží, a každý príjemca môže niesť inú sadu povolení vo svojej vlastnej obálke

Čím sa šifrovanie PDF certifikátmi líši od hesla?

PDF šifrované verejným kľúčom odvodzuje svoj file key z náhodného semienka plus presných bajtov každej príjemcovej obálky, nie z ničoho, čo napíše človek. Handler opisuje ISO 32000-1 §7.6.4 (§7.6.5 v ISO 32000-2) a obálky sú štruktúry CMS EnvelopedData podľa RFC 5652. HotPDF píše /Filter /Adobe.PubSec s /SubFilter /adbe.pkcs7.s5; pri AES-256 to znamená /V 5 a položku /DefaultCryptFilter pod /CF s /CFM /AESV3 a pole /Recipients býva vnútri toho crypt filtra. Každá obálka šifruje 24 bajtov: 20-bajtové semienko nasledované 32-bitovým slovom povolení toho príjemcu. Hodnota /P v šifrovacom slovníku je len zástupný symbol, lebo skutočné povolenia cestujú vnútri každej obálky. Pri načítaní rozbalí čítač jednu obálku, získa semienko a hashuje semienko spolu s každou obálkou v poradí /Recipients (SHA-256 pre AES-256, SHA-1 pre staršie šifry), aby znovu zložil file key. Ak sa ešte rozhodujete medzi týmto modelom a obyčajnými heslami, sprievodca AES-256 heslovým šifrovaním a flagmi povolení pokrýva druhú stranu toho kompromisu

Diagram public-key šifrovania HotPDF: EnablePubKeyEncryption fixuje 20-bajtové semienko, každá obálka CMS EnvelopedData šifruje tých 20 bajtov plus jedno 32-bitové slovo povolení vnútri /Filter /Adobe.PubSec s /SubFilter /adbe.pkcs7.s5 a /CFM /AESV3 a čítač rozbalí jednu obálku, získa semienko a hashuje ho s každou položkou /Recipients v poradí poľa, aby znovu zložil file key
Hodnota /P v šifrovacom slovníku je len zástupný symbol, lebo skutočné povolenia cestujú vnútri každej obálky, a nič po prúde nesmie preskladať ani prekódovať pole, cez ktoré beží digest

Zápis RSA príjemcov pomocou EnablePubKeyEncryption

Pre RSA certifikáty zavolajte EnablePubKeyEncryption s aes256 a potom AddPubKeyRecipientCertificate raz na každý DER-kódovaný certifikát pred BeginDoc. Helper stavia obálku RSAES-OAEP v procese s hodnotami THPDFRSAOAEPHash pre OAEP digest aj MGF1 digest (rohSHA256, rohSHA384 alebo rohSHA512) a obsah obálky šifruje 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);                      // presne 20 bajtov, aj pre AES-256
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // predvolený typ kľúča je aes128
    // Recenzent A smie tlačiť; recenzent B len číta a extrahuje
    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 detaily v tom výpise nesú váhu. Po prvé, dĺžka semienka je fixná na 20 bajtov pre každý typ kľúča, AES-256 vrátane; EnablePubKeyEncryption vyhodí výnimku pri akejkoľvek inej dĺžke. Po druhé, EnablePubKeyEncryption má predvolené aes128 a oba certifikátové helpery odmietnu bežať, pokiaľ typ kľúča nie je aes256, takže zabudnutý druhý argument vám dá výnimku „certificate envelopes require aes256". Staršie šifry (k40, k128, aes128) stále fungujú, ale len cez AddPubKeyRecipient s obálkou, ktorú si postavíte inde. Po tretie, public-key šifrovanie AES-256 je funkcia PDF 2.0, takže HotPDF zvýši verziu dokumentu na 2.0 automaticky. S StrictVersionLock nastaveným na nižšej verzii EnablePubKeyEncryption vráti bez povolenia čohokoľvek a zlyhanie sa ukáže až na ďalšom riadku ako „call EnablePubKeyEncryption first". Prepnutie šifrovania počas inkrementálnej aktualizácie vyhodí EInvalidOpException hneď

Pridávanie ECDH príjemcov: P-256, P-384, P-521, X25519 a X448

Pre elipticko-krivkové certifikáty AddPubKeyAgreementRecipientWithSecret zapíše key-agreement príjemcu CMS (KeyAgreeRecipientInfo, štruktúra KARI z RFC 5753, s profilom X25519 a X448 z RFC 8418) a spočíta ECDH zdieľané tajomstvo v procese. Krivku vyberiete hodnotou THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 alebo pkasX448. Schéma sa musí zhodovať s kľúčom v certifikáte, inak volanie vyhodí „Certificate key does not match the requested agreement scheme". Pod kapotou dostane každá obálka čerstvé náhodné 32-bajtové UKM, key-encryption key odvodenú stdDH KDF (SHA-256 pre P-256 a X25519, SHA-384 pre P-384, SHA-512 pre P-521 a X448) a AES-256 key wrap podľa RFC 3394. Samotné zdieľané tajomstvo pochádza z čistého Pascal kódu kriviek, bez zapojenia platformového crypto providera; článok o aritmetike NIST kriviek v čistom Pascale vysvetľuje, ako tá vrstva vznikla a overila sa. Pre Montgomery krivky sa celý dočasný kľúčový pár dá vygenerovať lokálne:

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

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Čerstvý dočasný scalar na každú obálku; clamping prebehne vnútri rebríka
  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: má zmysel len pri NIST krivkách
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST krivky vyžadujú od volajúceho viac. HotPDF dodáva public-key helpery len pre X25519 a X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), takže pre P-256, P-384 a P-521 vygenerujete dočasný kľúčový pár vlastným nástroím a podáte scalar big-endian presne veľkosti poľa (32, 48 alebo 66 bajtov) plus zodpovedajúci nekomprimovaný bod 0x04||X||Y ako OriginatorPublicKey. HotPDF validuje bod príjemcu proti krivkovej rovnici, ale nevie skontrolovať, že váš originátor public key naozaj patrí k vášmu scalaru. Nesúrodné polovice aj tak vyrobia dokonale korektne vyzerajúcu obálku, ktorú neotvorí žiadny príjemca, a preto round-trip načítanie patrí do vašej testovacej sady, nielen kontrola veľkosti súboru

Diagram ECDH dohody v HotPDF: AddPubKeyAgreementRecipientWithSecret odvodí zdieľané tajomstvo čistým Pascal kódom kriviek, zmieša čerstvé 32-bajtové UKM cez stdDH KDF so SHA-256 pre P-256 a X25519, SHA-384 pre P-384, SHA-512 pre P-521 a X448, potom zabalí content key cez AES-256 key wrap RFC 3394 do obálky KeyAgreeRecipientInfo
Hodnota schémy od pkasECDHP256 po pkasX448 sa musí zhodovať s kľúčom certifikátu a nesúrodné polovice scalar a public point vyrobia formálne korektnú obálku, ktorú neotvorí žiadny príjemca

Prečo záleží na poradí /Recipients?

Poradie /Recipients záleží, lebo file key je digest cez semienko a každú obálku v poradí poľa, takže pisateľ aj čítač musia hashovať tie isté bajty v tej istej postupnosti. HotPDF drží obálky v poradí, v akom ich pridáte, a zapisuje ich nezmenené, takže príjemcov môžete pridávať v ľubovoľnom poradí, ale nič po prúde nesmie pole preskladať, prekódovať ani „upratať". Väčšina reálnych chýb v tej oblasti bola variáciou na tú tému, keď dve strany hashovali mierne odlišné bajty:

  • Uloženie dynamických polí do TList cez Add drží len surový pointer, kým referenčný počet zostáva s lokálnou premennou. Nasledujúce SetLength uvoľní buffer a môže ho znovu použiť, takže každý slot skončil aliasom poslednej obálky a viacpríjemcové súbory odvodili zlý kľúč. Opravou je uložiť vlastnenú kópiu cez List.Add(Pointer(System.Copy(Bytes)))
  • Rozbaľovanie obálky parsuje DER na mieste a prieskum obnovy kľúča pôvodne hashoval tie isté živé polia. Čítač si teraz urobí snapshot nedotknutých kópií každej obálky skôr, než sa ich akékoľvek rozbalenie dotkne, a digest beží nad snapshotmi
  • Binárny DER prehodený cez Unicode TStringList dostane bajty od $80 nahor prekódované code page, takže HotPDF ukladá obálky interne ako hex text
  • Šifrované a binárne reťazce sa musia zapisovať ako hex reťazce. Literal string podlieha normalizácii koncov riadkov, kde CR, LF aj CRLF sa stanú jedným LF (ISO 32000-1 §7.3.4.2), a to potichu prepíše ciphertext. HotPDF vypúšťa každú položku /Recipients ako hex reťazec a vyňíma ju zo string šifrovania, keďže každý čítač potrebuje obálky, skôr než drží akýkoľvek kľúč
  • Prvý bajt DER BIT STRING počíta nepoužité bity a musí byť nula pre bajtovo zarovnané kľúče. Nechanie neinicializované po SetLength napísalo čokoľvek, čo ležalo na zásobníku, a prísny unwrapper odmietol originátor kľúč, takže sa súbor občas odmietol otvoriť s tým istým kľúčom, pre ktorý bol napísaný
  • Keď ten istý kľúč stále nedokáže dešifrovať, porovnávajte vrstvu po vrstve: file key, potom prefix ciphertextu (IV), potom object key, potom plaintext. Chyba býva hneď za prvou vrstvou, ktorá sa nezhoduje

Ako otvoriť certifikátom šifrované PDF privátnym kľúčom?

Ak chcete otvoriť certifikátom šifrované PDF, zaregistrujte privátny kľúčový materiál pred volaním LoadFromFile, lebo HotPDF obnovuje file key počas štrukturálneho prieskumu. RSA alebo EC kľúč parsnutý cez HPDFParsePFX priraďte do PubSecKeyMaterial, ďalšie RSA kľúče pridajte cez AddPubSecKeyMaterial a surové ECDH scalare registrujte cez AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint) s konštami HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 alebo HPDFOIDECP521. NIST krivky vyžadujú vlastný nekomprimovaný public point príjemcu; Montgomery krivky ho ignorujú

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);
    // Voliteľné: vyberte obálku priamo namiesto skúšania všetkých
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = skúšaj každú obálku v poradí
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Bez callbacku skúša HotPDF každú obálku proti každému registrovanému kľúču: najprv primárny kľúč, potom každý ďalší RSA kľúč, potom EC materiál. PubSecRecipientQuery dostane počet obálok a vráti index od nuly alebo -1 a index mimo poľa vyhodí výnimku namiesto pritnutia na hranici. Pozor, AddPubSecKeyMaterial prijíma len RSA materiál (trvá na module a privátnom exponente), takže EC kľúče patria do PubSecKeyMaterial alebo AddPubSecAgreementKeyMaterial. Keď žiadny kľúč nerozbalí žiadnu obálku, krok obnovy sa vráti bez file key namiesto výnimky, takže overte, že obsah, ktorý očakávate, sa naozaj dešifroval, namiesto veriť tomu, že volanie načítania sa vrátilo

Diagram načítania privátneho kľúča v HotPDF: PubSecKeyMaterial nesie primárny RSA alebo EC kľúč z HPDFParsePFX, AddPubSecKeyMaterial pridáva len RSA kľúče, AddPubSecAgreementKeyMaterial registruje surové ECDH scalare pod krivkovými OID od HPDFOIDX25519 po HPDFOIDP521 a pri LoadFromFile skúša provider primárny kľúč, potom každý ďalší RSA kľúč, potom EC materiál proti každej obálke
Keď žiadny kľúč nerozbalí žiadnu obálku, krok obnovy sa vráti bez file key namiesto výnimky, takže overte, že obsah sa naozaj dešifroval, alebo pripnite obálku cez PubSecRecipientQuery

Čo HotPDF negarantuje

HotPDF garantuje, že jeho vlastný pisateľ a čítač sa zhodnú bajt za bajtom, a stavia obálky nasledujúce citované CMS štruktúry. Negarantuje, že každý PDF prehliadač otvorí každú kombináciu. Podpora key transportu RSA-OAEP a príjemcov X25519 či X448 sa líši medzi čítačmi a verziami a výsledky kompatibility pre tieto kombinácie nezverejňujeme. Ak musí dokument otvoriť sa v konkrétnom prehliadači, zašifrujte testovací súbor pre testovací certifikát rovnakého typu kľúča a otvorte ho tam, skôr než sa zaviazete ku schéme. Povolenia nesúce sa v obálke ostávajú politikou, ktorú konformný softvér rešpektuje, presne ako pri heslovom šifrovaní. Kvalita semienka je aj vaša zodpovednosť: AESGenerateRandomBytes je na tú prácu a HotPDF vymaže svoju kópiu semienka, keď sa file key odvodí. Ak potrebujete navyše, aby string, stream alebo príloha používali iný crypt filter, sprievodca politikami crypt filtrov pre StmF, StrF a EFF ukazuje, ktoré mená filtrov public-key handler prijíma

Certifikátové šifrovanie, obálky príjemcov RSA-OAEP a ECDH a načítanie privátnych kľúčov prichádzajú všetky v HotPDF Delphi PDF component, vedľa heslového šifrovania, digitálnych podpisov a zvyšku ISO 32000 toolsetu pre Delphi a C++Builder