A HotPDF, a Delphi PDF komponens, mostantól elutasítja a signature wrappinget: a v2.759.0 óta mind a VerifyLoadedSignatureEx, mind a batch validátor megköveteli, hogy a két /ByteRange szegmens közti rés pontosan a /Contents hex string legyen, határolókkal együtt, a v2.761.0 pedig hozzáadja az AddLoadedSignedSignatureField-et, hogy egy már aláírt PDF-hez második aláírás csatolható tiszta inkrementális revízióként. A két változtatás összetartozik, mert egy helyes második aláírás pontosan az a felépítés, amit a szigorúbb validátor vár
A helyzet, ami felszínre hozta a problémát, hétköznapi. Egy szerződést aláír a szállító, majd továbbítódik egy jóváhagyóhoz, akinek ellenjegyzést kell tegyen az első aláírás bolygatása nélkül. A második revízió az első után fűződik, a saját /ByteRange-e átíveli a megnőtt teljes fájlt, és mindkét aláírásnak érvényesnek kellene lennie. Oda kézzel eljutni azt jelentette, hogy magad írod meg az inkrementális szekciót, és a pont ezt tevő teszt fixtúra kiderült, hogy tankönyvi signature wrapping struktúra, amit a régi validátor boldogan elfogadott. Ha még nem néztéd a validációs API-t, a PDF digitális aláírások HotPDF-fel való ellenőrzéséről szóló útmutató tárgyalja az alapokat, amire ez a cikk épít
Mi tartozik pontosan a ByteRange résbe?
A résnek a teljes /Contents értéket kell tartalmaznia, és semmi mást: az ISO 32000-1 §12.8.3.3-a szerint a hexadecimális string, a < és a > határolóival együtt, pontosan kitölti a két bájttartomány közti teret, az ISO 32000-2 §12.8.1-e pedig továbbviszi ugyanezt a szabályt. A 252. táblázat és a PAdES dokumentumok csak annyit mondanak, hogy a digest kizárja a Contents értéket, ami könnyen úgy olvasható, hogy csak a hex számjegyeket zárja ki. A korábbi HotPDF kiadások így értelmezték: a PreparePDFForSigning és a streamelő CMS előkészítés a kacsőrögeket is hashelte, forráskomment fájdalmasan hajthatva, hogy a szögletes zárójeleknek fedve kell lenniük. Azok a validátorok, amik a rést az aláírás értékéhez hasonlítják, érvénytelen bájttartományként jelzik azt a felépítést, ezért a v2.759.0 mindkét határolót kiköltözteti az aláírt tartományokból. Egy gyors független ellenőrzés bármilyen aláírt fájlon két bájt megnézése: a ByteRange[1] offseten lévő bájtnak <-nek, a ByteRange[2] - 1 offseten lévőnek >-nek kell lennie
Miért nem veszi észre a signature wrappinget a nem üres rés ellenőrzése?
Egy nem üres rés ellenőrzése csak azt bizonyítja, hogy valamit kihagytak a digestből, nem azt, hogy mit, és ez a teljes támadási felület. A /Contents placeholder ezer nulla számjeggyel van fenntartva, egy valódi CMS konténer pedig ritkán tölti ki. Egy támadó lezárhatja korán a hex stringet abban a nulla paddingben egy >-vel, új objektumokat vagy hamisított revíziót írhat a fenntartott tér maradékába, és érintetlenül hagyhatja a bájttartományokat. A CMS aláírás továbbra is érvényes, mert minden aláírt bájt változatlan, a tartományok továbbra is 0-nál kezdenek és a fájlméretnél végződnek, a régi HotPDF validátor pedig svValid-et jelentett CoversWholeDocument True beállítással. Egy PDF olvasó eközben parseol mindent, ami abban a nem aláírt lyukban ül
A HotPDF mostantól validálandó adatként kezeli a rést, bájtonként. A validátor kiolvassa a rést, leveszi a határolókat, csak hex számjegyeket és PDF whitespace-t fogad el (tab, sortörés, lapdobás, kocsi vissza, szóköz), dekódolja a számjegyeket, és megköveteli, hogy az eredmény pontosan egyezzen a szignatúra szótár /Contents értékével. Bármi más svInvalidByteRange-re fokozza le az eredményt. Az ellenőrzés mind az egyszignatúrás útvonalon, mind a ValidateLoadedSignatureBatch-ben fut, ami a saját lefedettségi logikáját tartotta, és ugyanarra a javításra szorult. A v2.759.0 előtti, HotPDF által gyártott fájlok, amiknek a resében csak számjegyek voltak, a zárójelekkel épp a tartományokon belül, továbbra is érvényesek, így az archivált dokumentumok nem válnak hirtelen pirossá
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('SignedTwice.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Status := Pdf.VerifyLoadedSignatureEx(I, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln(Info.FieldName, ': valid, covers the whole file')
else
Writeln(Info.FieldName, ': valid, ',
Info.UnsignedTrailingBytes, ' bytes appended later');
svInvalidByteRange:
Writeln(Info.FieldName, ': ByteRange gap rejected (wrapping?)');
else
Writeln(Info.FieldName, ': failed, status ', Ord(Status));
end;
end;
finally
Pdf.Free;
end;
end;
Hogyan adsz második aláírást egy már aláírt PDF-hez?
Nyisd meg az aláírt fájlt BeginIncrementalUpdate-tel, hívd az AddLoadedSignedSignatureField-et, ments SaveIncrementalUpdate-tel, majd írd alá az előkészített fájlt a THotPDF.SignPDFWithPFX class függvénnyel. A v2.761.0 előtt a dokumentált recept, a THPDFPage.AddSignedSignatureField hívása BeginIncrementalUpdate után, nem működhetett, mert inkrementális módban a CurrentPage nil, és semmi nem tudott /V placeholdert csatolni egy betöltött dokumentum mezőjéhez. Az új metódus a widgetet a betöltött oldalon hozza létre, és ugyanazt a placeholder szótárt akasztja a /V alá, amit az új dokumentum útvonala használ, így mindkét aláírási útvonal egy szerializációt oszt meg. Magához az első aláíráshoz a PAdES digitális aláírások készítéséről Delphiben szóló cikk végigviszi a PFX pipeline-ot
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// 0. oldal, widget téglalap pontban, 8192 bájt fenntartva a CMS-nek
FieldIndex := Pdf.AddLoadedSignedSignatureField(0, 320, 60, 520, 120,
'ApproverSignature', 8192);
if FieldIndex < 0 then
raise Exception.Create('Page index out of range');
Pdf.SaveIncrementalUpdate('Prepared.pdf');
finally
Pdf.Free;
end;
if not THotPDF.SignPDFWithPFX('Prepared.pdf', 'SignedTwice.pdf',
'approver.pfx', 'pfx-password') then
raise Exception.Create('Second signature failed');
end;
Az AddLoadedSignedSignatureField szándékosan csendesebb, mint a testvérei. A többi AddLoaded* mezőlétrehozó /NeedAppearances true-t állít az AcroFormon, ami azt üzeni a megjelenítőnek, generálja újra a mező megjelenéseit; aláírt dokumentumon az az újragenerálás átírhat aláírt tartalmat, ezért az új metódus újra eltávolítja a flaget, hacsak a forrás már nem hordozta. A /SigFlags megtartja az eredeti értékét OR 3 (SignaturesExist plusz AppendOnly, ISO 32000-1 219. táblázat). A MarkDirty-t sem kell meghívnod az oldalon: a /Annots-hoz és a /Fields-hez adás propagálja a dirty flaget a birtokló indirekt objektumra, és egy explicit oldaljelölés csak egy változatlan oldal szótárt húzna az új revízióba, amit a revízióelemzés aztán oldalmódosításként jelentene. Végül a placeholder a /ByteRange-et a /Contents elé írja, mert a patcher előbb megkeresi a /ByteRange sentinelt, majd előrefelé keresi a hozzá tartozó hex stringet
Mi változik, ha a CMS-t külső aláíró vagy HSM gyártja?
A munkafolyamatban semmi nem változik, de az offsetek mostantól azt jelentik, amit a specifikáció mond. A PreparePDFForSigning két nullától számolt tartományt ad vissza, amiknek a re az egész /Contents string van, a ContentsHexStart pedig az első hex számjegy 1-től számolt indexe az AnsiString-ben. Egy rövidebb CMS a végén 0-val párnázódik, a záró > előtt. Mivel a PreparePDFForSigning az első, még nem patchelt sentinelt patcheli, revíziónként pontosan egy placeholdert készíts elő, és ha korábbi aláírások már vannak a fájlban, a visszadott offsetekkel járó InsertSignatureHexAt-et részesítsd előnyben a kereséses InsertSignatureHex-szel szemben
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // a saját helpered
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// A rés a teljes hex string: a '<' zárja az 1. tartományt, a '>' előzi a 2.-at
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // a saját CMS aláíród, hex DER
if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
raise Exception.Create('CMS does not fit the reserved space');
SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf'); // a saját helpered
end;
Hol vannak az új ellenőrzések határai?
A rés ellenőrzése egy konkrét lyukat zár be, és nem szabad túlreklámolni. A svValid továbbra is bájtok épségét jelent plusz egy kulcsot, ami egyezik a beágyazott tanúsítvánnyal; a tanúsítványba vetett bizalom külön döntés. A rés csak akkor validálódik, ha a validátornak vannak forrásbájtjai, amiket a VerifyLoadedSignatureEx a betöltött fájlból olvas, a TStream overloadok pedig tőled kapnak. Az ellenjegyzett fájl első aláírására a CoversWholeDocument helyesen False, és az, hogy a hozzáfűzött revízió csak aláírást adott hozzá, vagy oldalakat is változtatott, a DocMDP, FieldMDP és revízióelemzés HotPDF-ben kérdése. Jegyezd meg továbbá, hogy a csatolt PDF MAC ellenőrzés az offseteket a < és a > pozícióihoz hasonlítja, így mind a régi, mind az új felépítést elfogadja; minden saját eszközöd, ami a v2.759.0 előtti offseteket hardcode-olja, előbb bukik el, amikor frissen aláírt fájlba botlik
Ha a Delphi vagy C++Builder alkalmazásod PDF-eket ír alá, ellenjegyez vagy auditál, a legbiztonságosabb út az, ha egyetlen library gyártja és validálja ugyanazt a felépítést. A HotPDF, a natív Delphi PDF komponens egyetlen komponensben szállítja a szigorúbb rés validációt, az inkrementális második aláírásokat és a fent látható külső aláíró hookokat