Techninis straipsnis

PDFium Component DocMDP: valdiklio /P nuslėptas pakeitimas

PDFium Component variantuose iki v3.126.2 TPdf.AnalyzeSignatureRevisions tikrą puslapio turinio redagavimą galėjo įvertinti kaip leistą anotacijų pakeitimą po DocMDP P=3, nes jos revizijų rolių grafas parašo valdiklio /P atgalinę nuorodą į jo puslapį laikė nuosavybe. Nuo v3.126.2 PDFium Component navigacijos briaunas atskiria nuo turimos apkrovos briaunų, tad puslapio turinys lieka puslapio turiniu. Už šį sutvarkymą stovinti klaidos ataskaita popieriuje atrodo nepavojinga. Sertifikuotas sutartis leidžia anotacijas, kontrahentas prideda vieną papildomą išsaugojimą, ir analizatorius sako, kad visi vėlesni pakeitimai leidžiami. Tada kas nors sulygina atvaizduotus puslapius, ir mokėjimo suma 2 puslapyje kita

Šis straipsnis – puolėjo akimis parašyta dalis prie apžvalgos revizijų pakeitimų analizė po parašo, tad praleidžia revizijų atkūrimo ir DocMDP vertinimo pagrindus ir eina tiesiai į objektų grafą: kaip buvo modeliuota nuosavybė, kodėl briaunos kryptis nusprendžia saugumo verdiktą, kas pasikeitė v3.126.2 ir kaip patikrinti savą priėmimo logiką

Kodėl puslapio redagavimas praeidavo kaip anotacijų pakeitimas po DocMDP P=3?

Puslapio redagavimas praeidavo, nes senasis rolių grafas sekė kiekvieną netiesioginę nuorodą žodyne tarsi nurodytas objektas priklausytų nurodančiajam, o parašo valdiklis rodo atgal į savo puslapį. Anotacijos žodynas neša /P – netiesioginę nuorodą į puslapio objektą, ant kurio tupi (ISO 32000-1 §12.5.2). Tas įrašas – navigacijos užuomina. Valdiklis puslapio nesavaldo; puslapis valdo valdiklį per savą /Annots masyvą

Analizatorius kiekvienam objektui priskiria rolių bitų aibę, dar neįvertinęs vėlesnių pakeitimų: puslapio, anotacijos, formos ir patikros medžiagos. Šakniniai objektai savo rolę gauna iš savo žodyno, ir rolė paskui plinta į viską, į ką jie rodo. Senojoje sklaidoje grandinė ėjo taip:

  1. Parašo valdiklis yra /Subtype /Widget su /FT /Sig, tad gauna anotacijos rolę
  2. Valdiklio /P anotacijos rolę nustumia ant puslapio žodyno, kuriame puslapio rolė jau yra
  3. Puslapis abi roles nustumia į /Contents, /Resources ir, per /Parent, aukštyn į Pages medį ir paskui į kiekvieną seserinį puslapį
  4. Turinio srauto žodynas, toks kaip << /Length 812 >>, /Type neturi, tad klasifikatorius grįždavo prie rolių bitų ir tikrindavo anotacijos rolę anksčiau už puslapio rolę
PDFium Component iki v3.126.2 buvusio DocMDP rolių grafo diagrama, kur parašo valdiklio /P atgalinė nuoroda anotacijos rolę nustumia ant puslapio žodyno, puslapis ją per /Contents išplinta ant turinio srauto be /Type įrašo, klasifikatorius išveda prckAnnotation, o P=3 vertinimas grąžina prdAllowed
Iki v3.126.2 rolių grafas kiekvieną netiesioginę nuorodą laikė nuosavybe, tad valdiklio /P įrašas anotacijos rolę nustumdavo ant puslapio, ir tikras puslapio redagavimas iš analizatoriaus išeidavo kaip leistas anotacijų pakeitimas

Pakeistas turinio srautas todėl išeidavo kaip prckAnnotation. Pagal ISO 32000-1 §12.8.2.2 DocMDP P=3 leidžia anotacijų pakeitimus, tad sprendimas buvo prdAllowed, o ataskaita susisuksi į prasAllowed. Tas pats failas po P=2 būdavo atmetamas, bet tik atsitiktinai: P=2 draudžia anotacijų pakeitimus, tad klaidingai pažymėtas srautas būdavo atmetamas dėl neteisingos priežasties. Fiksuota keturių praėjimų sklaidos kilpa pridėdavo antrą silpnumą. Apkrova, pasiekta per netiesioginį masyvą ar per ilgą grandinę, kurios objektų numeriai bėga atgal, gaudavo likti visai be rolės

