Tehnički članak

PDF Signature wrapping: ByteRange praznine i drugi potpisi

HotPDF, Delphi PDF komponenta, sada odbija signature wrapping: od v2.759.0 i VerifyLoadedSignatureEx i serijski validator zahtevaju da praznina između dva /ByteRange segmenta bude tačno /Contents hex string, delimiteri uključeni, a v2.761.0 dodaje AddLoadedSignedSignatureField tako da se drugi potpis može dodati na već potpisan PDF kao čista inkrementalna revizija. Dve izmene idu zajedno, jer je ispravan drugi potpis upravo onaj raspored koji strožiji verifikator očekuje

Situacija koja je otkrila problem je obična. Ugovor potpiše prodavac, pa ode na odobravajućeg koji mora da kontrapotpiše a da ne poremeti prvi potpis. Druga revizija dodaje se posle prve, njen sopstveni /ByteRange obuhvata ceo narasli fajl, i oba potpisa treba da prođu verifikaciju. Doći tamo ručno značilo je napisati inkrementalnu sekciju samome, i test fiksatura koja je to baš radila ispostavila se kao udžbenička signature-wrapping struktura koju je stari verifikator rado prihvatio. Ako verifikacioni API ranije niste gledali, vodič za verifikaciju PDF digitalnih potpisa uz HotPDF pokriva osnove na kojima ovaj članak gradi

Šta tačno pripada ByteRange praznini?

Praznina mora da sadrži kompletnu /Contents vrednost i ništa drugo: ISO 32000-1 §12.8.3.3 kaže da heksadecimalni string, sa svojim < i > delimiterima, staje tačno u prostor između dva bajt opsega, a ISO 32000-2 §12.8.1 isto pravilo prenosi dalje. Tabela 252 i PAdES dokumenti kažu samo da digest izostavlja Contents vrednost, što se lako pročita kao izostavljanje samih heks cifara. Ranija HotPDF izdanja čitala su tako: PreparePDFForSigning i streaming CMS priprema heširale su i uglaste zagrade, sa komentarom u izvoru koji je tvrdio da zagrade moraju biti pokrivene. Validatori koji prazninu porede sa vrednošću potpisa taj raspored označavaju kao neispravan byte range, pa v2.759.0 oba delimitera izvlači iz potpisanih opsega. Brza nezavisna provera na bilo kom potpisanom fajlu je pogled na dva bajta: bajt na offsetu ByteRange[1] mora biti < a bajt na offsetu ByteRange[2] - 1 mora biti >

Anatomija ispravno popunjenog PDF signature ByteRange-a u HotPDF-u: prvi opseg pokriva fajl od bajta nula, praznina drži kompletan /Contents hex string uključujući manje-od i veće-od delimiteri, drugi opseg pokriva trailer do kraja, i dve jednobajtne provere na ByteRange[1] i ByteRange[2] - 1 potvrđuju raspored na bilo kom potpisanom fajlu
Od v2.759.0 delimiteri sede van potpisanih opsega, pa digest pokriva samo cifre i praznina se može validirati bajt po bajt

Zašto provera neprazne praznine promaši signature wrapping?

Provera neprazne praznine dokazuje samo da je nešto izostavljeno iz digesta, ne šta je izostavljeno, i to je čitava napadačka površina. /Contents placeholder rezervisan je sa hiljadama nula-cifara, dok pravi CMS kontejner retko ispuni taj prostor. Napadač može heks string rano zatvoriti unutar te nula dopune sa >-om, upisati nove objekte ili krivotvorenu reviziju u ostatak rezervisanog prostora, i ostaviti bajt opsege netaknutim. CMS potpis i dalje prolazi jer je svaki potpisani bajt nepromenjen, opsezi i dalje počinju na 0 i završavaju na veličini fajla, i stari HotPDF verifikator je javljao svValid sa CoversWholeDocument postavljenim na True. PDF čitač, u međuvremenu, parsira šta god sede u toj nepotpisanoj rupi

HotPDF sada tretira prazninu kao podatke koje treba validirati bajt po bajt. Verifikator pročita prazninu, skine delimitere, prihvata samo heks cifre plus PDF beline (tab, line feed, form feed, carriage return, razmak), dekoduje cifre i zahteva da rezultat tačno bude jednak /Contents rečniku potpisa. Sve drugo spušta rezultat na svInvalidByteRange. Provera radi i u putanji jednog potpisa i u ValidateLoadedSignatureBatch, koji je čuvao sopstvenu logiku pokrivenosti i trebao je istu popravku. Fajlovi koje je HotPDF napravio pre v2.759.0, čija je praznina držala samo cifre sa zagradama baš unutar opsega, i dalje prolaze, pa arhivirani dokumenti odjednom ne pocrvene

Kako signature wrapping zloupotrebljava labavo proveren PDF ByteRange u Delphiju: napadač heks string rano zatvori unutar hiljada rezervisanih nula-cifara, upiše krivotvorenu reviziju u nepotpisanu prazninu a da nijedan pokriveni bajt ne dirne, i stara HotPDF provera javljala je svValid sa CoversWholeDocument true dok v2.759.0 nije počela da validira prazninu bajt po bajt
Neprazna praznina dokazuje samo da je nešto izostavljeno iz digesta, ne šta — dopunjena rupa je čitava napadačka površina
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 na već potpisan PDF?

