Technisch artikel

PDFium Component DocMDP: hoe widget /P pagina-edits verborg

In builds van PDFium Component vóór v3.126.2 kon TPdf.AnalyzeSignatureRevisions een echte paginainhoudsbewerking onder DocMDP P=3 beoordelen als een toegestane annotatiewijziging, omdat zijn rolgraaf van revisies de /P-terugverwijzing van de handtekeningwidget naar zijn pagina als ownership behandelde. Sinds v3.126.2 scheidt PDFium Component navigatie-edges van owned-payload-edges, dus paginainhoud blijft paginainhoud. De bugmelding achter deze fix ziet er op papier ongevaarlijk uit. Een gecertificeerd contract staat annotaties toe, een tegenpartij voegt één incrementele opslag toe, en de analyzer zegt dat elke latere wijziging is toegestaan. Dan vergelijkt iemand de gerenderde pagina's en staat het betalingsbedrag op pagina 2 er anders bij

Dit artikel is het vervolg vanuit het oogpunt van de aanvaller op het overzicht van analyse van revisiewijzigingen na ondertekening, dus het slaat de basis van revisieherbouw en DocMDP-beoordeling over en gaat rechtstreeks naar de objectgraaf: hoe ownership was gemodelleerd, waarom de richting van een edge over een beveiligingsuitslag beslist, wat er in v3.126.2 is veranderd, en hoe u uw eigen acceptatielogica auditt

Waarom ging een paginabewerking onder DocMDP P=3 door als annotatiewijziging?

De paginabewerking ging erdoorheen omdat de oude rolgraaf elke indirecte referentie in een dictionary volgde alsof het gerefereerde object bij de verwijzende hoorde, en de handtekeningwidget wijst terug naar zijn pagina. Een annotatiedictionary draagt /P, een indirecte referentie naar het paginaobject waarop hij staat (ISO 32000-1 §12.5.2). Die entry is een navigatiehint. De widget is geen eigenaar van de pagina; de pagina bezit de widget via zijn array /Annots

De analyzer geeft elk object een set rolbits voordat hij latere wijzigingen beoordeelt: pagina, annotatie, formulier en validatiemateriaal. Rootobjecten krijgen hun rol uit hun eigen dictionary, en de rol spreidt zich daarna uit over alles wat ze refereren. In de oude propagatie verliep de keten zo:

  1. De handtekeningwidget is een /Subtype /Widget met /FT /Sig, dus hij krijgt de annotatierol
  2. De /P van de widget duwt de annotatierol op de pagedictionary, die al de paginarol heeft
  3. De pagina duwt beide rollen in /Contents, /Resources en, via /Parent, omhoog in de Pages-boom en zijwaarts naar elke zusterpagina
  4. Een content-stream-dictionary zoals << /Length 812 >> heeft geen /Type, dus de classifier viel terug op rolbits en controleerde de annotatierol vóór de paginarol
PDFium Component-diagram van de DocMDP-rolgraaf vóór v3.126.2 waarin een /P-terugverwijzing van de handtekeningwidget de annotatierol op de pagedictionary duwt, de pagina haar via /Contents verspreidt over een content stream zonder /Type-entry, de classifier prckAnnotation uitvoert en P=3-beoordeling prdAllowed oplevert
Vóór v3.126.2 behandelde de rolgraaf elke indirecte referentie als ownership, dus de entry /P van de widget duwde de annotatierol op de pagina en een echte paginabewerking verliet de analyzer als een toegestane annotatiewijziging

De gewijzigde content stream kwam er daardoor als prckAnnotation uit. Volgens ISO 32000-1 §12.8.2.2 staat DocMDP P=3 annotatiewijzigingen toe, dus de beslissing was prdAllowed en het rapport rolt op tot prasAllowed. Hetzelfde bestand onder P=2 werd afgewezen, maar puur toevallig: P=2 verbiedt annotatiewijzigingen, dus de verkeerd gelabelde stream werd geweigerd om de verkeerde reden. Een vaste propagatielus van vier passes voegde een tweede zwakte toe. Payload die via een indirecte array werd bereikt, of via een lange keten waarvan de objectnummers achteruit lopen, kreeg misschien helemaal geen rol

Waarom moet een handtekeningsvalidator vragen wie een object bezit?

