Tehnički članak

Digitalni PDF potpisi i PAdES u Delphi-ju uz HotPDF

PDF potpis je uglavnom računanje bajtova, a računanje bajtova je mesto gde stvari kreću po zlu. Kriptografija se oslanja na kod koji je revidiran već dve decenije, i taj deo gotovo nikada ne otkazuje. Ono što otkazuje u produkciji je jednostavnije: mesto rezervisano za potpis je premalo za stvarni potpis, heš je izračunat preko pogrešnog dela fajla ili upisivanje izmena nakon potpisivanja koje tiho menja bajtove koje je potpis već fiksirao. Rasporedite bajtove ispravno i zeleni znak potvrde će se sam pojaviti

HotPDF pokriva potpisivanje za Delphi i C++Builder na tri nivoa, a izbor vršite odgovorom na jedno pitanje: gde se nalazi privatni ključ? Za PFX fajl na disku dovoljan je jedan poziv funkcije. Ključ zaključan u HSM-u ili servisu za daljinsko potpisivanje zahteva sekvencu rezervisanja, heširanja i umetanja, jer nijedna biblioteka ne može pristupiti tokenu i izvući ključ. Potpis koji mora da zadovolji evropske propise zahteva dodatne osnovne PAdES strukture. Odeljci u nastavku prate ovaj redosled

Kako /ByteRange definiše potpisane bajtove

Potpis mora da se nalazi unutar fajla koji potpisuje, a ne može da potpiše sam sebe. PDF prevazilazi ovaj paradoks ostavljanjem praznog prostora. Pre potpisivanja, modul za pisanje rezerviše unos /Contents fiksne veličine popunjen nulama i beleži niz /ByteRange za dva opsega sa obe njegove strane: sve pre praznog prostora i sve posle njega. Potpisnik hešira ta dva opsega i upisuje dobijeni CMS objekat u prazan prostor u heksadecimalnom formatu. Zamka je u reči fiksno. Obavezujete se na veličinu tog praznog prostora pre nego što saznate koliko će gotov potpis biti velik, tako da rezervacija mora biti sigurna prekomerna procena. Osam kilobajta je sasvim dovoljno za izdvojeni (detached) CMS potpis sa kratkim lancom sertifikata

HotPDF deli ova dva slučaja na dva poziva, a njihovo mešanje je česta početnička greška. Metoda AddSignatureField postavlja prazno, vidljivo polje koje korisnik može kasnije potpisati u programu za pregled. Metoda AddSignedSignatureField kreira polje i rezerviše prostor /Contents, što je opcija koju želite kada kod, a ne čovek, završava potpisivanje. Ako eksternom potpisniku prosledite prazno polje, on neće imati šta da popuni

Putanja sa jednim pozivom: potpisivanje pomoću PFX-a

Kada se sertifikat i njegov privatni ključ nalaze u PFX/PKCS#12 fajlu koji vaš proces može da pročita, ceo tok 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 uspe, problem je retko u PDF-u. Problem je u PFX-u. HotPDF čita kontejnere zaštićene sa PBES2, što znači PBKDF2 derivaciju ključa preko AES-256-CBC. PFX izvezen pomoću starijeg Windows čarobnjaka za sertifikate, ili pomoću OpenSSL-a pre verzije 3.0, obično je umotan u zastareli RC2 ili 3DES i jednostavno se neće učitati. Rešenje je da se kontejner ponovo izveze sa modernom zaštitom; današnji OpenSSL to radi po defaultu, i to ne zahteva izmenu koda. Dakle, kada potpisivanje odmah otkaže sa sertifikatom koji „radi svuda drugde”, pogledajte kako je PFX kreiran pre nego što posumnjate u sopstveni kod

Putanja rezervisanja, heširanja i umetanja za HSM-ove i tokene

Putanja sa jednim pozivom pretpostavlja da vaš proces može pročitati ključ kao fajl. Sve češće to nije moguće. Ključ se nalazi u HSM-u, na USB tokenu ili iza API-ja servisa za potpisivanje, i ne postoji način da mu biblioteka direktno pristupi. HotPDF to rešava deljenjem potpisivanja na korake na nivou bajtova: upisivanje dokumenta sa rezervisanom pozicijom, traženje heš opsega od biblioteke, prosleđivanje heš ulaza onome što drži ključ, a zatim umetanje vraćenog CMS-a nazad u rezervisani prostor

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 ovoj sekvenci uzrokuju većinu povremenih neuspeha. Prvi je da PreparePDFForSigning radi sa bajtovima gotovog fajla. Rezervisani prostor mora biti u potpunosti zapisan i sačuvan pre nego što ofseti dobiju smisao; ako ih računate na toku koji se još uvek sklapa, oni se neće poklapati sa bajtovima koje na kraju heširate. Drugi detalj je, ponovo, veličina rezervisanog prostora. Tih 8192 bajta koje ste tražili mora da primi konačni CMS, a potpis koji nosi posredne sertifikate, ili onaj koji servis ukrašava potpisanim atributima, može je premašiti. Metoda InsertSignatureHex neće proširiti prostor da bi napravila mesta. Znak za uzbunu je kada sistem radi dobro sa jednim sertifikatom, a otkaže sa sledećim; lek je da ponovo generišete rezervisani prostor sa veličinom izmerenom na osnovu stvarnog potpisa koji je proizveo stvarni potpisnik, a ne na osnovu nagađanja

