Technický článek

Digitální podpisy PDF a PAdES v Delphi s HotPDF

Podpis v PDF je z velké části účetnictví bajtů, a právě tam se to nejčastěji pokazí. Kryptografie běží na kódu, který je auditovaný už dvě desetiletí, a tahle část skoro nikdy neselže. To, co selhává v produkci, je skromnější: zástupný symbol (placeholder) rezervovaný příliš malý pro skutečný podpis, hash vypočtený přes špatný úsek souboru, nebo „uložení" po podpisu, které potichu přepsalo bajty, které podpis už zmrazil. Uspořádejte bajty správně a zelená fajfka se postará sama o sebe

HotPDF pokrývá podepisování pro Delphi a C++Builder na třech úrovních a mezi nimi si vyberete odpovědí na jednu otázku: kde žije privátní klíč? Soubor PFX na disku potřebuje jediné volání funkce. Klíč uzamčený v HSM nebo ve vzdálené podepisovací službě potřebuje sekvenci rezervace-hash-vložení, protože žádná knihovna se nedostane dovnitř tokenu a klíč z něj nevytáhne. Podpis, který musí splňovat evropskou legislativu, k tomu navíc potřebuje bazální struktury PAdES. Následující oddíly sledují právě tuto posloupnost

Rozhodovací diagram volící mezi jednovolacím podepisováním PFX HotPDF, cestou reserve-hash-insert, když klíč sedí v HSM nebo vzdálené službě, a základními strukturami PAdES pro regulované evropské podepisování
Vyberte úroveň podpisu otázkou, kde privátní klíč bydlí; čitelný soubor PFX srazí podpis na jediné volání, klíče držené v tokenu vynutí objížďku na úrovni bajtů a evropská regulace přidá vrstvu PAdES

Jak /ByteRange přesně určuje podepsané bajty

Podpis musí žít uvnitř souboru, který podepisuje, a nemůže podepsat sám sebe. PDF tento paradox obchází tak, že nechá díru. Před podepsáním writer rezervuje položku /Contents pevné velikosti vyplněnou nulami a zaznamená pole /ByteRange pro dva úseky po obou jejích stranách: vše před dírou, vše za ní. Podepisovatel oba úseky zahashuje a výsledný blob CMS zapíše do díry jako hexadecimální řetězec. Past je ve slově pevné. K velikosti té díry se zavazujete dřív, než víte, jak velký bude hotový podpis, takže rezervace musí být sebejistý odhad s rezervou. Osm kilobajtů pohodlně pojme odpojený (detached) podpis CMS s krátkým řetězcem certifikátů

HotPDF rozděluje tyto dva případy do dvou volání a jejich záměna je častá začátečnická chyba. AddSignatureField vloží prázdné, viditelné pole, které má v prohlížeči později podepsat člověk. AddSignedSignatureField vytvoří pole a rezervuje díru /Contents, což je ta, kterou chcete vždy, když podpis dokončí kód, ne člověk. Předáte-li externímu podepisovateli prázdné pole, nemá co vyplnit

Cesta jedním voláním: podepisování z PFX

Když certifikát a jeho privátní klíč leží v souboru PFX/PKCS#12, který váš proces umí přečíst, celý pipeline se zredukuje na jednu class function:

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');

Když tohle selže, problémem zřídkakdy bývá PDF. Bývá jím PFX. HotPDF čte kontejnery chráněné PBES2, tedy odvozením klíče PBKDF2 nad AES-256-CBC. PFX exportovaný starším průvodcem certifikáty ve Windows, nebo OpenSSL před verzí 3.0, bývá místo toho obvykle zabalený ve starším RC2 nebo 3DES, a takový se jednoduše neparsuje. Oprava je kontejner jednou znovu exportovat s moderní ochranou; dnešní OpenSSL to dělá standardně a nejde o žádnou změnu kódu. Takže když podepisování okamžitě spadne na certifikátu, který „funguje všude jinde", podívejte se nejdřív na to, jak byl PFX vytvořen, než začnete podezírat vlastní kód

Cesta rezervace-hash-vložení pro HSM a tokeny

Cesta jedním voláním předpokládá, že váš proces umí klíč přečíst jako soubor. Čím dál častěji to ale nejde. Klíč sedí v HSM, na USB tokenu, nebo za API podepisovací služby, a knihovna se k němu nemá jak dostat přímo. HotPDF to řeší rozdělením podepisování do kroků na úrovni bajtů: zapsat dokument se zástupným symbolem, požádat knihovnu o rozsahy pro hash, předat vstup hashe tomu, co drží klíč, a pak vrácený CMS vložit zpět do díry

HotPDF: Čtyřkroková pipeline reserve-hash-insert nad placeholder.pdf ukazující vyhrazenou díru /Contents mezi dvěma úseky ByteRange a HSM vyměňující digest za CMS hex
HotPDF vyhradí díru a nahlásí oba úseky ByteRange, váš držitel klíče je podepíše venku a vrácené CMS se zašije zpět bajt za bajtem, aniž by se dotklo zmraženého bajtu
var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Zapíše dokument s rezervovanou dírou /Contents
  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. Načte uložené bajty; vrácené offsety jsou 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. Zahashuje oba úseky a podepíše externě (HSM, token, služba)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // vaše integrace: vrací CMS jako hex

  // 4. Vloží podpis do rezervované díry
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Dva detaily v této sekvenci způsobují většinu přerušovaných selhání. Prvním je, že PreparePDFForSigning pracuje s bajty hotového souboru. Zástupný symbol musí být zapsaný a uložený celý, než offsety cokoli znamenají; spočítáte-li je proti streamu, který se ještě sestavuje, nebudou souhlasit s bajty, které nakonec zahashujete. Druhým detailem je znovu velikost rezervace. Vámi požadovaných 8192 bajtů musí pojmout finální CMS, a podpis nesoucí mezilehlé certifikáty, nebo ten, který služba ozdobí podepsanými atributy, může tuto hranici překročit. InsertSignatureHex díru kvůli místu nezvětší. Poznávacím znamením je pipeline, která s jedním certifikátem podepisuje bez problémů a s dalším selže; lékem je regenerovat zástupný symbol s rezervací odměřenou podle skutečného podpisu vytvořeného opravdovým podepisovatelem, ne odhadem

