Technical Article

Digitálne podpisy PDF a PAdES v Delphi pomocou HotPDF

Podpis PDF je z veľkej časti o počítaní bajtov a práve v tom nastávajú chyby. Kryptografia beží na kóde, ktorý bol auditovaný dve desaťročia, a táto časť takmer nikdy nezlyhá. To, čo zlyháva v produkcii, je oveľa jednoduchšie: vyhradený zástupný symbol (placeholder) je príliš malý pre skutočný podpis, hash sa vypočíta z nesprávnej časti súboru, alebo operácia uloženia po podpísaní potichu prepíše bajty, ktoré podpis už zmrazil. Usporiadajte bajty správne a zelené zaškrtnutie sa postará samo o seba.

HotPDF pokrýva podpisovanie pre Delphi a C++Builder na troch úrovniach, pričom si medzi nimi vyberáte na základe jedinej otázky: kde sa nachádza súkromný kľúč? Súbor PFX na disku vyžaduje jediné volanie funkcie. Kľúč uzamknutý v HSM alebo vzdialenej podpisovej službe vyžaduje postupnosť rezervácia-hash-vloženie, pretože žiadna knižnica nedokáže preniknúť do tokenu a vytiahnuť z neho kľúč. Podpis, ktorý musí spĺňať európske nariadenia, navyše vyžaduje základné štruktúry PAdES. Nasledujúce časti podrobne popisujú tento postup.

Ako /ByteRange vymedzuje podpísané bajty

Podpis musí byť umiestnený vo vnútri súboru, ktorý podpisuje, a nemôže podpísať sám seba. Formát PDF tento paradox obchádza ponechaním voľného miesta. Pred podpísaním zapisovač vyhradí položku /Contents s pevnou veľkosťou plnú núl a zaznamená pole /ByteRange pre dva úseky na oboch jej stranách: všetko pred voľným miestom a všetko za ním. Podpisovateľ vytvorí hash z týchto dvoch úsekov a zapíše výsledný CMS blob do vyhradeného miesta ako hexadecimálny reťazec. Úskalie spočíva v slove pevná. Veľkosť tohto miesta musíte určiť skôr, ako viete, aký veľký bude výsledný podpis, takže rezervácia musí byť dostatočne nadsadená. Osem kilobajtov pohodlne pojme odpojený (detached) CMS podpis s krátkym reťazcom certifikátov.

HotPDF rozdeľuje tieto dva prípady do dvoch volaní, pričom ich zámena je častou počiatočnou chybou. AddSignatureField vytvorí prázdne, viditeľné pole, ktoré môže neskôr podpísať používateľ v prehliadači. AddSignedSignatureField vytvorí pole a vyhradí miesto pre /Contents, čo je presne to, čo potrebujete, keď podpis dokončuje kód a nie človek. Ak poskytnete externému podpisovateľovi prázdne pole, nebude mať čo vyplniť.

Cesta na jedno volanie: podpisovanie zo súboru PFX

Ak sa certifikát a jeho súkromný kľúč nachádzajú v súbore PFX/PKCS#12 súbore, ktorý váš proces dokáže prečítať, celý postup sa zjednoduší na volanie triednej funkcie:

if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
    'company-cert.pfx', 'pfx-password') then
  Writeln('Signed: invoice-signed.pdf')
else
  raise Exception.Create('PFX signing failed');

Ak toto volanie zlyhá, problémom je zriedkakedy samotné PDF. Zvyčajne je na vine súbor PFX. HotPDF číta kontajnery chránené pomocou PBES2, čo znamená odvodzovanie kľúča PBKDF2 nad AES-256-CBC. Súbor PFX exportovaný starším sprievodcom certifikátmi v systéme Windows alebo staršou verziou OpenSSL pred 3.0 býva namiesto toho zabalený do staršieho šifrovania RC2 oder 3DES, čo znemožní jeho spracovanie. Nápravou je opätovný export kontajnera s modernou ochranou; súčasný OpenSSL to robí predvolene a nevyžaduje to žiadnu zmenu v kóde. Keď teda podpisovanie okamžite zlyhá na certifikáte, ktorý všade inde funguje, pred podozrievaním vlastného kódu skontrolujte, ako bol súbor PFX vytvorený.

Postupnosť rezervácia-hash-vloženie pre HSM a tokeny

Cesta na jedno volanie predpokladá, že váš proces môže prečítať kľúč ako súbor. Čoraz častejšie to však nie je možné. Kľúč je uložený v HSM, na USB tokene alebo za API podpisovej služby, a knižnica k nemu nemá priamy prístup. HotPDF to rieši rozdelením podpisovania na kroky na úrovni bajtov: zapíše sa zástupný dokument, knižnica poskytne rozsahy pre hash, vstup hashu sa odovzdá zariadeniu alebo službe, ktorá drží kľúč, a vrátený CMS sa spätne vloží do vyhradeného miesta.