PAdES osnove i vremenski žigovi koji održavaju potpis aktivnim

Ako potpisujete pod evropskim pravilima, standard koji se primenjuje je ETSI EN 319 142-1, koji definiše četiri PAdES nivoa (baseline). B-B je običan potpis. B-T dodaje pouzdani vremenski žig koji dokazuje kada je potpis napravljen. B-LT ugrađuje validacione podatke, sertifikate i informacije o opozivu unutar samog dokumenta kako bi se mogao proveriti godinama kasnije. B-LTA dodaje periodične vremenske žigove dokumenta na vrh, tako da dokazi nadživljavaju algoritme na kojima su izgrađeni. HotPDF generiše strukture na strani dokumenta za svaki nivo:

// 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 namerna. Autoritet za vremenski žig (TSA) vraća token koji sa sobom povlači sopstveni lanac sertifikata, pa mu rutinski treba više prostora od 8 KB koliko je dovoljno običnom potpisu. Ti vremenski žigovi dokumenata su takođe mehanizam iza B-LTA nivoa: ponovno stavljanje vremenskog žiga na arhivirani potpis svakih nekoliko godina, sa algoritmima koji su i dalje aktuelni, jeste ono što omogućava da dokument koji ste potpisali 2026. ostane proverljiv i 2040. godine

Reč-dve o stringovima za razlog, lokaciju i kontakt koje oba poziva polja prihvataju: to su samo praktični metapodaci i ništa više. HotPDF ih čuva kao obične unose u rečniku i prikazuje ih u vidljivom izgledu potpisa, ali ih nijedan validator ne proverava. Popunjavajte ih dosledno iz svojih podataka o toku posla, pošto ih revizori čitaju, ali ih nikada nemojte smatrati dokazom. Stvarna kriptografska tvrdnja leži u potpunosti u CMS-u i njegovom lancu sertifikata, dok verifikator potpuno ignoriše vidljivi tekst

Nakon potpisivanja, fajl sme samo da raste

U trenutku kada potpis postoji, bajtovi unutar njegovih opsega su zamrznuti. Jedini legitiman način da se dokument nakon toga promeni jeste inkrementalno ažuriranje prema ISO 32000-1 §7.5.6, koje dodaje nove i izmenjene objekte nakon originalnih bajtova i povezuje novi odeljak unakrsnih referenci (cross-reference) nazad na njih. Na taj način potpis ostaje važeći za svoju reviziju, a program za pregled prijavljuje stvarno stanje: potpisana revizija je netaknuta, a dokument je naknadno proširen. Ako umesto toga ponovo serijalizujete ceo fajl, prepisaćete potpisane opsege, što uništava potpis čak i ako se ništa vidljivo nije promenilo. Isti mehanizam revizije omogućava da jedan dokument nosi više potpisa: svaki novi potpis se smešta u sopstveno inkrementalno ažuriranje, a njegovi opsezi pokrivaju sve pre njega, uključujući i prethodne potpise. Mehanizam upisa isključivo dodavanjem na kraj (append-only), kao i vreme kada je bezbedno njihovo kompaktovanje, pokriveni su u članku o tokovima objekata i inkrementalnim ažuriranjima

Dva ograničenja vredi imati na umu prilikom projektovanja. HotPDF-ov PDF/A režim izvoza u potpunosti odbija polja za potpis, tako da se usklađenost sa arhiviranjem i ugrađeni potpis moraju isporučiti kao odvojeni fajlovi. Takođe, potpisivanje ne govori ništa o tajnosti: ono dokazuje ko je napravio dokument i da se on od tada nije menjao, ali svako i dalje može da ga pročita. Skrivanje sadržaja je poseban zadatak koji rešava AES-256 enkripcija i politika dozvola

Šta god da napravite, testirajte to programom koji je različit od koda koji je kreirao fajl. Otvorite izlazni fajl u Acrobat panelu za potpise i potvrdite tri stvari: potpis je važeći, identitet se povezuje sa korenom (root) koji ste očekivali i panel ne prijavljuje nikakve promene nakon potpisivanja. Zatim promenite samo jedan bajt unutar potpisanog opsega na probnoj kopiji i potvrdite da panel sada prijavljuje da je dokument izmenjen. Protok potpisivanja kod koga nikada niste videli kako odbija izmenjen fajl je onaj čija verifikacija zapravo nije ni testirana

Sva tri nivoa potpisivanja dolaze uz HotPDF komponentu za Delphi i C++Builder; stranica proizvoda sadrži link do kompletne referentne dokumentacije za API za potpisivanje