Technický článek

PDFium Component DocMDP: jak widget /P zakryl úpravy stránek

V buildech PDFium Component před v3.126.2 mohl TPdf.AnalyzeSignatureRevisions ohodnotit skutečnou úpravu obsahu stránky jako povolenou změnu anotace pod DocMDP P=3, protože jeho graf rolí revizí bral zpětnou referenci /P podpisového widgetu na jeho stránku jako vlastnictví. Od v3.126.2 odděluje PDFium Component navigační hrany od hran vlastněného payloadu, takže obsah stránky zůstává obsahem stránky. Bug report za touhle opravou vypadá na papíře neškodně. Certifikovaná smlouva povoluje anotace, protistrana přidá jeden inkrementální zápis a analyzátor tvrdí, že každá pozdější změna je povolená. Pak někdo porovná vykreslené stránky a částka k úhradě na straně 2 je jiná

Tenhle článek je útočníkem viděné pokračování přehledu analýzy změn revizí po podpisu, takže vynechává základy rekonstrukce revizí a hodnocení DocMDP a jde přímo na objektový graf: jak se vlastnictví modelovalo, proč směr hrany rozhoduje o bezpečnostním verdiktu, co se změnilo ve v3.126.2 a jak auditovat vlastní akceptační logiku

Proč prošla úprava stránky jako změna anotace pod DocMDP P=3?

Úprava stránky prošla, protože starý graf rolí následoval každou nepřímou referenci ve slovníku, jako by odkazovaný objekt patřil odkazujícímu, a podpisový widget se odkazuje zpět na svou stránku. Slovník anotace nese /P, nepřímou referenci na objekt stránky, na které sedí (ISO 32000-1 §12.5.2). Ta položka je navigační hint. Widget nevlastní stránku; stránka vlastní widget přes své pole /Annots

Analyzátor přiřadí každému objektu sadu rolových bitů, dřív než ohodnotí pozdější změny: stránka, anotace, formulář a validační materiál. Kořenové objekty dostanou roli z vlastního slovníku a role se pak šíří na všechno, na co odkazují. Ve staré propagaci šel řetězec takhle:

  1. Podpisový widget je /Subtype /Widget s /FT /Sig, takže dostane roli anotace
  2. /P widgetu natlačí roli anotace na slovník stránky, který roli stránky už má
  3. Stránka natlačí obě role do /Contents, /Resources a přes /Parent nahoru do stromu Pages a dál ke každé sourozennecké stránce
  4. Slovník content streamu jako << /Length 812 >> nemá /Type, takže klasifikátor padl na rolové bity a zkontroloval roli anotace dřív než roli stránky
Diagram grafu rolí DocMDP v PDFium Component před v3.126.2, kde zpětná reference /P podpisového widgetu natlačí roli anotace na slovník stránky, stránka ji přes /Contents rozšíří na content stream bez položky /Type, klasifikátor vypustí prckAnnotation a hodnocení P=3 vrátí prdAllowed
Před v3.126.2 bral graf rolí každou nepřímou referenci jako vlastnictví, takže položka /P widgetu natlačila roli anotace na stránku a skutečná úprava stránky vyšla z analyzátoru jako povolená změna anotace

Modifikovaný content stream proto vyšel jako prckAnnotation. Podle ISO 32000-1 §12.8.2.2 povoluje DocMDP P=3 změny anotací, takže rozhodnutím bylo prdAllowed a report se sroloval do prasAllowed. Tentýž soubor pod P=2 se odmítl, ale jen náhodou: P=2 zakazuje změny anotací, takže špatně označkovaný stream byl odmítnutý pro špatný důvod. Pevná čtyřprůchodová smyčka propagace přidala druhou slabost. Payload dospělý přes nepřímé pole, nebo dlouhým řetězcem, jehož čísla objektů běží pozpátku, nedostal nikdy žádnou roli

Proč se validátor podpisu musí ptát, kdo vlastní objekt?

