Teknisk artikel

PDFium Component DocMDP: Widget-/P skjulte sideændringer

I PDFium Component-builds før v3.126.2 kunne TPdf.AnalyzeSignatureRevisions bedømme en ægte sideindholdsændring som en tilladt annotation-ændring under DocMDP P=3, fordi dets revisionsrolle-graf behandlede signaturens widgets /P-tilbage-reference til sin side som ejerskab. Siden v3.126.2 adskiller PDFium Component navigationskanter fra ejet-payload-kanter, så sideindhold forbliver sideindhold. Bug-rapporten bag dette fix ser harmløs ud på papir. Et certificeret kontrakt tillader annotationer, en modpart tilføjer én inkrementel gemning, og analysatoren siger, at hver senere ændring er tilladt. Så diff'er nogen de renderede sider, og betalingsbeløbet på side 2 er anderledes

Denne artikel er angriberperspektiv-efterfølgeren til oversigten over post-signature revision change analysis, så den springer det grundlæggende i revisionsgenopbygning og DocMDP-bedømmelse over og går direkte til objektgrafen: hvordan ejerskab blev modelleret, hvorfor en kants retning afgør en sikkerhedsdom, hvad der ændrede sig i v3.126.2, og hvordan du reviderer din egen acceptlogik

Hvorfor gik en sideændring igennem som en annotation-ændring under DocMDP P=3?

Sideændringen gik igennem, fordi den gamle rollegraf fulgte hver indirekte reference i en dictionary, som om det refererede objekt tilhørte det refererende, og signaturens widget peger tilbage på sin side. En annotation-dictionary bærer /P, en indirekte reference til sideobjektet, den sidder på (ISO 32000-1 §12.5.2). Den post er et navigationshint. Widgetten ejer ikke siden; siden ejer widgetten gennem sit /Annots-array

Analysatoren tildeler hvert objekt et sæt rolle-bits, før den bedømmer senere ændringer: page, annotation, form og valideringsmateriale. Rodobjekter får deres rolle fra deres egen dictionary, og rollen spreder sig derefter til alt, hvad de refererer. I den gamle propagationskæde gik det sådan:

  1. Signaturens widget er en /Subtype /Widget med /FT /Sig, så den får annotation-rollen
  2. Widgettens /P skubber annotation-rollen over på side-dictionaryen, som allerede har page-rollen
  3. Siden skubber begge roller ind i /Contents, /Resources og gennem /Parent op i Pages-træet og hen over til hver søsterside
  4. En content stream-dictionary som << /Length 812 >> har ingen /Type, så klassifikatoren faldt tilbage på rolle-bits og tjekkede annotation-rollen før page-rollen
PDFium Component-diagram over DocMDP-rollegrafen før v3.126.2, hvor en signaturens widget /P-tilbage-reference skubber annotation-rollen over på side-dictionaryen, siden spreder den gennem /Contents ud på en content stream uden /Type-post, klassifikatoren outputter prckAnnotation, og P=3-bedømmelsen returnerer prdAllowed
Før v3.126.2 behandlede rollegrafen hver indirekte reference som ejerskab, så widgettens /P-post skubbede annotation-rollen over på siden, og en ægte sideændring forlod analysatoren som en tilladt annotation-ændring

Den ændrede content stream kom derfor ud som prckAnnotation. Under ISO 32000-1 §12.8.2.2 tillader DocMDP P=3 annotation-ændringer, så afgørelsen blev prdAllowed, og rapporten rullede op til prasAllowed. Samme fil under P=2 blev afvist, men kun af en tilfældighed: P=2 forbyder annotation-ændringer, så den fejl-labellede stream blev nægtet af den forkerte grund. En fast propagationsløkke med fire gennemløb tilføjede en anden svaghed. Payload nået gennem et indirekte array, eller gennem en lang kæde, hvis objektnumre løber baglæns, modtog måske aldrig nogen rolle overhovedet

Hvorfor skal en signaturvalidator spørge, hvem der ejer et objekt?

