Teknisk artikkel

PDFium DocMDP: Hvordan en widget /P skjulte sideendringer

I PDFium Component-bygg før v3.126.2 kunne TPdf.AnalyzeSignatureRevisions rangere en ekte sideinnholdsredigering som en tillatt annotasjonsendring under DocMDP P=3, fordi revisjonsrollegrafen behandlet signatur-widgetens /P-bakreferanse til siden sin som eierskap. Siden v3.126.2 skiller PDFium Component navigasjonskanter fra eide-last-kanter, så sideinnhold forblir sideinnhold. Feilrapporten bak denne fiksen ser harmløs ut på papiret. En sertifisert kontrakt tillater annotasjoner, en motpart legger til én inkrementell lagring, og analysatoren sier at enhver senere endring er tillatt. Så diff-er noen de renderete sidene, og betalingsbeløpet på side 2 er annerledes

Denne artikkelen er angriperperspektiv-oppfølgeren til oversikten over analyse av endringer etter signaturrevisjoner, så den hopper over det grunnleggende om revisjonsrebygging og DocMDP-rangering og går rett på objektgrafen: hvordan eierskap ble modellert, hvorfor retningen på en kant avgjør en sikkerhetsdom, hva som endret seg i v3.126.2, og hvordan du reviderer din egen akseptlogikk

Hvorfor passerte en sideendring som en annotasjonsendring under DocMDP P=3?

Sideendringen passerte fordi den gamle rollegrafen fulgte enhver indirekte referanse i en ordbok som om det refererte objektet tilhørte den refererende, og signatur-widgeten peker tilbake på siden sin. En annotasjonsordbok bærer /P, en indirekte referanse til sideobjektet den ligger på (ISO 32000-1 §12.5.2). Den oppføringen er et navigasjonshint. Widgeten eier ikke siden; siden eier widgeten gjennom /Annots-matrisen sin

Analysatoren tildeler hvert objekt et sett rollebiter før den rangerer senere endringer: side, annotasjon, skjema og valideringsmateriale. Rottobjekter får rollen sin fra sin egen ordbok, og rollen sprer seg så til alt de refererer. I den gamle propagasjonen gikk kjeden slik:

  1. Signatur-widgeten er en /Subtype /Widget med /FT /Sig, så den får annotasjonsrollen
  2. Widgetens /P skyver annotasjonsrollen over på sideordboken, som allerede har siderollen
  3. Siden skyver begge roller inn i /Contents, /Resources og, gjennom /Parent, opp i Pages-treet og over til hver søsterside
  4. En innholdsstrøm-ordbok som << /Length 812 >> har ingen /Type, så klassifikatoren falt tilbake til rollebiter og sjekket annotasjonsrollen før siderollen
PDFium Component-diagram av DocMDP-rollegrafen før v3.126.2 der en signatur-widget /P-bakreferanse skyver annotasjonsrollen over på sideordboken, siden sprer den gjennom /Contents over på en innholdsstrøm uten /Type-oppføring, klassifikatoren gir prckAnnotation og P=3-rangering returnerer prdAllowed
Før v3.126.2 behandlet rollegrafen enhver indirekte referanse som eierskap, så widgetens /P-oppføring skjøv annotasjonsrollen over på siden, og en ekte sideendring forlot analysatoren som en tillatt annotasjonsendring

Den endrede innholdsstrømmen kom derfor ut som prckAnnotation. Under ISO 32000-1 §12.8.2.2 tillater DocMDP P=3 annotasjonsendringer, så beslutningen var prdAllowed og rapporten rullet opp til prasAllowed. Samme fil under P=2 ble avvist, men bare ved en tilfeldighet: P=2 forbyr annotasjonsendringer, så den feilmerkte strømmen ble nektet av feil grunn. En fast firedelt propagasjonsløkke la til en andre svakhet. Last som nådde frem gjennom en indirekte matrise, eller gjennom en lang kjede hvis objektnumre løper baklengs, kunne aldri motta noen rolle i det hele tatt

Hvorfor må en signaturvalidator spørre hvem som eier et objekt?

