Artikel Teknis

DocMDP PDFium Component: Trik /P Widget dan Edit Halaman

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:

  1. Signature widget adalah /Subtype /Widget dengan /FT /Sig, jadi ia mendapat role annotation
  2. /P milik widget mendorong role annotation ke dictionary halaman, yang sudah punya role page
  3. Halaman mendorong kedua role itu ke /Contents, /Resources, dan, lewat /Parent, naik ke tree Pages dan menyebar ke setiap sibling page
  4. Dictionary content stream seperti << /Length 812 >> tak punya /Type, sehingga classifier jatuh kembali ke role bit dan mengecek role annotation sebelum role page
Diagram PDFium Component atas role graph DocMDP pra-v3.126.2 ketika back-reference /P milik signature widget mendorong role annotation ke dictionary halaman, halaman menyebarkannya lewat /Contents ke content stream tanpa entri /Type, classifier mengeluarkan prckAnnotation dan penilaian P=3 mengembalikan prdAllowed
Sebelum v3.126.2, role graph memperlakukan setiap indirect reference sebagai kepemilikan, sehingga entri /P milik widget mendorong role annotation ke halaman dan edit halaman sungguhan keluar dari analyzer sebagai perubahan annotation yang diizinkan

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 pemilikKey yang diperlakukan sebagai navigasiReferensi spesifikasi
Node halaman atau Pages/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
Annotation atau widget/PISO 32000-1 §12.5.2
Dictionary widget atau field/ParentISO 32000-1 §12.7.3
Object graph PDFium Component v3.126.2 yang memisahkan edge payload milik seperti /Contents dan /Annots, yang menyebarkan role page dan annotation, dari edge navigasi seperti /P widget, yang tak membawa role, dengan key navigasi per dictionary pemilik dan prckPageContent tetap menempel di content stream bahkan di bawah /Type palsu
v3.126.2 menjauhkan back-reference dari propagasi role: role hanya berjalan lewat kepemilikan nyata, sehingga content stream tetap konten halaman dan petunjuk /P tak menentukan apa pun

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 /Parent dari 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 prckPageContent meski revisi belakangan menulisnya ulang dengan /FT palsu, 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 /FT miliknya 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 itu prdDisallowed
  • Dengan FieldMDP Include atau Exclude, analyzer tak bisa menelusuri scalar bersama balik ke satu nama field, jadi keputusannya prdIndeterminate alih-alih tebakan
  • Tanpa FieldMDP, aturan annotation P=3 berlaku dan perubahan tetap diizinkan
Diagram keputusan FieldMDP PDFium Component ketika /V milik field Total terkunci dan /Contents milik annotation merujuk objek indirect yang sama, bercabang atas DocMDP P=2, FieldMDP All, FieldMDP Include atau Exclude, dan tanpa FieldMDP menuju vonis prdDisallowed, prdIndeterminate, atau prdAllowed untuk edit bersama yang sama
Ketika satu objek indirect membawa role annotation dan form sekaligus, keputusan annotation mengecek ulang lock FieldMDP, sehingga edit yang sama berkisar dari diizinkan hingga dilarang hingga indeterminate

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 prasAllowed untuk edit konten halaman di dokumen DocMDP P=3; jalankan ulang TPdf.AnalyzeSignatureRevisions pada 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 prasNoLaterChanges dan prasAllowed; perlakukan prasIndeterminate dan prasSuspicious sebagai tak terpercaya
  • Tes Report.Risks selain Report.Status, karena prrDuplicateObjectDefinition tak mengubah status dengan sendirinya
  • Jangan membaca array Changes kosong sebagai hasil bersih ketika status signature Indeterminate; role build yang gagal melaporkan nol perubahan
  • Jangan mengandalkan prrFieldMdpUnresolved saja 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 SaveAs tak 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