Tehnički članak

Enkripcija PDF-a certifikatima u Delphiju: RSA-OAEP i ECDH

HotPDF šifrira PDF za konkretnne vlasnike certifikata kroz ISO 32000 public-key security handler: EnablePubKeyEncryption prima nasumični seed od 20 bajtova, a svaki primatelj dobiva vlastitu CMS omotnicu, građenu s AddPubKeyRecipientCertificate za RSA ključeve (RSA-OAEP key transport) ili AddPubKeyAgreementRecipientWithSecret za eliptične krivulje (ECDH na P-256, P-384, P-521, X25519 ili X448). Nitko ne dijeli lozinku; tko god drži odgovarajući privatni ključ, otvara datoteku

Slučaj uporabe uvijek je neka verzija iste priče. Kvartalni auditorski paket ide trojici vanjskih revizora, pravni želi da ga sva trojica pročitaju, samo ga smije jedan ispisati, a nitko ne želi lozinku kako sjedi u email threadu uz prilog. Šifriranje lozinkom to ne zna izraziti. Šifriranje certifikatom zna, jer svaki primatelj otključava dokument ključem koji već drži, i svaki primatelj može u svojoj omotnici nositi drugačiji skup dozvola

Po čemu se šifriranje PDF-a certifikatima razlikuje od lozinke?

PDF šifriran javnim ključem izvodi ključ datoteke iz nasumičnog seeda plus točnih bajtova svake primateljske omotnice, a ne iz bilo čega što čovjek ukuca. Handler je opisan u ISO 32000-1 §7.6.4 (§7.6.5 u ISO 32000-2), a omotnice su CMS EnvelopedData strukture kako ih definira RFC 5652. HotPDF zapisuje /Filter /Adobe.PubSec s /SubFilter /adbe.pkcs7.s5; za AES-256 to znači /V 5 i unos /DefaultCryptFilter pod /CF s /CFM /AESV3, a /Recipients polje živi unutar tog crypt filtera. Svaka omotnica šifrira 24 bajta: seed od 20 bajtova iza kojeg slijedi 32-bitna permission riječ tog primatelja. Vrijednost /P u encryption rječniku samo je rezervirano mjesto, jer prave dozvole putuju unutar svake omotnice. Pri učitavanju čitač odmotava jednu omotnicu, izvuče seed, pa hashira seed zajedno sa svakom omotnicom u redoslijedu /Recipients (SHA-256 za AES-256, SHA-1 za starije šifre) da ponovno izgradi ključ datoteke. Ako se još odlučujete između ovog modela i običnih lozinki, vodič kroz AES-256 šifriranje lozinkom i dozvolene flagove pokriva drugu stranu tog kompromisa

Dijagram public-key šifriranja u HotPDF-u: EnablePubKeyEncryption učvrsti seed od 20 bajtova, svaka CMS EnvelopedData omotnica šifrira tih 20 bajtova plus jednu 32-bitnu permission riječ unutar /Filter /Adobe.PubSec s /SubFilter /adbe.pkcs7.s5 i /CFM /AESV3, a čitač odmotava jednu omotnicu, izvuče seed i hashira ga sa svakim /Recipients unosom u redoslijedu polja da ponovno izgradi ključ datoteke
Vrijednost /P u encryption rječniku samo je rezervirano mjesto jer prave dozvole putuju unutar svake omotnice, i ništa nizvodno ne smije preurediti ni ponovno kodirati polje preko kojega ide hash

Pisanje RSA primatelja s EnablePubKeyEncryption

Za RSA certifikate, zovite EnablePubKeyEncryption s aes256, pa AddPubKeyRecipientCertificate jednom po DER-kodiranom certifikatu prije BeginDoc. Helper gradi RSAES-OAEP omotnicu u procesu s THPDFRSAOAEPHash vrijednostima za OAEP hash i MGF1 hash (rohSHA256, rohSHA384 ili rohSHA512), a sadržaj omotnice šifrira 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 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);   // zadani tip ključa je aes128
    // Revizor A smije ispisati; revizor B smije samo čitati i izvlačiti
    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, duljina seeda fiksirana je na 20 bajtova za svaki tip ključa, AES-256 uključeno; EnablePubKeyEncryption baca iznimku na svaku drugu duljinu. Drugo, EnablePubKeyEncryption po zadanom je aes128, i oba certifikat helpera odbijaju raditi dok tip ključa nije aes256, pa vas zaboravljeni drugi argument košta iznimke "certificate envelopes require aes256". Legacy šifre (k40, k128, aes128) i dalje rade, ali samo kroz AddPubKeyRecipient s omotnicom koju ste sagradili negdje drugdje. Treće, AES-256 public-key šifriranje značajka je PDF 2.0, pa HotPDF sam podiže verziju dokumenta na 2.0. S postavljenim StrictVersionLock na nižoj verziji, EnablePubKeyEncryption se vrati ne uključivši ništa, i kvar se pojavi tek u sljedećem retku kao "call EnablePubKeyEncryption first". Promjena šifriranja tijekom inkrementalnog updatea odmah baca EInvalidOpException

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

