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
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
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:
// 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