Kodėl parašo tikrintojas turi klausti, kam priklauso objektas?

Parašo tikrintojas turi klausti, kam priklauso objektas, nes PDF papildomi atnaujinimai (ISO 32000-1 §7.5.6) leidžia bet kam pridėti reviziją, persibrežiančią esamą objekto numerį, o persibrežtas turinys neskelbia, kas jis toks. Parašas vis tiek patvirtinamas, nes dengia tik savos revizijos baitus. Kiekviena gynyba nuo manipuliacijos po pasirašymo todėl remiasi tuo, kad kiekvienas pakeistas objektas susiejamas su struktūra, kuri juo naudojasi, ir tada klausiama, ar pasirašęs leido tai struktūrai keistis

Keli paskelbti atakų klasės darbai būtent toje spragoje. Papildomo išsaugojimo atakos prideda reviziją, sukeičiančią puslapio turinį, ir remiasi tikrintoju, tikrinančiu tik pasirašytą baitų ruožą. Šešėlinės atakos pasodina paslėptą turinį iki pasirašymo ir suaktyvina jį vėliau nedideliu, nekaltai atrodančiu pakeitimu. Atakos prieš sertifikuotus dokumentus piktnaudžiauja tuo, kad P=2 ir P=3 aiškiai leidžia kai kuriuos vėlesnius redagavimus, ir tada draudžiamą redagavimą aprengia leistu. Tikrintojas, klasifikuojantis objektus pagal etiketes, tokias kaip /Type /Annot, arba pagal bet kurį atsitiktinai juos pasiekiantį nuorodų kelią, yra atviras trečiajai klasei: puolėjui užtenka vienos leistos struktūros, gebančios pasiekti draudžiamąją

Todėl klausimas ne tas, kurie objektai pasikeitė, o tas, kam jie priklauso. Turinio srautas, pasiektas iš puslapio per /Contents, yra puslapio turinys, kad ir kas dar į jį rodytų. Anotacija, atgal į puslapį rodanti per /P, sako, kur anotacija gyvena, o ne ką ji savo

Kaip PDFium Component v3.126.2 modeliuoja nuosavybę?

PDFium Component v3.126.2 atgalines nuorodas laiko navigacija, laiko jas už rolių sklaidos ribų, o tai, kurie raktai skaitosi navigacija, sprendžia iš žodyno, laikančio tuos raktus, struktūrinės rolės, o ne vien iš rakto pavadinimo. Lentelė apibendrina navigacijos raktus, kurie nebeneša nuosavybės

Nuosavybės žodynasRaktai, laikomi navigacijaSpecifikacijos nuoroda
Puslapio arba Pages mazgas/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
Anotacija arba valdiklis/PISO 32000-1 §12.5.2
Valdiklio arba lauko žodynas/ParentISO 32000-1 §12.7.3
PDFium Component v3.126.2 objektų grafas, atskiriantis turimos apkrovos briaunas, tokias kaip /Contents ir /Annots, skleidžiančias puslapio ir anotacijos roles, nuo navigacijos briaunų, tokių kaip valdiklio /P, nenešančių jokių rolių, su navigacijos raktais kiekvienam nuosavybės žodynui ir prckPageContent, išlikusiu ant turinio srauto net po suklastota /Type
v3.126.2 atgalines nuorodas laiko už rolių sklaidos: rolės keliauja tik per tikrą nuosavybę, tad turinio srautas lieka puslapio turiniu, o /P užuomina nesprendžia nieko

Visuotinis filtravimas pagal rakto pavadinimą būtų padaręs naują skylę. Šriftas arba XObject išteklius teisėtai gali vadintis /P, /Parent arba /Annots, o /Resources žodynas, išmetęs savą /P įrašą iš sklaidos, leistų puolėjui paslėpti puslapio valdomą XObject už nekaltai skambančio ištekliaus pavadinimo. v3.126.2 navigacijos filtras taikomas tik tada, kai nuosavybės žodynas iš tiesų yra puslapis, Pages mazgas, anotacija, valdiklis arba laukas. Jei vienas tokių žodynų neša padubuotą navigacijos raktą, pavyzdžiui du /P įrašus valdiklyje, analizatorius nespėlioja, kurio kopijos imtųsi peržiūrovas; rolių kūrimas žlunga, ir parašas tampa Indeterminate