Bazální úrovně PAdES a časová razítka, která udržují podpis naživu

Pokud podepisujete podle evropských pravidel, standardem ve hře je ETSI EN 319 142-1, který skládá na sebe čtyři bazální úrovně PAdES. B-B je prostý podpis. B-T přidává důvěryhodné časové razítko, které dokazuje, kdy vznikl. B-LT vkládá do dokumentu ověřovací materiál, certifikáty a data o odvolání, aby jej bylo možné ověřit i o roky později. B-LTA na to navrší periodická časová razítka dokumentu, takže důkaz přežije algoritmy, na kterých byl postaven. HotPDF pro každou úroveň vytváří struktury na straně dokumentu:

HotPDF: Stohované základní úrovně PAdES od B-B přes B-T a B-LT po B-LTA s obnovovací časovou osou ukazující, že periodická časová razítka dokumentu udrží podpis ověřitelný i po desetiletích
Každá úroveň vrství novou ochranu na poslední; B-LTA znovu a znovu aplikuje časová razítka dokumentu, takže důkazy přežijí algoritmy, na nichž byly původně postaveny
// Pole bazálního podpisu PAdES (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Časové razítko dokumentu: větší rezervace pro token TSA a řetěz
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Rezervace 16384 bajtů na časové razítko je záměrná. Autorita časových razítek vrací token, který s sebou táhne vlastní řetěz certifikátů, takže obvykle potřebuje víc místa než 8 KB, se kterými si vystačí prostý podpis. Tato časová razítka dokumentu jsou zároveň mechanismem za B-LTA: opětovné orazítkování archivovaného podpisu každých pár let, algoritmy, které jsou stále aktuální, je to, co udržuje dokument, který jste podepsali v roce 2026, ověřitelný i v roce 2040

Pár slov k řetězcům reason, location a contact, které obě volání pole přijímají: jsou to jen pomocná metadata a nic víc. HotPDF je ukládá jako obyčejné položky slovníku a vykresluje je do viditelného vzhledu podpisu, ale žádný validátor je proti ničemu neověřuje. Vyplňujte je konzistentně z dat vašeho workflow, protože auditoři je skutečně čtou, ale nikdy je nezaměňujte za důkaz. Skutečné kryptografické tvrzení žije zcela v CMS a jeho řetězu certifikátů a ověřovatel viditelný text úplně ignoruje

Po podepsání smí soubor už jen růst

Ve chvíli, kdy podpis existuje, jsou bajty v jeho rozsazích zmrazené. Jediný legitimní způsob, jak soubor později změnit, je přírůstková aktualizace (incremental update) podle ISO 32000-1 §7.5.6, která připojí nové a změněné objekty za původní bajty a zřetězí k nim čerstvou sekci cross-reference. Uděláno takto, podpis zůstává platný pro svou revizi a prohlížeč hlásí poctivý stav: podepsaná revize je neporušená, dokument byl následně rozšířen. Místo toho celý soubor znovu sériově zapsat znamená přepsat podepsané úseky, což podpis zničí, i když se navenek nic nezměnilo. Stejný revizní mechanismus je také tím, jak jeden dokument nese více podpisů: každý nový podpis přistane ve své vlastní přírůstkové aktualizaci a jeho rozsahy pokrývají vše před ním, včetně dřívějších podpisů. Mechanika append-only a to, kdy je bezpečné je zhutnit, jsou popsané v článku o object streamech a přírůstkových aktualizacích

Při návrhu stojí za to mít na paměti dvě hranice. Výstupní režim PDF/A v HotPDF podpisová pole rovnou odmítá, takže archivní shoda a vložený podpis musí jít ven jako dva oddělené soubory. A podepisování neříká nic o utajení: dokazuje, kdo dokument vytvořil a že se od té doby nezměnil, ale přečíst si jej stále může kdokoli. Skrytí obsahu je samostatný úkol, který řeší šifrování AES-256 a politika oprávnění

Ať postavíte cokoli, otestujte to něčím jiným, než kódem, který soubor napsal. Otevřete výstup v panelu podpisů Acrobatu a ověřte tři věci: podpis je platný, identita se řetězí ke kořeni, který jste čekali, a panel nehlásí žádné změny od podepsání. Pak v jednorázové kopii převraťte jediný bajt uvnitř podepsaného rozsahu a ověřte, že panel teď dokument označí za změněný. Podepisovací pipeline, u které jste nikdy neviděli odmítnutí zfalšovaného souboru, je taková, jejíž ověřování ve skutečnosti nebylo otestované

Všechny tři úrovně podepisování jsou součástí HotPDF Delphi Component pro Delphi a C++Builder; produktová stránka odkazuje na úplnou referenci signature API