Tehnički članak

PDF signature wrapping: ByteRange procijepi i drugi potpisi

HotPDF, Delphi PDF komponenta, sada odbija signature wrapping: od v2.759.0 i VerifyLoadedSignatureEx i batch validator traže da procijep između dvaju segmenata /ByteRange bude točno /Contents hex string, ograničitelje uključene, a v2.761.0 dodaje AddLoadedSignedSignatureField da se drugi potpis može nadodati već potpisanom PDF-u kao čista inkrementalna revizija. Dvije izmjene pripadaju zajedno, jer je ispravni drugi potpis upravo raspored koji stroži verifier očekuje

Situacija koja je obznanila problem svakidašnja je. Ugovor potpiše dobavljač, pa se usmjeri odobravatelju koji mora dodati protupotpis a da ne uznemiri prvi potpis. Druga se revizija nadodaje iza prve, njezin vlastiti /ByteRange obuhvaća cijelu naraslu datoteku, i oba bi se potpisa trebala verificirati. Doći tamo ručno značilo je sam napisati inkrementalnu sekciju, a testni fixture koji je točno to radio ispostavio se udžbeničkom signature-wrapping strukturom koju je stari verifier rado prihvatio. Ako ste verifikacijski API prije gledali, vodič kroz provjeru PDF digitalnih potpisa s HotPDF-om pokriva osnove na kojima ovaj članak gradi

Što točno pripada u ByteRange procijep?

Procijep mora sadržavati potpunu vrijednost /Contents i ništa drugo: ISO 32000-1 §12.8.3.3 kaže da heksadecimalni string, sa svojim ograničiteljima < i >, stane točno u prostor između dvaju byte raspona, i ISO 32000-2 §12.8.1 nosi isto pravilo dalje. Tablica 252 i PAdES dokumenti kažu samo da digest izostavlja vrijednost Contentsa, što je lako pročitati kao izostavljanje samih heksadecimalnih znamenki. Ranija HotPDF izdanja čitala su tako: PreparePDFForSigning i streaming CMS priprema hashirali su i uglaste zagrade, s komentarom u izvornom kodu koji je inzistirao da zagrade moraju biti pokrivene. Validatori koji uspoređuju procijep s vrijednošću potpisa označavaju taj raspored kao nevaljan byte raspon, pa v2.759.0 izvlači oba ograničitelja izvan potpisanih raspona. Brza neovisna provjera na bilo kojoj potpisanoj datoteci gleda dva bajta: bajt na offsetu ByteRange[1] mora biti < a bajt na offsetu ByteRange[2] - 1 mora biti >

Anatomija ispravno ispunjenog PDF signature ByteRangea u HotPDF-u: prvi raspon pokriva datoteku od bajta nula, procijep drži potpuni /Contents hex string uključivo manje-i-veće od ograničitelje, drugi raspon pokriva trailer do kraja, i dvije jednobajtne provjere na ByteRange[1] i ByteRange[2] - 1 potvrđuju raspored na bilo kojoj potpisanoj datoteci
Od v2.759.0 ograničitelji sjede izvan potpisanih raspona, pa digest pokriva samo znamenke i procijep se može validirati bajt po bajt

Zašto provjera nepraznog procijepa promaši signature wrapping?

Provjera nepraznog procijepa dokazuje samo da je nešto izostavljeno iz digesta, ne što je izostavljeno, a to je cijela površina napada. Placeholder /Contentsa rezervira se s tisućama nula znamenki, dok pravi CMS spremnik rijetko kad ispuni. Napadač može zatvoriti hex string rano unutar tog nula paddinga s >, zapisati nove objekte ili krivotvorenu reviziju u ostatak rezerviranog prostora, i ostaviti byte raspone nedirnutima. CMS potpis se i dalje verificira jer je svaki potpisani bajt nepromijenjen, rasponi i dalje kreću od 0 i završavaju na veličini datoteke, i stari je HotPDF verifier javljao svValid s CoversWholeDocument postavljenim na True. PDF preglednik, u međuvremenu, parsira što god sjedi u toj nepotpisanoj rupi

HotPDF sada tretira procijep kao podatke koje treba validirati bajt po bajt. Verifier pročita procijep, odere ograničitelje, prihvaća samo hex znamenke plus PDF bijeli prostor (tab, line feed, form feed, carriage return, razmak), dekodira znamenke i traži da rezultat točno jednak rječniku potpisa /Contents. Sve ostalo spusti rezultat na svInvalidByteRange. Provjera radi i u putu jednog potpisa i u ValidateLoadedSignatureBatch, koji je čuvao vlastitu logiku pokrivenosti i trebao isti ispravak. Datoteke koje je HotPDF proizveo prije v2.759.0, čiji je procijep držao samo znamenke s zagradama upravo unutar raspona, i dalje se verificiraju, pa arhivirani dokumenti ne pocrvene odjednom

Kako signature wrapping iskorištava labavo provjeravan PDF ByteRange u Delphiju: napadač zatvori hex string rano unutar tisuća rezerviranih nula znamenki, zapiše krivotvorenu reviziju u nepotpisani procijep bez diranja ijednog pokrivenog bajta, i stara je HotPDF provjera javljala svValid s CoversWholeDocument true dok v2.759.0 nije počela validirati procijep bajt po bajt
Neprazan procijep dokazuje samo da je nešto izostavljeno iz digesta, ne što — podložena rupa je cijela površina napada
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;

