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:
- De handtekeningwidget is een
/Subtype /Widgetmet/FT /Sig, dus hij krijgt de annotatierol - De
/Pvan de widget duwt de annotatierol op de pagedictionary, die al de paginarol heeft - De pagina duwt beide rollen in
/Contents,/Resourcesen, via/Parent, omhoog in de Pages-boom en zijwaarts naar elke zusterpagina - 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
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 dictionary | Keys die als navigatie gelden | Sectie in de specificatie |
|---|---|---|
| Pagina- of Pages-knoop | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotatie of widget | /P | ISO 32000-1 §12.5.2 |
| Widget- of fielddictionary | /Parent | ISO 32000-1 §12.7.3 |
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
/FTlost 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
Allis elk veld afgesloten, dus de gedeelde wijziging isprdDisallowed - Met FieldMDP
IncludeofExcludekan de analyzer een gedeelde scalar niet terugvolgen naar één veldnaam, dus de beslissing isprdIndeterminatein plaats van een gok - Zonder FieldMDP geldt de annotatieregel van P=3 en blijft de wijziging toegestaan
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
prasAllowedmelden voor pagina-inhoudsbewerkingen in DocMDP P=3-documenten; draaiTPdf.AnalyzeSignatureRevisionsopnieuw 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
prasNoLaterChangesenprasAllowed; behandelprasIndeterminateenprasSuspiciousals onbetrouwbaar - Test
Report.Risksevenzeer alsReport.Status, wantprrDuplicateObjectDefinitionverandert de status niet uit zichzelf - Lees een lege array
Changesniet als een schoon resultaat wanneer de handtekeningsstatus Indeterminate is; een mislukte rolbouw meldt geen wijzigingen - Vertrouw niet alleen op
prrFieldMdpUnresolvedom 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
SaveAsherschreven 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