En signaturvalidator må spørre hvem som eier et objekt fordi PDF inkrementelle oppdateringer (ISO 32000-1 §7.5.6) lar hvem som helst legge til en revisjon som redefinerer et eksisterende objektnummer, og den redefinerte kroppen annonserer ikke hva den er. Signaturen verifiseres fortsatt, siden den bare dekker bytene i sin egen revisjon. Hvert forsvar mot manipulasjon etter signering avhenger derfor av å kartlegge hvert endret objekt til strukturen som bruker det, og så spørre om signataren tillot at den strukturen endret seg

Flere publiserte angrepsklasser jobber nøyaktig i det gapet. Incremental saving attacks legger til en revisjon som bytter sideinnhold og stoler på at verifikatoren bare sjekker det signerte byteområdet. Shadow attacks planter skjult innhold før signering og aktiverer det etterpå med en liten, uskyldig utseende endring. Angrep på sertifiserte dokumenter misbruker det faktum at P=2 og P=3 eksplisitt tillater noen senere redigeringer, og kler så en forbudt redigering ut som en tillatt. En verifikator som klassifiserer objekter etter etiketter som /Type /Annot, eller etter en referansesti som tilfeldigvis når dem, er eksponert for den tredje klassen: angriperen trenger bare én tillatt struktur som kan nå den forbudte

Det er derfor spørsmålet ikke er hvilke objekter som endret seg, men hvem som eier dem. En innholdsstrøm nådd fra en side gjennom /Contents er sideinnhold uansett hva annet som peker på den. En annotasjon som peker tilbake på siden gjennom /P sier hvor annotasjonen bor, ikke hva den eier

Hvordan modellerer PDFium Component v3.126.2 eierskap?

PDFium Component v3.126.2 behandler bakreferanser som navigasjon, holder dem utenfor rollepropagasjon, og avgjør hvilke nøkler som teller som navigasjon ut fra den strukturelle rollen til ordboken som holder dem, ikke ut fra nøkkelnavnet alene. Tabellen oppsummerer navigasjonsnøklene som ikke lenger bærer eierskap

EierordbokNøkler behandlet som navigasjonSpesifikasjonsreferanse
Side- eller Pages-node/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
Annotasjon eller widget/PISO 32000-1 §12.5.2
Widget- eller feltordbok/ParentISO 32000-1 §12.7.3
PDFium Component v3.126.2 objektgraf som skiller eide-last-kanter som /Contents og /Annots, som sprer side- og annotasjonsroller, fra navigasjonskanter som widget /P, som ikke bærer roller, med navigasjonsnøklene per eierordbok og prckPageContent beholdt på innholdsstrømmen selv under et forfalsket /Type
v3.126.2 holder bakreferanser utenfor rollepropagasjonen: roller reiser bare gjennom ekte eierskap, så innholdsstrømmen forblir sideinnhold og /P-hintet avgjør ingenting

Å filtrere etter nøkkelnavn globalt ville skapt et nytt hull. En font- eller XObject-ressurs kan legitimt hete /P, /Parent eller /Annots, og en /Resources-ordbok som dropper /P-oppføringen sin fra propagasjonen ville la en angriper skjule et sideeid XObject bak et uskyldig ressursnavn. I v3.126.2 gjelder navigasjonsfilteret bare når eierordboken faktisk er en side, Pages-node, annotasjon, widget eller felt. Bærer én av de ordbøkene en duplisert navigasjonsnøkkel, som to /P-oppføringer i en widget, gjetter ikke analysatoren hvilken kopi en visningsprogram ville brukt; rollebyggingen feiler og signaturen blir Indeterminate

