Odborný článok

Wrapping PDF podpisov: medzery ByteRange a druhé podpisy

HotPDF, Delphi PDF komponent, teraz odmieta signature wrapping: od v2.759.0 vyžadujú aj VerifyLoadedSignatureEx, aj dávkový validator, aby medzera medzi dvoma segmentmi /ByteRange bola presne hex reťazec /Contents vrátane delimiterov, a v2.761.0 pridáva AddLoadedSignedSignatureField, takže druhý podpis sa dá pripojiť k už podpísanému PDF ako čistá inkrementálna revízia. Tieto dve zmeny patria k sebe, lebo korektný druhý podpis je presne to rozloženie, ktoré prísnejší verifikátor očakáva

Situácia, ktorá problém odhalila, je každodenná. Zmluvu podpísi dodávateľ, potom putuje k schvaľovateľovi, ktorý musí pripísať svoj podpis bez narušenia prvého. Druhá revízia sa pripojí za prvú, jej vlastný /ByteRange sa rozprestrie cez celý narastlý súbor a oba podpisy majú verifikovať. Dostať sa tam ručne znamenalo napísať si inkrementálnu sekciu sám, a testovacia fixtúra, ktorá presne to robila, sa ukázala byť učebnicová štruktúra signature wrappingu, ktorú starý verifikátor radiel prijímal. Ak ste sa k verifikačnému API ešte nepozreli, sprievodca verifikáciou digitálnych podpisov PDF s HotPDF pokrýva základy, na ktorých tento článok stavia

Čo presne patrí do medzery ByteRange?

Medzera musí obsahovať kompletnú hodnotu /Contents a nič iné: ISO 32000-1 §12.8.3.3 hovorí, že hexadecimálny reťazec so svojimi delimitrami < a > zapadá presne do priestoru medzi dvoma bajtovými rozsahmi a ISO 32000-2 §12.8.1 toto pravidlo nesie ďalej. Tabuľka 252 a dokumenty PAdES hovoria len to, že digest vylučuje hodnotu Contents, čo sa ľahko prečíta ako vylúčenie len hex číslic. Skoršie vydania HotPDF to čítali práve takto: PreparePDFForSigning aj streamová príprava CMS hashovali aj lomené zátvorky, so zdrojovým komentárom trvajúcim na tom, že zátvorky musia byť pokryté. Validátory, ktoré porovnávajú medzeru s hodnotou podpisu, označia toto rozloženie ako neplatný bajtový rozsah, takže v2.759.0 vyňal oba delimitre z podpísaných rozsahov. Rýchla nezávislá kontrola na akomkoľvek podpísanom súbore je pozrieť sa na dva bajty: bajt na offsete ByteRange[1] musí byť < a bajt na offsete ByteRange[2] - 1 musí byť >

Anatómia korektne vyplneného ByteRange PDF podpisu v HotPDF: prvý rozsah pokrýva súbor od nultého bajtu, medzera drží kompletný hex reťazec /Contents vrátane delimiterov menšie než a väčšie než, druhý rozsah pokrýva trailer až po koniec a dve jednobajtové kontroly na ByteRange[1] a ByteRange[2] - 1 potvrdia rozloženie na akomkoľvek podpísanom súbore
Od v2.759.0 sedia delimitre mimo podpísaných rozsahov, takže digest pokrýva len číslice a medzeru možno validovať bajt po bajte

Prečo kontrola neprázdnej medzery prehliadne signature wrapping?

Kontrola neprázdnej medzery dokazuje len to, že niečo ostalo mimo digestu, nie čo, a to je celá útočná plocha. Zástupný symbol /Contents sa rezervuje s tisíckami nulových číslic, kým reálny CMS kontajner ich zriedka naplní. Útočník môže uzavrieť hex reťazec skoro vnútri toho nulového paddingu >, zapísať nové objekty alebo sfalovanú revíziu do zvyšku rezervovaného priestoru a nechať bajtové rozsahy nedotknuté. CMS podpis stále verifikuje, lebo každý podpísaný bajt je nezmenený, rozsahy stále začínajú na 0 a končia na veľkosti súboru a starý verifikátor HotPDF hlásil svValid s CoversWholeDocument nastaveným na True. PDF čítač medzitým parsuje čokoľvek, čo sedí v tom nepodpísanom otvore

HotPDF teraz berie medzeru ako dáta na validáciu bajt po bajte. Verifikátor prečíta medzeru, odstráni delimitre, akceptuje len hex číslice plus PDF biely priestor (tab, line feed, form feed, carriage return, medzera), dekóduje číslice a vyžaduje, aby výsledok presne zodpovedal /Contents slovníka podpisu. Čokoľvek iné degraduje výsledok na svInvalidByteRange. Kontrola beží v ceste jedného podpisu aj v ValidateLoadedSignatureBatch, ktorá si držala vlastnú logiku pokrytia a potrebovala tú istú opravu. Súbory vyrobené HotPDF pred v2.759.0, ktorých medzera držala len číslice so zátvorkami tesne vo vnútri rozsahov, sa stále verifikujú, takže archivované dokumenty nezčervenejú zo dňa na deň

Ako exploituje signature wrapping slabo kontrolovaný ByteRange PDF v Delphi: útočník uzavrie hex reťazec skoro vnútri tisícok rezervovaných nulových číslic, zapíše sfalovanú revíziu do nepodpísanej medzery bez dotyku jediného pokrytého bajtu a stará kontrola HotPDF hlásila svValid s CoversWholeDocument true, kým v2.759.0 nezačala validovať medzeru bajt po bajte
Neprázdna medzera dokazuje len to, že niečo ostalo mimo digestu, nie čo — vypchadaná diera 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;