Za certifikate eliptičnih krivulja, AddPubKeyAgreementRecipientWithSecret zapisuje CMS key-agreement primatelja (KeyAgreeRecipientInfo, KARI struktura iz RFC 5753, s X25519 i X448 profilom iz RFC 8418) i izračunava ECDH zajedničku tajnu u procesu. Krivulju birate vrijednošću THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ili pkasX448. Shema se mora poklapati s ključem u certifikatu, ili poziv baca "Certificate key does not match the requested agreement scheme". Pod haubom, svaka omotnica dobiva svježi nasumični UKM od 32 bajta, key-encryption ključ 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 definira RFC 3394. Sama zajednička tajna dolazi iz čistog Pascal koda krivulja, bez ikakvog platformskog crypto providera; članak o aritmetici NIST krivulja u čistom Pascalu objašnjava kako je taj sloj sagrađen i verificiran. Za Montgomery krivulje cijeli se efemerni par ključeva može generirati lokalno:

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

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Svježi efemerni scalar po omotnici; clamping se događa unutar ljestve
  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: ima smisla samo za NIST krivulje
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST krivulje traže više od pozivatelja. HotPDF isporučuje public-key helpera samo za X25519 i X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), pa za P-256, P-384 i P-521 efemerni par ključeva generirate vlastitim alatima i predajete big-endian scalar točno veličine polja (32, 48 ili 66 bajtova) plus odgovarajuću nekompresiranu točku 0x04||X||Y kao OriginatorPublicKey. HotPDF validira primateljevu točku prema jednadžbi krivulje, ali ne može provjeriti pripada li vaš originator javni ključ doista vašem scalaru. Nepoklopljene polovice i dalje proizvedu posve uredno oblikovanu omotnicu koju nijedan primatelj ne može otvoriti, pa round-trip učitavanje pripada vašem test suiteu, a ne samo provjeri veličine datoteke

Dijagram ECDH dogovora u HotPDF-u: AddPubKeyAgreementRecipientWithSecret izvodi zajedničku tajnu čistim Pascal kodom krivulja, miješa svježi UKM od 32 bajta kroz stdDH KDF sa SHA-256 za P-256 i X25519, SHA-384 za P-384, SHA-512 za P-521 i X448, pa wrapa content ključ RFC 3394 AES-256 key wrapom gradeći KeyAgreeRecipientInfo omotnicu
Vrijednost sheme od pkasECDHP256 do pkasX448 mora se poklapati s ključem certifikata, a nepoklopljene polovice scalara i javne točke i dalje proizvode uredno oblikovanu omotnicu koju nijedan primatelj ne može otvoriti

Zašto redoslijed /Recipients ima veze?

Redoslijed /Recipients ima veze jer je ključ datoteke hash nad seedom i svakom omotnicom u redoslijedu polja, pa writer i reader moraju hashirati iste bajtove istim slijedom. HotPDF drži omotnice u redoslijedu kojim ih dodate i zapisuje ih nepromijenjene, što znači da primatelje možete dodavati kako hoćete, ali ih ništa nizvodno ne smije preurediti, ponovno kodirati ni "pospremiti". Većina stvarnih bugova na tom području bila je varijacija na tu temu, gdje su dvije strane hashirale malo drugačije bajtove:

  • Spremanje dinamičkih polja u TList preko Add zadržava samo sirovi pointer dok referentni brojač ostaje uz lokalnu varijablu. Sljedeći SetLength oslobodi buffer i može ga ponovno iskoristiti, pa je svaki slot završio kao alias zadnje omotnice i multi-recipient datoteke izvele su krivi ključ. Popravak je spremiti posjedovanu kopiju s List.Add(Pointer(System.Copy(Bytes)))
  • Odmotavanje omotnica parsira DER na mjestu, i pass za oporavak ključa isprva je hashirao ta ista živa polja. Reader sada snimi netaknute kopije svake omotnice prije nego bilo koje odmotavanje dira njih, i hash ide nad snimkama
  • Binarni DER propušten kroz Unicode TStringList dobije bajtove od $80 naviše ponovno kodirane po code pageu, pa HotPDF omotnice interno sprema kao hex tekst
  • Šifrirani i binarni stringovi moraju se pisati kao hex stringovi. Literalni string podliježe end-of-line normalizaciji, gdje CR, LF i CRLF svi postanu jedan LF (ISO 32000-1 §7.3.4.2), i to tiho prepisuje ciphertext. HotPDF ispisuje svaki /Recipients unos kao hex string i izuzima ga iz string šifriranja, jer svaki čitač treba omotnice prije nego drži bilo koji ključ
  • Prvi bajt DER BIT STRINGa broji nekorištene bitove i mora biti nula za ključeve poravnate na bajt. Ostavljen neinicijaliziran nakon SetLength pisao je što god je bilo na stacku, i strogi unwrapper odbio je originator ključ, pa se datoteka ponekad nije htjela otvoriti upravo ključem za koji je pisana
  • Kad isti ključ i dalje ne može dešifrirati, usporedite sloj po sloj: ključ datoteke, pa ciphertext prefiks (IV), pa ključ objekta, pa plaintext. Bug živi odmah iza prvog sloja koji se ne slaže

