Teknisk artikel

PDFium Component DocMDP: hur en widget-/P dolde sidändringar

I PDFium Component-byggen före v3.126.2 kunde TPdf.AnalyzeSignatureRevisions betygsätta en verklig sidinnehållsändring som en tillåten annoteringsändring under DocMDP P=3, för dess revisionsrollgraf behandlade signaturwidgets /P-bakåtreferens till sin sida som ägande. Sedan v3.126.2 skiljer PDFium Component navigationskanter från ägda-nylastkanter, så sidinnehåll förblir sidinnehåll. Buggrapporten bakom den här fixen ser harmlös ut på papperet. Ett certifierat avtal tillåter annoteringar, en motpart lägger till en stegvis sparning, och analysatorn säger att varje senare ändring är tillåten. Sedan diffar någon de renderade sidorna och betalningsbeloppet på sida 2 är ett annat

Den här artikeln är uppföljningen ur angriparens ögon till översikten av analys av revisionsändringar efter signatur, så den hoppar över grunderna i revisionsåterbyggnad och DocMDP-betygssättning och går rakt på objektgrafen: hur ägandet modellerades, varför en kants riktning avgör en säkerhetsdom, vad som ändrades i v3.126.2, och hur man granskar sin egen acceptanslogik

Varför gick en sidändring igenom som en annoteringsändring under DocMDP P=3?

Sidändringen gick igenom för att den gamla rollgrafen följde varje indirekt referens i en ordlista som om det refererade objektet tillhörde det refererande, och signaturwidgeten pekar tillbaka på sin sida. En annoteringsordlista bär /P, en indirekt referens till sidobjektet den sitter på (ISO 32000-1 §12.5.2). Den posten är en navigeringsledtråd. Widgeten äger inte sidan; sidan äger widgeten genom sin /Annots-matris

Analysatorn tilldelar varje objekt en uppsättning rollbitar innan den betygsätter senare ändringar: sida, annotering, formulär och valideringsmaterial. Rotobjekt får sin roll från sin egen ordlista, och rollen sprids sedan till allt de refererar. I den gamla spridningen gick kedjan så här:

  1. Signaturwidgeten är en /Subtype /Widget med /FT /Sig, så den får annoteringsrollen
  2. Widgetens /P trycker annoteringsrollen på sidordlistan, som redan har sidrollen
  3. Sidan trycker båda rollerna in i /Contents, /Resources och, via /Parent, upp i Pages-trädet och över till varje syskonsida
  4. En innehållsströmordlista som << /Length 812 >> har ingen /Type, så klassificeraren föll tillbaka på rollbitar och kontrollerade annoteringsrollen före sidrollen
PDFium Component-diagram över DocMDP-rollgrafen före v3.126.2 där en signaturwidgets /P-bakåtreferens trycker annoteringsrollen på sidordlistan, sidan sprider den via /Contents till en innehållsström utan /Type-post, klassificeraren matar ut prckAnnotation och P=3-betygssättningen returnerar prdAllowed
Före v3.126.2 behandlade rollgrafen varje indirekt referens som ägande, så widgetens /P-post tryckte annoteringsrollen på sidan och en verklig sidändring lämnade analysatorn som en tillåten annoteringsändring

Den ändrade innehållsströmmen kom därför ut som prckAnnotation. Enligt ISO 32000-1 §12.8.2.2 tillåter DocMDP P=3 annoteringsändringar, så beslutet var prdAllowed och rapporten rullades upp till prasAllowed. Samma fil under P=2 avvisades, men bara av en händelse: P=2 förbjuder annoteringsändringar, så den felmärkta strömmen avslogs av fel skäl. En fast fyra-pass-spridningsloop lade till en andra svaghet. Nylast som nås via en indirekt matris, eller via en lång kedja vars objektnummer löper baklänges, kunde aldrig få någon roll alls

Varför måste en signaturvaliderare fråga vem som äger ett objekt?

