Untuk mengetahui apa yang berubah di PDF setelah ditandatangani, PDFium Component untuk Delphi dan Lazarus menyediakan TPdf.AnalyzeSignatureRevisions, analyzer perubahan revisi pasca-tanda tangan yang membangun ulang setiap revisi inkremental dari byte berkas asli, menilai setiap perubahan objek berikutnya terhadap aturan DocMDP dan FieldMDP milik tanda tangan itu, dan melaporkan definisi objek shadow sebagai risiko terpisah. Situasi yang ia sasar akrab bagi siapa pun yang menangani kontrak: formulir tersertifikasi dikirim, kembali dengan dua simpanan inkremental tambahan, dan semua tanda tangan tetap terverifikasi. Itu memang wajar, karena tanda tangan hanya mencakup byte revisinya sendiri. Pertanyaan sebenarnya adalah apakah simpanan-simpanan berikutnya itu diizinkan, dan tanda centang hijau pada signature tidak menjawabnya
Mengapa API signature PDFium tak bisa menunjukkan apa yang berubah setelah penandatanganan?
API signature PDFium tak bisa menunjukkan perubahan pasca-tanda tangan karena ia hanya membaca dictionary signature: /Contents, /ByteRange, /SubFilter, dan nilai izin DocMDP. PDFium tidak punya graf revisi inkremental, tidak mengurai parameter transform FieldMDP, dan tak menawarkan diff level objek antar revisi, sehingga analyzer di FPdfPades.pas bekerja langsung pada byte mentah. Itu punya konsekuensi praktis yang harus Anda rancang di sekitarnya. TPdf.AnalyzeSignatureRevisions membaca byte yang dipertahankan saat dokumen dimuat, tidak pernah salinan yang dihasilkan SaveAs, karena berkas hasil tulis ulang sudah kehilangan struktur revisi yang justru sedang dianalisis. Bila dokumen datang dari sumber progresif yang unduhannya belum selesai, report mengembalikan SourceStatus = pvssIncomplete dan Status = prasIndeterminate alih-alih menganalisis berkas yang terpotong
Membangun ulang batas revisi dari startxref, xref stream, dan /Prev
Analyzer membangun ulang batas revisi dengan mengikuti setiap startxref mundur melewati tabel xref klasik, cross-reference stream, entri /XRefStm hibrida, dan rantai /Prev, sebagaimana didefinisikan untuk incremental update di ISO 32000-1 §7.5.6 dan §7.5.8. Panjang tercakup tiap signature adalah akhir dari span ByteRange keduanya, dan analyzer memetakan panjang itu ke revisi yang seksi xref-nya menaunginya. Ketika tak ada revisi yang cocok, signature mendapat prrCoveredRevisionNotFound dan status Indeterminate. State setiap objek lalu direplay sampai revisi tercakup, dan setiap entri xref berikutnya dibandingkan dengan state itu. Ini lebih penting daripada kedengarannya: sebagian writer menuliskan ulang tabel xref lengkap di setiap simpanan inkremental, dan entri yang masih menunjuk objek sama yang tak berubah dilewati alih-alih dilaporkan sebagai modifikasi. Tanpa perbandingan itu, pengisian formulir yang sepenuhnya legal akan tenggelam dalam ratusan perubahan palsu
Definisi shadow adalah kasus yang paling pantas diwaspadai. Body objek yang muncul di dalam rentang byte revisi berikutnya tapi tak direferensikan oleh xref revisi itu tak terlihat bagi viewer biasa, padahal ia persis jenis staging yang diandalkan serangan shadow: konten tersembunyi ditanam sebelum atau sesudah penandatanganan lalu diaktifkan kemudian dengan membalik sebuah referensi. AnalyzePadesSignatureRevisionsBytes mencatat objek semacam itu sebagai perubahan non-otoritatif dengan IsAuthoritative = False, menilainya prdSuspicious apa pun level izinnya, dan menambahkan prrUnreferencedObjectDefinition ke set risiko. Dua risiko terkait menutup trik struktural lain: prrDuplicateObjectDefinition menyala ketika satu seksi xref mendaftar objek yang sama lebih dari sekali, dan prrSignatureObjectRedefined menyala ketika revisi berikutnya mendefinisikan ulang objek signature yang sudah ada
uses
SysUtils, TypInfo, PDFium, FPdfPades;
const
ShadowTag: array[Boolean] of string = ('', ' (shadow)');
var
Pdf: TPdf;
Report: TPadesRevisionAnalysisReport;
i, j: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-returned.pdf';
Pdf.Active := True;
Report := Pdf.AnalyzeSignatureRevisions;
Writeln('Revisions: ', Report.RevisionCount,
' Signatures: ', Report.SignatureCount,
' Overall: ', GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus),
Ord(Report.Status)));
for i := 0 to High(Report.Signatures) do
with Report.Signatures[i] do
begin
Writeln(Format('Signature %d covers revision %d, %d later, P=%d, FieldMDP=%s',
[SignatureIndex, CoveredRevisionIndex, LaterRevisionCount,
DocMdpPermission,
GetEnumName(TypeInfo(TPadesFieldMdpAction), Ord(FieldMdpAction))]));
for j := 0 to High(Changes) do
Writeln(Format(' rev %d obj %d %s -> %s%s',
[Changes[j].RevisionIndex, Changes[j].ObjectNumber,
GetEnumName(TypeInfo(TPadesRevisionChangeKind), Ord(Changes[j].Kind)),
GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Changes[j].Decision)),
ShadowTag[not Changes[j].IsAuthoritative]]));
end;
finally
Pdf.Free;
end;
end;
Bagaimana DocMDP dan FieldMDP ditegakkan untuk tiap signature?
DocMDP dan FieldMDP ditegakkan terpisah untuk tiap signature, pada revisi tercakup milik signature itu sendiri, sehingga signature sertifikasi dan signature persetujuan berikutnya dalam satu berkas bisa mencapai vonis berbeda atas suntingan yang sama. Setiap objek berikutnya mula-mula diklasifikasikan ke TPadesRevisionChangeKind dari entri /Type, /Subtype, dan /FT-nya serta dari peran yang ia mainkan di graf halaman, formulir, anotasi, dan DSS. Apa pun yang membawa /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia, atau /EmbeddedFile menjadi prckActiveContent. Vonisnya lalu mengikuti ISO 32000-1 §12.8.2.2: dengan P=1, segala hal kecuali data cross-reference dan material validasi dilarang; P=2 mengizinkan pengisian formulir dan tanda tangan lanjutan tapi menolak perubahan anotasi; P=3 juga mengizinkan anotasi. Konten halaman, struktur dokumen, metadata, konten aktif, dan objek terhapus dilarang pada level DocMDP apa pun, dan dinilai prdSuspicious ketika signature sama sekali tak membawa DocMDP, karena signature persetujuan secara formal tak melarang apa pun, tapi pembaca tak lagi melihat apa yang ditandatangani
FieldMDP, dari ISO 32000-1 §12.8.2.4, mempersempit vonis form-field lebih jauh. pfmaAll mengunci semua field, pfmaInclude hanya mengunci field yang didaftar, dan pfmaExclude mengunci semuanya kecuali yang didaftar. Untuk menerapkan Include atau Exclude, analyzer me-resolve tiap field yang berubah ke nama terkualifikasi penuhnya lewat rantai /Parent dan membandingkannya dengan daftar kunci secara cocok-persis, jadi daftarkan nama field terminal daripada berharap nama induk menutup anak-anaknya. Ketika sebuah nama tak bisa di-resolve atau transform memakai aksi yang tak dikenali parser, perubahan menjadi prdIndeterminate dan prrFieldMdpUnresolved dimunculkan. Vonis per-perubahan lalu digulung worst-first, dengan Suspicious di atas Disallowed, Disallowed di atas Indeterminate, dan Indeterminate di atas Allowed, sehingga satu objek shadow mengungguli banyaknya pembaruan field yang sah
Mengapa sebagian perubahan kembali sebagai Indeterminate alih-alih aman?
Perubahan kembali sebagai Indeterminate setiap kali analyzer tak bisa membuktikan sebuah perubahan diizinkan, karena dalam pemeriksaan tanda tangan, hal yang tak diketahui tidak boleh dilaporkan sebagai diizinkan. Satu kasus umum justru ditangani presisi: validasi jangka panjang menambahkan /DSS dan menulis ulang katalog, yang kalau tidak akan dihitung sebagai perubahan struktural di bawah P=1. Analyzer melepas /DSS dan /Extensions dari dictionary katalog lama dan baru lalu membandingkan sisanya; ketika tak ada lagi yang berbeda, penulisan ulang itu diperlakukan sebagai pembaruan material validasi dan diizinkan, sehingga augmentasi B-LT dan B-LTA tidak mematahkan signature sertifikasi. Celah lain sengaja dibiarkan terbuka. Entri Tipe-2 di cross-reference stream menunjuk ke object stream terkompresi, dan analyzer tidak membentangkan object stream di dalam batas keamanan ini, sehingga perubahan itu muncul sebagai prckCompressedObject dengan prrCompressedObjectUnresolved, dilarang di bawah P=1 dan Indeterminate sisanya. Anggaran keras 1024 revisi, 1,000,000 nomor objek, dan 2,000,000 perubahan yang dilaporkan menghasilkan prrResourceLimitExceeded, dan rantai xref yang rusak menghasilkan prrMalformedRevisionChain; keduanya berakhir Indeterminate, tidak pernah lolos
const
StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrResourceLimitExceeded];
function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
// Sebagian risiko tercatat tanpa mengubah Status, jadi uji dulu risiko itu
if R.Risks * StructuralRisks <> [] then
Exit('review: structural risk in the revision chain');
case R.Status of
prasNoLaterChanges: Result := 'accept: nothing was added after signing';
prasAllowed: Result := 'accept: every later change is permitted';
prasDisallowed: Result := 'reject: a change violates DocMDP or FieldMDP';
prasSuspicious: Result := 'reject: shadow or unconstrained content change';
prasIndeterminate: Result := 'review: the analyzer could not decide';
else
Result := 'not checked: no signatures or no original bytes';
end;
end;
Urutan di gerbang itu disengaja. prrDuplicateObjectDefinition ditambahkan ke set risiko tanpa menurunkan Status dengan dirinya sendiri, dan transform FieldMDP yang tak bisa diurai hanya memengaruhi status begitu sebuah form field benar-benar berubah, sehingga gerbang yang hanya melihat Status bisa melewatkan bukti yang sudah ada di report. Ingat pula apa yang tidak diklaim report ini. TPadesRevisionAnalysisReport tak mengatakan apa pun soal apakah signature CMS sah secara kriptografis atau apakah sertifikat penandatangannya berantai ke root yang Anda percayai. Analisis revisi menjawab pertanyaan apa yang terjadi setelah penandatanganan, dan tempatnya di samping validasi struktural dan trust, bukan menggantikannya
Menulis seed value dan kunci MDP saat penandatanganan
Aturan yang sama bisa ditulis sendiri saat menandatangani lewat TPadesSignatureFieldOptions, yang merupakan anggota FieldOptions milik TPadesSignOptions maupun TPadesRemoteSignOptions. PDFium bisa membuat widget tapi tak bisa menulis /SV, /Lock, transform FieldMDP atau DocMDP, maupun dictionary /Perms katalog, sehingga writer PAdES inkremental milik komponen sendiri menghasilkan objek-objek ini di dalam update xref yang sama dengan signature. FieldName menyetel nama field root, RequiredSeedValues menjadi bit /Ff milik dictionary seed value yang dideskripsikan di ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations, dan AcceptableCertificates membatasi pilihan penandatangan berikutnya, LockAction dengan LockFields menulis /SigFieldLock tak langsung, dan CertificationPermission dari 1 sampai 3 mengubah signature menjadi signature sertifikasi. Transform DocMDP dan FieldMDP keduanya masuk ke satu array /Reference pada nilai signature, masing-masing dengan /Data yang menunjuk katalog
var
Options: TPadesSignOptions;
begin
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
Options.Reason := 'Approved for release';
Options.FieldOptions.FieldName := 'Certification';
Options.FieldOptions.CertificationPermission := 2; // hanya pengisian formulir dan penandatanganan
Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
Options.FieldOptions.LockAction := pfmaInclude; // kunci hanya field ini
SetLength(Options.FieldOptions.LockFields, 2);
Options.FieldOptions.LockFields[0] := 'Total';
Options.FieldOptions.LockFields[1] := 'IBAN';
if not Pdf.SignPades('contract-certified.pdf', Options) then
Writeln('Signing failed');
end;
Beberapa detail mudah keliru kalau Anda merakitnya sendiri. Katalog /Perms /DocMDP harus mereferensikan dictionary nilai signature, bukan anotasi widget, dan karena itulah writer mempertahankan nilai signature sebagai objek tak langsung tersendiri. Dictionary /Perms yang sudah ada mungkin sudah menampung hak penggunaan /UR3, jadi writer menyalinnya dan menyisipkan /DocMDP alih-alih menggantinya, mengikuti dictionary perizinan di ISO 32000-1 §12.8.4. Dokumen yang sudah membawa /DocMDP menolak signature sertifikasi kedua dengan EPadesCrypto, begitu juga opsi yang tak konsisten: kunci Include atau Exclude tanpa nama field, kunci All dengan daftar field, legal attestation pada signature non-sertifikasi, atau titik di nama field root. Remote signing menambah satu aturan lagi, karena sertifikat penandatangan belum diketahui ketika PreparePadesRemoteSignature berjalan: menyetel CertificateRequired di sana menuntut daftar AcceptableCertificates eksplisit, sementara penandatanganan lokal bisa jatuh kembali ke sertifikat penandatangan yang sudah di-resolve
Analisis revisi melengkapi kotak perkakas signature, bukan mengganti bagian mana pun darinya. Mulailah dari menginspeksi signature PDF dan level PAdES dengan PDFium Component untuk membaca dictionary dan level baseline, lihat kenapa validator menolak signature PAdES untuk kegagalan struktural yang datang sebelum pertanyaan revisi mana pun, lalu masukkan vonisnya ke audit risiko keamanan PDF yang lebih luas bersama pemeriksaan JavaScript dan embedded file. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions, dan writer PAdES inkremental yang ditampilkan di sini terkirim bersama PDFium Component untuk Delphi, C++Builder, dan Lazarus