Műszaki cikk

PDF aláírás burkolás: ByteRange rések és második aláírások

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

Egy helyesen kitöltött PDF signature ByteRange anatómiája a HotPDF-ben: az első tartomány a fájlt a nulladik bájttól fedi, a rés a teljes /Contents hex stringet hordozza a kisebb-nagyobb jel határolókkal együtt, a második tartomány a trailert a végéig, és két egys bájtos ellenőrzés a ByteRange[1] és a ByteRange[2] - 1 pozíciókon megerősíti a felépítést bármilyen aláírt fájlon
A v2.759.0 óta a határolók az aláírt tartományokon kívül ülnek, így a digest csak a számjegyeket fedi, és a rés bájtonként validálható

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á

Hogyan hasznosítja a signature wrapping a lazán ellenőrzött PDF ByteRange-et Delphiben: a támadó korán lezárja a hex stringet a több ezer fenntartott nulla számjegy belsejében, hamisított revíziót ír a nem aláírt résbe anélkül, hogy egyetlen fedett bájtot is megérintene, és a régi HotPDF ellenőrzés svValid-et jelentett CoversWholeDocument true értékkel, mígnem a v2.759.0 el nem kezdte bájtonként validálni a rést
Egy nem üres rés csak azt bizonyítja, hogy valami kimaradt a digestből, nem azt, hogy mi — a kipárnázott lyuk a teljes támadási felület
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

A HotPDF Delphi munkafolyamata egy már aláírt PDF ellenjegyzésére: a BeginIncrementalUpdate megnyitja a fájlt, az AddLoadedSignedSignatureField létrehozza a widgetet és fenntartja a /Contents placeholdert, a SaveIncrementalUpdate második revíziót fűz hozzá, a SignPDFWithPFX pedig kitölti azt, miközben az első aláírás UnsignedTrailingBytes-szel érvényes marad, és az új ByteRange átíveli a megnőtt teljes fájlt
Revíziónként egy placeholder, ugyanazon a szerializáción előkészítve és patchelve mindkét aláírási útvonalon — a tiszta felépítés, amit a szigorúbb validátor vár

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