Keli tolesni variantai uždarė likusius perženklinimo kelius:

  • Pages mazgai patys savaime yra puslapio rolės šaknys, tad iš Pages medžio paveldėti ištekliai (ISO 32000-1 §7.7.3.4) į puslapio kontekstą patenka per tikrą nuosavybę, o ne per /Parent ėjimą nuo vaiko puslapio
  • Anotacijos ar formos rolė, atkeliavusi į katalogo, Pages mazgo, puslapio, anotacijos ar lauko žodyną, ten sustoja, nes tie struktūriniai objektai patys nustato savas roles, ir atkeliaujanti apkrovos rolė jų nepakeisti negali
  • Puslapio rolė klasifikavimo metu sprendžia: puslapio valdomas objektas lieka prckPageContent, net jei vėlesnė revizija jį perrašo su suklastota /FT, /Type /Annot etikete arba dalijasi juo su išvaizdos srautu
  • Form XObject, naudojamas vien kaip lauko ar anotacijos išvaizda, išlaiko savo formos ar anotacijos kategoriją, tad paprastas išvaizdos atkūrimas po formos užpildymo vis tiek vertinamas pagal įprastas leidimų taisykles
  • Valdiklis be sava /FT paveldėtą lauko tipą išspręndžia per /Parent grandinę, o neišsprendžiama grandinė žlugdo rolių kūrimą vietoj numatymo į anotaciją
  • Rolių bitai iš kiekvienos vėlesnės revizijos suliejami į dengiamosios revizijos roles, tad vėlesnis atnaujinimas negali ištrinti ankstesnio puslapio nuosavybės santykio, pirmiau atskyręs srautą ir tada jį paredagavęs

Stovis vietoj fiksuoto praėjimų skaičiaus

Rolių pasiekiamumas v3.126.2 lekia kaip darbų eilė, kartojanti, kol joks objektas daugiau negauna naujo rolių bito – tikras stovis, nesvarbu, kokia grandinės gelmė ar objektų numeracija. Netiesioginiai masyvai, tokie kaip /Contents masyvas, saugomas kaip sava objektas, taip pat apeinami. Kiekvienas objektas gali gauti daugiausia keturis skirtingus rolių bitus, tad eilė ribojama keturiais įrašais objekto numeriui; viršijus tą biudžetą keliama prrResourceLimitExceeded. Nuoroda į laisvą objektą, kartos nesutapimas ar sugadinta objekto antraštė kelia prrMalformedRevisionChain, o apkrova suspausto objektų srauto viduje kelia prrCompressedObjectUnresolved. Kiekviena šių nesėkmių pasibaigia prasIndeterminate, niekada leistu verdiktu, o kai nesėkmė iškyla kuriant dengiamosios revizijos roles, parašas visai nepraneša jokių Changes

Ši procedūra išvardija puslapio turinio redagavimus, išgyvenančius šią analizę. prckPageContent pakeitimas niekada nevertinamas prdAllowed: DocMDP P=1, 2 arba 3 padaro jį prdDisallowed, o parašas be DocMDP įvertina jį prdSuspicious

uses
  SysUtils, TypInfo, PDFium, FPdfPades;

procedure ListPageContentEdits(const FileName: string);
const
  ShadowTag: array[Boolean] of string = (' (unreferenced shadow)', '');
var
  Pdf: TPdf;
  Report: TPadesRevisionAnalysisReport;
  Sig: TPadesSignatureRevisionAnalysis;
  Change: TPadesRevisionObjectChange;
  i, j: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    Report := Pdf.AnalyzeSignatureRevisions;   // įrašas, nieko atlaisvinti
    for i := 0 to High(Report.Signatures) do
    begin
      Sig := Report.Signatures[i];
      if not (prrPageContentChanged in Sig.Risks) then
        Continue;
      Writeln(Format('Signature %d (DocMDP P=%d): page content changed',
        [Sig.SignatureIndex, Sig.DocMdpPermission]));
      for j := 0 to High(Sig.Changes) do
      begin
        Change := Sig.Changes[j];
        if Change.Kind <> prckPageContent then
          Continue;
        Writeln(Format('  revision %d  object %d %d R  %s%s',
          [Change.RevisionIndex, Change.ObjectNumber, Change.Generation,
           GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Change.Decision)),
           ShadowTag[Change.IsAuthoritative]]));
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Kas nutinka, kai FieldMDP ir anotacija dalijasi vienu objektu?

