Műszaki cikk

PDF digitális aláírások és PAdES Delphi-ben HotPDF-fel

A PDF-aláírás javarészt bájtkönyvelés, és a bájtkönyvelésnél szokott elromlani. A kriptográfia két évtizede auditált kódon fut, és az a rész szinte soha nem hibázik. Éles környezetben szerényebb dolgok buknak el: a valódi aláíráshoz túl kicsire foglalt helykitöltő, a fájl rossz szakaszán elvégzett kivonatolás, vagy egy aláírás utáni „mentés”, amely csendben újraírta azokat a bájtokat, amelyeket az aláírás már befagyasztott. Rakja ki helyesen a bájtokat, és a zöld pipa magától megjön

A HotPDF három szinten fedi le az aláírást Delphihez és C++Builderhez, és egyetlen kérdés megválaszolásával választ közülük: hol lakik a magánkulcs? Egy lemezen lévő PFX-fájlhoz egyetlen függvényhívás kell. Egy HSM-be vagy távoli aláíró szolgáltatásba zárt kulcshoz a „foglalás, kivonatolás, beillesztés” sorrend kell, mert egyetlen könyvtár sem tud benyúlni egy tokenbe és kihúzni belőle a kulcsot. Az európai szabályozásnak megfelelő aláíráshoz mindezek tetejére a PAdES alapszintű szerkezetei kellenek. Az alábbi szakaszok ezt a haladási sorrendet követik

Döntési diagram: HotPDF PFX egyhívásos aláírás, a reserve-hash-insert út, ha a kulcs HSM-ben vagy távoli szolgáltatásban ül, és PAdES baseline struktúrák szabályozott európai aláíráshoz
Az aláírási szintet a privát kulcs lakhelye szerint válassza; egy olvasható PFX fájl az aláírást egyetlen hívásba sűríti, a tokenben lakó kulcsok bájtszintű kitérőt kényszerítenek, az európai szabályozás pedig hozzáteszi a PAdES réteget

Hogyan szegezi le a /ByteRange az aláírt bájtokat

Az aláírásnak abban a fájlban kell laknia, amelyet aláír, és önmagát nem tudja aláírni. A PDF úgy kerüli meg a paradoxont, hogy lyukat hagy. Aláírás előtt az író egy rögzített méretű, nullákkal teli /Contents bejegyzést foglal le, és feljegyez egy /ByteRange tömböt a lyuk két oldalán fekvő szakaszra: mindenre előtte és mindenre utána. Az aláíró ezt a két szakaszt kivonatolja, és a kapott CMS-blobot hexadecimálisan írja a lyukba. A csapda a rögzített szóban van. A lyuk méretét azelőtt kell eldöntenie, hogy tudná, mekkora lesz a kész aláírás, ezért a foglalásnak magabiztos túlbecslésnek kell lennie. Nyolc kilobájt kényelmesen elbír egy leválasztott CMS-aláírást rövid tanúsítványlánccal

A HotPDF két hívásra bontja a két esetet, és ezek összekeverése gyakori kezdeti hiba. Az AddSignatureField egy üres, látható mezőt tesz le, hogy azt később egy ember írja alá a megjelenítőben. Az AddSignedSignatureField létrehozza a mezőt, és lefoglalja a /Contents lyukat – ez kell mindig, amikor nem ember, hanem kód fejezi be az aláírást. Adjon egy külső aláírónak üres mezőt, és nem lesz mit kitöltenie

Az egyhívásos út: aláírás PFX-ből

Ha a tanúsítvány és a magánkulcsa olyan PFX/PKCS#12 fájlban ül, amelyet a folyamata el tud olvasni, az egész feldolgozósor egyetlen osztályfüggvényre zsugorodik:

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

Amikor ez elbukik, ritkán a PDF a hibás. Hanem a PFX. A HotPDF a PBES2 védelemmel ellátott konténereket olvassa, vagyis a PBKDF2 kulcsszármaztatást AES-256-CBC felett. Egy régebbi Windows-tanúsítványvarázslóval vagy a 3.0 előtti OpenSSL-lel exportált PFX rendszerint a régi RC2 vagy 3DES burokban van, és egyszerűen nem elemezhető. A megoldás az, hogy a konténert egyszer újraexportálja korszerű védelemmel; a mai OpenSSL alapból ezt teszi, és ez nem kódváltoztatás. Tehát ha az aláírás azonnal elhal egy olyan tanúsítványon, amely „mindenhol máshol működik”, előbb nézze meg, hogyan készült a PFX, mint hogy a saját kódjára gyanakodna