Ako pridať druhý podpis k už podpísanému PDF?

Otvorte podpísaný súbor cez BeginIncrementalUpdate, zavolajte AddLoadedSignedSignatureField, uložte cez SaveIncrementalUpdate a potom podpíšte pripravený súbor triedovou funkciou THotPDF.SignPDFWithPFX. Pred v2.761.0 zdokumentovaný recept volania THPDFPage.AddSignedSignatureField po BeginIncrementalUpdate nemohol fungovať, lebo CurrentPage je v inkrementálnom režime nil a nič nedokázalo zavesiť zástupný symbol /V na pole v načítanom dokumente. Nová metóda vytvorí widget na načítanej strane a zavesí pod /V ten istý zástupný slovník, ktorý používa cesta nového dokumentu, takže obe podpisové cesty zdieľajú jednu serializáciu. Prvý podpis samotný prechádza PFX pipeline v článku o tvorbe digitálnych podpisov PAdES v Delphi

var
  Pdf: THotPDF;
  FieldIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('Signed.pdf');
    // Strana 0, obdĺžnik widgetu v bodoch, 8192 bajtov rezervovaných pre 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ámerne tichšia než jej sestry. Ostatní stavitelia polí AddLoaded* nastavia na AcroForme /NeedAppearances true, čo prikáže prehliadaču regenerovať appearances polí; na podpísanom dokumente môže táto regenerácia prepísať podpísaný obsah, takže nová metóda flag zas odstráni, pokiaľ ho zdroj už neniesol. /SigFlags drží svoju pôvodnú hodnotu OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 tabuľka 219). Tiež nemusíte volať MarkDirty na strane: pridanie do /Annots a /Fields prenesie dirty flag na vlastniaci nepriamy objekt a explicitná značka strany by do novej revízie len potiahla nezmenený slovník strany, čo by analýza revízií potom hlásila ako zmenu strany. Napokon zástupný symbol zapisuje /ByteRange pred /Contents, lebo patcher nájde najprv strážny /ByteRange a dopredu hľadá príslušný hex reťazec

Workflow HotPDF v Delphi na pripísanie podpisu pod už podpísané PDF: BeginIncrementalUpdate otvorí súbor, AddLoadedSignedSignatureField vytvorí widget a rezervuje zástupný symbol /Contents, SaveIncrementalUpdate pripojí druhú revíziu a SignPDFWithPFX ju vyplní, pritom prvý podpis ostane platný s UnsignedTrailingBytes, zatiaľ čo nový ByteRange sa rozprestrie cez celý narastlý súbor
Jeden zástupný symbol na revíziu, pripravený a zapatchovaný rovnakou serializáciou na oboch podpisových cestách — čisté rozloženie, ktoré prísnejší verifikátor očakáva

Čo sa mení, keď CMS vyrába externý podpisovateľ alebo HSM?

Vo workflow sa nemení nič, ale offsety teraz znamenajú to, čo hovorí špecifikácia. PreparePDFForSigning vráti dva rozsahy od nuly, ktorých medzerou je celý reťazec /Contents, a ContentsHexStart je index prvej hex číslice v AnsiString od jedničky. Kratší CMS sa doplní 0 na konci, pred uzatvárajúcou >. Keďže PreparePDFForSigning zapatchuje prvý nenapatchovaný strážny znak, ktorý nájde, pripravte presne jeden zástupný symbol na revíziu a uprednostnite InsertSignatureHexAt s vrátenými offsetmi pred vyhľadávacím InsertSignatureHex, keď v súbore už existujú skoršie podpisy

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

  // Medzerou je celý hex reťazec: '<' ukončuje rozsah 1, '>' predchádza 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 podpisovateľ, 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 sú hranice nových kontrol?

Kontrola medzery zatvára jednu konkrétnu dieru a netreba ju predávať nadsene. svValid stále znamená integritu bajtov plus kľúč zodpovedajúci vloženému certifikátu; dôvera v ten certifikát je samostatné rozhodnutie. Medzera sa validuje len vtedy, keď má verifikátor zdrojové bajty, ktoré VerifyLoadedSignatureEx číta z načítaného súboru a overloady TStream preberajú od vás. Pri prvom podpise v súbore s pripísaným podpisom je CoversWholeDocument korektne False a či pripojená revízia len pridala podpis, alebo zmenila aj strany, je otázka pre analýzu DocMDP, FieldMDP a revízií v HotPDF. Všimnite si tiež, že kontrola pripojeného PDF MAC porovnáva offsety s pozíciami < a >, takže prijíma staré aj nové rozloženie; akýkoľvek vlastný nástroj, ktorý natvrdo kóduje offsety z čias pred v2.759.0, zlyhá prvý, keď stretne čerstvo podpísaný súbor

Ak vaša aplikácia v Delphi či C++Builder podpisuje, pripisuje podpisy alebo audituje PDF, najbezpečnejšia cesta je nechať jednu knižnicu vyrábať aj verifikovať to isté rozloženie. HotPDF, natívny Delphi PDF komponent dodáva prísnejšiu validáciu medzery, inkrementálne druhé podpisy aj háky pre externých podpisovateľov zobrazené vyššie v jednom komponente