Kai lauko /V ir anotacijos /Contents rodo į tą patį netiesioginį objektą, v3.126.2 FieldMDP užraktą palaiko galiojantį, nors pakeitimas klasifikuojamas kaip anotacijos redagavimas. Scenarijų lengva susukti rankomis: pasirašęs užrakina Total lauką su FieldMDP (ISO 32000-1 §12.8.2.4), o puolėjas padaro, kad tekstinės anotacijos /Contents rodo į tą patį eilutės objektą, laikantį lauko reikšmę. Po P=3 anotacijos redagavimas leidžiamas, tad iki pataisymo tos bendros eilutės perrašymas pakeisdavo užrakinto lauko reikšmę su leistu verdiktu

Objektas dabar neša ir anotacijos, ir formos roles, o anotacijos sprendimas bet kada, kai parašas turi FieldMDP transformaciją, dar kartą patikrina formos pusę:

  • Po P=2 anotacijos pakeitimas draudžiamas be pasigailėjimo – tiksliai kaip ir anksčiau
  • Su FieldMDP All užrakinti visi laukai, tad bendras pakeitimas yra prdDisallowed
  • Su FieldMDP Include arba Exclude analizatorius bendro skalioriaus negali atsekti iki vieno lauko pavadinimo, tad sprendimas – prdIndeterminate, o ne spėjimas
  • Be FieldMDP galioja P=3 anotacijos taisyklė, ir pakeitimas lieka leistinas
PDFium Component FieldMDP sprendimų diagrama, kur užrakinto Total lauko /V ir anotacijos /Contents rodo į tą patį netiesioginį objektą, šakojantis per DocMDP P=2, FieldMDP All, FieldMDP Include arba Exclude ir FieldMDP nebuvimą į prdDisallowed, prdIndeterminate arba prdAllowed verdiktus tam pačiam bendram pakeitimui
Kai vienas netiesioginis objektas neša ir anotacijos, ir formos roles, anotacijos sprendimas dar kartą tikrina FieldMDP užraktą, tad tas pats redagavimas svyruoja nuo leistino iki draudžiamo iki neaiškaus

Viena ataskaitų detalė svarbi vartų kodui. Bendras atvejis pranešamas kaip Kind = prckAnnotation su Decision = prdIndeterminate, o prrFieldMdpUnresolved į rizikų aibę pridedamas tik pakeitimams, klasifikuotiems kaip formos laukai. Vartai, ieškantys prrFieldMdpUnresolved ir ignoruojantys Status, šio atvejo nepastebi visiškai

Kaip Delphi kodas turėtų kloti saugiai revizijų analizėje?

Delphi kodas pasirašytą dokumentą turėtų priimti tik tada, kai analizės būsena yra prasNoLaterChanges arba prasAllowed ir nėra jokios struktūrinės rizikos, o prasIndeterminate ir prasSuspicious turėtų laikyti nepasitikėjimo reikšmėmis, o ne į žurnalą įrašomais ir praleidžiamais įspėjimais. Indeterminate reiškia, kad analizatorius nepajėgė įrodyti, jog vėlesnės revizijos buvo leistos; puolėjui įvestis, patikimai duodanti Indeterminate, toks pat naudingas dalykas kaip duodanti Allowed, jei jūsų kodas ją praleidžia. Globalioji AnalyzePadesSignatureRevisions ima bet kokį TStream ir skaito jį nuo 0 pozicijos – tinka įkėlimo doroklėms, kurioms dokumento atvaizduoti nereikia

uses
  Classes, SysUtils, TypInfo, FPdfPades;

const
  // Dubliuoti apibrėžimai užfiksuojami nežeminant Status
  BlockingRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
    prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
    prrSignatureObjectRedefined, prrCompressedObjectUnresolved,
    prrResourceLimitExceeded];

