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:
- Signaturens widget er en
/Subtype /Widgetmed/FT /Sig, så den får annotation-rollen - Widgettens
/Pskubber annotation-rollen over på side-dictionaryen, som allerede har page-rollen - Siden skubber begge roller ind i
/Contents,/Resourcesog gennem/Parentop i Pages-træet og hen over til hver søsterside - 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
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-dictionary | Nøgler behandlet som navigation | Spec-reference |
|---|---|---|
| Page- eller Pages-knude | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotation eller widget | /P | ISO 32000-1 §12.5.2 |
| Widget- eller field-dictionary | /Parent | ISO 32000-1 §12.7.3 |
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
/FToplø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
Aller alle fields låst, så den delte ændring erprdDisallowed - Med FieldMDP
IncludeellerExcludekan analysatoren ikke spore en delt skalar tilbage til ét fieldnavn, så afgørelsen erprdIndeterminatefrem for et gæt - Uden FieldMDP gælder P=3-annotation-reglen, og ændringen forbliver tilladt
É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
prasAllowedfor sideindholds-ændringer i DocMDP P=3-dokumenter; genkørTPdf.AnalyzeSignatureRevisionspå 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
prasNoLaterChangesogprasAllowed; behandlprasIndeterminateogprasSuspicioussom utroværdige - Test
Report.Risksså vel somReport.Status, forprrDuplicateObjectDefinitionæ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å
prrFieldMdpUnresolvedtil 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
SaveAsindeholder 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