I PDFium Component-byggen före v3.126.2 kunde TPdf.AnalyzeSignatureRevisions betygsätta en verklig sidinnehållsändring som en tillåten annoteringsändring under DocMDP P=3, för dess revisionsrollgraf behandlade signaturwidgets /P-bakåtreferens till sin sida som ägande. Sedan v3.126.2 skiljer PDFium Component navigationskanter från ägda-nylastkanter, så sidinnehåll förblir sidinnehåll. Buggrapporten bakom den här fixen ser harmlös ut på papperet. Ett certifierat avtal tillåter annoteringar, en motpart lägger till en stegvis sparning, och analysatorn säger att varje senare ändring är tillåten. Sedan diffar någon de renderade sidorna och betalningsbeloppet på sida 2 är ett annat
Den här artikeln är uppföljningen ur angriparens ögon till översikten av analys av revisionsändringar efter signatur, så den hoppar över grunderna i revisionsåterbyggnad och DocMDP-betygssättning och går rakt på objektgrafen: hur ägandet modellerades, varför en kants riktning avgör en säkerhetsdom, vad som ändrades i v3.126.2, och hur man granskar sin egen acceptanslogik
Varför gick en sidändring igenom som en annoteringsändring under DocMDP P=3?
Sidändringen gick igenom för att den gamla rollgrafen följde varje indirekt referens i en ordlista som om det refererade objektet tillhörde det refererande, och signaturwidgeten pekar tillbaka på sin sida. En annoteringsordlista bär /P, en indirekt referens till sidobjektet den sitter på (ISO 32000-1 §12.5.2). Den posten är en navigeringsledtråd. Widgeten äger inte sidan; sidan äger widgeten genom sin /Annots-matris
Analysatorn tilldelar varje objekt en uppsättning rollbitar innan den betygsätter senare ändringar: sida, annotering, formulär och valideringsmaterial. Rotobjekt får sin roll från sin egen ordlista, och rollen sprids sedan till allt de refererar. I den gamla spridningen gick kedjan så här:
- Signaturwidgeten är en
/Subtype /Widgetmed/FT /Sig, så den får annoteringsrollen - Widgetens
/Ptrycker annoteringsrollen på sidordlistan, som redan har sidrollen - Sidan trycker båda rollerna in i
/Contents,/Resourcesoch, via/Parent, upp i Pages-trädet och över till varje syskonsida - En innehållsströmordlista som
<< /Length 812 >>har ingen/Type, så klassificeraren föll tillbaka på rollbitar och kontrollerade annoteringsrollen före sidrollen
Den ändrade innehållsströmmen kom därför ut som prckAnnotation. Enligt ISO 32000-1 §12.8.2.2 tillåter DocMDP P=3 annoteringsändringar, så beslutet var prdAllowed och rapporten rullades upp till prasAllowed. Samma fil under P=2 avvisades, men bara av en händelse: P=2 förbjuder annoteringsändringar, så den felmärkta strömmen avslogs av fel skäl. En fast fyra-pass-spridningsloop lade till en andra svaghet. Nylast som nås via en indirekt matris, eller via en lång kedja vars objektnummer löper baklänges, kunde aldrig få någon roll alls
Varför måste en signaturvaliderare fråga vem som äger ett objekt?
En signaturvaliderare måste fråga vem som äger ett objekt för att PDF-stegvisa uppdateringar (ISO 32000-1 §7.5.6) låter vem som helst lägga till en revision som omdefinierar ett befintligt objektnummer, och den omdefinierade kroppen ropar inte ut vad den är. Signaturen verifieras fortfarande, eftersom den bara täcker bytes i sin egen revision. Varje försvar mot manipulation efter signeringen beror därför på att mappa varje ändrat objekt till strukturen som använder det, och sedan fråga om undertecknaren tillät att den strukturen ändrades
Flera publicerade attackklasser arbetar exakt i det glappet. Incremental saving attacks lägger till en revision som byter ut sidinnehåll och förlitar sig på att verifieraren bara kontrollerar det signerade byte-området. Shadow attacks planterar dolt innehåll före signeringen och aktiverar det efteråt med en liten, oskyldigt seende ändring. Attacker på certifierade dokument missbrukar det faktum att P=2 och P=3 uttryckligen tillåter vissa senare ändringar, och klär sedan ut en förbjuden ändring som en tillåten. En verifierare som klassificerar objekt efter etiketter som /Type /Annot, eller efter vilken referensväg som råkar nå dem, är utsatt för den tredje klassen: angriparen behöver bara en tillåten struktur som kan nå den förbjudna
Det är därför frågan inte är vilka objekt som ändrades utan vem som äger dem. En innehållsström som nås från en sida via /Contents är sidinnehåll oavsett vad annat pekar på den. En annotering som pekar tillbaka på sidan via /P säger var annoteringen bor, inte vad den äger
Hur modellerar PDFium Component v3.126.2 ägande?
PDFium Component v3.126.2 behandlar bakåtreferenser som navigering, håller dem utanför rollspridningen, och avgör vilka nycklar som räknas som navigering utifrån den strukturella rollen hos ordlistan som håller dem, inte från nyckelnamnet ensamt. Tabellen sammanfattar navigeringsnycklarna som inte längre bär ägande
| Ägande ordlista | Nycklar behandlade som navigering | Specifikationsreferens |
|---|---|---|
| Sida eller Pages-nod | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotering eller widget | /P | ISO 32000-1 §12.5.2 |
| Widget- eller fältordlista | /Parent | ISO 32000-1 §12.7.3 |
Att filtrera på nyckelnamn globalt hade skapat ett nytt hål. En typsnitts- eller XObject-resurs kan legitimt heta /P, /Parent eller /Annots, och en /Resources-ordlista som släpper sin /P-post ur spridningen skulle låta en angripare gömma ett sida-ägt XObject bakom ett oskyldigt resursnamn. I v3.126.2 tillämpas navigeringsfiltret bara när den ägande ordlistan faktiskt är en sida, Pages-nod, annotering, widget eller fält. Bär en av de ordlistorna en duplicerad navigeringsnyckel, som två /P-poster i en widget, gissar analysatorn inte vilken kopia en visare skulle använda; rollbygget misslyckas och signaturen blir Indeterminate
Flera ytterligare regler stänger de kvarvarande ommärkningsvägarna:
- Pages-noder är sidrollsrötter i egen rätt, så resurser ärvda från Pages-trädet (ISO 32000-1 §7.7.3.4) kommer in i sidokontexten genom verkligt ägande, inte genom en
/Parent-vandring från en barnsida - En annoterings- eller formulärroll som anländer till en katalog, Pages-nod, sida, annotering eller fältordlista stannar där, för de strukturella objekten etablerar egna roller och en inkommande nylastroll får inte åsidosätta dem
- Sidrollen är auktoritativ under klassificeringen: ett sida-ägt objekt är
prckPageContentäven om en senare revision skriver om det med en förfalskad/FT, en/Type /Annot-etikett, eller delar det med en utseendeström - Ett Form XObject använt bara som utseende för ett fält eller en annotering behåller sin formulär- eller annoteringskategori, så ordinell utseenderegeneration efter en formulärifyllning betygsätts fortfarande under de normala tillståndsreglerna
- En widget utan egen
/FTlöser upp den ärvda fälttypen genom/Parent-kedjan, och en ouppklarlig kedja får rollbygget att misslyckas i stället för att falla tillbaka på annotering - Rollbitar från varje senare revision slås samman till den täckta revisionens roller, så en senare uppdatering kan inte radera en tidigare sida-ägande-relation genom att först frigöra en ström och redigera den efteråt
Fixpunkt i stället för ett fast passantal
Rollräckvidd i v3.126.2 körs som en arbetskö som itererar tills inget objekt vinner en ny rollbit, vilket är en äkta fixpunkt oavsett kedjedjup eller objektnummerering. Indirekta matriser, som en /Contents-matris lagrad som sitt eget objekt, vandras också. Varje objekt kan vinna högst fyra distinkta rollbitar, så kön är avgränsad till fyra poster per objektnummer; överskrids den budgeten kastas prrResourceLimitExceeded. En referens till ett fritt objekt, en felmatchande generation eller ett trasigt objekthuvud kastar prrMalformedRevisionChain, och nylast inuti en komprimerad objektström kastar prrCompressedObjectUnresolved. Var och en av dessa misslyckanden slutar med prasIndeterminate, aldrig med en tillåtande dom, och sker misslyckandet medan den täckta revisionens roller byggs rapporterar signaturen inga Changes alls
Följande rutin listar sidinnehållsändringarna som överlever den här analysen. En prckPageContent-ändring betygsätts aldrig prdAllowed: DocMDP P=1, 2 eller 3 gör den till prdDisallowed, och en signatur utan DocMDP betygsätter 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, inget att frigöra
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;
Vad händer när FieldMDP och en annotering delar ett objekt?
När ett fälts /V och en annoterings /Contents pekar på samma indirekta objekt håller v3.126.2 FieldMDP-låset kvar även om ändringen klassificeras som en annoteringsredigering. Scenariot är lätt att bygga för hand: en undertecknare låser fältet Total med FieldMDP (ISO 32000-1 §12.8.2.4), och angriparen låter en textannoterings /Contents referera samma strängobjekt som håller fältvärdet. Under P=3 är annoteringsredigeringen tillåten, så före fixen ändrade en omskrivning av den delade strängen ett låst fältvärde med en tillåtande dom
Objektet bär nu både annoterings- och formulärrollerna, och annoteringsbeslutet kontrollerar formulärsidan på nytt närhelst signaturen har en FieldMDP-transform:
- Under P=2 är annoteringsändringen förbjuden rakt av, precis som före
- Med FieldMDP
Allär varje fält låst, så den delade ändringen ärprdDisallowed - Med FieldMDP
IncludeellerExcludekan analysatorn inte spåra en delad skalär tillbaka till ett fältnamn, så beslutet ärprdIndeterminatei stället för en gissning - Utan FieldMDP gäller P=3-annoteringsregeln och ändringen förblir tillåten
En rapporteringsdetalj spelar roll för grindkod. Det delade fallet rapporteras som Kind = prckAnnotation med Decision = prdIndeterminate, och prrFieldMdpUnresolved läggs till riskuppsättningen bara för ändringar klassificerade som formulärfält. En grind som söker efter prrFieldMdpUnresolved och ignorerar Status missar det här fallet helt
Hur ska Delphi-kod avvisa vid osäkerhet i revisionsanalys?
Delphi-kod ska acceptera ett signerat dokument bara när analysstatusen är prasNoLaterChanges eller prasAllowed och ingen strukturell risk finns, och den ska behandla prasIndeterminate och prasSuspicious som otillförlitliga, inte som varningar att logga och släppa fram. Indeterminate betyder att analysatorn inte kunde bevisa att de senare revisionerna var tillåtna; för en angripare är en indata som pålitligt producerar Indeterminate lika användbar som en som producerar Allowed om din kod släpper igenom den. Den globala AnalyzePadesSignatureRevisions tar vilken TStream som helst och läser den från position 0, vilket passar uppladdningshanterare som aldrig behöver rendera dokumentet
uses
Classes, SysUtils, TypInfo, FPdfPades;
const
// Duplicata definitioner registreras utan att sänka 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 och prasSuspicious är avvisanden, inte varningar
Reason := 'revision status ' +
GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
end;
end;
Två gränser är värda att säga rakt ut. TPadesRevisionAnalysisReport säger ingenting om CMS-integritet eller certifikattillit, så den här grinden sitter bredvid kryptografisk och tillitsvalidering, inte i deras ställe. Och en korrekt ägandegraf gör inte P=3 säkert för varje arbetsflöde. P=3 tillåter genuint annoteringar, och en annotering med ett opak utseende kan täcka signerad text utan att röra en enda innehållsström. Är dina certifierade dokument avtal i stället för granskkopior, certifiera antingen med P=2 eller dirigera tillåtna annoteringsändringar till en människa, som i den här hjälparen:
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;
Granskningschecklista för signaturrevisioner
Använd listan för att kontrollera om din verifieringspipeline var utsatt och om den nu avvisar vid osäkerhet:
- Byggen av PDFium Component före v3.126.2 kunde rapportera
prasAllowedför sidinnehållsändringar i DocMDP P=3-dokument; körTPdf.AnalyzeSignatureRevisionsigen på certifierade P=3-filer som äldre byggen accepterat - Kontrollera P=3-dokument med FieldMDP-lås igen där ett fältvärde och en annotering kan dela ett indirekt objekt
- Acceptera bara
prasNoLaterChangesochprasAllowed; behandlaprasIndeterminateochprasSuspicioussom otillförlitliga - Testa
Report.Riskslikväl somReport.Status, förprrDuplicateObjectDefinitionändrar inte statusen av sig själv - Läs inte en tom
Changes-matris som ett rent resultat när signaturstatusen är Indeterminate; ett misslyckat rollbygge rapporterar inga ändringar - Förlita dig inte på
prrFieldMdpUnresolvedensamt för att fånga FieldMDP-problem, ty det delade annoteringsfallet yttrar sig bara genom beslutet och statusen - Bestäm om tillåtna annoteringsändringar under P=3 behöver mänsklig granskning i ditt arbetsflöde
- Analysera originalfilens bytes; ett dokument omskrivet av
SaveAsinnehåller inte längre revisionskedjan
Revisionsanalys är ett lager i en signaturkontroll. Para den med granskning av PDF-digitala signaturer och PAdES-nivåer för ordlistan och baslinjenivån, och med en bredare PDF-säkerhetsriskgranskning för JavaScript, startåtgärder och inbäddade filer. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions och den ägandemedvetna rollgrafen som beskrivs här medföljer PDFium Component för Delphi, C++Builder och Lazarus