En signaturvaliderare måste fråga vem som äger ett objekt för att PDF-stegvisa uppdateringar (ISO 32000-1 §7.5.6) låter vem som helst lägga till en revision som omdefinierar ett befintligt objektnummer, och den omdefinierade kroppen ropar inte ut vad den är. Signaturen verifieras fortfarande, eftersom den bara täcker bytes i sin egen revision. Varje försvar mot manipulation efter signeringen beror därför på att mappa varje ändrat objekt till strukturen som använder det, och sedan fråga om undertecknaren tillät att den strukturen ändrades

Flera publicerade attackklasser arbetar exakt i det glappet. Incremental saving attacks lägger till en revision som byter ut sidinnehåll och förlitar sig på att verifieraren bara kontrollerar det signerade byte-området. Shadow attacks planterar dolt innehåll före signeringen och aktiverar det efteråt med en liten, oskyldigt seende ändring. Attacker på certifierade dokument missbrukar det faktum att P=2 och P=3 uttryckligen tillåter vissa senare ändringar, och klär sedan ut en förbjuden ändring som en tillåten. En verifierare som klassificerar objekt efter etiketter som /Type /Annot, eller efter vilken referensväg som råkar nå dem, är utsatt för den tredje klassen: angriparen behöver bara en tillåten struktur som kan nå den förbjudna

Det är därför frågan inte är vilka objekt som ändrades utan vem som äger dem. En innehållsström som nås från en sida via /Contents är sidinnehåll oavsett vad annat pekar på den. En annotering som pekar tillbaka på sidan via /P säger var annoteringen bor, inte vad den äger

Hur modellerar PDFium Component v3.126.2 ägande?

PDFium Component v3.126.2 behandlar bakåtreferenser som navigering, håller dem utanför rollspridningen, och avgör vilka nycklar som räknas som navigering utifrån den strukturella rollen hos ordlistan som håller dem, inte från nyckelnamnet ensamt. Tabellen sammanfattar navigeringsnycklarna som inte längre bär ägande

Ägande ordlistaNycklar behandlade som navigeringSpecifikationsreferens
Sida eller Pages-nod/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
Annotering eller widget/PISO 32000-1 §12.5.2
Widget- eller fältordlista/ParentISO 32000-1 §12.7.3
PDFium Component v3.126.2-objektgraf som skiljer ägda-nylastkanter som /Contents och /Annots, som sprider sid- och annoteringsroller, från navigationskanter som widget-/P, som inte bär roller, med navigeringsnycklarna per ägande ordlista och prckPageContent kvar på innehållsströmmen även under en förfalskad /Type
v3.126.2 håller bakåtreferenser utanför rollspridningen: roller färdas bara genom verkligt ägande, så innehållsströmmen förblir sidinnehåll och /P-ledtråden avgör ingenting

Att filtrera på nyckelnamn globalt hade skapat ett nytt hål. En typsnitts- eller XObject-resurs kan legitimt heta /P, /Parent eller /Annots, och en /Resources-ordlista som släpper sin /P-post ur spridningen skulle låta en angripare gömma ett sida-ägt XObject bakom ett oskyldigt resursnamn. I v3.126.2 tillämpas navigeringsfiltret bara när den ägande ordlistan faktiskt är en sida, Pages-nod, annotering, widget eller fält. Bär en av de ordlistorna en duplicerad navigeringsnyckel, som två /P-poster i en widget, gissar analysatorn inte vilken kopia en visare skulle använda; rollbygget misslyckas och signaturen blir Indeterminate