Flere ytterligere regler lukker de gjenværende ommmerkingsrutene:

  • Pages-noder er siderolle-røtter i egen rett, så ressurser arvet fra Pages-treet (ISO 32000-1 §7.7.3.4) kommer inn i sidekontekst gjennom ekte eierskap, ikke gjennom en /Parent-vandring fra en barneside
  • En annotasjons- eller skjemarolle som ankommer en katalog, Pages-node, side, annotasjon eller feltordbok, stopper der, fordi de strukturelle objektene etablerer sine egne roller, og en innkommende last-rolle må ikke overstyre dem
  • Siderollen er autoritativ under klassifisering: et sideeid objekt er prckPageContent selv om en senere revisjon omskriver det med en forfalsket /FT, en /Type /Annot-etikett, eller deler det med en appearance-strøm
  • Et Form XObject brukt bare som felt- eller annotasjonsutseende beholder sin skjema- eller annotasjonskategori, så vanlig utseende-regenerering etter utfylling av et skjema rangeres fortsatt under de normale tillatelsesreglene
  • En widget uten egen /FT løser den arvede felttypen gjennom /Parent-kjeden, og en uløselig kjede feiler rollebyggingen i stedet for å velge annotasjon som standard
  • Rollebiter fra hver senere revisjon slås sammen inn i den dekkede revisjonens roller, så en senere oppdatering ikke kan viske ut en tidligere sideeierskapsrelasjon ved først å koble fra en strøm og så redigere den

Fast punkt i stedet for fast passantall

Rolle-nåbarhet i v3.126.2 kjører som en arbeidskø som itererer til intet objekt vinner en ny rollebit, noe som er et ekte fast punkt uansett kjededybde eller objektnummerering. Indirekte matriser, som en /Contents-matrise lagret som sitt eget objekt, vandres også. Hvert objekt kan vinne høyst fire distinkte rollebiter, så køen er bundet til fire oppføringer per objektnummer; overskrider den budsjettet, reises prrResourceLimitExceeded. En referanse til et fritt objekt, en generasjonsmismatch eller et ødelagt objekthode reiser prrMalformedRevisionChain, og last inni et komprimert objektstrøm-objekt reiser prrCompressedObjectUnresolved. Hver av disse feilene ender med prasIndeterminate, aldri med en tillatt dom, og skjer feilen under bygging av den dekkede revisjonens roller, rapporterer signaturen ingen Changes i det hele tatt

Rutinen under lister sideinnholdsendringene som overlever denne analysen. En prckPageContent-endring rangeres aldri som prdAllowed: DocMDP P=1, 2 eller 3 gjør den til prdDisallowed, og en signatur uten DocMDP rangerer den som 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, ingenting å frigjø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;

Hva skjer når FieldMDP og en annotasjon deler ett objekt?

Når et felts /V og en annotasjons /Contents peker på samme indirekte objekt, holder v3.126.2 FieldMDP-låsen i kraft selv om endringen klassifiseres som en annotasjonsredigering. Scenariet er lett å bygge for hånd: en signatar låser Total-feltet med FieldMDP (ISO 32000-1 §12.8.2.4), og angriperen lar en tekstannotasjons /Contents referere det samme strengobjektet som holder feltverdien. Under P=3 er annotasjonsredigeringen tillatt, så før fiksen omskrev omskriving av den delte strengen en låst feltverdi med en tillatt dom

Objektet bærer nå både annotasjons- og skjemarollen, og annotasjonsbeslutningen sjekker skjemasiden på nytt når signaturen har en FieldMDP-transform:

  • Under P=2 er annotasjonsendringen avvist på rent ut, nøyaktig som før
  • Med FieldMDP All er hvert felt låst, så den delte endringen er prdDisallowed
  • Med FieldMDP Include eller Exclude kan ikke analysatoren spore en delt skalar tilbake til ett feltnavn, så beslutningen er prdIndeterminate i stedet for en gjetning
  • Uten FieldMDP gjelder P=3-annotasjonsregelen, og endringen forblir tillatt
PDFium Component FieldMDP-beslutningsdiagram der et låst Total-felt /V og en annotasjon /Contents refererer samme indirekte objekt, forgrenet over DocMDP P=2, FieldMDP All, FieldMDP Include eller Exclude og ingen FieldMDP til prdDisallowed-, prdIndeterminate- eller prdAllowed-dommer for samme delte redigering
Når ett indirekte objekt bærer både annotasjons- og skjemarollen, sjekker annotasjonsbeslutningen FieldMDP-låsen på nytt, så samme redigering spenner fra tillatt til avvist til ubestemt

