Technický článek

Signature wrapping v PDF: mezery ByteRange a druhé podpisy

HotPDF, komponenta PDF pro Delphi, teď odmítá signature wrapping: od v2.759.0 vyžadují VerifyLoadedSignatureEx i batch validator, aby mezera mezi oběma segmenty /ByteRange byla přesně hex string /Contents, včetně delimiterů, a v2.761.0 přidává AddLoadedSignedSignatureField, takže druhý podpis lze k už podepsanému PDF připojit jako čistou inkrementální revizi. Ty dvě změny patří k sobě, protože korektní druhý podpis je přesně to rozvržení, které přísnější verifikátor očekává

Situace, která problém vystavila na světlo, je běžná. Smlouvu podepíše dodavatel, pak putuje ke schvalovateli, který musí podepsat doplňkově, aniž by poprvé vytvořený podpis narušil. Druhá revize se připojí za tou první, její vlastní /ByteRange se táhne přes celý vyrostlý soubor a oba podpisy by měly projít ověřením. Dostat se tam ručně znamenalo napsat inkrementální sekci vlastníma rukama a testovací fixture, která přesně to dělala, se ukázala jako učebnicová struktura signature wrappingu, kterou starý verifikátor rád přijal. Pokud jste verifikační API ještě neviděli, průvodce ověřováním digitálních podpisů PDF s HotPDF pokrývá základy, na nichž tenhle článek staví

Co přesně patří do mezery ByteRange?

Mezera musí obsahovat kompletní hodnotu /Contents a nic víc: ISO 32000-1 §12.8.3.3 říká, že hexadecimální řetězec, včetně delimiterů < a >, zapadne přesně do prostoru mezi oběma bajtovými rozsahy, a ISO 32000-2 §12.8.1 pravidlo nese dál. Tabulka 252 a dokumenty PAdES říkají jen, že digest hodnotu Contents vynechává, což se lehce přečte jako vynechání jen hexadecimálních číslic. Dřívější vydání HotPDF to tak četly: PreparePDFForSigning i streamová CMS příprava hashovaly i hranaté závorky, se zdrojovým komentářem, který naléhal, že závorky kryté být musí. Validátory, které srovnávají mezeru s hodnotou podpisu, označují takové rozvržení jako nevalidní bajtový rozsah, takže v2.759.0 přesune oba delimitery mimo podepsané rozsahy. Rychlá nezávislá kontrola jakéhokoli podepsaného souboru je podívat se na dva bajty: bajt na offsetu ByteRange[1] musí být < a bajt na offsetu ByteRange[2] - 1 musí být >

Anatomie korektně vyplněného ByteRange podpisu PDF v HotPDF: první rozsah kryje soubor od bajtu nula, mezera drží kompletní hex string /Contents včetně delimiterů menší-než a větší-než, druhý rozsah kryje trailer až do konce a dvě jednobajtové kontroly na ByteRange[1] a ByteRange[2] - 1 potvrdí rozvržení na jakémkoli podepsaném souboru
Od v2.759.0 sedí delimitery mimo podepsané rozsahy, takže digest kryje jen číslice a mezeru lze validovat bajt po bajtu

Proč kontrola neprázdné mezery signature wrapping minout?

Kontrola neprázdné mezery dokáže jen to, že z digestu bylo vypuštěno něco, ne co, a přesně tohle je celá útočná plocha. Placeholder /Contents si rezervuje tisíce nulových číslic, zatímco skutečný CMS kontejner je zaplní zřídka. Útočník může hexadecimální řetězec uzavřít brzo uvnitř té nulové výplně pomocí >, zapsat do zbytku rezervovaného prostoru nové objekty nebo zfalšovanou revizi a bajtové rozsahy nechat nedotčené. CMS podpis stále projde, protože každý podepsaný bajt je beze změny, rozsahy stále začínají na 0 a končí na velikosti souboru a starý verifikátor HotPDF hlásil svValid s CoversWholeDocument nastaveným na True. PDF čtenář mezitím parsuje, co se v tom nepodepsaném otvoru nachází

HotPDF teď bere mezeru jako data k validaci bajt po bajtu. Verifikátor přečte mezeru, ořeže delimitery, přijme jen hexadecimální číslice plus PDF bílé znaky (tabulátor, line feed, form feed, carriage return, mezera), dekóduje číslice a vyžaduje, aby výsledek odpovídal slovníku podpisu /Contents přesně. Cokoli jiného degraduje výsledek na svInvalidByteRange. Kontrola běží jak v cestě jednotlivého podpisu, tak v ValidateLoadedSignatureBatch, která si držela vlastní logiku pokrytí a potřebovala tutéž opravu. Soubory vyrobené HotPDF před v2.759.0, jejichž mezera držela jen číslice se závorkami sedícími hned uvnitř rozsahů, pořád projdou, takže archivované dokumenty nezčervenají ze dne na den

Jak signature wrapping zneužívá volně kontrolovaný ByteRange PDF v Delphi: útočník uzavře hex string brzo uvnitř tisíců rezervovaných nulových číslic, zapíše zfalšovanou revizi do nepodepsané mezery, aniž se dotkne krytého bajtu, a stará kontrola HotPDF hlásila svValid s CoversWholeDocument true, dokud v2.759.0 nezačala validovat mezeru bajt po bajtu
Neprázdná mezera dokazuje jen, že z digestu bylo vypuštěno něco, ne co — vypsaný otvor je celá útočná plocha
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;