Flera ytterligare regler stänger de kvarvarande ommärkningsvägarna:

  • Pages-noder är sidrollsrötter i egen rätt, så resurser ärvda från Pages-trädet (ISO 32000-1 §7.7.3.4) kommer in i sidokontexten genom verkligt ägande, inte genom en /Parent-vandring från en barnsida
  • En annoterings- eller formulärroll som anländer till en katalog, Pages-nod, sida, annotering eller fältordlista stannar där, för de strukturella objekten etablerar egna roller och en inkommande nylastroll får inte åsidosätta dem
  • Sidrollen är auktoritativ under klassificeringen: ett sida-ägt objekt är prckPageContent även om en senare revision skriver om det med en förfalskad /FT, en /Type /Annot-etikett, eller delar det med en utseendeström
  • Ett Form XObject använt bara som utseende för ett fält eller en annotering behåller sin formulär- eller annoteringskategori, så ordinell utseenderegeneration efter en formulärifyllning betygsätts fortfarande under de normala tillståndsreglerna
  • En widget utan egen /FT löser upp den ärvda fälttypen genom /Parent-kedjan, och en ouppklarlig kedja får rollbygget att misslyckas i stället för att falla tillbaka på annotering
  • Rollbitar från varje senare revision slås samman till den täckta revisionens roller, så en senare uppdatering kan inte radera en tidigare sida-ägande-relation genom att först frigöra en ström och redigera den efteråt

Fixpunkt i stället för ett fast passantal

Rollräckvidd i v3.126.2 körs som en arbetskö som itererar tills inget objekt vinner en ny rollbit, vilket är en äkta fixpunkt oavsett kedjedjup eller objektnummerering. Indirekta matriser, som en /Contents-matris lagrad som sitt eget objekt, vandras också. Varje objekt kan vinna högst fyra distinkta rollbitar, så kön är avgränsad till fyra poster per objektnummer; överskrids den budgeten kastas prrResourceLimitExceeded. En referens till ett fritt objekt, en felmatchande generation eller ett trasigt objekthuvud kastar prrMalformedRevisionChain, och nylast inuti en komprimerad objektström kastar prrCompressedObjectUnresolved. Var och en av dessa misslyckanden slutar med prasIndeterminate, aldrig med en tillåtande dom, och sker misslyckandet medan den täckta revisionens roller byggs rapporterar signaturen inga Changes alls

Följande rutin listar sidinnehållsändringarna som överlever den här analysen. En prckPageContent-ändring betygsätts aldrig prdAllowed: DocMDP P=1, 2 eller 3 gör den till prdDisallowed, och en signatur utan DocMDP betygsätter den 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, inget att frigöra
    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;

Vad händer när FieldMDP och en annotering delar ett objekt?

När ett fälts /V och en annoterings /Contents pekar på samma indirekta objekt håller v3.126.2 FieldMDP-låset kvar även om ändringen klassificeras som en annoteringsredigering. Scenariot är lätt att bygga för hand: en undertecknare låser fältet Total med FieldMDP (ISO 32000-1 §12.8.2.4), och angriparen låter en textannoterings /Contents referera samma strängobjekt som håller fältvärdet. Under P=3 är annoteringsredigeringen tillåten, så före fixen ändrade en omskrivning av den delade strängen ett låst fältvärde med en tillåtande dom

Objektet bär nu både annoterings- och formulärrollerna, och annoteringsbeslutet kontrollerar formulärsidan på nytt närhelst signaturen har en FieldMDP-transform:

  • Under P=2 är annoteringsändringen förbjuden rakt av, precis som före
  • Med FieldMDP All är varje fält låst, så den delade ändringen är prdDisallowed
  • Med FieldMDP Include eller Exclude kan analysatorn inte spåra en delad skalär tillbaka till ett fältnamn, så beslutet är prdIndeterminate i stället för en gissning
  • Utan FieldMDP gäller P=3-annoteringsregeln och ändringen förblir tillåten
PDFium Component FieldMDP-beslutsdiagram där ett låst Total-fälts /V och en annoteringens /Contents refererar samma indirekta objekt, förgrenande över DocMDP P=2, FieldMDP All, FieldMDP Include eller Exclude och ingen FieldMDP till prdDisallowed-, prdIndeterminate- eller prdAllowed-domar för samma delade ändring
När ett indirekt objekt bär både annoterings- och formulärrollerna kontrollerar annoteringsbeslutet FieldMDP-låset på nytt, så samma ändring spänner från tillåten till förbjuden till obestämd