Validátor podpisu se musí ptát, kdo vlastní objekt, protože inkrementální aktualizace PDF (ISO 32000-1 §7.5.6) dovolí komukoli připsat revizi, která předefinuje existující číslo objektu, a předefinované tělo neoznamuje, čím je. Podpis pořád ověří, protože pokrývá jen bajty vlastní revize. Každá obrana proti manipulaci po podpisu proto stojí na mapování každého změněného objektu na strukturu, která ho používá, a na otázce, zda podepisující dovolil, aby se tahle struktura měnila

Několik publikovaných tříd útoků funguje přesně v té díře. Incremental saving attacks připsají revizi, která vymění obsah stránky, a spoléhají na to, že verifikátor kontroluje jen podepsaný bajtový rozsah. Shadow attacks zasadí skrytý obsah před podpisem a aktivují ho potom malou, nevinně vypadající změnou. Útoky na certifikované dokumenty zneužívají to, že P=2 a P=3 explicitně povolují některé pozdější úpravy, a pak obléknou zakázanou úpravu do kabátu povolené. Verifikátor, který klasifikuje objekty podle štítků jako /Type /Annot, nebo podle jakékoli referenční cesty, která je náhodou dostane, je vystavený třetí třídě: útočník potřebuje jedinou povolenou strukturu, která dosáhne na zakázanou

Proto otázka nezní, které objekty se změnily, ale kdo je vlastní. Content stream dospělý ze stránky přes /Contents je obsah stránky, ať na něj ukazuje cokoli dalšího. Anotace odkazující zpět na stránku přes /P říká, kde anotace bydlí, ne co vlastní

Jak modeluje PDFium Component v3.126.2 vlastnictví?

PDFium Component v3.126.2 bere zpětné reference jako navigaci, drží je mimo propagaci rolí a rozhoduje o tom, které klíče se počítají za navigaci, podle strukturální role slovníku, který je drží, ne podle samotného názvu klíče. Tabulka shrnuje navigační klíče, které už vlastnictví nenosí

Vlastnící slovníkKlíče bráné jako navigaceReference na specifikaci
Uzel Page nebo Pages/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
Anotace nebo widget/PISO 32000-1 §12.5.2
Slovník widgetu nebo pole/ParentISO 32000-1 §12.7.3
Objektový graf PDFium Component v3.126.2 oddělující hrany vlastněného payloadu jako /Contents a /Annots, které šíří role stránky a anotace, od navigačních hran jako /P widgetu, které žádné role nenosí, s navigačními klíči podle vlastnícího slovníku a prckPageContent zachovaným na content streamu i pod padělaným /Type
v3.126.2 drží zpětné reference mimo propagaci rolí: role putují jen skutečným vlastnictvím, takže content stream zůstává obsahem stránky a hint /P nerozhoduje o ničem

Globální filtrování podle názvu klíče by vybilo novou díru. Font nebo XObject resource se může legitimně jmenovat /P, /Parent nebo /Annots a slovník /Resources, který vypustí svou položku /P z propagace, by útočníkovi dovolil schovat stránkou vlastněný XObject za nevinný název resource. Ve v3.126.2 se navigační filtr uplatní jen tehdy, když vlastnící slovník doopravdy je stránka, uzel Pages, anotace, widget nebo pole. Nese-li jeden z těch slovníků zdvojený navigační klíč, třeba dvě položky /P ve widgetu, analyzátor nehádá, kterou kopii by prohlížeč použil; stavba rolí selže a podpis se stane Indeterminate

