Technical Article

Digitalni podpisi PDF in PAdES v Delphi s HotPDF

Podpis PDF je večinoma računanje bajtov in ravno pri računanju bajtov gre pogosto kaj narobe. Kriptografija teče na kodi, ki je bila revidirana dve desetletji, in ta del skoraj nikoli ne odpove. Tisto, kar odpove v produkciji, je bolj preprosto: rezervirano mesto (ograda) je premajhno za dejanski podpis, zgostitev (hash) je izračunana na napačnem delu datoteke ali pa "shranjevanje" po podpisovanju tiho prepisuje bajte, ki jih je podpis že zamrznil. Pravilno razporedite bajte in zelena kljukica se bo pojavila sama od sebe.

HotPDF pokriva podpisovanje za Delphi in C++Builder na treh ravneh, in med njimi izbirate z odgovorom na eno vprašanje: kje se nahaja zasebni ključ? Datoteka PFX na disku zahteva en sam funkcijski klic. Ključ, zaklenjen v modulu HSM ali oddaljeni storitvi podpisovanja, zahteva zaporedje rezervacija-zgostitev-vstavitev, saj nobena knjižnica ne more poseči v žeton in izvleči ključa. Podpis, ki mora izpolnjevati evropske predpise, potrebuje poleg tega še osnovne strukture PAdES. Spodnji razdelki sledijo temu razvoju.

Kako /ByteRange določi podpisane bajte

Podpis mora biti znotraj datoteke, ki jo podpisuje, in ne more podpisati samega sebe. PDF ta paradoks rešuje tako, da pusti vrzel (luknjo). Pred podpisovanjem zapisovalnik rezervira vnos /Contents fiksne velikosti, napolnjen z ničlami, in zabeleži polje /ByteRange za oba razpona na obeh straneh vrzeli: vse pred vrzeljo in vse za njo. Podpisnik izračuna zgostitev obeh razponov in zapiše nastali CMS objekt v to vrzel kot šestnajstiški niz. Past je v besedi fiksno. Velikosti te vrzeli se zavežete, še preden veste, kako velik bo končni podpis, zato mora biti rezervacija precejšnja ocena čez palec. Osem kilobajtov brez težav zadošča za ločen podpis CMS s kratko verigo certifikatov.

HotPDF loči ta dva primera v dva klica, in njuno zamenjevanje pa je pogosta začetna napaka. Metoda AddSignatureField ustvari prazno, vidno polje, ki ga lahko uporabnik kasneje podpiše v pregledovalniku. Metoda AddSignedSignatureField ustvari polje in rezervira vrzel /Contents, kar je tisto, kar želite, kadar bo podpis dokončala koda in ne človek. Če zunanjemu podpisniku predate prazno polje, nima česa izpolniti.

Pot z enim klicem: podpisovanje iz datoteke PFX

Ko sta certifikat in njegov zasebni ključ v datoteki PFX/PKCS#12, ki jo vaš proces lahko prebere, se celoten postopek skrajša na razredno funkcijo:

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

Ko to ne uspe, je PDF redko krivec. Običajno je težava v PFX. HotPDF bere vsebnike, zaščitene s PBES2, kar pomeni izpeljavo ključa PBKDF2 prek AES-256-CBC. PFX, izvožen s starejšim čarovnikom za certifikate v sistemu Windows ali z OpenSSL pred različico 3.0, je običajno zaščiten z zastarelim RC2 ali 3DES in se preprosto ne bo razčlenil. Rešitev je enkraten ponovni izvoz vsebnika s sodobno zaščito. Današnji OpenSSL to počne privzeto, kar ne zahteva spremembe kode. Če podpisovanje takoj odpove pri certifikatu, ki "drugje deluje", preverite, kako je bil PFX ustvarjen, preden posumite na svojo kodo.

Pot z rezervacijo, zgostitvijo in vstavljanjem za HSM in žetone

Pot z enim klicem predvideva, da lahko vaš proces prebere ključ kot datoteko. Vse pogosteje pa to ni mogoče. Ključ se nahaja v HSM, na žetonu USB ali za API-jem storitve podpisovanja in knjižnica nima neposrednega dostopa do njega. HotPDF to rešuje tako, da podpisovanje razdeli na korake na ravni bajtov: zapiše dokument z rezerviranim mestom, od knjižnice zahteva razpone zgostitve, posreduje vhodne podatke za zgostitev tistemu, kar drži ključ, in nato vrnjeni CMS vstavi nazaj v rezervirano vrzel.

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;

