Tekninen artikkeli

Allekirjoituskierto: ByteRange-jaot ja toinen allekirjoitus

HotPDF, Delphi-PDF-komponentti, torjuu nyt allekirjoitusten kietoutumisen: versiosta v2.759.0 alkaen sekä VerifyLoadedSignatureEx että erävalidaattori vaativat, että kahden /ByteRange-sekmentin välinen rako on täsmälleen /Contents-heksamerkkijono erottimineen, ja v2.761.0 lisää metodin AddLoadedSignedSignatureField, jolloin toinen allekirjoitus voidaan liittää jo allekirjoitettuun PDF:ään siistinä inkrementaalisena revisiona. Kaksi muutosta kuuluvat yhteen, koska oikea toinen allekirjoitus on täsmälleen se asettelu, jota tiukempi verifioija odottaa

Tilanne, joka paljasti ongelman, on tavallinen. Sopimuksen allekirjoittaa toimittaja, minkä jälkeen se kulkee hyväksyjälle, jonka on allekirjoitettava häiritsemättä ensimmäistä allekirjoitusta. Toinen revisio liitetään ensimmäisen perään, sen oma /ByteRange ulottuu koko kasvaneeseen tiedostoon, ja molempien allekirjoitusten pitäisi verifioitua. Sinne pääseminen käsin merkitsi inkrementaalisen osion kirjoittamista itse, ja testifixture, joka teki juuri niin, osoittautui oppikirjamaiseksi allekirjoitusten kietoutumisen rakenteeksi, jonka vanha verifioija hyväksyi mielellään. Jos et ole aiemmin katsonut verifiointi-API:a, opas PDF-digitaalisten allekirjoitusten verifiointi HotPDF:llä kattaa perusteet, joille tämä artikkeli rakentuu

Mitä ByteRange-rakoon täsmälleen kuuluu?

Rakon on sisällettävä täydellinen /Contents-arvo eikä mitään muuta: ISO 32000-1 §12.8.3.3 sanoo, että heksamerkkijono, <- ja >-erottimineen, mahtuu täsmälleen kahden tavualueen väliseen tilaan, ja ISO 32000-2 §12.8.1 vie saman säännön eteenpäin. Taulukko 252 ja PAdES-asiakirjat sanovat vain, että tiivistys sulkee pois Contents-arvon, mikä on helppo lukea niin, että vain heksanumerot suljetaan pois. Aiemmat HotPDF-julkaisut lukivat sen noin: PreparePDFForSigning ja striimaava CMS-valmistelu tiivistivät myös kulmasulkeet, ja lähdekoodin kommentti väitti, että sulkeiden on kuuluttava kattavuuteen. Validaattorit, jotka vertaavat rakoa allekirjoitusarvoon, liputtavat kyseisen asettelun epäkelvoksi tavualueeksi, joten v2.759.0 siirtää molemmat erottimet allekirjoitettujen alueiden ulkopuolelle. Nopea itsenäinen tarkistus mille tahansa allekirjoitetulle tiedostolle on katsoa kahta tavua: tavun offsetissa ByteRange[1] on oltava < ja tavun offsetissa ByteRange[2] - 1 on oltava >

Oikein täytetyn PDF-allekirjoituksen ByteRangen anatomia HotPDF:ssä: ensimmäinen alue kattaa tiedoston tavusta nolla alkaen, rako pitää sisällään täydellisen /Contents-heksamerkkijonon pienempi kuin - ja suurempi kuin -erottimia myöten, toinen alue kattaa trailerin loppuun asti, ja kaksi yhden tavun tarkistusta kohdissa ByteRange[1] ja ByteRange[2] - 1 vahvistavat asettelun mille tahansa allekirjoitetulle tiedostolle
Versiosta v2.759.0 alkaen erottimet istuvat allekirjoitettujen alueiden ulkopuolella, joten tiivistys kattaa vain numerot ja rako voidaan validoida tavu kerrallaan

Miksi ei-tyhjän raon tarkistus ohittaa allekirjoitusten kietoutumisen?