En signaturvalidator skal spørge, hvem der ejer et objekt, fordi PDF inkrementelle opdateringer (ISO 32000-1 §7.5.6) lader enhver tilføje en revision, der redefinerer et eksisterende objektnummer, og den redefinerede body annoncerer ikke, hvad den er. Signaturen verificerer stadig, da den kun dækker bytes af sin egen revision. Ethvert forsvar mod manipulation efter signering afhænger derfor af at afbilde hvert ændret objekt til den struktur, der bruger det, og derefter spørge, om signeren tillod, at den struktur ændredes

Flere publicerede angrebsklasser arbejder præcis i det hul. Incremental saving attacks tilføjer en revision, der bytter sideindhold ud, og stoler på, at verifikatoren kun tjekker den signerede byte-range. Shadow attacks planter skjult indhold før signering og aktiverer det bagefter med en lille, uskyldigt udseende ændring. Angreb på certificerede dokumenter udnytter, at P=2 og P=3 eksplicit tillader visse senere ændringer, og klæder derefter en forbudt ændring ud som en tilladt. En verifikator, der klassificerer objekter efter labels som /Type /Annot, eller efter enhver referencevej, der tilfældigt når dem, er udsat for den tredje klasse: angriberen behøver kun én tilladt struktur, der kan nå den forbudte

Derfor er spørgsmålet ikke, hvilke objekter der ændredes, men hvem der ejer dem. En content stream nået fra en side gennem /Contents er sideindhold, uanset hvad andet peger på den. En annotation, der peger tilbage på siden gennem /P, siger, hvor annotationen bor, ikke hvad den ejer

Hvordan modellerer PDFium Component v3.126.2 ejerskab?

PDFium Component v3.126.2 behandler tilbage-referencer som navigation, holder dem ude af rollepropagation og beslutter, hvilke nøgler der tæller som navigation, ud fra dictionaryens strukturelle rolle, der holder dem, ikke alene ud fra nøglenavnet. Tabellen opsummerer de navigationsnøgler, der ikke længere bærer ejerskab

Ejer-dictionaryNøgler behandlet som navigationSpec-reference
Page- eller Pages-knude/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
Annotation eller widget/PISO 32000-1 §12.5.2
Widget- eller field-dictionary/ParentISO 32000-1 §12.7.3
PDFium Component v3.126.2-objektgraf, der adskiller ejet-payload-kanter som /Contents og /Annots, som spreder page- og annotation-roller, fra navigationskanter som widget /P, som ikke bærer roller, med navigationsnøglerne pr. ejer-dictionary og prckPageContent bevaret på content stream'en selv under en forfalsket /Type
v3.126.2 holder tilbage-referencer ude af rollepropagation: roller rejser kun gennem ægte ejerskab, så content stream'en forbliver sideindhold, og /P-hintet afgør ingenting

At filtrere globalt efter nøglenavn ville have skabt et nyt hul. En font- eller XObject-ressource kan legitimt hedde /P, /Parent eller /Annots, og en /Resources-dictionary, der dropper sin /P-post fra propagation, ville lade en angriber gemme et side-ejet XObject bag et uskyldigt ressourcenavn. I v3.126.2 gælder navigationsfilteret kun, når ejer-dictionaryen faktisk er en page, Pages-knude, annotation, widget eller field. Bærer en af disse dictionaries en duplikeret navigationsnøgle, som to /P-poster i en widget, gætter analysatoren ikke på, hvilken kopi en viewer ville bruge; rollebygningen fejler, og signaturen bliver Indeterminate