En rapporteringsdetalj spelar roll för grindkod. Det delade fallet rapporteras som Kind = prckAnnotation med Decision = prdIndeterminate, och prrFieldMdpUnresolved läggs till riskuppsättningen bara för ändringar klassificerade som formulärfält. En grind som söker efter prrFieldMdpUnresolved och ignorerar Status missar det här fallet helt

Hur ska Delphi-kod avvisa vid osäkerhet i revisionsanalys?

Delphi-kod ska acceptera ett signerat dokument bara när analysstatusen är prasNoLaterChanges eller prasAllowed och ingen strukturell risk finns, och den ska behandla prasIndeterminate och prasSuspicious som otillförlitliga, inte som varningar att logga och släppa fram. Indeterminate betyder att analysatorn inte kunde bevisa att de senare revisionerna var tillåtna; för en angripare är en indata som pålitligt producerar Indeterminate lika användbar som en som producerar Allowed om din kod släpper igenom den. Den globala AnalyzePadesSignatureRevisions tar vilken TStream som helst och läser den från position 0, vilket passar uppladdningshanterare som aldrig behöver rendera dokumentet

uses
  Classes, SysUtils, TypInfo, FPdfPades;

const
  // Duplicata definitioner registreras utan att sänka 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 och prasSuspicious är avvisanden, inte varningar
    Reason := 'revision status ' +
      GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
  end;
end;

Två gränser är värda att säga rakt ut. TPadesRevisionAnalysisReport säger ingenting om CMS-integritet eller certifikattillit, så den här grinden sitter bredvid kryptografisk och tillitsvalidering, inte i deras ställe. Och en korrekt ägandegraf gör inte P=3 säkert för varje arbetsflöde. P=3 tillåter genuint annoteringar, och en annotering med ett opak utseende kan täcka signerad text utan att röra en enda innehållsström. Är dina certifierade dokument avtal i stället för granskkopior, certifiera antingen med P=2 eller dirigera tillåtna annoteringsändringar till en människa, som i den här hjälparen:

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;

Granskningschecklista för signaturrevisioner

Använd listan för att kontrollera om din verifieringspipeline var utsatt och om den nu avvisar vid osäkerhet:

  • Byggen av PDFium Component före v3.126.2 kunde rapportera prasAllowed för sidinnehållsändringar i DocMDP P=3-dokument; kör TPdf.AnalyzeSignatureRevisions igen på certifierade P=3-filer som äldre byggen accepterat
  • Kontrollera P=3-dokument med FieldMDP-lås igen där ett fältvärde och en annotering kan dela ett indirekt objekt
  • Acceptera bara prasNoLaterChanges och prasAllowed; behandla prasIndeterminate och prasSuspicious som otillförlitliga
  • Testa Report.Risks likväl som Report.Status, för prrDuplicateObjectDefinition ändrar inte statusen av sig själv
  • Läs inte en tom Changes-matris som ett rent resultat när signaturstatusen är Indeterminate; ett misslyckat rollbygge rapporterar inga ändringar
  • Förlita dig inte på prrFieldMdpUnresolved ensamt för att fånga FieldMDP-problem, ty det delade annoteringsfallet yttrar sig bara genom beslutet och statusen
  • Bestäm om tillåtna annoteringsändringar under P=3 behöver mänsklig granskning i ditt arbetsflöde
  • Analysera originalfilens bytes; ett dokument omskrivet av SaveAs innehåller inte längre revisionskedjan

Revisionsanalys är ett lager i en signaturkontroll. Para den med granskning av PDF-digitala signaturer och PAdES-nivåer för ordlistan och baslinjenivån, och med en bredare PDF-säkerhetsriskgranskning för JavaScript, startåtgärder och inbäddade filer. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions och den ägandemedvetna rollgrafen som beskrivs här medföljer PDFium Component för Delphi, C++Builder och Lazarus