W buildach PDFium Component sprzed v3.126.2 TPdf.AnalyzeSignatureRevisions potrafił ocenić prawdziwą edycję treści strony jako dozwoloną zmianę adnotacji przy DocMDP P=3, bo jego graf ról rewizji traktował wsteczną referencję /P widgeta podpisu do jego strony jako własność. Od v3.126.2 PDFium Component rozdziela krawędzie nawigacyjne od krawędzi własności z ładunkiem, więc treść strony pozostaje treścią strony. Bugreport stojący za tą poprawką wygląda na papierze niewinnie. Certyfikowana umowa dopuszcza adnotacje, druga strona dokłada jeden zapis przyrostowy, a analizator twierdzi, że każda późniejsza zmiana jest dozwolona. Potem ktoś porównuje wyrenderowane strony i kwota płatności na stronie 2 jest inna
Ten artykuł to sequela z perspektywy atakującego dla przeglądu analizy zmian rewizji po podpisie, więc pomija podstawy przebudowy rewizji i oceniania DocMDP, a idzie prosto do grafu obiektów: jak modelowano własność, dlaczego kierunek krawędzi rozstrzyga werdykt bezpieczeństwa, co się zmieniło w v3.126.2 i jak poddać audytowi własną logikę akceptacji
Dlaczego edycja strony przeszła jako zmiana adnotacji przy DocMDP P=3?
Edycja strony przeszła, bo stary graf ról szedł za każdą referencją pośrednią w słowniku tak, jakby wskazywany obiekt należał do wskazującego, a widget podpisu wskazuje z powrotem na swoją stronę. Słownik adnotacji niesie /P, pośrednią referencję do obiektu strony, na której leży (ISO 32000-1 §12.5.2). Ten wpis to podpowiedź nawigacyjna. Widget nie posiada strony; strona posiada widget przez swoją tablicę /Annots
Analizator nadaje każdemu obiektowi zbiór bitów ról, zanim oceni późniejsze zmiany: strona, adnotacja, formularz i materiał walidacyjny. Obiekty korzeniowe biorą rolę z własnego słownika, a rola rozlewa się potem na wszystko, co wskazują. W starej propagacji łańcuch biegł tak:
- widget podpisu to
/Subtype /Widgetz/FT /Sig, więc dostaje rolę adnotacji /Pwidgeta wpycha rolę adnotacji na słownik strony, który już ma rolę strony- strona wpycha obie role do
/Contents,/Resourcesi, przez/Parent, w górę drzewa Pages i w poprzek do każdej strony siostrzanej - słownik strumienia treści jak
<< /Length 812 >>nie ma/Type, więc klasyfikator schodził do bitów ról i sprawdzał rolę adnotacji przed rolą strony
Zmodyfikowany strumień treści wychodził więc jako prckAnnotation. Zgodnie z ISO 32000-1 §12.8.2.2 DocMDP P=3 dopuszcza zmiany adnotacji, więc decyzją było prdAllowed, a raport zwinął się do prasAllowed. Ten sam plik przy P=2 był odrzucany, ale wyłącznie przez przypadek: P=2 zabrania zmian adnotacji, więc źle oznaczony strumień był odrzucany z powodu niewłaściwego. Stała pętla propagacji o czterech przebiegach dokładała drugą słabość. Żaden ładunek dotarty przez tablicę pośrednią albo przez długi łańcuch o numerach obiektów biegnących wstecz mógł w ogóle nie dostać żadnej roli
Dlaczego walidator podpisu musi pytać, kto posiada obiekt?
Walidator podpisu musi pytać, kto posiada obiekt, bo aktualizacje przyrostowe PDF (ISO 32000-1 §7.5.6) pozwalają każdemu dokleić rewizję przedefiniowującą istniejący numer obiektu, a przedefiniowane ciało nie oznajmia, czym jest. Podpis wciąż się weryfikuje, bo obejmuje wyłącznie bajty własnej rewizji. Każda obrona przed manipulacją po podpisie zależy więc od mapowania każdego zmienionego obiektu na strukturę, która go używa, i od pytania, czy podpisujący pozwolił tej strukturze się zmienić
Kilka opublikowanych klas ataków działa dokładnie w tej luce. Incremental saving attacks doklejają rewizję podmieniającą treść strony i liczą na walidatora sprawdzającego tylko podpisany zakres bajtów. Shadow attacks zasadzają ukrytą treść przed podpisaniem i aktywują ją po fakcie małą, niewinnie wyglądającą zmianą. Ataki na dokumenty certyfikowane nadużywają faktu, że P=2 i P=3 wprost dopuszczają niektóre późniejsze edycje, po czym przebierają zakazaną edycję za dozwoloną. Walidator klasyfikujący obiekty po etykietach takich jak /Type /Annot albo po dowolnej ścieżce referencji, która akurat do nich dociera, jest wystawiony na trzecią klasę: atakującemu wystarczy jedna dozwolona struktura, która sięga tamtej zakazanej
Dlatego pytanie brzmi nie „które obiekty się zmieniły”, tylko „kto je posiada”. Strumień treści osiągnięty ze strony przez /Contents jest treścią strony, cokolwiek innego by na niego nie wskazywało. Adnotacja wskazująca z powrotem na stronę przez /P mówi, gdzie adnotacja mieszka, a nie co posiada
Jak PDFium Component v3.126.2 modeluje własność?
PDFium Component v3.126.2 traktuje referencje wsteczne jako nawigację, trzyma je poza propagacją ról i rozstrzyga, które klucze liczą się jako nawigacja, na podstawie strukturalnej roli słownika, który je trzyma — nie samej nazwy klucza. Tabela podsumowuje klucze nawigacyjne, które już nie niosą własności
| Słownik właściciela | Klucze traktowane jako nawigacja | Odniesienie w specyfikacji |
|---|---|---|
| Węzeł Page albo Pages | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Adnotacja albo widget | /P | ISO 32000-1 §12.5.2 |
| Słownik widgeta albo pola | /Parent | ISO 32000-1 §12.7.3 |
Filtrowanie po nazwie klucza globalnie stworzyłoby nową dziurę. Zasób czcionki albo XObject może legalnie nazywać się /P, /Parent albo /Annots, a słownik /Resources, który wyrzuciłby swój wpis /P z propagacji, pozwoliłby atakującemu schować XObject należący do strony za niewinną nazwą zasobu. W v3.126.2 filtr nawigacyjny działa tylko wtedy, gdy posiadający słownik faktycznie jest stroną, węzłem Pages, adnotacją, widgetem albo polem. Jeśli któryś z tych słowników niesie zduplikowany klucz nawigacyjny, jak dwa /P w jednym widgetcie, analizator nie zgaduje, której kopii użyłby viewer; budowanie ról kończy się porażką, a podpis staje się Indeterminate
Kilka dalszych reguł zamyka pozostałe trasy przemianowywania etykiet:
- węzły Pages są korzeniami roli strony we własnym prawie, więc zasoby dziedziczone z drzewa Pages (ISO 32000-1 §7.7.3.4) wchodzą do kontekstu strony przez prawdziwą własność, a nie przez spacer po
/Parentod strony potomnej - rola adnotacji albo formularza, która dociera do katalogu, węzła Pages, strony, adnotacji albo słownika pola, zatrzymuje się tam, bo te strukturalne obiekty ustanawiają własne role, a przychodzącej roli ładunku nie wolno ich nadpisać
- rola strony jest rozstrzygająca przy klasyfikacji: obiekt posiadany przez stronę to
prckPageContentnawet wtedy, gdy późniejsza rewizja przepisze go z podrobionym/FT, etykietą/Type /Annotalbo udostępni go ze strumieniem wyglądu - Form XObject używany wyłącznie jako wygląd pola albo adnotacji zachowuje kategorię formularza albo adnotacji, więc zwyczajna regeneracja wyglądu po wypełnieniu formularza jest nadal oceniana według zwykłych reguł zezwoleń
- widget bez własnego
/FTrozwiązuje odziedziczony typ pola przez łańcuch/Parent, a nierozwiązywalny łańcuch wywala budowę ról, zamiast domyślać się adnotacji - bity ról z każdej późniejszej rewizji są scalane z rolami rewizji objętej podpisem, więc późniejsza aktualizacja nie może wymazać wcześniejszej relacji własności strony przez odpięcie strumienia najpierw i edycję go potem
Punkt stały zamiast stałej liczby przebiegów
Osiągalność ról w v3.126.2 biegnie jako kolejka robocza iterująca, dopóki żaden obiekt nie zyska nowego bitu roli, czyli prawdziwy punkt stały, niezależnie od głębokości łańcucha i numeracji obiektów. Tablice pośrednie, jak tablica /Contents zapisana jako osobny obiekt, też są chodzone. Każdy obiekt może zyskać najwyżej cztery odrębne bity ról, więc kolejka jest ograniczona do czterech wpisów na numer obiektu; przekroczenie tego budżetu podnosi prrResourceLimitExceeded. Referencja do obiektu wolnego, niedopasowanie generacji albo zepsuty nagłówek obiektu podnoszą prrMalformedRevisionChain, a ładunek w środku skompresowanego strumienia obiektów podnosi prrCompressedObjectUnresolved. Każda z tych porażek kończy się prasIndeterminate, nigdy werdyktem dozwolonym, a gdy porażka wydarzy się przy budowaniu ról rewizji objętej podpisem, podpis w ogóle nie raportuje żadnych Changes
Poniższa procedura wypisuje edycje treści stron, które przeżywają tę analizę. Zmiana prckPageContent nigdy nie jest oceniana jako prdAllowed: DocMDP P=1, 2 albo 3 czyni ją prdDisallowed, a podpis bez DocMDP ocenia ją jako 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; // rekord, nic do zwolnienia
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;
Co się dzieje, gdy FieldMDP i adnotacja dzielą jeden obiekt?
Gdy /V pola i /Contents adnotacji wskazują ten sam obiekt pośredni, v3.126.2 trzyma blokadę FieldMDP w mocy, choć zmiana jest klasyfikowana jako edycja adnotacji. Scenariusz łatwo zbudować ręcznie: podpisujący blokuje pole Total przez FieldMDP (ISO 32000-1 §12.8.2.4), a atakujący sprawia, że /Contents adnotacji tekstowej referuje ten sam obiekt tekstowy, który trzyma wartość pola. Przy P=3 edycja adnotacji jest dozwolona, więc przed poprawką przepisanie tej współdzielonej wartości tekstowej zmieniało zablokowane pole z werdyktem dozwolonym
Obiekt niesie teraz zarówno rolę adnotacji, jak i formularza, a decyzja o adnotacji ponownie sprawdza stronę formularza, ilekroć podpis ma transformację FieldMDP:
- przy P=2 zmiana adnotacji jest z góry niedozwolona, dokładnie jak wcześniej
- przy FieldMDP
Allkażde pole jest zablokowane, więc współdzielona zmiana toprdDisallowed - przy FieldMDP
IncludealboExcludeanalizator nie umie wyśledzić współdzielonego skalara do jednej nazwy pola, więc decyzją jestprdIndeterminate, a nie zgadywanka - bez FieldMDP obowiązuje reguła adnotacji P=3 i zmiana pozostaje dozwolona
Jeden szczegół raportowania ma znaczenie dla kodu bramek. Przypadek współdzielony jest raportowany jako Kind = prckAnnotation z Decision = prdIndeterminate, a prrFieldMdpUnresolved trafia do zestawu ryzyk wyłącznie dla zmian sklasyfikowanych jako pola formularza. Bramka szukająca prrFieldMdpUnresolved i ignorująca Status gubi ten przypadek całkowicie
Jak kod w Delphi powinien stosować fail closed przy analizie rewizji?
Kod w Delphi powinien akceptować podpisany dokument tylko wtedy, gdy status analizy to prasNoLaterChanges albo prasAllowed i nie ma żadnego strukturalnego ryzyka, a prasIndeterminate i prasSuspicious traktować jako niezaufane, a nie jako ostrzeżenia do zalogowania i przepuszczenia. Indeterminate znaczy, że analizator nie zdołał udowodnić, iż późniejsze rewizje były dozwolone; dla atakującego wejście, które niezawodnie daje Indeterminate, jest równie użyteczne jak takie, które daje Allowed, jeśli twój kod je przepuści. Globalny AnalyzePadesSignatureRevisions bierze dowolny TStream i czyta go od pozycji 0, co pasuje handlerom uploadu, które nie muszą renderować dokumentu
uses
Classes, SysUtils, TypInfo, FPdfPades;
const
// Zduplikowane definicje są rejestrowane bez degradacji 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 i prasSuspicious to odrzucenia, nie ostrzeżenia
Reason := 'revision status ' +
GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
end;
end;
Dwie granice warto wypowiedzieć wprost. TPadesRevisionAnalysisReport nie mówi niczego o integralności CMS ani o zaufaniu do certyfikatów, więc ta bramka siedzi obok walidacji kryptograficznej i zaufania, a nie w ich miejscu. I poprawny graf własności nie czyni P=3 bezpiecznym dla każdego przepływu. P=3 szczerze dopuszcza adnotacje, a adnotacja z nieprzejrzystym wyglądem może zakryć podpisany tekst, nie dotykając żadnego strumienia treści. Jeśli twoje certyfikowane dokumenty to umowy, a nie kopie do recenzji, certyfikuj z P=2 albo kieruj dozwolone zmiany adnotacji do człowieka, jak w tym pomocniku:
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;
Lista kontrolna audytu rewizji podpisów
Użyj tej listy, żeby sprawdzić, czy twój potok weryfikacji był wystawiony i czy teraz zawodzi zamknięty:
- buildy PDFium Component sprzed v3.126.2 potrafiły raportować
prasAlloweddla edycji treści stron w dokumentach DocMDP P=3; przepuść ponownieTPdf.AnalyzeSignatureRevisionspo certyfikowanych plikach P=3 zaakceptowanych przez starsze buildy - sprawdź ponownie dokumenty P=3 z blokadami FieldMDP tam, gdzie wartość pola i adnotacja mogą dzielić obiekt pośredni
- akceptuj wyłącznie
prasNoLaterChangesiprasAllowed; traktujprasIndeterminateiprasSuspiciousjako niezaufane - testuj
Report.RisksobokReport.Status, boprrDuplicateObjectDefinitionsamo w sobie nie zmienia statusu - nie czytaj pustej tablicy
Changesjako czystego wyniku, gdy status podpisu to Indeterminate; nieudana budowa ról nie raportuje żadnych zmian - nie polegaj na samym
prrFieldMdpUnresolvedw wyłapywaniu problemów FieldMDP, bo współdzielony przypadek adnotacji wychodzi na jaw tylko przez decyzję i status - zdecyduj, czy dozwolone zmiany adnotacji przy P=3 potrzebują przeglądu człowieka w twoim przepływie
- analizuj oryginalne bajty pliku; dokument przepisany przez
SaveAsnie zawiera już łańcucha rewizji
Analiza rewizji to jedna warstwa sprawdzania podpisu. Sparuj ją z inspekcją cyfrowych podpisów PDF i poziomów PAdES dla słownika i poziomu bazowego oraz z szerszym audytem ryzyk bezpieczeństwa PDF dla JavaScriptu, akcji uruchamiania i plików osadzonych. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions i graf ról świadomy własności opisany tutaj jedzie w pakiecie z PDFium Component dla Delphi, C++Buildera i Lazarusa