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:
- Podpisový widget je
/Subtype /Widgets/FT /Sig, takže dostane roli anotace /Pwidgetu natlačí roli anotace na slovník stránky, který roli stránky už má- Stránka natlačí obě role do
/Contents,/Resourcesa přes/Parentnahoru do stromu Pages a dál ke každé sourozennecké stránce - 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
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ík | Klíče bráné jako navigace | Reference na specifikaci |
|---|---|---|
| Uzel Page nebo Pages | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Anotace nebo widget | /P | ISO 32000-1 §12.5.2 |
| Slovník widgetu nebo pole | /Parent | ISO 32000-1 §12.7.3 |
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
/Parentz 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 /Annotnebo 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
/FTrozvíjí zděděný typ pole přes řetěz/Parenta 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
Allje zamčeno každé pole, takže sdílená změna jeprdDisallowed - S FieldMDP
IncludeneboExcludenemůže analyzátor vystopovat sdílený skalár zpět k jednomu názvu pole, takže rozhodnutím jeprdIndeterminatemísto odhadu - Bez FieldMDP platí pravidlo anotace P=3 a změna zůstává povolená
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
prasAllowedpro úpravy obsahu stránek v dokumentech DocMDP P=3; pusťte znovuTPdf.AnalyzeSignatureRevisionsnad 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
prasNoLaterChangesaprasAllowed;prasIndeterminateaprasSuspiciousberte jako nedůvěryhodné - Testujte
Report.Risksstejně jakoReport.Status, protožeprrDuplicateObjectDefinitionnezmění stav sám o sobě - Nečtěte prázdné pole
Changesjako čistý výsledek, když je stav podpisu Indeterminate; selhaná stavba rolí nehlásí žádné změny - Nespoléhejte na samotné
prrFieldMdpUnresolvedk 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
SaveAsuž ř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