Techninis straipsnis

PDF parašo apvyniojimas: ByteRange tarpai ir antri parašai

HotPDF, Delphi PDF komponentas, dabar atmeta parašo apvyniojimą: nuo v2.759.0 tiek VerifyLoadedSignatureEx, tiek paketinis tikrintuvas reikalauja, kad tarpas tarp dviejų /ByteRange segmentų būtų lygiai /Contents šešioliktainė eilutė, skirtukai įskaitant, o v2.761.0 prideda AddLoadedSignedSignatureField, kad antrą parašą būtų galima pridėti prie jau pasirašyto PDF kaip švarią prieaugio reviziją. Šie du pakeitimai priklauso kartu, nes teisingas antras parašas yra lygiai tas išdėstymas, kurio tikisi griežtesnis tikrintuvas

Situacija, atvėrusi problemą, paprasta. Sutartį pasirašo tiekėjas, tada ji keliauja pas tvirtintoją, kuris turi parašyti antrą kartą nedrumstamas pirmojo parašo. Antra revizija priklijuojama po pirmosios, jos paties /ByteRange apima visą paaugusį failą, ir abu parašai turėtų pasitikrinti teigiamai. Tai padaryti rankomis reiškė pačiam rašyti prieaugio sekciją, o testo fikstūra, kuri būtent tai ir darė, pasirodė esanti vadovėlinė parašo apvyniojimo struktūra, kurią senasis tikrintuvas maloniai priimdavo. Jei dar nesidairyta į patikros API, vadovė apie PDF skaitmeninių parašų tikrinimą su HotPDF dengia pagrindus, ant kurių šis straipsnis stovi

Kas lygiai turi gulėti ByteRange tarpe?

Tarpas privalo turėti pilną /Contents reikšmę ir nieko kito: ISO 32000-1 §12.8.3.3 sako, kad šešioliktainė eilutė, su savo < ir > skirtukais, telpa lygiai į erdvę tarp dviejų baitų diapazonų, o ISO 32000-2 §12.8.1 tą pačią taisyklę neša toliau. 252 lentelė ir PAdES dokumentai sako tik tai, kad santrauka neima Contents reikšmės, o tai lengva perskaityti kaip „neima vien šešioliktainių skaitmenų“. Ankstesnės HotPDF laidos taip ir skaitė: PreparePDFForSigning ir srautinė CMS paruoša suhashuodavo ir smailiuosius skliaustus, šaltinio komentaras tvirtindamas, kad skliaustai privalo būti padengti. Tikrintuvai, kurie tarpą lygina su parašo reikšme, tą išdėstymą pažymi kaip netinkamą baitų diapazoną, tad v2.759.0 abu skirtukus iškelia už pasirašytų diapazonų. Greita nepriklausoma patikra bet kuriam pasirašytam failui – pažiūrėti į du baitus: baitas poslinkyje ByteRange[1] privalo būti <, o baitas poslinkyje ByteRange[2] - 1 privalo būti >

Teisingai užpildyto PDF parašo ByteRange HotPDF anatomija: pirmasis diapazonas dengia failą nuo nulinio baito, tarpe guli pilna /Contents šešioliktainė eilutė su mažiau-ir-daugiau skirtukais, antrasis diapazonas dengia trailerį iki galo, o dvi vieno baito patikros ties ByteRange[1] ir ByteRange[2] - 1 patvirtina išdėstymą bet kuriame pasirašytame faile
Nuo v2.759.0 skirtukai sėdi už pasirašytų diapazonų, tad santrauka dengia vien skaitmenis, o tarpą galima tikrinti baitas po baito

Kodėl netuščio tarpo patikra nepagaua parašo apvyniojimo?