Een handtekeningsvalidator moet vragen wie een object bezit, omdat incrementele updates van PDF (ISO 32000-1 §7.5.6) iedereen toestaan een revisie toe te voegen die een bestaand objectnummer herdefinieert, en het herdefinieerde lichaam verkondigt niet wat het is. De handtekening verifieert nog steeds, want hij dekt alleen de bytes van zijn eigen revisie. Elk verweer tegen manipulatie na ondertekening hangt er dus van af dat elk gewijzigd object wordt gemapt op de structuur die hem gebruikt, en dat daarna wordt gevraagd of de ondertekenaar die structuur mocht laten veranderen

Meerdere gepubliceerde aanvalsklassen spelen precies in die opening. Incremental saving attacks voegen een revisie toe die paginainhoud verwisselt en vertrouwen erop dat de verifier alleen het getekende bytebereik controleert. Shadow attacks planten verborgen inhoud vóór het ondertekenen en activeren haar daarna met een kleine, onschuldig ogende wijziging. Aanvallen op gecertificeerde documenten misbruiken dat P=2 en P=3 expliciet sommige latere bewerkingen toestaan, en kleden dan een verboden bewerking als een toegestane. Een verifier die objecten classificeert op labels zoals /Type /Annot, of op een referentiepad dat er toevallig heen loopt, is aan de derde klasse blootgesteld: de aanvaller heeft maar één toegestane structuur nodig die de verboden kan bereiken

Daarom luidt de vraag niet welke objecten zijn veranderd, maar wie ze bezit. Een content stream die via /Contents vanuit een pagina wordt bereikt is paginainhoud, wat er ook nog naar wijst. Een annotatie die via /P terugwijst naar de pagina zegt waar de annotatie woont, niet wat haar bezit

Hoe modelleert PDFium Component v3.126.2 ownership?

PDFium Component v3.126.2 behandelt terugverwijzingen als navigatie, houdt ze buiten de rolpropagatie en beslist aan de hand van de structurele rol van de dictionary die ze bevat welke keys als navigatie tellen, niet uitsluitend aan de keynaam. De tabel vat de navigatiekeys samen die ownership niet meer dragen

Bezittende dictionaryKeys die als navigatie geldenSectie in de specificatie
Pagina- of Pages-knoop/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
Annotatie of widget/PISO 32000-1 §12.5.2
Widget- of fielddictionary/ParentISO 32000-1 §12.7.3
PDFium Component v3.126.2-objectgraaf die owned-payload-edges zoals /Contents en /Annots, die pagina- en annotatierollen verspreiden, scheidt van navigatie-edges zoals widget /P, die geen rollen dragen, met de navigatiekeys per bezittende dictionary en prckPageContent op de content stream gehouden zelfs onder een vervalste /Type
v3.126.2 houdt terugverwijzingen buiten de rolpropagatie: rollen reizen alleen door echt ownership, dus de content stream blijft paginainhoud en de hint /P beslist niets

Globaal filteren op keynaam had een nieuw gat gegraven. Een font- of XObject-resource mag legitiem /P, /Parent of /Annots heten, en een dictionary /Resources die zijn entry /P uit de propagatie laat zou een aanvaller toestaan een aan de pagina toebehorend XObject achter een onschuldige resourcenaam te verstoppen. In v3.126.2 geldt het navigatiefilter alleen wanneer de bezittende dictionary werkelijk een pagina, Pages-knoop, annotatie, widget of veld is. Draagt een van die dictionaries een gedupliceerde navigatiekey, zoals twee entries /P in een widget, dan gokt de analyzer niet welke kopie een viewer zou gebruiken; de rolbouw faalt en de handtekening wordt Indeterminate