A „foglalás, kivonatolás, beillesztés” út HSM-ekhez és tokenekhez

Az egyhívásos út feltételezi, hogy a folyamata fájlként el tudja olvasni a kulcsot. Ezt egyre gyakrabban nem teheti meg. A kulcs egy HSM-ben, egy USB-tokenen vagy egy aláíró szolgáltatás API-ja mögött ül, és a könyvtár sehogy nem érheti el közvetlenül. A HotPDF ezt úgy kezeli, hogy az aláírást bájtszintű lépésekre bontja: kiír egy helykitöltős dokumentumot, elkéri a könyvtártól a kivonatolandó tartományokat, átadja a kivonat bemenetét annak, aki a kulcsot birtokolja, majd a visszakapott CMS-t beilleszti a lyukba

HotPDF: négyfázisú reserve-hash-insert folyamat a placeholder.pdf felett: a foglalt /Contents lyuk a két ByteRange szakasz között, és egy HSM, amely a kivonatot CMS hexre cseréli
A HotPDF kifoglalja a lyukat, és jelenti mindkét ByteRange tartományt, az Ön kulcstartója kívül aláírja őket, a visszakapott CMS pedig bájtról bájtra illeszkedik vissza egyetlen befagyott bájt megérintése nélkül
var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Írd ki a dokumentumot lefoglalt /Contents lyukkal
  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. Töltsd be a mentett bájtokat; a visszaadott eltolások 0-tól indulnak
  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. Kivonatold mindkét szakaszt, és írd alá kívül (HSM, token, szolgáltatás)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // a te integrációd: hexben adja vissza a CMS-t

  // 4. Illeszd be az aláírást a lefoglalt lyukba
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Ebben a sorrendben két részlet okozza a szórványos hibák többségét. Az első az, hogy a PreparePDFForSigning egy kész fájl bájtjain dolgozik. A helykitöltőt teljesen ki kell írni és menteni, mielőtt az eltolások bármit jelentenének; ha még összeálló adatfolyamon számolja ki őket, nem fognak illeszkedni azokhoz a bájtokhoz, amelyeket végül kivonatol. A második megint a foglalás mérete. A kért 8192 bájtnak a végleges CMS-t kell elbírnia, és egy köztes tanúsítványokat cipelő aláírás – vagy olyan, amelyet a szolgáltatás aláírt attribútumokkal díszít – túlfuthat rajta. Az InsertSignatureHex nem növeli meg a lyukat, hogy helyet csináljon. Az árulkodó jel az a feldolgozósor, amely az egyik tanúsítvánnyal szépen aláír, a következővel viszont elbukik; a gyógymód az, hogy a helykitöltőt olyan foglalással generálja újra, amelyet a tényleges aláíró által előállított valódi aláíráson mértek le, nem pedig megtippeltek

PAdES alapszintek és az aláírást életben tartó időbélyegek

Ha európai szabályok szerint ír alá, akkor az ETSI EN 319 142-1 a mérvadó szabvány, amely négy PAdES alapszintet épít egymásra. A B-B a sima aláírás. A B-T megbízható időbélyeggel egészíti ki, amely bizonyítja, mikor készült. A B-LT beágyazza a validációs anyagot, a tanúsítványokat és a visszavonási adatokat a dokumentumba, hogy évek múlva is ellenőrizhető legyen. A B-LTA ennek tetejére időszakos dokumentum-időbélyegeket rétegez, hogy a bizonyíték túlélje azokat az algoritmusokat, amelyekre épült. A HotPDF minden szinthez kibocsátja a dokumentumoldali szerkezeteket:

HotPDF: egymásra halmozott PAdES baseline szintek B-Btől B-Tn és B-LTn át B-LTAig, megújítási idővonallal, amely megmutatja, hogy a periodikus dokumentum-időbélyegek évtizedekkel később is ellenőrizhetővé teszik az aláírást
Minden szint új védelmet halmoz az előzőre; a B-LTA újra és újra dokumentum-időbélyegeket alkalmaz, hogy a bizonyíték túlélje azokat az algoritmusokat, amelyekre először épült
// PAdES alapszintű aláírásmező (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Dokumentum-időbélyeg: nagyobb foglalás a TSA-tokennek és a láncnak
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Az időbélyegnél szereplő 16384 bájtos foglalás szándékos. Az időbélyeg-hatóság olyan tokent ad vissza, amely magával vonszolja a saját tanúsítványláncát, ezért rendszerint több helyet kíván, mint az a 8 KB, amellyel egy sima aláírás beéri. Ezek a dokumentum-időbélyegek egyben a B-LTA gépezetét is adják: egy archivált aláírás néhány évenkénti újra-időbélyegzése – még aktuális algoritmusokkal – az, ami egy 2026-ban aláírt dokumentumot 2040-ben is ellenőrizhetővé tesz

Néhány szó az ok-, hely- és kapcsolatsztringről, amelyet mindkét mezőhívás elfogad: ezek kényelmi metaadatok, semmi több. A HotPDF egyszerű szótárbejegyzésként tárolja őket, és belefesti a látható aláírás megjelenésébe, de egyetlen validátor sem veti össze őket semmivel. Töltse ki őket következetesen a munkamenet adataiból, mert az auditorok olvassák őket, de soha ne tévessze össze őket bizonyítékkal. A tényleges kriptográfiai állítás teljes egészében a CMS-ben és annak tanúsítványláncában lakik, az ellenőrző pedig a látható szöveget teljesen figyelmen kívül hagyja

Aláírás után a fájl csak nőhet

Abban a pillanatban, hogy egy aláírás létezik, a tartományain belüli bájtok befagynak. A fájl utólagos módosításának egyetlen jogszerű módja az ISO 32000-1 §7.5.6 szerinti inkrementális frissítés, amely az eredeti bájtok után fűzi az új és megváltozott objektumokat, és friss kereszthivatkozási szakaszt láncol vissza hozzájuk. Így elvégezve az aláírás érvényes marad a saját revíziójára, a megjelenítő pedig őszinte állapotot jelent: az aláírt revízió sértetlen, a dokumentumot utólag kiegészítették. Ha ehelyett az egész fájlt újrasorosítja, azzal újraírja az aláírt szakaszokat, ami akkor is tönkreteszi az aláírást, ha semmi látható nem változott. Ugyanez a revíziós mechanizmus az, ahogyan egy dokumentum több aláírást is elbír: minden új aláírás a saját inkrementális frissítésében landol, és a tartományai mindent lefednek előtte, beleértve a korábbi aláírásokat is. A csak hozzáfűző működést és azt, mikor biztonságos tömöríteni, az objektumfolyamokról és az inkrementális frissítésekről szóló cikk tárgyalja

Tervezés közben két határt érdemes fejben tartani. A HotPDF PDF/A kimeneti módja kereken elutasítja az aláírásmezőket, tehát az archiválási megfelelőséget és a beágyazott aláírást külön fájlként kell szállítani. Az aláírás pedig semmit nem mond a titkosságról: azt bizonyítja, ki állította elő a dokumentumot, és hogy az azóta nem változott, de elolvasni továbbra is bárki elolvashatja. A tartalom elrejtése külön feladat, amelyet az AES-256 titkosítás és jogosultsági szabályzat old meg

Bármit épít is, tesztelje mással, mint a fájlt kiíró kóddal. Nyissa meg a kimenetet az Acrobat aláíráspaneljén, és győződjön meg három dologról: az aláírás érvényes, az identitás a várt gyökérig láncolódik, és a panel nem jelez változást az aláírás óta. Utána egy eldobható másolat aláírt tartományában billentsen át egyetlen bájtot, és ellenőrizze, hogy a panel most már módosítottnak minősíti a dokumentumot. Az az aláíró feldolgozósor, amelyet még soha nem látott meghamisított fájlt visszautasítani, olyan, amelynek az ellenőrzését valójában nem tesztelték

Mind a három aláírási szint a Delphihez és C++Builderhez készült HotPDF Delphi Component részeként érkezik; a termékoldal a teljes aláírási API-referenciára hivatkozik