Netuščio tarpo patikra įrodo tik tai, kad kažkas buvo palikta už santraukos, o ne ką – ir būtent tai yra visas atakos paviršius. /Contents vietos laikiklis rezervuojamas tūkstančiais nulinių skaitmenų, o tikras CMS konteineris jo retai prisipildo. Puolėjas gali uždaryti šešioliktainę eilutę anksčiau laiko to nuliniame užpilde su >, parašyti naujų objektų arba suklastotą reviziją į likusią rezervuotą erdvę ir baitų diapazonų neliesti. CMS parašas vis tiek pasitikrina, nes kiekvienas pasirašytas baitas nepakitęs, diapazonai vis tiek prasideda nuo 0 ir baigiasi failo dydžiu, o senasis HotPDF tikrintuvas pranešdavo svValid su CoversWholeDocument, nustatytu į True. PDF skaitytuvas tuo metu išanalysuoja bet ką, kas guli toje nepasirašytoje skylėje

HotPDF dabar tarpą traktuoja kaip duomenis, kuriuos reikia tikrinti baitas po baito. Tikrintuvas perskaito tarpą, nukerpa skirtukus, priima vien šešioliktainius skaitmenis plus PDF tarpus (tabuliavimas, eilutės tiekimas, formos tiekimas, grąžinimo vežimėlis, tarpas), iškoduoja skaitmenis ir reikalauja, kad rezultatas sutaptų su parašo žodyno /Contents lygiai. Bet kas kita nulemia rezultatą iki svInvalidByteRange. Patikra veikia ir vieno parašo kelyje, ir ValidateLoadedSignatureBatch, kuri turėjo savąją aprėpties logiką ir reikalavo tos pačios pataisos. Failai, pagaminti HotPDF iki v2.759.0, kurių tarpe gulėjo vien skaitmenys, o skliaustai sėdėjo ką tik už diapazonų ribų, vis tiek pasitikrina, tad archyvuoti dokumentai netampa raudoni

Kaip parašo apvyniojimas išnaudoja laisvai tikrinamą PDF ByteRange Delphi: puolėjas uždaro šešioliktainę eilutę anksčiau laiko tūkstančiuose rezervuotų nulinių skaitmenų, į nepasirašytą tarpą rašo suklastotą reviziją neliesdamas nė vieno padengto baito, o senasis HotPDF patikrinimas, kol v2.759.0 pradėjo tikrinti tarpą baitas po baito, pranešdavo svValid su CoversWholeDocument true
Netuščias tarpas įrodo tik tai, kad kažkas palikta už santraukos, o ne ką – išpūsta skylė yra visas atakos paviršius
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;

Kaip pridėti antrą parašą prie jau pasirašyto PDF?

Atverkite pasirašytą failą su BeginIncrementalUpdate, kvieskite AddLoadedSignedSignatureField, išsaugokite su SaveIncrementalUpdate, o tada paruoštą failą pasirašykite klasės funkcija THotPDF.SignPDFWithPFX. Iki v2.761.0 dokumentuotas receptas kviesti THPDFPage.AddSignedSignatureField po BeginIncrementalUpdate negalėjo veikti, nes CurrentPage prieaugio režime yra nil, ir niekas negalėjo prikabinti /V vietos laikiklio prie lauko pakrautame dokumente. Naujasis metodas sukuria valdiklį pakrautame puslapyje ir pakabina tą patį vietos laikiklio žodyną, kurį naudoja naujo dokumento kelias, po /V, tad abi pasirašymo kryptys dalijasi viena serijalizacija. Dėl paties pirmojo parašo straipsnis apie PAdES skaitmeninių parašų kūrimą Delphi praveda pro PFX pipeline

var
  Pdf: THotPDF;
  FieldIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('Signed.pdf');
    // 0 puslapis, valdiklio stačiakampis taškais, 8192 baitai rezervuota 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 sąmoningai ramesnė už savo seseris. Kiti AddLoaded* laukų kūrėjai ant AcroForm nustato /NeedAppearances true, kas liepia peržiūryklei atkurti laukų išvaizdas; pasirašytame dokumente tas atkūrimas gali perrašyti pasirašytą turinį, tad naujasis metodas vėliavėlę vėl nuima, nebent šaltinis jos jau nešė. /SigFlags išlaiko savo pradinę reikšmę OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 219 lentelė). Jums taip pat nereikia kviesti MarkDirty ant puslapio: pridėjimas prie /Annots ir /Fields purviną vėliavėlę perduoda nuosavam netiesioginiam objektui, o atviras puslapio žymėjimas tik nutemptų nepakitusį puslapio žodyną į naują reviziją, kurį revizijų analizė tada praneštų kaip puslapio pakeitimą. Galiausiai vietos laikiklis rašo /ByteRange prieš /Contents, nes lopinys pirmiausiai suranda žymeklį /ByteRange ir paskui į priekį ieško poruojamos šešioliktainės eilutės