Enkele verdere regels sluiten de overgebleven herlabelroutes:

  • Pages-knopen zijn op zichzelf paginarol-roots, dus resources die uit de Pages-boom worden geërfd (ISO 32000-1 §7.7.3.4) komen via echt ownership in de paginacontext, niet via een /Parent-wandeling vanaf een onderliggende pagina
  • Een annotatie- of formulierrol die een catalog, Pages-knoop, pagina, annotatie of fielddictionary bereikt, stopt daar, want die structurele objecten leggen hun eigen rollen vast en een binnenkomende payloadrol mag ze niet overschrijven
  • De paginarol is leidend tijdens de classificatie: een aan de pagina toebehorend object is prckPageContent, ook als een latere revisie hem herschrijft met een vervalste /FT, een label /Type /Annot, of hem deelt met een appearance stream
  • Een Form XObject dat alleen als appearance van een veld of annotatie dient houdt zijn formulier- of annotatiecategorie, dus gewone appearance-regeneratie na het invullen van een formulier wordt nog steeds beoordeeld onder de normale toestemmingsregels
  • Een widget zonder eigen /FT lost het geërfde veldtype op via de /Parent-keten, en een onoplosbare keten laat de rolbouw falen in plaats van op annotatie terug te vallen
  • Rolbits uit elke latere revisie worden samengevoegd met de rollen van de gedekte revisie, dus een latere update kan een eerdere pagina-ownership-relatie niet uitwissen door eerst een stream los te koppelen en haar daarna te bewerken

Vast punt in plaats van een vast aantal passes

Rolbereikbaarheid draait in v3.126.2 als een werkwachtrij die itereert totdat geen enkel object meer een nieuwe rolbit krijgt, een echt vast punt ongeacht ketendiepte of objectnummering. Indirecte arrays zoals een /Contents-array die als eigen object is opgeslagen worden ook gelopen. Elk object kan hooguit vier onderscheiden rolbits krijgen, dus de wachtrij is begrensd op vier entries per objectnummer; het budget overschrijden roept prrResourceLimitExceeded op. Een verwijzing naar een vrij object, een generatiemismatch of een kapotte objectheader roept prrMalformedRevisionChain op, en payload binnen een gecomprimeerde object stream roept prrCompressedObjectUnresolved op. Elk van deze mislukkingen eindigt met prasIndeterminate, nooit met een toegestane uitslag, en gebeurt de mislukking tijdens het bouwen van de rollen van de gedekte revisie, dan meldt de handtekening helemaal geen Changes

De volgende routine zet de pagina-inhoudsbewerkingen op een rij die deze analyse overleven. Een wijziging prckPageContent wordt nooit prdAllowed beoordeeld: DocMDP P=1, 2 of 3 maakt haar prdDisallowed, en een handtekening zonder DocMDP beoordeelt haar als 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, niets om vrij te geven
    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;

Wat gebeurt er wanneer FieldMDP en een annotatie één object delen?

Wanneer de /V van een veld en de /Contents van een annotatie naar hetzelfde indirecte object wijzen, houdt v3.126.2 het FieldMDP-slot in stand ook al wordt de wijziging geclassificeerd als annotatiebewerking. Het scenario is makkelijk met de hand te bouwen: een ondertekenaar sluit het veld Total af met FieldMDP (ISO 32000-1 §12.8.2.4), en de aanvaller laat de /Contents van een tekstannotatie naar hetzelfde stringobject verwijzen dat de veldwaarde bevat. Onder P=3 is de annotatiebewerking toegestaan, dus veranderde het herschrijven van die gedeelde string vóór de fix een afgesloten veldwaarde met een toegestane uitslag

Het object draagt nu zowel de annotatie- als de formulierrol, en de annotatiebeslissing controleert de formulierkant opnieuw zodra de handtekening een FieldMDP-transformatie heeft:

  • Onder P=2 is de annotatiewijziging ronduit verboden, precies zoals voorheen
  • Met FieldMDP All is elk veld afgesloten, dus de gedeelde wijziging is prdDisallowed
  • Met FieldMDP Include of Exclude kan de analyzer een gedeelde scalar niet terugvolgen naar één veldnaam, dus de beslissing is prdIndeterminate in plaats van een gok
  • Zonder FieldMDP geldt de annotatieregel van P=3 en blijft de wijziging toegestaan
PDFium Component-beslissingsdiagram voor FieldMDP waarin de /V van een afgesloten Total-veld en de /Contents van een annotatie naar hetzelfde indirecte object verwijzen, met vertakkingen over DocMDP P=2, FieldMDP All, FieldMDP Include of Exclude en geen FieldMDP naar de uitslagen prdDisallowed, prdIndeterminate of prdAllowed voor dezelfde gedeelde bewerking
Als één indirect object zowel de annotatie- als de formulierrol draagt, controleert de annotatiebeslissing het FieldMDP-slot opnieuw, dus dezelfde bewerking loopt uiteen van toegestaan via verboden naar onbepaald