Kako dodati drugi potpis već potpisanom PDF-u?

Otvorite potpisanu datoteku s BeginIncrementalUpdateom, zovite AddLoadedSignedSignatureField, spremite s SaveIncrementalUpdateom, pa potpišite pripremljenu datoteku class funkcijom THotPDF.SignPDFWithPFX. Prije v2.761.0 dokumentirani recept zvanja THPDFPage.AddSignedSignatureField nakon BeginIncrementalUpdatea nije mogao raditi, jer je CurrentPage nil u inkrementalnom modu i ništa nije moglo prikačiti /V placeholder na polje na učitanom dokumentu. Nova metoda stvara widget na učitanoj stranici i objesi isti placeholder rječnik koji koristi put novog dokumenta pod /V, pa obje rute potpisivanja dijele jednu serializaciju. Za prvi potpis sam, članak o stvaranju PAdES digitalnih potpisa u Delphiju provlači PFX pipeline

var
  Pdf: THotPDF;
  FieldIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('Signed.pdf');
    // Stranica 0, pravokutnik widgeta u točkama, 8192 bajta rezervirano za 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 namjerno je tiši od svoje braće. Ostali tvorci polja AddLoaded* postavljaju /NeedAppearances true na AcroFormu, što priča pregledniku da regenerira appearance polja; na potpisanom dokumentu ta regeneracija može prepraviti potpisani sadržaj, pa nova metoda opet skida flag osim ako ga je izvor već nosio. /SigFlags čuva izvornu vrijednost OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 Tablica 219). Ne trebate ni zvati MarkDirty na stranici: dodavanje u /Annots i /Fields širi dirty flag na posjedujući indirektni objekt, a izričito označavanje stranice samo bi odvuklo nepromijenjen rječnik stranice u novu reviziju, koju bi analiza revizija zatim prijavila kao izmjenu stranice. Napokon, placeholder zapisuje /ByteRange prije /Contents, jer patcher locira sentinel /ByteRange prvi i traži naprijed pripadajući hex string

HotPDF Delphi tijek za protupotpisivanje već potpisanog PDF-a: BeginIncrementalUpdate otvori datoteku, AddLoadedSignedSignatureField stvori widget i rezervira /Contents placeholder, SaveIncrementalUpdate nadoda drugu reviziju, a SignPDFWithPFX je ispuni, ostavljajući prvi potpis valjan s UnsignedTrailingBytes dok novi ByteRange obuhvaća cijelu naraslu datoteku
Jedan placeholder po reviziji, pripremljen i zakrpljen istom serializacijom na objema rutama potpisivanja — čist raspored koji stroži verifier očekuje

Što se mijenja kad CMS proizvede vanjski potpisnik ili HSM?

U tijeku rada se ništa ne mijenja, ali offseti sada znače ono što kaže specifikacija. PreparePDFForSigning vraća dva 0-based raspona čiji je procijep cijeli string /Contentsa, a ContentsHexStart jest 1-based indeks prve hex znamenke u AnsiStringu. Kraći CMS podloži se s 0 na kraju, ispred zatvarajućeg >. Pošto PreparePDFForSigning zakrpi prvi nezakrpljeni sentinel koji nađe, pripremite točno jedan placeholder po reviziji, i dajte prednost InsertSignatureHexAtu s vraćenim offsetima pred pretraživanjem baziranim InsertSignatureHexom kad raniji potpisi već postoje u datoteci

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

  // Procijep je cijeli hex string: '<' zatvara raspon 1, '>' prethodi rasponu 2
  Assert(Bytes[R1Start + R1Len + 1] = '<');
  Assert(Bytes[R2Start] = '>');

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

Gdje su granice novih provjera?

Provjera procijepa zatvara jednu konkretnu rupu i ne bi je trebalo prepjevati. svValid i dalje znači integritet bajtova plus ključ koji se poklapa s ugrađenim certifikatom; povjerenje u taj certifikat zasebna je odluka. Procijep se validira samo kad verifier ima izvorne bajtove, koje VerifyLoadedSignatureEx čita iz učitane datoteke, a overloadi TStreama preuzimaju od vas. Za prvi potpis u protupotpisanoj datoteci CoversWholeDocument ispravno je False, i to je li nadodana revizija samo dodala potpis ili je mijenjala i stranice pitanje za DocMDP, FieldMDP i analizu revizija u HotPDF-u. Primijetite i da provjera priloženog PDF MAC-a uspoređuje offsete s pozicijama < i >, pa prima i stari i novi raspored; svaki vlastiti alat koji hardkodira offsete prije v2.759.0 past će prvi čim sretne svježe potpisanu datoteku

Ako vaša Delphi ili C++Builder aplikacija potpisuje, protupotpisuje ili auditira PDF-ove, najsigurniji je put dati jednoj biblioteci da proizvede i verificira isti raspored. HotPDF, nativna Delphi PDF komponenta isporučuje strožu validaciju procijepa, inkrementalne druge potpise i kuke za vanjske potpisnike prikazane gore u jednoj komponenti