HotPDF Delphi darbo eiga antram parašui jau pasirašytame PDF: BeginIncrementalUpdate atveria failą, AddLoadedSignedSignatureField sukuria valdiklį ir rezervuoja /Contents vietos laikiklį, SaveIncrementalUpdate priklijuoja antrą reviziją, o SignPDFWithPFX ją užpilda, palikdamas pirmąjį parašą teisėtu su UnsignedTrailingBytes, kol naujasis ByteRange apima visą paaugusį failą
Po vieną vietos laikiklį revizijai, paruoštą ir sulopintą tos pačios serijalizacijos abiejose pasirašymo kryptyse – švarus išdėstymas, kurio tikisi griežtesnis tikrintuvas

Kas keičiasi, kai CMS pagamina išorinis pasirašytojas arba HSM?

Darbo eigoje niekas nesikeičia, bet poslinkiai dabar reiškia tai, ką sako specifikacija. PreparePDFForSigning grąžina du nuo nulio skaičiuojamus diapazonus, kurių tarpas yra visa /Contents eilutė, o ContentsHexStart yra pirmojo šešioliktainio skaitmens indeksas AnsiString, skaičiuojamas nuo vieneto. Trumpesnis CMS pabaigoje, prieš uždarantįjį >, išpučiamas 0. Kadangi PreparePDFForSigning sulopina pirmąjį nesulopintą žymeklį, kurį randa, paruoškite lygiai po vieną vietos laikiklį revizijai, ir kai faile jau gyvena ankstesni parašai, teikite pirmenybę InsertSignatureHexAt su grąžintais poslinkiais, o ne paieška paremtai InsertSignatureHex

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

  // Tarpas yra visa šešioliktainė eilutė: '<' užbaigia 1 diapazoną, '>' eina prieš 2
  Assert(Bytes[R1Start + R1Len + 1] = '<');
  Assert(Bytes[R2Start] = '>');

  ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
  CmsHex := SignDetachedWithHsm(ToSign);           // jūsų CMS pasirašytojas, šešioliktainis DER
  if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
    raise Exception.Create('CMS does not fit the reserved space');
  SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf');  // jūsų pagalbinė funkcija
end;

Kur naujųjų patikrų ribos?

Tarpų patikra uždaro vieną konkrečią skylę ir jos nereikėtų perparduoti. svValid vis dar reiškia baitų vientisumą plus raktą, atitinkantį įdėtąjį sertifikatą; pasitikėjimas tą sertifikatą yra atskiras sprendimas. Tarpas tikrinamas tik tada, kai tikrintuvas turi šaltinio baitus – juos VerifyLoadedSignatureEx skaito iš pakrauto failo, o TStream perkrodos juos ima iš jūsų. Pirmajam parašui antru parašu papildytame faile CoversWholeDocument teisingai yra False, o klausimas, ar pridėtoji revizija tik pridėjo parašą, ar pakeitė ir puslapius, yra DocMDP, FieldMDP ir revizijų analizės HotPDF dalykas. Atkreipkite dėmesį ir tai, kad pridėtoji PDF MAC patikra poslinkius lygina su < ir > pozicijomis, tad ji priima ir senąjį, ir naująjį išdėstymą; bet koks jūsų paties įrankis, įkoduotas į prieš v2.759.0 esančius poslinkius, pirmas suklys, kai sutiks šviežiai pasirašytą failą

Jei jūsų Delphi arba C++Builder taikomoji programa pasirašo, antrą kartą pasirašo arba audituoja PDF, saugiausias kelias yra leisti vienai bibliotekai gaminti ir tikrinti tą patį išdėstymą. HotPDF, gimtasis Delphi PDF komponentas, viename komponente atneša griežtesnę tarpo patikrą, prieaugio antrus parašus ir išorinio pasirašytojo kabliukus, parodytus aukščiau