Jak přidat druhý podpis k už podepsanému PDF?

Otevřete podepsaný soubor přes BeginIncrementalUpdate, zavolejte AddLoadedSignedSignatureField, uložte přes SaveIncrementalUpdate a pak připravený soubor podepište class funkcí THotPDF.SignPDFWithPFX. Před v2.761.0 nemohl zdokumentovaný recept, tedy volání THPDFPage.AddSignedSignatureField po BeginIncrementalUpdate, vůbec fungovat, protože CurrentPage je v inkrementálním režimu nil a nic nemohlo přivěsit placeholder /V k poli na načteném dokumentu. Nová metoda vytvoří widget na načtené stránce a pověsí pod /V tutéž placeholder slovník, kterouž cesta nového dokumentu používá, takže obě podepisovací trasy sdílejí jednu serializaci. U samotného prvního podpisu provede článek o vytváření digitálních podpisů PAdES v Delphi PFX pipeline

var
  Pdf: THotPDF;
  FieldIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('Signed.pdf');
    // Stránka 0, obdélník widgetu v bodech, 8192 bajtů rezervováno pro CMS
    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;

AddLoadedSignedSignatureField je záměrně tichší než její sourozenci. Ostatní tvůrci polí AddLoaded* nastavují na AcroForm /NeedAppearances true, což prohlížeči říká, aby regeneroval appearances polí; na podepsaném dokumentu může tahle regenerace přepsat podepsaný obsah, takže nová metoda flag zase odstraní, pokud už ho zdroj nenese. /SigFlags si drží původní hodnotu OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 tabulka 219). Na stránce taky nemusíte volat MarkDirty: přidání do /Annots a /Fields propaguje dirty flag na vlastnící nepřímý objekt a explicitní označení stránky by jen vleklo nezměněný slovník stránky do nové revize, kterouž revizní analýza pak hlásí jako úpravu stránky. Nakonec placeholder zapisuje /ByteRange před /Contents, protože patcher nejdřív najde sentinel /ByteRange a hledá odpovídající hex string dopředu

Workflow HotPDF v Delphi pro doplňkový podpis už podepsaného PDF: BeginIncrementalUpdate otevře soubor, AddLoadedSignedSignatureField vytvoří widget a rezervuje placeholder /Contents, SaveIncrementalUpdate připojí druhou revizi a SignPDFWithPFX ji zaplní, takže první podpis zůstává validní s UnsignedTrailingBytes, zatímco nový ByteRange se táhne přes celý vyrostlý soubor
Jeden placeholder na revizi, připravený a zaplátovaný stejnou serializací na obou podepisovacích trasách — čisté rozvržení, které přísnější verifikátor očekává

Co se změní, když CMS vyrobí externí signer nebo HSM?

Ve workflow se nezmění nic, ale offsety nyní znamenají to, co říká specifikace. PreparePDFForSigning vrací dva 0-based rozsahy, jejichž mezera je celý řetězec /Contents, a ContentsHexStart je 1-based index první hexadecimální číslice v AnsiString. Kratší CMS se doplní 0 na konci, před uzavírajícím >. Protože PreparePDFForSigning zaplátuje první nezaplátovaný sentinel, který najde, připravujte přesně jeden placeholder na revizi a upřednostněte InsertSignatureHexAt s vrácenými offsety před vyhledáváním založeným InsertSignatureHex, když už v souboru dřívější podpisy existují

var
  Bytes, ToSign, CmsHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
  Bytes := LoadFileAsAnsiString('Prepared.pdf');   // váš helper
  if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
    R2Start, R2Len, HexStart, HexLen) then
    raise Exception.Create('No signature placeholder found');

  // Mezera je celý hex string: '<' ukončuje rozsah 1, '>' předchází rozsahu 2
  Assert(Bytes[R1Start + R1Len + 1] = '<');
  Assert(Bytes[R2Start] = '>');

  ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
  CmsHex := SignDetachedWithHsm(ToSign);           // váš CMS signer, 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');  // váš helper
end;

Kde jsou meze nových kontrol?

Kontrola mezery zavírá jeden konkrétní otvor a nesmí se to přeprodávat. svValid pořád znamená bajtovou integritu plus klíč, který odpovídá vloženému certifikátu; důvěra v tenhle certifikát je samostatné rozhodnutí. Mezera se validuje, jen když má verifikátor zdrojové bajty, které si VerifyLoadedSignatureEx přečte z načteného souboru a overloady přes TStream dostanou od vás. U prvního podpisu v doplňkově podepsaném souboru je CoversWholeDocument správně False a to, zda připojená revize jen přidala podpis, nebo měnila i stránky, je otázka pro DocMDP, FieldMDP a analýzu revizí v HotPDF. Všimněte si také, že připojená kontrola PDF MAC srovnává offsety s pozicemi < a >, takže přijme staré i nové rozvržení; jakýkoli váš vlastní nástroj, který má offsety z doby před v2.759.0 natvrdo, selže nejdřív, až potká čerstvě podepsaný soubor

Pokud vaše aplikace v Delphi nebo C++Builder podepisuje, podepisuje doplňkově nebo audituje PDF, nejbezpečnější cesta je nechat jednu knihovnu vyrábět i ověřovat totéž rozvržení. HotPDF, nativní komponenta PDF pro Delphi, dodává přísnější validaci mezery, inkrementální druhé podpisy i hooky pro externí signery ukázané výše v jedné komponentě