Flere yderligere regler lukker de resterende relabeling-ruter:

  • Pages-knuder er page-rolle-rødder i egen ret, så ressourcer nedarvet fra Pages-træet (ISO 32000-1 §7.7.3.4) kommer ind i page-kontekst gennem ægte ejerskab, ikke gennem en /Parent-gang fra en barne-side
  • En annotation- eller form-rolle, der ankommer til en catalog, Pages-knude, page, annotation eller field-dictionary, stopper dér, fordi disse strukturelle objekter etablerer deres egne roller, og en indkommende payload-rolle må ikke tilsidesætte dem
  • Page-rollen er autoritativ under klassifikation: et side-ejet objekt er prckPageContent, selv hvis en senere revision omskriver det med en forfalsket /FT, et /Type /Annot-label, eller deler det med en appearance stream
  • Et Form XObject brugt kun som field- eller annotation-appearance beholder sin form- eller annotation-kategori, så almindelig appearance-genregenerering efter en form-udfyldning stadig bedømmes under de normale tilladelsesregler
  • En widget uden egen /FT opløser den nedarvede field-type gennem /Parent-kæden, og en uopløselig kæde fejler rollebygningen i stedet for at default'e til annotation
  • Rolle-bits fra hver senere revision flettes ind i den dækkede revisions roller, så en senere opdatering ikke kan udviske en tidligere side-ejerskabsrelation ved først at frakoble en stream og redigere den bagefter

Fixpunkt i stedet for et fast antal gennemløb

Rolle-rækkevidde i v3.126.2 kører som en arbejdskø, der itererer, til intet objekt vinder en ny rolle-bit, hvilket er et ægte fixpunkt uanset kæde-dybde eller objekt-nummerering. Indirekte arrays som et /Contents-array gemt som sit eget objekt gennemgås også. Hvert objekt kan vinde højst fire forskellige rolle-bits, så køen er afgrænset til fire indgange pr. objektnummer; overskrides det budget, udløses prrResourceLimitExceeded. En reference til et frit objekt, en generation-mismatch eller en i stykker objektheader udløser prrMalformedRevisionChain, og payload inde i et komprimeret objekt-stream udløser prrCompressedObjectUnresolved. Hver af disse fejler ender med prasIndeterminate, aldrig med en tilladt dom, og sker fejlen under opbygningen af den dækkede revisions roller, rapporterer signaturen slet ingen Changes

Følgende rutine lister de sideindholds-ændringer, der overlever denne analyse. En prckPageContent-ændring bedømmes aldrig som prdAllowed: DocMDP P=1, 2 eller 3 gør den til prdDisallowed, og en signatur uden DocMDP bedømmer 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, intet at frigøre
    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;

Hvad sker der, når FieldMDP og en annotation deler ét objekt?

Når et fields /V og en annotations /Contents peger på det samme indirekte objekt, holder v3.126.2 FieldMDP-låsen i kraft, selv om ændringen klassificeres som en annotation-redigering. Scenariet er let at bygge i hånden: en signerer låser Total-fieldet med FieldMDP (ISO 32000-1 §12.8.2.4), og angriberen lader en tekstannotations /Contents referere den samme streng-object, der holder fieldværdien. Under P=3 er annotation-redigeringen tilladt, så før fixet ændrede omskrivning af den delte streng et låst fieldværdi med en tilladt-dom

Objektet bærer nu både annotation- og form-rollen, og annotation-afgørelsen tjekker formsiden igen, når signaturen har en FieldMDP-transform:

  • Under P=2 er annotation-ændringen afvist på stedet, præcis som før
  • Med FieldMDP All er alle fields låst, så den delte ændring er prdDisallowed
  • Med FieldMDP Include eller Exclude kan analysatoren ikke spore en delt skalar tilbage til ét fieldnavn, så afgørelsen er prdIndeterminate frem for et gæt
  • Uden FieldMDP gælder P=3-annotation-reglen, og ændringen forbliver tilladt
PDFium Component FieldMDP-beslutningsdiagram, hvor et låst Total field /V og en annotation /Contents refererer det samme indirekte objekt, forgrenende over DocMDP P=2, FieldMDP All, FieldMDP Include eller Exclude og ingen FieldMDP til prdDisallowed-, prdIndeterminate- eller prdAllowed-domme for den samme delte ændring
Når ét indirekte objekt bærer både annotation- og form-rollen, tjekker annotation-afgørelsen FieldMDP-låsen igen, så den samme ændring spænder fra tilladt over afvist til indeterminate