var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Write the document with a reserved /Contents hole
  Doc := THotPDF.Create(nil);
  try
    Doc.FileName := 'placeholder.pdf';
    Doc.BeginDoc;
    Doc.CurrentPage.AddSignedSignatureField('Sig1',
      Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
      'Contract approval', 'Boston, MA', 'legal@example.com');
    Doc.EndDoc;
  finally
    Doc.Free;
  end;

  // 2. Load the saved bytes; the returned offsets are 0-based
  Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
  try
    SetLength(PdfBytes, Fs.Size);
    Fs.ReadBuffer(PdfBytes[1], Fs.Size);
  finally
    Fs.Free;
  end;
  THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
    CStart, CLen);

  // 3. Hash both spans and sign externally (HSM, token, service)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // your integration: returns CMS as hex

  // 4. Splice the signature into the reserved hole
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Väčšinu občasných chýb v tejto postupnosti spôsobujú dva detaily. Prvým je, že PreparePDFForSigning pracuje s bajtmi hotového súboru. Zástupný dokument musí byť kompletne zapísaný a uložený, až potom majú posuny (offsets) nejaký zmysel. Ak ich vypočítate voči streamu, ktorý sa stále zostavuje, nebudú súhlasiť s bajtmi, ktoré nakoniec hashujete. Druhým detailom je opäť veľkosť rezervácie. Vami požadovaných 8192 bajtov musí pojať finálny CMS podpis. Podpis, ktorý obsahuje stredné certifikáty (intermediate certificates) alebo ktorý služba dopĺňa o podpísané atribúty, môže túto veľkosť prekročiť. Funkcia InsertSignatureHex nezväčší vyhradené miesto na vytvorenie priestoru. Príznakom je stav, keď podpisovanie funguje s jedným certifikátom a zlyháva s ďalším. Riešením je vygenerovať zástupný dokument s rezerváciou zmeranou podľa skutočného podpisu vytvoreného reálnym podpisovateľom, nie iba odhadnutou.

Základné úrovne PAdES a časové pečiatky, ktoré udržujú podpis platný

Ak podpisujete podľa európskych pravidiel, platnou normou je ETSI EN 319 142-1, ktorá definuje štyri základné úrovne PAdES. B-B je bežný podpis. B-T pridáva dôveryhodnú časovú pečiatku, ktorá preukazuje čas vytvorenia podpisu. B-LT vkladá overovacie údaje, certifikáty a údaje o odvolaní priamo do dokumentu, takže ho možno overiť aj po rokoch. B-LTA vrství periodické časové pečiatky dokumentu, čím sa zabezpečí, že dôkazy prežijú aj samotné kryptografické algoritmy, na ktorých boli postavené. HotPDF generuje štruktúry na strane dokumentu pre každú úroveň:

// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Rezervácia 16384 bajtov pre časovú pečiatku je zámerná. Autorita časovej pečiatky (TSA) vracia token, ktorý so sebou nesie vlastný reťazec certifikátov, takže bežne vyžaduje viac miesta ako 8 KB, ktoré stačia pre bežný podpis. Tieto časové pečiatky dokumentov sú tiež základom pre úroveň B-LTA: opätovné opečiatkovanie archivovaného podpisu každých niekoľko rokov pomocou aktuálnych algoritmov zabezpečí, že dokument podpísaný v roku 2026 bude overiteľný aj v roku 2040.

Pár slov o reťazcoch pre dôvod, umiestnenie a kontakt, ktoré prijímajú obe volania polí: ide len o užitočné metadáta a nič viac. HotPDF ich ukladá ako bežné položky slovníka a vykresľuje ich do viditeľného vzhľadu podpisu, avšak žiadny overovací nástroj ich neporovnáva s ničím. Vyplňte ich konzistentne na základe vašich procesných údajov, keďže audítori ich čítajú, no nikdy ich nepovažujte za samotný dôkaz. Skutočné kryptografické tvrdenie spočíva výhradne v CMS a jeho reťazci certifikátov, zatiaľ čo overovací nástroj viditeľný text úplne ignoruje.

Po podpísaní sa súbor môže len zväčšovať

V okamihu vytvorenia podpisu sú bajty v jeho rozsahoch zmrazené. Jediným legitímnym spôsobom, ako neskôr súbor zmeniť, je prírastková aktualizácia (incremental update) podľa ISO 32000-1 §7.5.6, ktorá pripojí nové a zmenené objekty za pôvodné bajty a naviaže na ne novú sekciu krížových odkazov (cross-reference). Pri tomto postupe zostáva podpis platný pre danú revíziu a prehliadač zobrazí pravdivý stav: podpísaná revízia je neporušená, dokument bol neskôr rozšírený. Ak namiesto toho znova serializujete celý súbor, prepíšete podpísané úseky, čo zničí podpis, aj keď sa vizuálne nič nezmenilo. Rovnaký mechanizmus revízií umožňuje, aby jeden dokument niesol viacero podpisov: každý nový podpis sa zapíše do vlastnej prírastkovej aktualizácie a jeho rozsahy pokrývagy všetko, čo mu predchádzalo, vrátane predchádzajúcich podpisov. Princíp zápisu iba na koniec (append-only) a podmienky, za ktorých je bezpečné ich skomprimovať, sú popísané v článku o prúdoch objektov a prírastkových aktualizáciách.

Pri návrhu je vhodné pamätať na dve obmedzenia. Režim výstupu PDF/A v HotPDF úplne odmieta polia pre podpisy, takže archívna zhoda a vložený podpis musia byť distribuované ako samostatné súbory. Podpísanie navyše nehovorí nič o utajení: dokazuje, kto dokument vytvoril a že sa odvtedy nezmenil, no čítať ho môže ktokoľvek. Skrytie obsahu je samostatná úloha, ktorú rieši šifrovanie AES-256 a politika prístupových práv.

Bez ohľadu na to, čo vytvoríte, otestujte to iným kódom, než ktorý súbor zapísal. Otvorte výstup v paneli podpisov v programe Acrobat a overte tri veci: podpis je platný, identita sa spája s očakávaným koreňovým certifikátom a panel nehlási žiadne zmeny od podpísania. Potom zmeňte jediný bajt v podpísanom rozsahu skúšobnej kópie a overte, že panel teraz označí dokument za zmenený. Podpisovací reťazec, u ktorého ste nikdy nevideli odmietnutie poškodeného súboru, nie je skutočne overený.

Všetky tri podpisové úrovne sa dodávajú s komponentom HotPDF Component pre Delphi a C++Builder. Stránka produktu obsahuje odkaz na kompletnú referenčnú príručku podpisového rozhrania API.