Kako otvoriti certifikatima šifriran PDF s privatnim ključem?

Da otvorite PDF šifriran certifikatom, registrirajte materijal privatnih ključeva prije zvanja LoadFromFile, jer HotPDF oporavlja ključ datoteke tijekom strukturnog prolaza. RSA ili EC ključ parsiran s HPDFParsePFX dodijelite PubSecKeyMaterial, dodatne RSA ključeve dodajte s AddPubSecKeyMaterial, a sirove ECDH scalare registrirajte s AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), koristeći konstante HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ili HPDFOIDECP521. NIST krivulje traže primateljevu vlastitu nekompresiranu javnu točku; Montgomery krivulje je ignoriraju

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);
    // Opcionalno: odaberi omotnicu izravno umjesto isprobavanja svih
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = isprobaj svaku omotnicu po redu
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Bez callbacka, HotPDF isprobava svaku omotnicu protiv svakog registriranog ključa: prvo primarni ključ, pa svaki dodatni RSA ključ, pa EC materijal. PubSecRecipientQuery prima broj omotnica i vraća indeks od nule ili -1, i indeks izvan polja baca iznimku umjesto da se prikovi. Napomenite da AddPubSecKeyMaterial prima samo RSA materijal (inzistira na modulusu i privatnom eksponentu), pa EC ključevi pripadaju u PubSecKeyMaterial ili AddPubSecAgreementKeyMaterial. Kad nijedan ključ ne uspije odmotati nijednu omotnicu, korak oporavka vrati se bez ključa datoteke umjesto da baci iznimku, pa provjerite da se sadržaj koji očekujete stvarno dešifrirao, umjesto da vjerujete da se poziv učitavanja vratio

Dijagram učitavanja privatnih ključeva u HotPDF-u: PubSecKeyMaterial nosi primarni RSA ili EC ključ iz HPDFParsePFX, AddPubSecKeyMaterial dodaje samo RSA ključeve, AddPubSecAgreementKeyMaterial registrira sirove ECDH scalare pod HPDFOIDX25519 do HPDFOIDP521 OID-ovima krivulja, a pri LoadFromFile provider isprobava primarni ključ, pa svaki dodatni RSA ključ, pa EC materijal protiv svake omotnice
Kad nijedan ključ ne odmotava nijednu omotnicu, korak oporavka vrati se bez ključa datoteke umjesto da baci iznimku, pa provjerite da se sadržaj stvarno dešifrirao ili prikovite omotnicu kroz PubSecRecipientQuery

Što HotPDF ne jamči

HotPDF jamči da se njegov vlastiti writer i reader slažu bajt za bajt, i gradi omotnice koje slijede gore citirane CMS strukture. Ne jamči da će svaki PDF preglednik otvoriti svaku kombinaciju. Podrška za RSA-OAEP key transport i za X25519 ili X448 primatelje razlikuje se među čitačima i verzijama, i rezultate kompatibilnosti za te kombinacije nismo objavili. Ako se dokument mora otvoriti u konkretnom pregledniku, zašifrirajte testnu datoteku za testni certifikat istog tipa ključa i otvorite je tamo prije nego se obvežete na shemu. Dozvole nošene u omotnici ostaju politika koju sukladni softver poštuje, baš kao i pod šifriranjem lozinkom. Kvaliteta seeda i vaša je odgovornost: AESGenerateRandomBytes tu je za taj posao, a HotPDF obriše svoju kopiju seeda jednom kad je ključ datoteke izveden. Ako trebate da string, stream ili prilog koristi drugi crypt filter, vodič kroz crypt filter politike za StmF, StrF i EFF pokazuje koja imena filtera public-key handler prima

Šifriranje certifikatima, RSA-OAEP i ECDH primateljske omotnice te učitavanje privatnih ključeva svi isporučuju se u HotPDF Delphi PDF komponenti, uz šifriranje lozinkom, digitalne potpise i ostatak ISO 32000 alata za Delphi i C++Builder