Dve podrobnosti v tem zaporedju povzročata večino občasnih napak. Prva je ta, da PreparePDFForSigning deluje na bajtih dokončane datoteke. Rezervirano mesto mora biti v celoti zapisano in shranjeno, preden odmiki sploh kaj pomenijo. Če jih izračunate na toku, ki se še sestavlja, se ne bodo ujemali z bajti, ki jih na koncu zgostite. Druga podrobnost je ponovno velikost rezervacije. 8192 bajtov, ki ste jih zahtevali, mora vsebovati končni CMS. Podpis, ki vsebuje vmesne certifikate, ali tisti, ki ga storitev okrasi s podpisanimi atributi, lahko to mejo preseže. Metoda InsertSignatureHex ne bo povečala vrzeli, da bi naredila prostor. Znak za to je postopek, ki se uspešno podpiše z enim certifikatom, pri naslednjem pa odpove. Rešitev je ponovna ustvaritev vsebnika z rezervacijo, izmerjeno na podlagi dejanskega podpisa, ki ga je ustvaril podpisnik, in ne na podlagi ugibanja.

Osnovne ravni PAdES in časovni žigi, ki ohranjajo podpis veljaven

Če podpisujete po evropskih pravilih, velja standard ETSI EN 319 142-1, ki opredeljuje štiri osnovne ravni PAdES. B-B je preprost podpis. B-T doda zaupanja vreden časovni žig, ki dokazuje čas nastanka. B-LT vgradi podatke za preverjanje, certifikate in podatke o preklicih neposredno v dokument, da ga je mogoče preveriti tudi čez leta. B-LTA dodaja periodične časovne žige dokumenta, tako da dokazi preživijo algoritme, na katerih so bili zgrajeni. HotPDF ustvari ustrezne strukture na strani dokumenta za vsako raven:

// 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 16384 bajtov za časovni žig je namerna. Organ za časovno žigosanje vrne žeton, ki s seboj prinese lastno verigo certifikatov, zato običajno potrebuje več prostora kot 8 KB, ki zadostujejo za preprost podpis. Ti časovni žigi dokumentov so tudi mehanizem za B-LTA: ponovno časovno žigosanje arhiviranega podpisa vsakih nekaj let z algoritmi, ki so še veljavni, omogoča, da bo dokument, podpisan leta 2026, ostal preverljiv tudi leta 2040.

Beseda o nizih za razlog, lokacijo in stik, ki jih sprejemata oba klica polja: gre zgolj za priročne metapodatke. HotPDF jih shrani kot navadne vnose v slovar in jih izriše v vidni videz podpisa, vendar jih noben pregledovalnik ne preverja z ničimer. Dosledno jih izpolnite iz svojih podatkov o poteku dela, saj jih revizorji berejo, vendar jih nikoli ne zamenjujte za dokaze. Dejanska kriptografska trditev živi v celoti v CMS in njegovi verigi certifikatov, preverjevalnik pa vidno besedilo popolnoma prezre.

Po podpisovanju lahko datoteka le še raste

V trenutku, ko podpis obstaja, so bajti znotraj njegovih razponov zamrznjeni. Edini zakoniti način za kasnejšo spremembo datoteke je inkrementalna posodobitev po standardu ISO 32000-1 §7.5.6, ki doda nove in spremenjene objekte za prvotnimi bajti ter nanje poveže nov del navzkrižnih sklicev. Na ta način podpis ostane veljaven za svojo revizijo in pregledovalnik poroča o poštenem stanju: podpisana revizija je nedotaknjena, dokument pa je bil kasneje razširjen. Če namesto tega ponovno serializirate celotno datoteko, prepišete podpisana območja, kar uniči podpis, tudi če se ni spremenilo nič vidnega. Isti revizijski mehanizem omogoča tudi, da en dokument nosi več podpisov: vsak nov podpis se nahaja v svoji inkrementalni posodobitvi, njegovi razponi pa pokrivajo vse pred njim, vključno s prejšnjimi podpisi. Mehanika dodajanja na konec datoteke in kdaj jih je varno stisniti, je opisana v članku o tokovih objektov in inkrementalnih posodobitvah.

Pri načrtovanju je vredno upoštevati dve meji. Izhodni način PDF/A v HotPDF popolnoma zavrne polja za podpis, zato morata arhivska skladnost in vgrajeni podpis biti poslana kot ločeni datoteki. Prav tako podpisovanje ne pove ničesar o tajnosti: dokazuje le, kdo je dokument ustvaril in da se od takrat ni spremenil, prebere pa ga lahko še vedno vsak. Prikrivanje vsebine je ločeno opravilo, ki ga rešujeta šifriranje AES-256 in politika dovoljenj.

Karkoli zgradite, to preizkusite z nečim drugim kot s kodo, ki je datoteko zapisala. Odprite izhod v Acrobatovi plošči za podpise in potrdite tri stvari: podpis je veljaven, identiteta se povezuje z pričakovano korensko verigo in plošča poroča, da po podpisovanju ni bilo sprememb. Nato spremenite en sam bajt znotraj podpisanega območja testne kopije in potrdite, da plošča zdaj javlja, da je dokument spremenjen. Podpisni postopek, pri katerem še nikoli niste videli zavrnitve spremenjene datoteke, je postopek, katerega preverjanje ni bilo zares preizkušeno.

Vse tri podpisne ravni so na voljo s komponento HotPDF za Delphi in C++Builder; stran izdelka vsebuje povezavo do celotne reference API za podpisovanje.