Několik dalších pravidel zavírá zbývající cesty přelepkování rolí:

  • Uzly Pages jsou kořeny role stránky samy o sobě, takže resources děděné ze stromu Pages (ISO 32000-1 §7.7.3.4) vstupují do kontextu stránky skutečným vlastnictvím, ne chůzí po /Parent z dětské stránky
  • Role anotace nebo formuláře, která dorazí ke slovníku katalogu, uzlu Pages, stránce, anotaci nebo poli, se tam zastaví, protože ty strukturální objekty zakládají vlastní role a příchozí roli payloadu nesmí přebít
  • Role stránky je při klasifikaci autoritativní: stránkou vlastněný objekt je prckPageContent, i když pozdější revize ho přepíše padělaným /FT, štítkem /Type /Annot nebo ho nasdílí s appearance streamem
  • Form XObject použitý jen jako appearance pole nebo anotace si drží kategorii formuláře nebo anotace, takže obyčejná regenerace appearance po vyplnění formuláře se pořád hodnotí pod normálními pravidly oprávnění
  • Widget bez vlastního /FT rozvíjí zděděný typ pole přes řetěz /Parent a nevyřešitelný řetěz zboří stavbu rolí místo výchozí hodnoty anotace
  • Rolové bity z každé pozdější revize se slijí do rolí pokryté revize, takže pozdější aktualizace nesmí smazat dřívější vztah vlastnictví stránky tím, že stream nejdřív odpojí a pak ho upraví

Fixní bod místo pevného počtu průchodů

Dosažitelnost rolí ve v3.126.2 běží jako pracovní fronta, která iteruje, dokud žádný objekt nezíská nový rolový bit, což je pravý fixní bod bez ohledu na hloubku řetězce nebo číslování objektů. Nepřímá pole, jako pole /Contents uložené jako vlastní objekt, se taky procházejí. Každý objekt může získat nejvýš čtyři různé rolové bity, takže fronta se omezuje na čtyři položky na číslo objektu; překročí-li se ten rozpočet, vyskočí prrResourceLimitExceeded. Reference na volný objekt, nesouhlas generace nebo porouchaná hlavička objektu vyhodí prrMalformedRevisionChain a payload uvnitř komprimovaného object streamu vyhodí prrCompressedObjectUnresolved. Každé z těch selhání končí prasIndeterminate, nikdy povoleným verdiktem, a když se selhání stane při stavbě rolí pokryté revize, nehlásí podpis vůbec žádné Changes

Následující rutina vypisuje úpravy obsahu stránky, které touto analýzou projdou. Změna prckPageContent se nikdy nehodnotí jako prdAllowed: DocMDP P=1, 2 nebo 3 z ní udělá prdDisallowed a podpis bez DocMDP ji hodnotí jako 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;   // record, nic k uvolnění
    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;

Co se stane, když FieldMDP a anotace sdílejí jeden objekt?

Když /V pole a /Contents anotace ukazují na týž nepřímý objekt, drží v3.126.2 zámek FieldMDP v platnosti, i když je změna klasifikovaná jako úprava anotace. Scénář se dá postavit ručně snadno: podepisující zamkne pole Total přes FieldMDP (ISO 32000-1 §12.8.2.4) a útočník donutí /Contents textové anotace, aby odkazovalo na tentýž řetězcový objekt, který drží hodnotu pole. Pod P=3 je úprava anotace povolená, takže před opravou přepsání toho sdíleného řetězce změnilo zamčenou hodnotu pole s povoleným verdiktem

Objekt teď nese roli anotace i formuláře a rozhodnutí o anotaci překontroluje stranu formuláře vždycky, když má podpis transformaci FieldMDP:

  • Pod P=2 je změna anotace rovnou zakázaná, úplně jako dřív
  • S FieldMDP All je zamčeno každé pole, takže sdílená změna je prdDisallowed
  • S FieldMDP Include nebo Exclude nemůže analyzátor vystopovat sdílený skalár zpět k jednomu názvu pole, takže rozhodnutím je prdIndeterminate místo odhadu
  • Bez FieldMDP platí pravidlo anotace P=3 a změna zůstává povolená
Diagram rozhodování FieldMDP v PDFium Component, kde zamčené pole Total s /V a anotace s /Contents odkazují na týž nepřímý objekt, s větvením nad DocMDP P=2, FieldMDP All, FieldMDP Include nebo Exclude a bez FieldMDP na verdikty prdDisallowed, prdIndeterminate a prdAllowed pro tutéž sdílenou úpravu
Nese-li jeden nepřímý objekt roli anotace i formuláře, překontroluje rozhodnutí o anotaci zámek FieldMDP, takže tutáž úprava sahá od povolené přes zakázanou po neurčitou

