Pada build PDFium Component sebelum v3.126.2, TPdf.AnalyzeSignatureRevisions bisa menilai edit konten halaman sungguhan sebagai perubahan annotation yang diizinkan di bawah DocMDP P=3, karena revision role graph-nya memperlakukan back-reference /P milik signature widget ke halamannya sebagai kepemilikan. Sejak v3.126.2, PDFium Component memisahkan edge navigasi dari edge payload milik, sehingga konten halaman tetap konten halaman. Laporan bug di balik perbaikan ini tampak tak berbahaya di atas kertas. Kontrak tersertifikasi mengizinkan annotation, pihak lawan menambah satu incremental save, dan analyzer menyatakan setiap perubahan belakangan diizinkan. Lalu ada yang men-diff halaman hasil render dan jumlah pembayaran di halaman 2 ternyata berbeda
Artikel ini adalah sekuel dari sudut pandang penyerang atas gambaran umum analisis perubahan revisi pasca-tanda tangan, jadi ia melewati dasar-dasar pembangunan ulang revisi dan penilaian DocMDP serta langsung ke object graph: bagaimana kepemilikan dimodelkan, kenapa arah sebuah edge menentukan vonis keamanan, apa yang berubah di v3.126.2, dan cara mengaudit logika penerimaan Anda sendiri
Mengapa edit halaman lolos sebagai perubahan annotation di bawah DocMDP P=3?
Edit halaman itu lolos karena role graph lama mengikuti setiap indirect reference dalam dictionary seolah objek yang dirujuk milik yang merujuk, dan signature widget menunjuk balik ke halamannya. Dictionary annotation membawa /P, indirect reference ke objek halaman tempat ia berada (ISO 32000-1 §12.5.2). Entri itu adalah petunjuk navigasi. Widget tidak memiliki halaman; halaman yang memiliki widget lewat array /Annots-nya
Analyzer meng-assign setiap objek sekumpulan role bit sebelum menilai perubahan belakangan: page, annotation, form, dan validation material. Objek root mendapat role-nya dari dictionary-nya sendiri, dan role itu lalu menyebar ke semua yang mereka rujuk. Pada propagasi lama, rantainya berjalan begini:
- Signature widget adalah
/Subtype /Widgetdengan/FT /Sig, jadi ia mendapat role annotation /Pmilik widget mendorong role annotation ke dictionary halaman, yang sudah punya role page- Halaman mendorong kedua role itu ke
/Contents,/Resources, dan, lewat/Parent, naik ke tree Pages dan menyebar ke setiap sibling page - Dictionary content stream seperti
<< /Length 812 >>tak punya/Type, sehingga classifier jatuh kembali ke role bit dan mengecek role annotation sebelum role page
Content stream yang dimodifikasi karenanya keluar sebagai prckAnnotation. Di bawah ISO 32000-1 §12.8.2.2, DocMDP P=3 mengizinkan perubahan annotation, jadi keputusannya prdAllowed dan laporannya tergulung ke prasAllowed. File yang sama di bawah P=2 ditolak, tapi hanya karena kebetulan: P=2 melarang perubahan annotation, jadi stream yang salah label itu ditolak dengan alasan yang salah. Loop propagasi empat pass yang tetap menambah kelemahan kedua. Payload yang terjangkau lewat array indirect, atau lewat rantai panjang yang nomor objeknya berjalan mundur, bisa jadi tak pernah menerima role apa pun
Mengapa signature validator harus bertanya siapa pemilik sebuah objek?
Signature validator harus bertanya siapa pemilik sebuah objek karena incremental update PDF (ISO 32000-1 §7.5.6) membiarkan siapa pun menambah revisi yang mendefinisikan ulang nomor objek yang ada, dan body hasil redefinisi itu tak mengumumkan apa dirinya. Tanda tangan tetap terverifikasi, karena ia hanya mencakup byte revisinya sendiri. Setiap pertahanan terhadap manipulasi pasca-tanda tangan karena itu bergantung pada pemetaan tiap objek yang berubah ke struktur yang memakainya, lalu bertanya apakah penanda tangan mengizinkan struktur itu berubah
Beberapa kelas serangan yang sudah terpublikasi bekerja persis di celah itu. Incremental saving attack menambah revisi yang menukar konten halaman dan bertaruh pada verifier yang hanya mengecek byte range yang ditandatangani. Shadow attack menanam konten tersembunyi sebelum penandatanganan dan mengaktifkannya belakangan dengan perubahan kecil yang tampak tak berbahaya. Serangan terhadap dokumen tersertifikasi menyalahgunakan fakta bahwa P=2 dan P=3 secara eksplisit mengizinkan beberapa edit belakangan, lalu menyamarkan edit terlarang sebagai edit yang diizinkan. Verifier yang mengklasifikasi objek menurut label seperti /Type /Annot, atau menurut jalur reference apa pun yang kebetulan menjangkaunya, terekspos ke kelas ketiga: penyerang hanya butuh satu struktur yang diizinkan yang bisa menjangkau yang terlarang
Itulah kenapa pertanyaannya bukan objek mana yang berubah, tapi siapa pemiliknya. Content stream yang terjangkau dari halaman lewat /Contents adalah konten halaman apa pun yang lain yang menunjuknya. Annotation yang menunjuk balik ke halaman lewat /P menyatakan di mana annotation itu tinggal, bukan apa yang dimilikinya
Bagaimana PDFium Component v3.126.2 memodelkan kepemilikan?
PDFium Component v3.126.2 memperlakukan back-reference sebagai navigasi, menjauhkannya dari propagasi role, dan memutuskan key mana yang dihitung navigasi dari role struktural dictionary yang memegangnya, bukan dari nama key saja. Tabel berikut meringkas key navigasi yang tak lagi membawa kepemilikan
| Dictionary pemilik | Key yang diperlakukan sebagai navigasi | Referensi spesifikasi |
|---|---|---|
| Node halaman atau Pages | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotation atau widget | /P | ISO 32000-1 §12.5.2 |
| Dictionary widget atau field | /Parent | ISO 32000-1 §12.7.3 |
Menyaring berdasarkan nama key secara global justru akan menciptakan lubang baru. Resource font atau XObject sah-sah saja bisa dinamai /P, /Parent, atau /Annots, dan dictionary /Resources yang membuang entri /P-nya dari propagasi akan membiarkan penyerang menyembunyikan XObject milik halaman di balik nama resource yang tak berbahaya. Di v3.126.2, filter navigasi hanya berlaku ketika dictionary pemiliknya memang halaman, node Pages, annotation, widget, atau field. Kalau salah satu dictionary itu membawa key navigasi duplikat, misalnya dua entri /P di sebuah widget, analyzer tak menebak salinan mana yang akan dipakai viewer; role build gagal dan signature menjadi Indeterminate
Beberapa aturan lanjutan menutup rute relabeling yang tersisa:
- Node Pages adalah root role page atas haknya sendiri, sehingga resource yang diwarisi dari tree Pages (ISO 32000-1 §7.7.3.4) masuk ke konteks halaman lewat kepemilikan nyata, bukan lewat penelusuran
/Parentdari halaman anak - Role annotation atau form yang tiba di dictionary catalog, node Pages, halaman, annotation, atau field berhenti di sana, karena objek struktural itu menetapkan role-nya sendiri dan role payload yang datang tak boleh menimpanya
- Role page bersifat otoritatif selama klasifikasi: objek milik halaman adalah
prckPageContentmeski revisi belakangan menulisnya ulang dengan/FTpalsu, label/Type /Annot, atau membaginya dengan appearance stream - Form XObject yang hanya dipakai sebagai appearance field atau annotation mempertahankan kategori form atau annotation-nya, sehingga regenerasi appearance biasa setelah pengisian form tetap dinilai di bawah aturan izin normal
- Widget tanpa
/FTmiliknya sendiri me-resolve field type yang diwarisi lewat rantai/Parent, dan rantai yang tak ter-resolve menggagalkan role build alih-alih default ke annotation - Role bit dari setiap revisi belakangan digabung ke role revisi yang tercakup, sehingga update belakangan tak bisa menghapus relasi kepemilikan halaman sebelumnya dengan melepas sebuah stream dulu lalu mengeditnya
Fixed point alih-alih jumlah pass tetap
Role reachability di v3.126.2 berjalan sebagai work queue yang beriterasi sampai tak ada objek yang mendapat role bit baru, suatu fixed point sesungguhnya terlepas dari kedalaman rantai maupun penomoran objek. Array indirect seperti array /Contents yang disimpan sebagai objeknya sendiri juga ditelusuri. Tiap objek paling banyak bisa mendapat empat role bit berbeda, jadi queue-nya terbatas empat entri per nomor objek; melebihi anggaran itu memicu prrResourceLimitExceeded. Reference ke objek bebas, ketidakcocokan generation, atau header objek rusak memicu prrMalformedRevisionChain, dan payload di dalam compressed object stream memicu prrCompressedObjectUnresolved. Setiap kegagalan itu berakhir di prasIndeterminate, tak pernah vonis diizinkan, dan ketika kegagalan terjadi saat membangun role revisi tercakup, signature tak melaporkan Changes sama sekali
Routine berikut mencatat edit konten halaman yang selamat dari analisis ini. Perubahan prckPageContent tak pernah dinilai prdAllowed: DocMDP P=1, 2, atau 3 menjadikannya prdDisallowed, dan signature tanpa DocMDP menilainya 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, tak ada yang perlu di-free
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;
Apa yang terjadi ketika FieldMDP dan sebuah annotation berbagi satu objek?
Ketika /V milik field dan /Contents milik annotation menunjuk objek indirect yang sama, v3.126.2 menjaga lock FieldMDP tetap berlaku meski perubahannya diklasifikasi sebagai edit annotation. Skenarionya mudah dibangun manual: penanda tangan mengunci field Total dengan FieldMDP (ISO 32000-1 §12.8.2.4), dan penyerang membuat /Contents milik sebuah text annotation merujuk objek string yang sama dengan yang menyimpan nilai field. Di bawah P=3, edit annotation diizinkan, jadi sebelum perbaikan, menulis ulang string bersama itu mengubah nilai field terkunci dengan vonis diizinkan
Objek kini membawa role annotation dan form sekaligus, dan keputusan annotation mengecek ulang sisi form kapan pun signature punya transform FieldMDP:
- Di bawah P=2, perubahan annotation dilarang langsung, persis seperti sebelumnya
- Dengan FieldMDP
All, semua field terkunci, jadi perubahan bersama ituprdDisallowed - Dengan FieldMDP
IncludeatauExclude, analyzer tak bisa menelusuri scalar bersama balik ke satu nama field, jadi keputusannyaprdIndeterminatealih-alih tebakan - Tanpa FieldMDP, aturan annotation P=3 berlaku dan perubahan tetap diizinkan
Satu detail pelaporan penting untuk kode gate. Kasus bersama dilaporkan sebagai Kind = prckAnnotation dengan Decision = prdIndeterminate, dan prrFieldMdpUnresolved ditambahkan ke risk set hanya untuk perubahan yang diklasifikasi sebagai form field. Gate yang mencari prrFieldMdpUnresolved dan mengabaikan Status melewatkan kasus ini sepenuhnya
Bagaimana kode Delphi seharusnya fail closed atas analisis revisi?
Kode Delphi sebaiknya menerima dokumen bertanda tangan hanya ketika status analisisnya prasNoLaterChanges atau prasAllowed dan tak ada risiko struktural, serta memperlakukan prasIndeterminate dan prasSuspicious sebagai tak terpercaya, bukan sebagai warning yang dicatat lalu diloloskan. Indeterminate berarti analyzer tak bisa membuktikan revisi belakangan diizinkan; bagi penyerang, input yang andal menghasilkan Indeterminate sama bergunanya dengan yang menghasilkan Allowed kalau kode Anda meloloskannya. AnalyzePadesSignatureRevisions global menerima TStream apa pun dan membacanya dari posisi 0, cocok untuk upload handler yang tak perlu merender dokumen
uses
Classes, SysUtils, TypInfo, FPdfPades;
const
// Definisi duplikat dicatat tanpa menurunkan 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 dan prasSuspicious adalah penolakan, bukan warning
Reason := 'revision status ' +
GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
end;
end;
Dua batas patut dinyatakan terang-terangan. TPadesRevisionAnalysisReport tak mengatakan apa pun tentang integritas CMS maupun kepercayaan sertifikat, jadi gate ini duduk di samping validasi kriptografi dan trust, bukan menggantikannya. Dan graph kepemilikan yang benar tak membuat P=3 aman untuk semua workflow. P=3 memang mengizinkan annotation, dan annotation dengan appearance opaque bisa menutupi teks yang sudah ditandatangani tanpa menyentuh satu pun content stream. Kalau dokumen tersertifikasi Anda adalah kontrak dan bukan salinan review, sertifikasi dengan P=2 atau arahkan perubahan annotation yang diizinkan ke manusia, seperti helper ini:
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;
Checklist audit revisi signature
Pakai daftar ini untuk mengecek apakah pipeline verifikasi Anda pernah terekspos dan apakah kini fail closed:
- Build PDFium Component sebelum v3.126.2 bisa melaporkan
prasAlloweduntuk edit konten halaman di dokumen DocMDP P=3; jalankan ulangTPdf.AnalyzeSignatureRevisionspada file P=3 tersertifikasi yang diterima build lama - Periksa ulang dokumen P=3 dengan lock FieldMDP tempat nilai field dan sebuah annotation mungkin berbagi objek indirect
- Terima hanya
prasNoLaterChangesdanprasAllowed; perlakukanprasIndeterminatedanprasSuspicioussebagai tak terpercaya - Tes
Report.RisksselainReport.Status, karenaprrDuplicateObjectDefinitiontak mengubah status dengan sendirinya - Jangan membaca array
Changeskosong sebagai hasil bersih ketika status signature Indeterminate; role build yang gagal melaporkan nol perubahan - Jangan mengandalkan
prrFieldMdpUnresolvedsaja untuk menangkap masalah FieldMDP, karena kasus annotation bersama muncul hanya lewat keputusan dan status - Putuskan apakah perubahan annotation yang diizinkan di bawah P=3 butuh review manusia di workflow Anda
- Analisis byte file asli; dokumen yang ditulis ulang oleh
SaveAstak lagi memuat rantai revisi
Analisis revisi adalah satu lapisan dari pemeriksaan signature. Padukan dengan inspeksi signature digital PDF dan level PAdES untuk dictionary dan level baseline, dan dengan audit risiko keamanan PDF yang lebih luas untuk JavaScript, launch action, dan embedded file. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions, dan role graph sadar-kepemilikan yang dibahas di sini dikirim dalam PDFium Component untuk Delphi, C++Builder, dan Lazarus