function SignedRevisionsAcceptable(const FileName: string;
  out Reason: string): Boolean;
var
  Source: TFileStream;
  Report: TPadesRevisionAnalysisReport;
begin
  Result := False;
  Reason := '';
  Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Report := AnalyzePadesSignatureRevisions(Source);
  finally
    Source.Free;
  end;
  if Report.SignatureCount = 0 then
  begin
    Reason := 'no signature anchors the analysis';
    Exit;
  end;
  if Report.Risks * BlockingRisks <> [] then
  begin
    Reason := 'structural risk in the revision chain';
    Exit;
  end;
  case Report.Status of
    prasNoLaterChanges, prasAllowed:
      Result := True;
  else
    // prasIndeterminate ir prasSuspicious – atmetimai, o ne įspėjimai
    Reason := 'revision status ' +
      GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
  end;
end;

Dvi ribos verta pasakytos atviro tekstu. TPadesRevisionAnalysisReport apie CMS vientisumą ar liudijimų pasitikėjimą nesako nieko, tad šie vartai stovi šalia kriptografinės ir pasitikėjimo patikros, o ne jos vietoje. Ir teisingas nuosavybės grafas P=3 kiekvienam darbui saugiu nepadaro. P=3 anotacijas leidžia iš tikrųjų, o anotacija su nepermatoma išvaizda gali uždengti pasirašytą tekstą neliesdama nė vieno turinio srauto. Jei jūsų sertifikuoti dokumentai yra sutartys, o ne peržiūros kopijos, arba sertifikuokite su P=2, arba leistus anotacijų pakeitimus nukreipkite žmogui, kaip šiame pagalbininke:

function AllowedAnnotationEditsUnderP3(
  const Report: TPadesRevisionAnalysisReport): Integer;
var
  i, j: Integer;
begin
  Result := 0;
  for i := 0 to High(Report.Signatures) do
    if Report.Signatures[i].DocMdpPermission = 3 then
      for j := 0 to High(Report.Signatures[i].Changes) do
        if (Report.Signatures[i].Changes[j].Kind = prckAnnotation) and
           (Report.Signatures[i].Changes[j].Decision = prdAllowed) then
          Inc(Result);
end;

Parašo revizijų audito kontrolinis sąrašas

Naudokite šį sąrašą, kad patikrintumėte, ar jūsų patikros grandinė buvo atvira, ir ar dabar ji kloja saugiai:

  • PDFium Component variantai iki v3.126.2 galėjo pranešti prasAllowed puslapio turinio redagavimams DocMDP P=3 dokumentuose; paleiskite TPdf.AnalyzeSignatureRevisions dar kartą ant sertifikuotų P=3 failų, priimtų senesnių variantų
  • Dar kartą patikrinkite P=3 dokumentus su FieldMDP užraktais, kur lauko reikšmė ir anotacija gali dalintis netiesioginiu objektu
  • Priimkite tik prasNoLaterChanges ir prasAllowed; prasIndeterminate ir prasSuspicious laikykite nepasitikėjimo reikšmėmis
  • Tikrinkite Report.Risks taip pat kaip Report.Status, nes prrDuplicateObjectDefinition pats savaime būsenos nekeičia
  • Tuščio Changes masyvo nelaikykite švariu rezultatu, kai parašo būsena Indeterminate; žlugęs rolių kūrimas nepraneša jokių pakeitimų
  • Vien prrFieldMdpUnresolved remdamiesi FieldMDP problemų negausite, nes bendros anotacijos atvejis iškyla tik per sprendimą ir būseną
  • Nuspręskite, ar jūsų darbe leistini anotacijų pakeitimai po P=3 reikalauja žmogaus peržiūros
  • Analizuokite originalaus failo baitus; SaveAs perrašytas dokumentas revizijų grandinės daugiau neturi

Revizijų analizė – vienas parašo patikros sluoksnis. Suporuokite ją su PDF skaitmeninių parašų ir PAdES lygių apžiūra žodynui ir baseline lygiui, ir su platesniu PDF saugumo rizikų auditu dėl JavaScript, paleidimo veiksmų ir įdėtųjų failų. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions ir čia aprašytas nuosavybę atsimenantis rolių grafas keliauja su PDFium Component Delphi, C++Builder ir Lazarus