Jeden detail reportování záleží pro bránový kód. Sdílený případ se hlásí jako Kind = prckAnnotation s Decision = prdIndeterminate a prrFieldMdpUnresolved se do sady rizik přidává jen u změn klasifikovaných jako formulářová pole. Brána, která hledá prrFieldMdpUnresolved a ignoruje Status, tenhle případ minie úplně

Jak má kód v Delphi selhat zavřeně (fail closed) při analýze revizí?

Kód v Delphi by měl přijmout podepsaný dokument jen tehdy, když je stav analýzy prasNoLaterChanges nebo prasAllowed a není přítomné žádné strukturální riziko, a prasIndeterminate a prasSuspicious by měl brát jako nedůvěryhodné, ne jako varování k zalogování a propuštění. Indeterminate znamená, že analyzátor nedokázal dokázat, že pozdější revize byly povolené; pro útočníka je vstup, který spolehlivě vyrobí Indeterminate, stejně užitečný jako ten, který vyrobí Allowed, pokud ho váš kód pustí dál. Globální AnalyzePadesSignatureRevisions bere jakýkoli TStream a čte ho od pozice 0, což sedí upload handlerům, které dokument nemusí nikdy vykreslit

uses
  Classes, SysUtils, TypInfo, FPdfPades;

const
  // Zdvojené definice se zaznamenají bez snížení 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 a prasSuspicious jsou zamítnutí, ne varování
    Reason := 'revision status ' +
      GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
  end;
end;

Dvě hranice stojí za výslovné řeč. TPadesRevisionAnalysisReport neříká nic o integritě CMS ani o důvěře v certifikáty, takže tahle brána sedí vedle kryptografické a trust validace, ne na jejich místě. A správný graf vlastnictví nedělá P=3 bezpečné pro každý workflow. P=3 anotace doopravdy povoluje a anotace s neprůhledným appearance může překrýt podepsaný text, aniž by se dotkla jediného content streamu. Jsou-li vaše certifikované dokumenty smlouvy, ne recenzní kopie, certifikujte buď s P=2, nebo nechávejte povolené změny anotací na člověka, jako v tomhle pomocníkovi:

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;

Kontrolní seznam auditu revizí podpisu

Použijte tenhle seznam ke kontrole, jestli byl váš validační pipeline vystavený a jestli teď selhává zavřeně:

  • Buildy PDFium Component před v3.126.2 mohly hlásit prasAllowed pro úpravy obsahu stránek v dokumentech DocMDP P=3; pusťte znovu TPdf.AnalyzeSignatureRevisions nad certifikovanými soubory P=3 přijatými staršími buildy
  • Znovu zkontrolujte dokumenty P=3 se zámky FieldMDP, kde hodnota pole a anotace mohou sdílet nepřímý objekt
  • Přijímejte jen prasNoLaterChanges a prasAllowed; prasIndeterminate a prasSuspicious berte jako nedůvěryhodné
  • Testujte Report.Risks stejně jako Report.Status, protože prrDuplicateObjectDefinition nezmění stav sám o sobě
  • Nečtěte prázdné pole Changes jako čistý výsledek, když je stav podpisu Indeterminate; selhaná stavba rolí nehlásí žádné změny
  • Nespoléhejte na samotné prrFieldMdpUnresolved k odchytu problémů FieldMDP, protože sdílený případ anotace vypluje jen přes rozhodnutí a stav
  • Rozhodněte, zda povolené změny anotací pod P=3 potřebují ve vašem workflow lidskou revizi
  • Analyzujte původní bajty souboru; dokument přepsaný přes SaveAs už řetěz revizí neobsahuje

Analýza revizí je jedna vrstva kontroly podpisu. Spojte ji s prohlížením digitálních podpisů PDF a úrovní PAdES kvůli slovníkům a baseline úrovním a se širším auditem bezpečnostních rizik PDF kvůli JavaScriptu, launch akcím a vloženým souborům. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions a graf rolí citlivý na vlastnictví popsaný tady dodává PDFium Component pro Delphi, C++Builder a Lazarus