Ei-tyhjän raon tarkistus todistaa vain, että jotain jätettiin tiivistyksen ulkopuolelle, ei sitä, mitä jätettiin, ja juuri se on koko hyökkäyspinta. /Contents-paikanpitäjä varataan tuhansilla nollanumeroilla, kun taas oikea CMS-säiliö täyttää sen harvoin. Hyökkääjä voi sulkea heksamerkkijonon aikaisin tuon nollatäytteen sisällä >:llä, kirjoittaa uusia objekteja tai väärennetyn revision loppuun varattua tilaa ja jättää tavualueet koskemattomiksi. CMS-allekirjoitus verifioituu yhä, koska jokainen allekirjoitettu tavu on muuttumaton, alueet alkavat yhä arvosta 0 ja päättyvät tiedoston kokoon, ja vanha HotPDF-verifioija raportoi tilan svValid arvolla CoversWholeDocument True. PDF-lukija puolestaan jäsentää sen, mitä tuossa allekirjoittamattomassa reiässä istuu

HotPDF kohtelee rakoa nyt datana, joka on validoitava tavu kerrallaan. Verifioija lukee raon, irrottaa erottimet, hyväksyy vain heksanumerot sekä PDF:n tyhjät merkit (sarkain, rivinsyöttö, lomakkeensyöttö, vaununpalautus, välilyönti), dekoodaa numerot ja vaatii tuloksen olevan täsmälleen yhtä kuin allekirjoitussanakirjan /Contents. Mikä tahansa muu alentaa tuloksen tilaksi svInvalidByteRange. Tarkistus ajaa sekä yksittäisen allekirjoituksen polulla että ValidateLoadedSignatureBatchissa, joka piti oman kattavuuslogiikkansa ja tarvitsi saman korjauksen. HotPDF:n tuottamat tiedostot ennen v2.759.0:a, joiden rakossa oli vain numeroita sulkeiden istuessa juuri alueiden sisäpuolella, verifioituvat yhä, joten arkistoidut asiakirjat eivät yhtäkkiä muutu punaisiksi

Miten allekirjoitusten kietoutuminen hyödyntää löyhästi tarkistettua PDF:n ByteRangea Delphissä: hyökkääjä sulkee heksamerkkijonon aikaisin tuhansien varattujen nollanumeroiden sisällä, kirjoittaa väärennetyn revision allekirjoittamattomaan rakoon koskematta yhteenkään katettuun tavuun, ja vanha HotPDF-tarkistus raportoi svValidin arvolla CoversWholeDocument tosi, kunnes v2.759.0 alkoi validoida rakon tavu kerrallaan
Ei-tyhjä rako todistaa vain, että jotain jätettiin tiivistyksen ulkopuolelle, ei mitä — täytetty reikä on koko hyökkäyspinta
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;

Miten allekirjoitettuun PDF:ään lisätään toinen allekirjoitus?

Avaa allekirjoitettu tiedosto metodilla BeginIncrementalUpdate, kutsu metodia AddLoadedSignedSignatureField, tallenna metodilla SaveIncrementalUpdate ja allekirjoita sitten valmisteltu tiedosto luokkafunktiolla THotPDF.SignPDFWithPFX. Ennen v2.761.0 dokumentoitu resepti, jonka mukaan kutsutaan metodia THPDFPage.AddSignedSignatureField metodin BeginIncrementalUpdate jälkeen, ei voinut toimia, koska CurrentPage on nil inkrementaalisessa tilassa eikä mikään voinut kiinnittää /V-paikanpitäjää ladatun asiakirjan kenttään. Uusi metodi luo widgetin ladatulle sivulle ja ripustaa saman paikanpitäjäsanakirjan, jota uuden asiakirjan polku käyttää, /V:n alle, joten molemmat allekirjoitusreitit jakavat yhden sarjoinnin. Ensimmäiseen allekirjoitukseen itseensä kirjoitus PAdES-digitaalisten allekirjoitusten luominen Delphissä kävelee läpi PFX-putken

var
  Pdf: THotPDF;
  FieldIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('Signed.pdf');
    // Sivu 0, widgetin suorakulmio pisteinä, CMS:lle varattu 8192 tavua
    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 on tahallaan hiljaisempi kuin sisarensa. Muut AddLoaded*-kenttien luojat asettavat /NeedAppearances true:n AcroFormille, mikä käskee katselimen uudelleengeneroimaan kenttien ulkoasut; allekirjoitetulla asiakirjalla tuo uudelleengenerointi voi kirjoittaa uudelleen allekirjoitettua sisältöä, joten uusi metodi poistaa lipun jälleen, ellei lähde jo kantanut sitä. /SigFlags pitää alkuperäisen arvonsa OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 taulukko 219). Sinun ei myöskään tarvitse kutsua MarkDirtya sivulla: lisääminen /Annotsiin ja /Fieldsiin välittää dirty-lipun omistavalle epäsuoralle objektille, ja eksplisiittinen sivumerkintä vain raahaisi muuttumattoman sivusanakirjan uuteen revisioon, jonka revisioanalyysi raportoi sitten sivun muutoksena. Lopuksi paikanpitäjä kirjoittaa /ByteRangein ennen /Contentsia, koska paikkaaja paikantaa sentinelin /ByteRangein ensin ja etsii eteenpäin täsmäävää heksamerkkijonoa