Otvorite potpisani fajl sa BeginIncrementalUpdate-om, zovite AddLoadedSignedSignatureField, sačuvajte sa SaveIncrementalUpdate-om, pa potpišite pripremljeni fajl klasnom funkcijom THotPDF.SignPDFWithPFX. Pre v2.761.0 dokumentovani recept da se posle BeginIncrementalUpdate-a zove THPDFPage.AddSignedSignatureField nije mogao da radi, jer je CurrentPage u inkrementalnom režimu nil i ništa nije moglo da zakači /V placeholder na polje na učitanom dokumentu. Nova metoda kreira vidžet na učitanoj stranici i okače isti placeholder rečnik koji koristi putanja novog dokumenta ispod /V-a, pa obe potpisne rute dele jednu serijalizaciju. Za sam prvi potpis, članak o kreiranju PAdES digitalnih potpisa u Delphiju provodi vas kroz PFX cevovod

var
  Pdf: THotPDF;
  FieldIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('Signed.pdf');
    // Stranica 0, pravougaonik vidžeta u poenima, 8192 bajta rezervisano 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 je namerno tiša od svojih sestara. Ostali kreatori polja AddLoaded* postavljaju /NeedAppearances true na AcroForm-u, što pregledaču govori da regeneriše izglede polja; na potpisanom dokumentu ta regeneracija može prepraviti potpisani sadržaj, pa nova metoda zastavicu opet skida osim ako je izvor već nosi. /SigFlags zadržava prvobitnu vrednost OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 tabela 219). Ne treba zvati ni MarkDirty na stranici: dodavanje u /Annots i /Fields raznosi dirty zastavicu do vlasničkog indirektnog objekta, a eksplicitna oznaka stranice samo bi povukla nepromenjen page rečnik u novu reviziju, koju bi analiza revizije tada prijavila kao izmenu stranice. Naposletku, placeholder upisuje /ByteRange pre /Contents-a, jer patcher prvo nalazi sentinel /ByteRange i unapred traži odgovarajući hex string

HotPDF Delphi tok rada za kontrapotpisivanje već potpisanog PDF-a: BeginIncrementalUpdate otvara fajl, AddLoadedSignedSignatureField kreira vidžet i rezerviše /Contents placeholder, SaveIncrementalUpdate dodaje drugu reviziju, a SignPDFWithPFX je popuni, prvi potpis ostaje validan sa UnsignedTrailingBytes dok novi ByteRange obuhvata ceo narasli fajl
Jedan placeholder po reviziji, pripremljen i zakrpljen istom serijalizacijom na obe potpisne rute — čist raspored koji strožiji verifikator očekuje

Šta se menja kad CMS proizvede eksterni potpisivač ili HSM?

U toku rada se ništa ne menja, ali offseti sada znače ono što specifikacija kaže. PreparePDFForSigning vraća dva 0-bazirana opsega čija je praznina ceo /Contents string, a ContentsHexStart je 1-bazirani indeks prve hex cifre u AnsiString-u. Kraći CMS dopunjuje se sa 0 na kraju, pre zatvarajućeg >-a. Pošto PreparePDFForSigning zakrpi prvi nezakrpljeni sentinel koji nađe, pripremite tačno jedan placeholder po reviziji, i dajte prednost InsertSignatureHexAt-u sa vraćenim offsetima preko pretrazivačkog InsertSignatureHex-a kad u fajlu već postoje raniji potpisi

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

  // Praznina je ceo hex string: '<' zatvara opseg 1, '>' prethodi opsegu 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 potpisivač, 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;

Gde su granice novih provera?

Provera praznine zatvara jednu konkretnu rupu i ne treba je preprodavati. svValid i dalje znači bajt integritet plus ključ koji poklapa ugrađeni sertifikat; poverenje u taj sertifikat odvojena je odluka. Praznina se validira samo kad verifikator ima izvorne bajtove, koje VerifyLoadedSignatureEx čita iz učitanog fajla a TStream overloadi uzimaju od vas. Za prvi potpis u kontrapotpisanom fajlu, CoversWholeDocument je ispravno False, a da li je dodata revizija samo unela potpis ili menjala i stranice pitanje je za DocMDP, FieldMDP i analizu revizije u HotPDF-u. Primetite i da prikačena PDF MAC provera poredi offsete sa pozicijama <-a i >-a, pa prihvata i stari i novi raspored; svaki vaš sopstveni alat koji je ukuckao offsete pre v2.759.0 prvo će pasti kad sretne sveže potpisan fajl

Ako vaša Delphi ili C++Builder aplikacija potpisuje, kontrapotpisuje ili revidira PDF-ove, najsigurniji put je da jedna biblioteka proizvodi i verifikuje isti raspored. HotPDF, nativna Delphi PDF komponenta isporučuje strožiju validaciju praznine, inkrementalne druge potpise i kuke za eksterne potpisivače prikazane iznad u jednoj komponenti