Tehnični članak

Ovijanje podpisov PDF: vrzeli ByteRange in drugi podpisi

HotPDF, komponenta PDF za Delphi, zdaj odkloni ovijanje podpisov: od v2.759.0 tako VerifyLoadedSignatureEx kot paketni validator zahtevata, da je vrzel med dvema segmentoma /ByteRange točno šestnajstiški niz /Contents, vključno z ločili, v2.761.0 pa doda AddLoadedSignedSignatureField, tako da se lahko drugi podpis pripne že podpisanemu PDF kot čista priraščajoča revizija. Spremembi spadata skupaj, ker je pravilen drugi podpis točno tista postavitev, ki jo pričakuje strožji preverjevalnik

Situacija, ki je razkrila problem, je vsakdanja. Pogodbo podpiše prodajalec, nato odpotuje k odobravatelju, ki mora dopisati podpis, ne da bi motil prvega. Druga revizija se pripne za prvo, njen lastni /ByteRange se razteza čez celo zraslo datoteko, oba podpisa pa bi se morala preveriti. Priditi tja z roko je pomenilo napisati priraščajoči razdelek sami, preizkusna fiksna datoteka, ki je točno to storila, pa se je izkazala za učbeniško strukturo ovijanja podpisov, ki jo je stari preverjevalnik srečno sprejel. Če še niste gledali API preverjanja, vodnik o preverjanju digitalnih podpisov PDF s HotPDF pokrije osnove, na katerih ta članek gradi

Kaj točno spada v vrzel ByteRange?

Vrzel mora vsebovati celotno vrednost /Contents in nič drugega: ISO 32000-1 §12.8.3.3 pravi, da se šestnajstiški niz, s svojima ločiloma < in >, natanko prilega prostoru med obema bajtnima obsegoma, ISO 32000-2 §12.8.1 pa pravilo nosi naprej. Tabela 252 in dokumenti PAdES pravijo samo, da povzetek izključuje vrednost Contents, kar je lahko prebrati kot izključevanje samo šestnajstiških števk. Zgodnejše izdaje HotPDF so to prebrale tako: PreparePDFForSigning in tekoča priprava CMS sta hesirali tudi oglate oklepaje, s komentarjem v izvorni kodi, ki je vztrajal, da morajo biti oklepaji pokriti. Validatorji, ki primerjajo vrzel s podpisno vrednostjo, to postavitev označijo kot neveljaven bajtni obseg, zato v2.759.0 preseli oba ločila izven podpisanih obsegov. Hitri neodvisni preizkus na kateri koli podpisani datoteki je pogledati dva bajta: bajt pri odmiku ByteRange[1] mora biti < in bajt pri odmiku ByteRange[2] - 1 mora biti >

Anatomija pravilno zapolnjenega ByteRange podpisa PDF v HotPDF: prvi obseg pokrije datoteko od bajta nič, vrzel drži celoten šestnajstiški niz /Contents, vključno z ločiloma manj-kot in več-kot, drugi obseg pokrije priklopnik do konca, dva eno-bajtna preizkusa pri ByteRange[1] in ByteRange[2] - 1 pa potrdita postavitev na kateri koli podpisani datoteki
Od v2.759.0 ločila sedijo izven podpisanih obsegov, zato povzetek pokrije samo števke in vrzel je mogoče preveriti bajt za bajtom

Zakaj preizkus ne-prazne vrzeli spregleda ovijanje podpisov?

Preizkus ne-prazne vrzeli dokazuje samo, da je bilo kaj izpuščeno iz povzetka, ne kaj, in to je celotna napadalna površina. Ogradero /Contents si rezervira s tisoči ničelnih števk, pravi vsebnik CMS pa jo redko napolni. Napadalec lahko zapre šestnajstiški niz zgodaj znotraj tega ničelnega oblazinjenja z >, zapiše nove objekte ali ponarejeno revizijo v preostanek rezerviranega prostora in pusti bajtne obsege pri miru. Podpis CMS se še vedno preveri, ker je vsak podpisani bajt nespremenjen, obsegi se še vedno začnejo pri 0 in končajo pri velikosti datoteke, stari preverjevalnik HotPDF pa je poročal svValid z CoversWholeDocument nastavljenim na True. Bralnik PDF medtem razčleni kar koli, kar sedi v tej nepodpisani luknji

HotPDF zdaj ravna z vrzeljo kot s podatki, ki jih je treba preveriti bajt za bajtom. Preverjevalnik prebere vrzel, odstrani ločila, sprejme samo šestnajstiške števke plus beli prostor PDF (tabulator, pomik v novo vrstico, pomik na novo stran, povratni voziček, presledek), dekodira števke in zahteva, da je rezultat točno enak /Contents slovarja podpisa. Karkoli drugega razvrsti rezultat na svInvalidByteRange. Preizkus teče tako v poti enojnega podpisa kot v ValidateLoadedSignatureBatch, ki je obdržal lastno logiko pokritosti in potreboval isti popravek. Datoteke, izdelane s HotPDF pred v2.759.0, katerih vrzel je držala samo števke z oklepaji, sedajočimi prav znotraj obsegov, se še vedno preverijo, zato arhivirani dokumenti ne postanejo nenadoma rdeči

Kako ovijanje podpisov izkorišča ohlapno preverjen ByteRange PDF v Delphiju: napadalec zapre šestnajstiški niz zgodaj znotraj tisočev rezerviranih ničelnih števk, zapiše ponarejeno revizijo v nepodpisano vrzel, ne da bi se dotaknil katerega koli pokritega bajta, stari preizkus HotPDF pa je poročal svValid z CoversWholeDocument true, dokler v2.759.0 ni začel preverjati vrzeli bajt za bajtom
Ne-prazna vrzel dokazuje samo, da je bilo kaj izpuščeno iz povzetka, ne kaj — oblazinjena luknja je celotna napadalna 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 dodate drugi podpis že podpisanemu PDF?