Eén rapportagedetail doet ertoe voor gatecode. Het gedeelde geval wordt gemeld als Kind = prckAnnotation met Decision = prdIndeterminate, en prrFieldMdpUnresolved komt alleen in de risksset terecht bij wijzigingen die als formuliervelden zijn geclassificeerd. Een gate die naar prrFieldMdpUnresolved zoekt en Status negeert ziet dit geval volledig over het hoofd

Hoe zou Delphi-code op revisieanalyse defensief moeten afsluiten?

Delphi-code hoort een getekend document alleen te accepteren wanneer de analysestatus prasNoLaterChanges of prasAllowed is en er geen structureel risico bestaat, en hoort prasIndeterminate en prasSuspicious als onbetrouwbaar te behandelen, niet als waarschuwingen om te loggen en door te laten. Indeterminate betekent dat de analyzer niet kon bewijzen dat de latere revisies waren toegestaan; voor een aanvaller is een input die betrouwbaar Indeterminate oplevert net zo nuttig als een die Allowed oplevert, zolang uw code haar doorlaat. De globale AnalyzePadesSignatureRevisions neemt elke TStream en leest hem vanaf positie 0, wat past bij upload-handlers die het document nooit hoeven te renderen

uses
  Classes, SysUtils, TypInfo, FPdfPades;

const
  // Gedupliceerde definities worden vastgelegd zonder Status te verlagen
  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 en prasSuspicious zijn afwijzingen, geen waarschuwingen
    Reason := 'revision status ' +
      GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
  end;
end;

Twee grenzen zijn een kale mededeling waard. TPadesRevisionAnalysisReport zegt niets over CMS-integriteit of certificaatvertrouwen, dus deze gate staat naast cryptografische en vertrouwensvalidatie, niet op hun plaats. En een correcte ownershipgraaf maakt P=3 niet veilig voor elke workflow. P=3 staat annotaties echt toe, en een annotatie met een opake appearance kan getekende tekst bedekken zonder ook maar één content stream aan te raken. Zijn uw gecertificeerde documenten contracten in plaats van recensie-exemplaren, certificeer dan met P=2 of stuur toegestane annotatiewijzigingen naar een mens, zoals in deze helper:

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;

Auditchecklist voor handtekeningrevisies

Gebruik deze lijst om te controleren of uw verificatiepipeline was blootgesteld en of hij nu defensief afsluit:

  • Builds van PDFium Component vóór v3.126.2 konden prasAllowed melden voor pagina-inhoudsbewerkingen in DocMDP P=3-documenten; draai TPdf.AnalyzeSignatureRevisions opnieuw op gecertificeerde P=3-bestanden die oudere builds accepteerden
  • Controleer P=3-documenten met FieldMDP-slots opnieuw waar een veldwaarde en een annotatie misschien een indirect object delen
  • Accepteer alleen prasNoLaterChanges en prasAllowed; behandel prasIndeterminate en prasSuspicious als onbetrouwbaar
  • Test Report.Risks evenzeer als Report.Status, want prrDuplicateObjectDefinition verandert de status niet uit zichzelf
  • Lees een lege array Changes niet als een schoon resultaat wanneer de handtekeningsstatus Indeterminate is; een mislukte rolbouw meldt geen wijzigingen
  • Vertrouw niet alleen op prrFieldMdpUnresolved om FieldMDP-problemen te vangen, want het gedeelde annotatiegeval komt alleen via de beslissing en de status aan het licht
  • Beslis of toegestane annotatiewijzigingen onder P=3 in uw workflow menselijke beoordeling nodig hebben
  • Analyseer de originele bestandsbytes; een door SaveAs herschreven document bevat de revisieketen niet meer

Revisieanalyse is één laag van een handtekeningcontrole. Combineer haar met PDF-digitale handtekeningen en PAdES-niveaus inspecteren voor de dictionary en het baseline-niveau, en met een bredere PDF-beveiligingsrisicoaudit voor JavaScript, launch actions en embedded files. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions en de ownership-bewuste rolgraaf die hier wordt beschreven zitten in de PDFium Component voor Delphi, C++Builder en Lazarus