HotPDF Delphi -työnkulku jo allekirjoitetun PDF:n mukaan allekirjoittamiseen: BeginIncrementalUpdate avaa tiedoston, AddLoadedSignedSignatureField luo widgetin ja varaa /Contents-paikanpitäjän, SaveIncrementalUpdate liittää toisen revision, ja SignPDFWithPFX täyttää sen jättäen ensimmäisen allekirjoituksen kelvolliseksi UnsignedTrailingBytes-arvolla, kun taas uusi ByteRange ulottuu koko kasvaneeseen tiedostoon
Yksi paikanpitäjä per revisio, valmisteltuna ja paikattuna samalla sarjoinnilla molemmilla allekirjoitusreiteillä — siisti asettelu, jota tiukempi verifioija odottaa

Mitä muuttuu, kun ulkoinen allekirjoittaja tai HSM tuottaa CMS:n?

Työnkulussa ei muutu mitään, mutta offsetit tarkoittavat nyt sitä, mitä spesifikaatio sanoo. PreparePDFForSigning palauttaa kaksi nollapohjaista aluetta, joiden rako on koko /Contents-merkkijono, ja ContentsHexStart on ensimmäisen heksanumeron yhdestä alkava indeksi AnsiStringissä. Lyhyempi CMS täytetään arvolla 0 loppuun, ennen sulkevaa >:ä. Koska PreparePDFForSigning paikkaa ensin löytämänsä paikkaamattoman sentinelin, valmistele täsmälleen yksi paikanpitäjä per revisio ja suosi metodia InsertSignatureHexAt palautetuilla offseteilla hakupohjaisen InsertSignatureHexin sijaan, kun tiedostossa on jo aiempia allekirjoituksia

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

  // Rako on koko heksamerkkijono: '<' päättää alueen 1, '>' edeltää aluetta 2
  Assert(Bytes[R1Start + R1Len + 1] = '<');
  Assert(Bytes[R2Start] = '>');

  ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
  CmsHex := SignDetachedWithHsm(ToSign);           // oma CMS-allekirjoittajasi, DER heksana
  if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
    raise Exception.Create('CMS does not fit the reserved space');
  SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf');  // oma apurisi
end;

Missä uusien tarkistusten rajat ovat?

Rakotarkistus sulkee yhden tietyn reiän, eikä sitä pidä myydä yli. svValid tarkoittaa yhä tavueheyttä plus avainta, joka täsmää upotettuun sertifikaattiin; luottamus kyseiseen sertifikaattiin on erillinen päätös. Rako validoidaan vain, kun verifioijalla on lähdetavut, jotka VerifyLoadedSignatureEx lukee ladatusta tiedostosta ja joiden TStream-overloadit otat itse. Kaksiallekirjoitetun tiedoston ensimmäiselle allekirjoitukselle CoversWholeDocument on oikein False, ja se, lisäsikö liitetty revisio vain allekirjoituksen vai muuttiko myös sivuja, on kysymys kirjoitukselle DocMDP, FieldMDP ja revisioanalyysi HotPDF:ssä. Huomaa myös, että liitetty PDF MAC -tarkistus vertaa offseteja <- ja >-paikkoihin, joten se hyväksyy sekä vanhan että uuden asettelun; mikä tahansa oma työkalusi, joka kovakoodaa versiota edeltävät offsetit, kaatuu ensin kohdatessaan tuoreesti allekirjoitetun tiedoston

Jos Delphi- tai C++Builder-sovelluksesi allekirjoittaa, vastallekirjoittaa tai auditoi PDF:eitä, turvallisin polku on antaa yhden kirjaston tuottaa ja verifioida sama asettelu. HotPDF, natiivi Delphi PDF -komponentti toimittaa tiukemman rakovalidoinnin, inkrementaaliset toiset allekirjoitukset ja yllä näytetyt ulkoisten allekirjoittajien koukut yhdessä komponentissa