Odprite podpisano datoteko z BeginIncrementalUpdate, pokličite AddLoadedSignedSignatureField, shranite s SaveIncrementalUpdate, pripravljeno datoteko pa podpišite s razredno funkcijo THotPDF.SignPDFWithPFX. Pred v2.761.0 dokumentirani recept klicanja THPDFPage.AddSignedSignatureField po BeginIncrementalUpdate ni mogel delovati, ker je CurrentPage v priraščajočem načinu nil in nič ni moglo pripeti ograderke /V k polju na naloženem dokumentu. Nova metoda ustvari gradnik na naloženi strani in obesi isti slovar ograderk, ki ga uporablja pot novega dokumenta, pod /V, zato si obe poti podpisovanja delita eno serializacijo. Za prvi podpis sam članek o ustvarjanju digitalnih podpisov PAdES v Delphiju prehodi cevovod PFX

var
  Pdf: THotPDF;
  FieldIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('Signed.pdf');
    // Stran 0, pravokotnik gradnika v točkah, 8192 bajtov rezerviranih 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šji od svojih sorojencev. Ostali ustvarjalci polj AddLoaded* nastavijo /NeedAppearances true na AcroForm, kar pove pregledovalniku, naj znova izdela videze polj; na podpisanem dokumentu ta ponovna izdelava lahko prepiše podpisano vsebino, zato nova metoda zastavico spet odstrani, razen če jo je izvirnik že nosil. /SigFlags obdrži svojo izvirno vrednost ALI 3 (SignaturesExist plus AppendOnly, ISO 32000-1 tabela 219). Tudi klicanja MarkDirty na strani ne potrebujete: dodajanje k /Annots in /Fields razširi umazano zastavico na lastniški posredni objekt, izrecna oznaka strani pa bi samo povlekla nespremenjen slovar strani v novo revizijo, ki jo analiza revizij nato poroča kot spremembo strani. Nazadnje ogradero zapiše /ByteRange pred /Contents, ker zalezovalec najprej najde čuvajoči /ByteRange in naprej išče ujemajoči šestnajstiški niz

Potek dela HotPDF Delphi za dopisovanje podpisa že podpisanemu PDF: BeginIncrementalUpdate odpre datoteko, AddLoadedSignedSignatureField ustvari gradnik in rezervira ogradero /Contents, SaveIncrementalUpdate pripne drugo revizijo, SignPDFWithPFX pa jo napolni, pri čemer prvi podpis ostane veljaven z UnsignedTrailingBytes, novi ByteRange pa se razteza čez celo zraslo datoteko
Ena ogradero na revizijo, pripravljena in zakrpljena z isto serializacijo na obeh poteh podpisovanja — čista postavitev, ki jo pričakuje strožji preverjevalnik

Kaj se spremeni, ko CMS izdela zunanji podpisnik ali HSM?

V poteku dela se nič ne spremeni, odmiki pa zdaj pomenijo tisto, kar pravi specifikacija. PreparePDFForSigning vrne dva obsega od nič, katerih vrzel je celoten niz /Contents, ContentsHexStart pa je eno-osnovno kazalo prve šestnajstiške števke v AnsiString. Krajši CMS se oblazini z 0 na koncu, pred zaključnim >. Ker PreparePDFForSigning zakrpi prvi nezakrpani čuvajoči, ki ga najde, pripravite točno eno ogradero na revizijo in dajte prednost InsertSignatureHexAt z vrnjenimi odmiki pred iskanjem osnovanim InsertSignatureHex, kadar prejšnji podpisi že obstajajo v datoteki

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

  // Vrzel je celoten šestnajstiški niz: '<' konča obseg 1, '>' stoji pred obsegom 2
  Assert(Bytes[R1Start + R1Len + 1] = '<');
  Assert(Bytes[R2Start] = '>');

  ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
  CmsHex := SignDetachedWithHsm(ToSign);           // vaš podpisnik CMS, šestnajstiški 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š pomožnik
end;

Kje so meje novih preizkusov?

Preizkus vrzeli zapre eno specifično luknjo in se ga ne smete predajati pretirano. svValid še vedno pomeni bajtno celovitost plus ključ, ki se ujema z vdelanim certifikatom; zaupanje temu certifikatu je ločena odločitev. Vrzel se preveri samo, kadar preverjevalnik ima izvorne bajte, ki jih VerifyLoadedSignatureEx bere iz naložene datoteke, preobremenitve TStream pa jih vzamejo od vas. Za prvi podpis v datoteki z dopisanim podpisom je CoversWholeDocument pravilno False, vprašanje, ali je pripeta revizija samo dodala podpis ali pa spremenila tudi strani, pa je za analizo DocMDP, FieldMDP in revizij v HotPDF. Upoštevajte tudi, da preizkus priloženega PDF MAC primerja odmike s položajema < in >, zato sprejme tako staro kot novo postavitev; vsako orodje po meri, ki trdo kodira odmike pred v2.759.0, bo odpovedalo najprej, ko sreča sveže podpisano datoteko

Če vaša aplikacija Delphi ali C++Builder podpisuje, dopisuje podpise ali revizira PDF, je najvarnejša pot pustiti eni knjižnici, da izdela in preveri isto postavitev. HotPDF, domorodna komponenta PDF za Delphi nosi strožjo validacijo vrzeli, priraščajoče druge podpise in kljuke za zunanje podpisnike, prikazane zgoraj, v eni sami komponenti