Én rapporteringsdetalj betyr noe for portkode. Det delte tilfellet rapporteres som Kind = prckAnnotation med Decision = prdIndeterminate, og prrFieldMdpUnresolved legges til risikosettet bare for endringer klassifisert som skjemafelt. En port som søker etter prrFieldMdpUnresolved og ignorerer Status, mister dette tilfellet fullstendig

Hvordan bør Delphi-kode feile lukket på revisjonsanalyse?

Delphi-kode bør akseptere et signert dokument bare når analysestatusen er prasNoLaterChanges eller prasAllowed og ingen strukturell risiko finnes, og den bør behandle prasIndeterminate og prasSuspicious som utruelige, ikke som advarsler å logge og slippe gjennom. Indeterminate betyr at analysatoren ikke kunne bevise at de senere revisjonene var tillatt; for en angriper er en inndata som pålitelig produserer Indeterminate like nyttig som én som produserer Allowed, hvis koden din slipper den gjennom. Den globale AnalyzePadesSignatureRevisions tar enhver TStream og leser den fra posisjon 0, noe som passer opplastingsbehandlere som aldri trenger å rendre dokumentet

uses
  Classes, SysUtils, TypInfo, FPdfPades;

const
  // Duplikate definisjoner registreres uten å 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 avvisninger, ikke advarsler
    Reason := 'revision status ' +
      GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
  end;
end;

To grenser er verdt å si rent ut. TPadesRevisionAnalysisReport sier ingenting om CMS-integritet eller sertifikattillit, så denne porten står ved siden av kryptografisk og tillitsvalidering, ikke i stedet for dem. Og en korrekt eiergraf gjør ikke P=3 trygt for enhver arbeidsflyt. P=3 tillater faktisk annotasjoner, og en annotasjon med et ugjennomsiktig utseende kan dekke signert tekst uten å røre en enkelt innholdsstrøm. Er de sertifiserte dokumentene dine kontrakter snarere enn gjennomgangseksemplarer, sertifiserer du enten med P=2 eller ruter tillatte annotasjonsendringer til et menneske, som i denne hjelperen:

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;

Sjekkliste for revisjonsrevisjon av signaturer

Bruk denne listen til å sjekke om verifiseringsrørledningen din var eksponert, og om den nå feiler lukket:

  • Bygg av PDFium Component før v3.126.2 kunne rapportere prasAllowed for sideinnholdsendringer i DocMDP P=3-dokumenter; kjør TPdf.AnalyzeSignatureRevisions på nytt på sertifiserte P=3-filer godtatt av eldre bygg
  • Sjekk P=3-dokumenter med FieldMDP-låser på nytt der en feltverdi og en annotasjon kan dele et indirekte objekt
  • Godta bare prasNoLaterChanges og prasAllowed; behandle prasIndeterminate og prasSuspicious som utruelige
  • Test Report.Risks like fullt som Report.Status, for prrDuplicateObjectDefinition endrer ikke statusen av seg selv
  • Ikke les en tom Changes-matrise som et rent resultat når signaturens status er Indeterminate; en feilet rollebygging rapporterer ingen endringer
  • Ikke stol på prrFieldMdpUnresolved alene for å fange FieldMDP-problemer, siden det delte annotasjonstilfellet bare kommer til syne gjennom beslutningen og statusen
  • Avgjør om tillatte annotasjonsendringer under P=3 trenger menneskelig gjennomgang i arbeidsflyten din
  • Analyser originalfilens byte; et dokument omskrevet av SaveAs inneholder ikke lenger revisjonskjeden

Revisjonsanalyse er ett lag av en signatursjekk. Parr den med inspeksjon av digitale PDF-signaturer og PAdES-nivåer for ordboken og baseline-nivået, og med en bredere revisjon av PDF-sikkerhetsrisikoer for JavaScript, start-handlinger og innebygde filer. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions og den eierskapsbevisste rollegrafen beskrevet her følger med i PDFium Component for Delphi, C++Builder og Lazarus