PDF potpis je uglavnom obračun bajtova (byte accounting), a obračun bajtova je upravo ono gdje dolazi do pogrešaka. Kriptografija se izvodi na kodu koji je revidiran već dva desetljeća i taj dio gotovo nikada ne zakazuje. Ono što zakazuje u produkciji znatno je skromnije: rezervirano mjesto (placeholder) koje je premalo za stvarni potpis, hash izračunat preko pogrešnog dijela datoteke ili "spremanje" nakon potpisivanja koje je tiho prepisalo bajtove koje je potpis već zamrznuo. Ispravno rasporedite bajtove i zelena kvačica će se sama pobrinuti za sebe.
HotPDF pokriva potpisivanje za Delphi i C++Builder na tri razine, a između njih birate odgovarajući na jedno pitanje: gdje se nalazi privatni ključ? PFX datoteka na disku zahtijeva samo jedan funkcijski poziv. Ključ zaključan u HSM-u ili udaljenoj usluzi potpisivanja zahtijeva sekvencu rezerviranja, hashanja i umetanja (reserve-hash-insert) jer nijedna knjižnica ne može pristupiti tokenu i izvući ključ. Potpis koji mora zadovoljiti europske propise zahtijeva i PAdES osnovne (baseline) strukture povrh toga. Odjeljci u nastavku prate taj tijek.
Kako /ByteRange definira potpisane bajtove
Potpis mora živjeti unutar datoteke koju potpisuje, a ne može potpisati sam sebe. PDF zaobilazi ovaj paradoks ostavljanjem rupe. Prije potpisivanja, pisač rezervira unos /Contents fiksne veličine pun nula i bilježi polje /ByteRange za dva raspona s obje njegove strane: sve prije rupe i sve nakon nje. Potpisnik izvodi hash nad ta dva raspona i zapisuje rezultirajući CMS blob u rupu kao heksadecimalni niz. Zamka leži u riječi fiksno. Obvezujete se na veličinu te rupe prije nego što znate koliki će biti konačni potpis, pa rezervacija mora biti sigurna procjena s viškom. Osam kilobajta bez problema može primiti odvojeni (detached) CMS potpis s kratkim lancem certifikata.
HotPDF dijeli ova dva slučaja u dva poziva, a njihovo miješanje česta je rana pogreška. AddSignatureField dodaje prazno, vidljivo polje koje osoba može potpisati kasnije u pregledniku. AddSignedSignatureField stvara polje i rezervira rupu za /Contents, što je ono što želite kad god kod, a ne čovjek, dovršava potpisivanje. Ako vanjskom potpisniku predate prazno polje, on neće imati što ispuniti.
Putanja s jednim pozivom: potpisivanje iz PFX-a
Kada se certifikat i njegov privatni ključ nalaze u PFX/PKCS#12 datoteci koju vaš proces može pročitati, cijeli cjevovod se svodi na jednu klasnu funkciju:
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');
Kada ovo ne uspije, problem je rijetko u samom PDF-u. Problem je u PFX-u. HotPDF čita spremnike zaštićene s PBES2, što znači izvedbu ključa PBKDF2 preko AES-256-CBC. PFX uvezen pomoću starijeg čarobnjaka za certifikate sustava Windows ili pomoću OpenSSL-a starijeg od verzije 3.0 obično je umotan u zastarjeli RC2 ili 3DES i jednostavno se neće moći parsirati. Rješenje je ponovno izvesti spremnik s modernom zaštitom; današnji OpenSSL to radi prema zadanim postavkama i to ne zahtijeva izmjenu koda. Stoga, kada potpisivanje trenutačno zakaže s certifikatom koji "works everywhere else," pogledajte kako je PFX stvoren prije nego što posumnjate u vlastiti kod.
Putanja reserve-hash-insert za HSM-ove i tokene
Putanja s jednim pozivom pretpostavlja da vaš proces može pročitati ključ kao datoteku. Sve češće to nije moguće. Ključ se nalazi u HSM-u, na USB tokenu ili iza API-ja usluge potpisivanja i ne postoji način da ga knjižnica izravno dohvati. HotPDF to rješava dijeljenjem potpisivanja na korake na razini bajtova: zapišite privremeni dokument, zatražite od knjižnice raspone hasha, proslijedite podatke za hash onome što drži ključ, a zatim umetnite vraćeni CMS natrag u rupu.
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;
Dva detalja u ovom slijedu uzrokuju većinu povremenih pogrešaka. Prvi je taj što PreparePDFForSigning radi na bajtovima dovršene datoteke. Rezervirano mjesto (placeholder) mora biti u potpunosti zapisano i spremljeno prije nego što pomaci (offsets) išta znače; ako ih izračunate u odnosu na tok (stream) koji se još uvijek sastavlja, oni se neće podudarati s bajtovima koje na kraju hasharate. Drugi je ponovno veličina rezervacije. Tih 8192 bajtova koje ste zatražili mora sadržavati konačni CMS, a potpis koji nosi posredne certifikate, ili onaj koji usluga ukrašava potpisanim atributima, može to premašiti. InsertSignatureHex neće povećati rupu kako bi napravio mjesta. Tipičan znak ovog problema je sustav koji s jednim certifikatom potpisuje ispravno, a sa sljedećim ne uspijeva; rješenje je ponovno generirati rezervirano mjesto s rezervacijom izmjerenom iz stvarnog potpisa koji je proizveo stvarni potpisnik, a ne nagađanjem.
PAdES osnovne linije i vremenski žigovi koji održavaju potpis valjanim
Ako potpisujete prema europskim propisima, mjerodavni standard je ETSI EN 319 142-1, koji definira prema standardu četiri razine PAdES osnovne linije (baseline). B-B je običan potpis. B-T dodaje pouzdani vremenski žig koji dokazuje kada je stvoren. B-LT ugrađuje materijale za provjeru, certifikate i podatke o opozivu unutar dokumenta kako bi se i godinama kasnije mogao provjeriti. B-LTA dodaje periodične vremenske žigove dokumenta povrh toga, tako da dokazi nadživljuju algoritme na kojima su izgrađeni. HotPDF generira strukture na strani dokumenta za svaku razinu:
// 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);
Rezervacija od 16384 bajta za vremenski žig je namjerna. Tijelo za izdavanje vremenskog žiga (TSA) vraća token koji sa sobom povlači vlastiti lanac certifikata, pa rutinski zahtijeva više prostora od 8 KB koji su dovoljni za običan potpis. Ti vremenski žigovi dokumenta također su mehanizam koji stoji iza B-LTA: ponovno stavljanje vremenskog žiga na arhivirani potpis svakih nekoliko godina, pomoću algoritama koji su još uvijek aktualni, omogućuje da dokument koji ste potpisali 2026. godine ostane provjerljiv i 2040. godine.
Napomena o tekstualnim nizovima za razlog, lokaciju i kontakt koje oba poziva polja prihvaćaju: to su samo informativni metapodaci i ništa više. HotPDF ih pohranjuje kao obične unose u rječniku i ispisuje ih u vidljivom izgledu potpisa, ali ih nijedan validator ne provjerava. Ispunite ih dosljedno iz podataka vašeg tijeka rada budući da ih revizori čitaju, no nemojte ih smatrati dokazom. Stvarna kriptografska tvrdnja u potpunosti živi u CMS-u i njegovom lancu certifikata, a sustav za provjeru potpuno zanemaruje vidljivi tekst.
Nakon potpisivanja datoteka može samo rasti
U trenutku kada potpis postoji, bajtovi unutar njegovih raspona su zamrznuti. Jedini legitiman način da se datoteka nakon toga promijeni je inkrementalno ažuriranje prema ISO 32000-1 §7.5.6, koje dodaje nove i izmijenjene objekte nakon izvornih bajtova i povezuje novi odjeljak unakrsnih referenci (cross-reference) natrag s njima. Na taj način potpis ostaje valjan za svoju reviziju, a preglednik prikazuje stvarno stanje: potpisana revizija je netaknuta, dokument je naknadno proširen. Ako umjesto toga ponovno serijalizirate cijelu datoteku, prepisat ćete potpisane raspone, što uništava potpis čak i ako se vizualno ništa nije promijenilo. Isti mehanizam revizije također je način na koji jedan dokument nosi nekoliko potpisa: svaki novi potpis smješta se u vlastito inkrementalno ažuriranje, a njegovi rasponi pokrivaju sve prije njega, uključujući i ranije potpise. Mehanika dodavanja na kraj datoteke (append-only) i kada ih je sigurno sažeti pokriveni su u članku o tokovima objekata i inkrementalnim ažuriranjima.
Dva ograničenja vrijedi imati na umu tijekom dizajna. HotPDF-ov PDF/A izlazni način rada u potpunosti odbacuje polja potpisa, tako da arhivska usklađenost i ugrađeni potpis must biti isporučeni kao zasebne datoteke. Također, potpisivanje ne jamči tajnost: ono dokazuje tko je izradio dokument i da se on od tada nije mijenjao, ali ga i dalje svatko može pročitati. Skrivanje sadržaja zaseban je zadatak koji se rješava pomoću AES-256 enkripcije i pravila dopuštenja.
Što god izradili, testirajte to s nečim drugim osim koda koji je zapisao datoteke. Otvorite izlaznu datoteku na ploči s potpisima (signature panel) u Acrobat-u i potvrdite tri stvari: potpis je valjan, identitet se povezuje s očekivanim korijenskim certifikatom i ploča javlja da nema promjena od potpisivanja. Zatim promijenite jedan bajt unutar potpisanog raspona u testnoj kopiji i potvrdite da ploča sada prijavljuje kako je dokument izmijenjen. Sustav potpisivanja za koji nikada niste vidjeli da je odbacio izmijenjenu datoteku zapravo nije prošao pravu provjeru.
Sve tri razine potpisivanja dolaze s komponentom HotPDF Component za Delphi i C++Builder; stranica proizvoda sadrži poveznicu na cjelovitu referencu API-ja za potpisivanje.