Én rapportdetalje tæller for gate-kode. Det delte tilfælde rapporteres som Kind = prckAnnotation med Decision = prdIndeterminate, og prrFieldMdpUnresolved tilføjes til risksættet kun for ændringer klassificeret som form fields. En gate, der søger efter prrFieldMdpUnresolved og ignorerer Status, miser dette tilfælde helt

Hvordan bør Delphi-kode fejle lukket ved revisionsanalyse?

Delphi-kode bør kun acceptere et signeret dokument, når analysestatus er prasNoLaterChanges eller prasAllowed, og der ikke er nogen strukturel risiko til stede, og den bør behandle prasIndeterminate og prasSuspicious som utroværdige, ikke som advarsler at logge og lade passere. Indeterminate betyder, at analysatoren ikke kunne bevise, at de senere revisioner var tilladte; for en angriber er et input, der pålideligt producerer Indeterminate, lige så brugbart som ét, der producerer Allowed, hvis din kode slipper det igennem. Den globale AnalyzePadesSignatureRevisions tager enhver TStream og læser den fra position 0, hvilket passer til upload-handlere, der aldrig behøver at rendere dokumentet

uses
  Classes, SysUtils, TypInfo, FPdfPades;

const
  // Duplikerede definitioner registreres uden at nedgradere 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 og prasSuspicious er afvisninger, ikke advarsler
    Reason := 'revision status ' +
      GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
  end;
end;

To grænser er værd at sige klart. TPadesRevisionAnalysisReport siger ingenting om CMS-integritet eller certifikattillid, så denne gate sidder ved siden af kryptografisk og tillidsvalidering, ikke i stedet for dem. Og en korrekt ejerskabsgraf gør ikke P=3 sikkert for enhver arbejdsgang. P=3 tillader genuint annotationer, og en annotation med en opak appearance kan dække signeret tekst uden at røre en eneste content stream. Er dine certificerede dokumenter kontrakter frem for review-eksemplarer, så certificér enten med P=2, eller send tilladte annotation-ændringer videre til et menneske, som i denne hjælper:

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;

Tjekliste til signaturrevisions-audit

Brug listen til at tjekke, om din verifikationspipeline var udsat, og om den nu fejler lukket:

  • Builds af PDFium Component før v3.126.2 kunne rapportere prasAllowed for sideindholds-ændringer i DocMDP P=3-dokumenter; genkør TPdf.AnalyzeSignatureRevisions på certificerede P=3-filer accepteret af ældre builds
  • Gentjek P=3-dokumenter med FieldMDP-låse, hvor en fieldværdi og en annotation muligvis deler et indirekte objekt
  • Acceptér kun prasNoLaterChanges og prasAllowed; behandl prasIndeterminate og prasSuspicious som utroværdige
  • Test Report.Risks så vel som Report.Status, for prrDuplicateObjectDefinition ændrer ikke status af sig selv
  • Læs ikke et tomt Changes-array som et rent resultat, når signaturens status er Indeterminate; en fejlet rollebygning rapporterer ingen ændringer
  • Stol ikke alene på prrFieldMdpUnresolved til at fange FieldMDP-problemer, da det delte annotation-tilfælde kun kommer frem gennem afgørelsen og status
  • Beslut, om tilladte annotation-ændringer under P=3 behøver menneskelig gennemgang i din arbejdsgang
  • Analysér de oprindelige fil-bytes; et dokument omskrevet af SaveAs indeholder ikke længere revisionskæden

Revisionsanalyse er ét lag af et signaturtjek. Par det med inspektion af PDF digitale signaturer og PAdES-niveauer for dictionary- og baseline-niveauet, og med en bredere PDF security risk-audit for JavaScript, launch actions og indlejrede filer. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions og den ejerskabsbevidste rollegraf beskrevet her skiber med PDFium Component til Delphi, C++Builder og Lazarus